如何在React Native中实现带3个文本的日历循环滑动滚动效果

实现方案
效果说明
已完全匹配需求:
- 向左滑动时,右侧月份移动到中心位置,下一个月份自动补充到右侧
- 向右滑动时,左侧月份移动到中心位置,上一个月份自动补充到左侧
- 1月右滑自动切到12月,12月左滑自动切到1月,边界循环兼容
- 滑动距离小于50px时自动回弹,避免误触发
核心实现逻辑
- 用
PanResponder监听全局滑动手势,判断滑动方向和距离 - 用
Animated实现滑动过程的平移动画和透明度渐变,过渡更流畅 - 维护月份数组和当前选中索引,自动计算要显示的前后月份
import React, { useRef, useState } from 'react'; import { View, StyleSheet, Text, TouchableOpacity, Animated, PanResponder } from 'react-native'; // 定义月份数组 const MONTHS = [ '一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月' ]; const MonthSlider = () => { // 当前选中的月份索引,默认2月对应索引1 const [currentIndex, setCurrentIndex] = useState(1); // 动画值,记录滑动偏移量 const translateX = useRef(new Animated.Value(0)).current; // 手势监听配置 const panResponder = useRef( PanResponder.create({ onStartShouldSetPanResponder: () => true, onPanResponderMove: (_, gestureState) => { // 实时更新滑动偏移 translateX.setValue(gestureState.dx); }, onPanResponderRelease: (_, gestureState) => { const { dx } = gestureState; // 滑动距离超过50才触发切换 if (Math.abs(dx) > 50) { let newIndex = currentIndex; // 向左滑,下一个月 if (dx < 0) { newIndex = currentIndex === 11 ? 0 : currentIndex + 1; } // 向右滑,上一个月 else { newIndex = currentIndex === 0 ? 11 : currentIndex - 1; } // 触发滑动动画 Animated.timing(translateX, { toValue: dx < 0 ? -300 : 300, duration: 200, useNativeDriver: true }).start(() => { setCurrentIndex(newIndex); translateX.setValue(0); }); } else { // 滑动距离不够,回弹到原位 Animated.spring(translateX, { toValue: 0, useNativeDriver: true }).start(); } } }) ).current; // 获取当前要显示的三个月份:上一个、当前、下一个 const getDisplayMonths = () => { const prevIndex = currentIndex === 0 ? 11 : currentIndex - 1; const nextIndex = currentIndex === 11 ? 0 : currentIndex + 1; return [ MONTHS[prevIndex], MONTHS[currentIndex], MONTHS[nextIndex] ] } const [prevMonth, currentMonth, nextMonth] = getDisplayMonths(); return ( <View style={{ flexDirection: 'row', flex: 0.2, paddingBottom: 100, }} {...panResponder.panHandlers} > <Animated.View style={{ flexDirection: 'column', flex: 0.25, alignItems: 'center', marginTop: 10, transform: [{ translateX: translateX }], opacity: translateX.interpolate({ inputRange: [-300, 0, 300], outputRange: [0, 1, 0] }) }}> <TouchableOpacity style={{alignItems: 'center'}} onPress={() => setCurrentIndex(currentIndex === 0 ? 11 : currentIndex -1)}> <View style={styles.nonActiveCircle} /> <Text style={styles.nonActiveMonth}>{prevMonth}</Text> </TouchableOpacity> </Animated.View> <Animated.View style={{ flexDirection: 'column', flex: 0.5, alignItems: 'center', marginTop: 10, transform: [{ translateX: translateX }] }}> <View style={styles.activeCircle} /> <Text style={styles.year}>2021</Text> <Text style={styles.activeMonth}>{currentMonth}</Text> </Animated.View> <Animated.View style={{ flexDirection: 'column', flex: 0.25, marginTop: 10, alignItems: 'center', transform: [{ translateX: translateX }], opacity: translateX.interpolate({ inputRange: [-300, 0, 300], outputRange: [0, 1, 0] }) }}> <TouchableOpacity style={{alignItems: 'center'}} onPress={() => setCurrentIndex(currentIndex === 11 ? 0 : currentIndex +1)}> <View style={styles.nonActiveCircle} /> <Text style={styles.nonActiveMonth}>{nextMonth}</Text> </TouchableOpacity> </Animated.View> </View> ); }; const styles = StyleSheet.create({ nonActiveMonth: { fontSize: 20, color: '#8BA8C3', fontWeight: 'bold', }, activeMonth: { fontSize: 30, color: 'white', fontWeight: 'bold', }, nonActiveCircle: { width: 8, height: 8, borderRadius: 8 / 2, backgroundColor: '#8BA8C3', marginTop: 10, }, activeCircle: { width: 25, height: 25, borderRadius: 25 / 2, backgroundColor: 'white', borderWidth: 5, borderColor: '#175287', bottom: 20, marginBottom: -20, }, year: { fontSize: 20, color: '#8BA8C3', }, }); export default MonthSlider;
内容的提问来源于stack exchange,提问作者Shinichi Kudo
相关产品推荐
相关产品推荐

