Leo小李头像
关注

当列表重排遇上 FLIP 动画:前端列表元素位置变化的动画实现原理与优化

当列表重排遇上 FLIP 动画:前端列表元素位置变化的动画实现原理与优化

一、深度引言与场景痛点

列表重排是常见的交互场景——拖拽排序、筛选过滤、数据更新后重新排列。没有动画的重排像一场突然的座位调整——元素瞬间跳到新位置,用户需要重新识别整个列表。FLIP 动画让重排像一场有序的换座位——每个元素先记下旧位置,再计算新位置,然后用 transform 动画从旧位置滑到新位置,列表的变化有了叙事感。

FLIP 是 First-Last-Invert-Play 的缩写:First 记录元素的初始位置,Last 记录元素的目标位置,Invert 用 transform 把元素从目标位置反向偏移回初始位置(视觉上还在旧位置),Play 用动画把 transform 从偏移值过渡到零(元素滑到新位置)。四步操作让布局变化有了动画过渡,但每一步都有工程细节需要踩实。

二、底层机制与原理深度剖析

sequenceDiagram
    participant DOM as DOM 布局引擎
    participant FLIP as FLIP 算法
    participant AN as 动画执行

    DOM->>FLIP: 步骤1 First: 记录每个元素当前位置 (rect)
    Note over FLIP: getBoundingClientRect() 批量调用

    DOM->>DOM: 触发布局变化(排序/筛选/更新)
    DOM->>FLIP: 步骤2 Last: 记录每个元素新位置 (rect)
    Note over FLIP: 第二次 getBoundingClientRect()

    FLIP->>FLIP: 步骤3 Invert: 计算偏移量 Δx=old.x-new.x, Δy=old.y-new.y
    FLIP->>DOM: 设置 transform: translate(Δx, Δy) 让元素视觉回到旧位置

    FLIP->>AN: 步骤4 Play: 动画 transform 从 translate(Δx, Δy) 到 translate(0,0)
    AN->>DOM: 元素平滑滑到新位置

    Note over FLIP: 关键:Invert 让布局变化瞬间完成<br/>Play 让动画视觉平滑过渡

三、生产级代码实现与最佳实践

// utils/flip-animation.ts
interface FlipRecord {
  element: HTMLElement;
  firstRect: DOMRect;
  lastRect: DOMRect;
  deltaX: number;
  deltaY: number;
}

export function animateFlip(
  container: HTMLElement,
  duration: number = 300,
  easing: string = 'cubic-bezier(0.22, 0.61, 0.36, 1)'
): void {
  // 步骤1: First — 记录所有子元素的当前位置
  const children = Array.from(container.children) as HTMLElement[];
  const firstRects = new Map<HTMLElement, DOMRect>();
  children.forEach(el => {
    firstRects.set(el, el.getBoundingClientRect());
  });

  // 触发布局变化(外部操作:排序/筛选/更新DOM)
  // 此步骤由调用者完成,然后调用 animateFlip

  // 步骤2: Last — 记录所有子元素的新位置
  const flipRecords: FlipRecord[] = [];
  children.forEach(el => {
    const firstRect = firstRects.get(el)!;
    const lastRect = el.getBoundingClientRect();

    // 步骤3: Invert — 计算偏移量
    const deltaX = firstRect.left - lastRect.left;
    const deltaY = firstRect.top - lastRect.top;

    if (Math.abs(deltaX) > 0.5 || Math.abs(deltaY) > 0.5) {
      flipRecords.push({ element: el, firstRect, lastRect, deltaX, deltaY });

      // 立即设置 Invert transform:元素视觉回到旧位置
      el.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
      el.style.transition = 'none';  // 禁止过渡,瞬间跳回
    }
  });

  // 步骤4: Play — 动画 transform 到零
  // 关键:需要等待下一帧浏览器渲染 Invert 状态后再启动动画
  requestAnimationFrame(() => {
    requestAnimationFrame(() => {
      flipRecords.forEach(record => {
        record.element.style.transition = `transform ${duration}ms ${easing}`;
        record.element.style.transform = 'translate(0, 0)';
      });

      // 动画结束后清理
      setTimeout(() => {
        flipRecords.forEach(record => {
          record.element.style.transition = '';
          record.element.style.transform = '';
        });
      }, duration + 50);
    });
  });
}

React Hook 封装:

// hooks/useFlipAnimation.ts
import { useRef, useCallback } from 'react';
import { animateFlip } from '../utils/flip-animation';

export function useFlipAnimation(duration: number = 300) {
  const containerRef = useRef<HTMLDivElement>(null);

  const triggerFlip = useCallback(() => {
    if (containerRef.current) {
      animateFlip(containerRef.current, duration);
    }
  }, [duration]);

  return { containerRef, triggerFlip };
}

// 使用示例:拖拽排序列表
function SortableList({ items }: { items: Item[] }) {
  const { containerRef, triggerFlip } = useFlipAnimation(250);

  const handleSort = (newOrder: Item[]) => {
    // 更新数据触发DOM重排
    setItems(newOrder);
    // 在DOM更新后触发FLIP动画
    requestAnimationFrame(() => triggerFlip());
  };

  return (
    <div ref={containerRef}>
      {items.map(item => (
        <div key={item.id} className="list-item">
          {item.content}
        </div>
      ))}
    </div>
  );
}

四、边界分析与架构权衡

getBoundingClientRect 的性能。 批量调用 getBoundingClientRect(First 和 Last 两次)会强制浏览器进行布局计算(强制同步布局),在列表元素数量超过 500 时可能产生性能问题。优化:只对位置发生变化的元素计算偏移量,跳过位置不变的元素。

新增和删除元素的处理。 新增元素没有 First 位置(无法计算 Invert),删除元素没有 Last 位置(无法计算 Play)。新增元素用 fade-in 动画替代 FLIP,删除元素用 fade-out + shrink 动画替代。两种动画与 FLIP 同时执行,总时长一致确保视觉协调。

双层 requestAnimationFrame 的必要性。 Invert transform 设置后必须等浏览器渲染(第一帧),再在第二帧启动 Play 动画。如果只用一层 rAF,浏览器可能在同一帧内完成 Invert 和 Play,动画不生效。双层 rAF 确保两个状态分别在不同帧渲染。

五、总结

FLIP 动画让列表重排从瞬间跳变变为平滑滑翔——First 记录旧位置,Last 记录新位置,Invert 用 transform 回到旧位置,Play 用动画滑到新位置。四步操作把布局变化的视觉冲击降到最低,让用户看到每个元素"去了哪里"。

FLIP 的核心思想是"先布局后动画"——布局瞬间完成(DOM 立即更新),动画视觉过渡(transform 插值)。布局的准确性由 DOM 保证,动画的流畅性由 transform 保证,两者分工不交叉。先做对的事(正确的布局),再用动画让它看起来优雅(平滑的过渡),这才是 FLIP 的工程哲学。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/leopold_man/article/details/162716850

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--