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

基于Reanimated 2、React Native Gesture Handler和Expo的可拖拽滚动ScrollView实现

Expo + Reanimated 2 + React Native Gesture Handler 可拖拽滚动视图优化示例

针对你遇到的iPhone端拖拽至顶部/底部时无法正常滚动的问题,核心原因是原示例未实现边界自动滚动逻辑,仅依赖手指位移触发微小滚动。以下是优化后的完整实现,解决边界滚动问题同时保留拖拽排序功能:

依赖安装

确保已安装所需依赖(Expo项目直接执行以下命令):

npx expo install react-native-reanimated react-native-gesture-handler

完整代码实现

import React, { useState, useRef } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  useAnimatedScrollHandler,
  withTiming,
  runOnJS,
  cancelAnimation,
} from 'react-native-reanimated';
import { PanGestureHandler, PanGestureHandlerGestureEvent } from 'react-native-gesture-handler';

// 列表项数据类型
type Item = {
  id: string;
  text: string;
};

export default function DragSortScrollView() {
  // 列表数据
  const [items, setItems] = useState<Item[]>([
    { id: '1', text: '列表项 1' },
    { id: '2', text: '列表项 2' },
    { id: '3', text: '列表项 3' },
    { id: '4', text: '列表项 4' },
    { id: '5', text: '列表项 5' },
    { id: '6', text: '列表项 6' },
    { id: '7', text: '列表项 7' },
    { id: '8', text: '列表项 8' },
    { id: '9', text: '列表项 9' },
    { id: '10', text: '列表项 10' },
  ]);

  // 滚动视图的滚动偏移量
  const scrollOffset = useSharedValue(0);
  // 当前拖拽项的索引和偏移量
  const activeIndex = useSharedValue(-1);
  const dragOffset = useSharedValue({ x: 0, y: 0 });
  // 边界滚动动画的定时器引用
  const scrollTimer = useRef<NodeJS.Timeout | null>(null);

  // 滚动事件处理,更新scrollOffset
  const scrollHandler = useAnimatedScrollHandler({
    onScroll: (event) => {
      scrollOffset.value = event.contentOffset.y;
    },
  });

  // 处理拖拽项的位置更新
  const updateDragPosition = (index: number, y: number) => {
    activeIndex.value = index;
    dragOffset.value = { x: 0, y };
  };

  // 处理边界自动滚动
  const handleBoundaryScroll = (direction: 'up' | 'down') => {
    cancelAnimation(scrollOffset);
    // 清除之前的定时器
    if (scrollTimer.current) clearInterval(scrollTimer.current);

    // 每隔一段时间触发滚动
    scrollTimer.current = setInterval(() => {
      scrollOffset.value = withTiming(
        scrollOffset.value + (direction === 'down' ? 20 : -20),
        { duration: 16 },
        () => {
          // 滚动后更新拖拽项位置(保持相对手指的位置)
          if (activeIndex.value !== -1) {
            dragOffset.value = {
              ...dragOffset.value,
              y: dragOffset.value.y + (direction === 'down' ? 20 : -20),
            };
          }
        }
      );
    }, 16);
  };

  // 停止边界滚动
  const stopBoundaryScroll = () => {
    if (scrollTimer.current) {
      clearInterval(scrollTimer.current);
      scrollTimer.current = null;
    }
    cancelAnimation(scrollOffset);
  };

  // 拖拽结束后排序并重置状态
  const handleDragEnd = () => {
    stopBoundaryScroll();
    if (activeIndex.value === -1) return;

    // 计算拖拽项的最终位置对应的新索引
    const itemHeight = 60; // 列表项高度,需和样式中一致
    const finalY = dragOffset.value.y - scrollOffset.value;
    const newIndex = Math.round(finalY / itemHeight);

    // 确保索引在有效范围内
    const clampedIndex = Math.max(0, Math.min(items.length - 1, newIndex));

    // 更新列表数据排序
    runOnJS(setItems)((prevItems) => {
      const newItems = [...prevItems];
      const [movedItem] = newItems.splice(activeIndex.value, 1);
      newItems.splice(clampedIndex, 0, movedItem);
      return newItems;
    });

    // 重置拖拽状态
    activeIndex.value = -1;
    dragOffset.value = { x: 0, y: 0 };
  };

  return (
    <GestureHandlerRootView style={styles.container}>
      <Animated.ScrollView
        style={styles.scrollView}
        onScroll={scrollHandler}
        scrollEventThrottle={16}
        showsVerticalScrollIndicator={false}
        // 绑定滚动偏移量到ScrollView
        contentOffset={{ y: scrollOffset.value }}
      >
        {items.map((item, index) => {
          // 拖拽项的动画样式
          const animatedStyle = useAnimatedStyle(() => {
            if (activeIndex.value === index) {
              return {
                transform: [
                  { translateX: dragOffset.value.x },
                  { translateY: dragOffset.value.y - scrollOffset.value },
                ],
                zIndex: 100,
                opacity: 0.8,
              };
            }
            // 非拖拽项在排序时的偏移动画
            if (activeIndex.value !== -1) {
              const delta = activeIndex.value < index ? -60 : 60;
              return {
                transform: [{ translateY: activeIndex.value !== -1 ? delta : 0 }],
              };
            }
            return {};
          });

          return (
            <PanGestureHandler
              key={item.id}
              onGestureEvent={(event: PanGestureHandlerGestureEvent) => {
                const { y } = event.nativeEvent.translation;
                // 计算拖拽项的绝对位置
                const itemTop = index * 60 + scrollOffset.value;
                updateDragPosition(index, itemTop + y);

                // 判断是否触达边界,触发自动滚动
                const screenHeight = 600; // 可根据实际屏幕高度动态获取
                const dragTop = dragOffset.value.y - scrollOffset.value;
                if (dragTop < 50) {
                  handleBoundaryScroll('up');
                } else if (dragTop > screenHeight - 100) {
                  handleBoundaryScroll('down');
                } else {
                  stopBoundaryScroll();
                }
              }}
              onEnded={handleDragEnd}
            >
              <Animated.View style={[styles.item, animatedStyle]}>
                <Text style={styles.itemText}>{item.text}</Text>
              </Animated.View>
            </PanGestureHandler>
          );
        })}
      </Animated.ScrollView>
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f5f5f5',
  },
  scrollView: {
    flex: 1,
  },
  item: {
    height: 60,
    backgroundColor: '#fff',
    marginVertical: 4,
    marginHorizontal: 8,
    borderRadius: 8,
    justifyContent: 'center',
    paddingHorizontal: 16,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 2,
  },
  itemText: {
    fontSize: 16,
    color: '#333',
  },
});

关键优化点说明

  • 边界自动滚动:当拖拽项接近屏幕顶部/底部时,通过定时器驱动ScrollView持续滚动,同时同步更新拖拽项的位置,避免手指移动才能滚动的问题
  • 滚动偏移同步:使用scrollOffset共享值实时同步ScrollView的滚动位置,确保拖拽项的绝对位置计算准确
  • 惯性滚动处理:拖拽结束时取消所有滚动动画,避免残留滚动影响排序结果
  • 排序动画优化:非拖拽项在排序时添加平滑的偏移动画,提升交互体验

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 04:10:28