什么是对列表重新排序进行动画处理的 react.js 友好方式?

IT技术 javascript jquery css reactjs
2021-04-30 00:05:46

我有一个带有分数的项目列表,按分数排序,由 react.js 呈现为垂直方向的矩形项目列表(最高分数在顶部)。对单个项目的悬停和其他点击可能会显示/隐藏额外信息,从而改变它们的垂直高度。

新信息的到来会稍微改变分数,使一些项目在重新排序后排名更高,而其他项目排名更低。我希望这些物品同时动画到它们的新位置,而不是立即出现在它们的新位置。

在 React.js 中是否有推荐的方法来做到这一点,也许是一个流行的附加组件?

(在过去使用 D3 的类似情况中,我使用的技术大致是:

  1. 以自然顺序显示包含 item DOM 节点的列表,并具有相对定位。通过相对定位,其他小的变化——CSS 或 JS 触发的——到个别项目的程度会按预期移动其他项目。
  2. 一步修改所有 DOM 节点,将它们的实际相对坐标作为新的绝对坐标——DOM 更改不会导致视觉变化。
  3. 将项目 DOM 节点在其父项中重新排序为新的排序顺序——另一个 DOM 更改不会导致视觉变化。
  4. 根据新排序中所有前面项目的高度,将所有节点的顶部偏移设置为它们的新计算值。这是唯一视觉上活跃的步骤。
  5. 将所有项目 DOM 节点突变回非偏移相对定位。同样,这不会导致视觉上的变化,但是现在相对定位的 DOM 节点,按照底层列表的自然顺序,将通过适当的移动来处理内部悬停/展开/等样式的变化。

现在我希望以 React-ish 的方式产生类似的效果......)

4个回答

我刚刚发布了一个module来解决这个问题

https://github.com/joshwcomeau/react-flip-move

它与 Magic Move / Shuffle 有一些不同:

  1. 它使用硬件加速 60FPS+ 动画的 FLIP 技术
  2. 它提供了通过逐步抵消延迟或持续时间来“人性化”洗牌的选项
  3. 它优雅地处理中断,没有奇怪的故障影响
  4. 一堆其他整洁的东西,如开始/结束回调

查看演示:

http://joshwcomeau.github.io/react-flip-move/examples/#/shuffle

React Shuffle 是可靠且最新的。它的灵感来自 Ryan Florences Magic Move 演示

https://github.com/FormidableLabs/react-shuffle

我意识到这是一个老问题,您现在可能已经找到了解决方案,但是对于遇到此问题的其他人,请查看 Ryan Florence 的 MagicMove 库。这是一个 React 库来处理您描述的确切场景:https : //github.com/ryanflorence/react-magic-move

看看它的实际效果:https : //www.youtube.com/watch?v=z5e7kWSHWTg#t=424

编辑:这在 React 0.14 中被破坏了。请参阅下面的 Tim Arney 建议的React Shuffle作为替代方案。

我不想为我的动画使用第三方库,所以我使用 React 生命周期方法 getSnapshotBeforeUpdate() 和 componentDidUpdate()实现了“ FLIP ”动画技术。

当列表项的 props.index 更改时,旧位置在 getSnapshotBeforeUpdate() 和 componentDidUpdate() 中捕获,css 变换:translateY() 被应用,以便项目看起来从旧位置到当前位置动画。

class TreeListItem extends Component {

  constructor(props) {
    super(props);
    this.liRef = React.createRef();
  }

  getSnapshotBeforeUpdate(prevProps) {
    if (prevProps.index !== this.props.index) {
      // list index is changing, prepare to animate
      if (this.liRef && this.liRef.current) {
        return this.liRef.current.getBoundingClientRect().top;
      }
    }
    return null;
  }


  componentDidUpdate(prevProps, prevState, snapshot) {
    if (prevProps.index !== this.props.index && snapshot) {
      if (this.liRef && this.liRef.current) {
        let el = this.liRef.current;
        let newOffset = el.getBoundingClientRect().top;
        let invert = parseInt(snapshot - newOffset);
        el.classList.remove('animate-on-transforms');
        el.style.transform = `translateY(${invert}px)`;
        // Wait for the next frame so we
        // know all the style changes have
        // taken hold.
        requestAnimationFrame(function () {
          // Switch on animations.
          el.classList.add('animate-on-transforms');
          // GO GO GOOOOOO!
          el.style.transform = '';
        });
      }
    }
  }

  render() {
    return <li ref={this.liRef}>
    </li >;
  }
}

class TreeList extends Component {

  render() {
    let treeItems = [];
    if (this.props.dataSet instanceof Array) {
      this.props.dataSet.forEach((data, i) => {
        treeItems.push(<TreeListItem index={i} key={data.value} />)
      });
    }

    return <ul>
      {treeItems}
      </ul>;
  }
}
.animate-on-transforms {
  /* animate list item reorder */
  transition: transform 300ms ease;
}