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

React Native实现TikTok/YouTube Shorts式粘性视频滚动问题求助

实现TikTok/YouTube Shorts风格的粘性垂直滚动(短距离回弹、长距离切换)

我尝试用FlatList、ScrollView和react-native-reanimated-carousel实现类似TikTok或YouTube Shorts的粘性滚动效果:只有当滚动距离超过特定阈值时才切换到下一个/上一个视频,滚动距离较小时自动回弹到当前视频。其中react-native-reanimated-carousel的效果最接近,但无法实现短距离滚动后回弹到当前元素的功能。

原react-native-reanimated-carousel代码

<Carousel
   defaultIndex={currentItem}
   enabled={isCarouselScroll}
   loop={false}
   width={width}
   height={videoHeight - 60}
   vertical
   data={dataForVideos}
   onSnapToItem={(index) => setCurrentVideo(index)}
   renderItem={({ item, index }) => (
     <View key={index}>
       <VideoItem  lessonId={lessonsIds[index]} navigation={navigation} current={currentVideo === index} url={videoUrls[index]} />
     </View>
   )}
/>

原FlatList尝试代码

import React, { useRef, useState } from 'react';
import { View, Text, PanResponder, FlatList, Dimensions, TouchableOpacity } from 'react-native';

const videoWidth = Dimensions.get('window').width;
const videoHeight = Dimensions.get('window').height;
const TikTokScroll = () => {
  const itemHeight = videoHeight; 
  const [scrollDistance, setScrollDistance] = useState(0);
  const flatListRef = useRef(null);

  const data = ['content1', 'content1 2', 'content1 3'];

  const renderItem = ({ item, index }) => (
    <TouchableOpacity onPress={() => scrollToIndex(index)}>
      <View style={{ height: itemHeight, borderWidth: 2, width: videoWidth }}>
        <Text>{item}</Text>
      </View>
    </TouchableOpacity>
  );

  const panResponder = PanResponder.create({
    onMoveShouldSetPanResponder: () => true,
    onPanResponderMove: (evt, gestureState) => {
      setScrollDistance(gestureState.dy);
    },
  });

  const scrollToIndex = (index) => {
    flatListRef.current.scrollToIndex({ animated: true, index });
  };

  return (
    <FlatList
      ref={flatListRef}
      {...panResponder.panHandlers}
      data={data}
      renderItem={renderItem}
      keyExtractor={(_, index) => index.toString()}
      scrollEnabled={true}
    />
  );
};

export default TikTokScroll;

解决方案

方案1:改造react-native-reanimated-carousel实现阈值控制

通过结合Reanimated的动画逻辑,自定义滚动结束后的判断逻辑,实现阈值控制切换或回弹:

import Animated, {
  useAnimatedScrollHandler,
  useSharedValue,
  withSpring,
  runOnJS,
} from 'react-native-reanimated';

// ...其他原有代码

const scrollOffset = useSharedValue(0);
const currentIndex = useSharedValue(currentVideo);
const THRESHOLD = videoHeight * 0.3; // 设定切换阈值为屏幕高度的30%

const scrollHandler = useAnimatedScrollHandler({
  onScroll: (event) => {
    scrollOffset.value = event.contentOffset.y;
  },
  onEndDrag: () => {
    const diff = scrollOffset.value - currentIndex.value * videoHeight;
    if (Math.abs(diff) > THRESHOLD) {
      // 超过阈值,切换到对应索引
      const newIndex = diff > 0 ? currentIndex.value + 1 : currentIndex.value - 1;
      currentIndex.value = Math.max(0, Math.min(newIndex, dataForVideos.length - 1));
      runOnJS(setCurrentVideo)(currentIndex.value);
    } else {
      // 未超过阈值,回弹到当前索引
      scrollOffset.value = withSpring(currentIndex.value * videoHeight);
    }
  },
});

// ...替换原有Carousel组件
<Carousel
  defaultIndex={currentVideo}
  enabled={isCarouselScroll}
  loop={false}
  width={width}
  height={videoHeight - 60}
  vertical
  data={dataForVideos}
  onScroll={scrollHandler}
  onSnapToItem={(index) => setCurrentVideo(index)}
  renderItem={({ item, index }) => (
    <View key={index}>
      <VideoItem  lessonId={lessonsIds[index]} navigation={navigation} current={currentVideo === index} url={videoUrls[index]} />
    </View>
  )}
/>

方案2:用FlatList实现完整逻辑

关闭FlatList默认分页,自定义滚动结束后的阈值判断,实现粘性效果:

import React, { useRef, useState } from 'react';
import { View, Text, FlatList, Dimensions, TouchableOpacity } from 'react-native';

const videoWidth = Dimensions.get('window').width;
const videoHeight = Dimensions.get('window').height;
const THRESHOLD = videoHeight * 0.3; // 切换阈值

const TikTokScroll = () => {
  const itemHeight = videoHeight; 
  const flatListRef = useRef(null);
  const [currentIndex, setCurrentIndex] = useState(0);
  const data = ['content1', 'content1 2', 'content1 3'];

  const renderItem = ({ item, index }) => (
    <TouchableOpacity onPress={() => scrollToIndex(index)}>
      <View style={{ height: itemHeight, borderWidth: 2, width: videoWidth, justifyContent: 'center', alignItems: 'center' }}>
        <Text style={{ fontSize: 24 }}>{item}</Text>
      </View>
    </TouchableOpacity>
  );

  const scrollToIndex = (index) => {
    flatListRef.current?.scrollToIndex({ animated: true, index });
    setCurrentIndex(index);
  };

  const handleScrollEnd = (event) => {
    const offsetY = event.nativeEvent.contentOffset.y;
    const diff = offsetY - currentIndex * itemHeight;
    
    if (Math.abs(diff) > THRESHOLD) {
      // 超过阈值,计算新索引并切换
      const newIndex = diff > 0 ? currentIndex + 1 : currentIndex - 1;
      const clampedIndex = Math.max(0, Math.min(newIndex, data.length - 1));
      scrollToIndex(clampedIndex);
    } else {
      // 未超过阈值,回弹到当前视频
      scrollToIndex(currentIndex);
    }
  };

  return (
    <FlatList
      ref={flatListRef}
      data={data}
      renderItem={renderItem}
      keyExtractor={(_, index) => index.toString()}
      scrollEnabled={true}
      pagingEnabled={false}
      onMomentumScrollEnd={handleScrollEnd}
      onScrollEndDrag={handleScrollEnd}
      snapToAlignment="start"
      decelerationRate="fast"
    />
  );
};

export default TikTokScroll;

内容的提问来源于stack exchange,提问作者Mykhailo Yeromin

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 12:07:50