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

如何为React Native FlatList实现重排序动画?(点击图标置顶选中项)

嘿,刚好我之前做过几乎一模一样的需求!这种「点击选中项跳转到顶部+交换动画」的实现,核心是用React Native的Animated库精准控制列表项的位置过渡,而不是依赖FlatList的默认排序逻辑。我给你拆解下具体实现思路和可运行的代码示例:

核心思路
  1. 状态分层管理:用数组维护列表数据源,标记选中项;用useRef存储每个列表项的实时布局位置和动画实例,避免状态频繁更新导致的重绘问题。
  2. 动画触发逻辑:
    • 首次选中:让目标项从原位置平滑移动到列表顶部,动画结束后更新数据源顺序。
    • 切换选中:同时触发旧选中项和新选中项的位置交换动画,动画完成后再更新数据源(避免列表瞬间重排打断动画)。
  3. 布局精准获取:通过onLayout事件记录每个项的坐标,确保动画位移的计算完全贴合实际界面位置。
完整代码示例
import React, { useState, useRef, useCallback } from 'react';
import { View, Text, TouchableOpacity, Animated, FlatList, StyleSheet } from 'react-native';

export default function AnimatedSortList() {
  // 列表数据源:包含ID、文本、选中状态
  const [data, setData] = useState([
    { id: '1', text: '日常购物清单', isSelected: false },
    { id: '2', text: '工作待办事项', isSelected: false },
    { id: '3', text: '周末出行计划', isSelected: false },
    { id: '4', text: '学习笔记整理', isSelected: false },
  ]);

  // 存储每个列表项的布局信息(坐标、宽高)
  const itemLayouts = useRef({});
  // 存储每个项的动画实例,避免重复创建
  const animatedValues = useRef({});

  // 记录列表项的实时布局
  const handleItemLayout = useCallback((itemId) => (event) => {
    const { x, y, width, height } = event.nativeEvent.layout;
    itemLayouts.current[itemId] = { x, y, width, height };
  }, []);

  // 处理选中事件:核心动画逻辑
  const handleSelectItem = useCallback((selectedId) => {
    const currentSelectedIdx = data.findIndex(item => item.isSelected);
    const newSelectedIdx = data.findIndex(item => item.id === selectedId);

    // 重复点击同一项,直接返回
    if (currentSelectedIdx === newSelectedIdx) return;

    const newSelectedLayout = itemLayouts.current[selectedId];
    // 首次选中:直接移到顶部
    if (currentSelectedIdx === -1) {
      const anim = new Animated.ValueXY({ x: 0, y: newSelectedLayout.y });
      animatedValues.current[selectedId] = anim;

      Animated.timing(anim, {
        toValue: { x: 0, y: 0 },
        duration: 350,
        useNativeDriver: true,
      }).start(() => {
        // 动画结束后更新数据源
        const newData = [...data];
        const selectedItem = newData.splice(newSelectedIdx, 1)[0];
        selectedItem.isSelected = true;
        newData.unshift(selectedItem);
        setData(newData);
        // 清理动画实例,避免内存泄漏
        delete animatedValues.current[selectedId];
      });
      return;
    }

    // 切换选中项:交换两个项的位置动画
    const currentSelectedItem = data[currentSelectedIdx];
    const currentSelectedLayout = itemLayouts.current[currentSelectedItem.id];
    
    // 初始化两个动画实例
    const currentAnim = new Animated.ValueXY({ x: 0, y: 0 });
    const newAnim = new Animated.ValueXY({ x: 0, y: 0 });
    animatedValues.current[currentSelectedItem.id] = currentAnim;
    animatedValues.current[selectedId] = newAnim;

    // 计算位移距离:旧选中项移到新项原位置,新项移到顶部
    const currentTargetY = newSelectedLayout.y - currentSelectedLayout.y;
    const newTargetY = 0 - newSelectedLayout.y;

    // 并行执行两个动画
    Animated.parallel([
      Animated.timing(currentAnim, {
        toValue: { x: 0, y: currentTargetY },
        duration: 350,
        useNativeDriver: true,
      }),
      Animated.timing(newAnim, {
        toValue: { x: 0, y: newTargetY },
        duration: 350,
        useNativeDriver: true,
      }),
    ]).start(() => {
      // 更新数据源顺序
      const newData = [...data];
      newData[currentSelectedIdx].isSelected = false;
      const movedItem = newData.splice(newSelectedIdx, 1)[0];
      movedItem.isSelected = true;
      newData.unshift(movedItem);
      setData(newData);
      // 清理动画实例
      delete animatedValues.current[currentSelectedItem.id];
      delete animatedValues.current[selectedId];
    });
  }, [data]);

  // 带淡出动画的删除逻辑
  const handleDeleteItem = useCallback((itemId) => {
    const anim = new Animated.Value(1);
    animatedValues.current[itemId] = anim;

    Animated.timing(anim, {
      toValue: 0,
      duration: 200,
      useNativeDriver: true,
    }).start(() => {
      setData(prev => prev.filter(item => item.id !== itemId));
      delete animatedValues.current[itemId];
    });
  }, []);

  // 渲染单个列表项
  const renderItem = ({ item }) => {
    const anim = animatedValues.current[item.id];
    // 组合动画变换:位移+透明度
    const transform = anim ? anim.getTranslateTransform() : [];
    const opacity = anim ? anim.y.interpolate({
      inputRange: [0, 100],
      outputRange: [1, 1], // 删除时单独用透明度动画,这里保持默认
    }) : 1;

    return (
      <Animated.View
        style={[styles.itemContainer, { transform, opacity }]}
        onLayout={handleItemLayout(item.id)}
      >
        {/* 选中/未选中圆形图标 */}
        <TouchableOpacity
          style={[styles.selectIcon, item.isSelected && styles.selectedIcon]}
          onPress={() => handleSelectItem(item.id)}
        >
          {item.isSelected && <View style={styles.innerCircle} />}
        </TouchableOpacity>
        <Text style={styles.itemText}>{item.text}</Text>
        <TouchableOpacity onPress={() => handleDeleteItem(item.id)}>
          <Text style={styles.deleteText}>删除</Text>
        </TouchableOpacity>
      </Animated.View>
    );
  };

  return (
    <FlatList
      data={data}
      keyExtractor={(item) => item.id}
      renderItem={renderItem}
      style={styles.list}
      contentContainerStyle={{ padding: 16 }}
    />
  );
}

const styles = StyleSheet.create({
  list: { flex: 1, backgroundColor: '#f8f8f8' },
  itemContainer: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    padding: 16,
    marginVertical: 8,
    backgroundColor: '#fff',
    borderRadius: 12,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
  },
  selectIcon: {
    width: 24,
    height: 24,
    borderRadius: 12,
    borderWidth: 2,
    borderColor: '#999',
    alignItems: 'center',
    justifyContent: 'center',
    marginRight: 12,
  },
  selectedIcon: {
    borderColor: '#2563eb',
  },
  innerCircle: {
    width: 12,
    height: 12,
    borderRadius: 6,
    backgroundColor: '#2563eb',
  },
  itemText: { flex: 1, fontSize: 16, color: '#333' },
  deleteText: { color: '#dc2626', fontSize: 14 },
});
关键注意点
  1. 动画时机:一定要在动画完全结束后再更新数据源,否则FlatList会立刻重排,导致动画被打断。
  2. 布局复用问题:FlatList的项会被复用,所以onLayout事件要每次都记录最新的布局位置,避免使用旧坐标导致动画错位。
  3. 内存泄漏:动画结束后必须删除animatedValues中的对应实例,防止内存占用过高。
  4. 性能优化:开启useNativeDriver: true,让动画在原生线程执行,避免JS线程阻塞导致的卡顿。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.22 08:10:45