You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.24 01:24:07