css 转换:翻译转换行为异常

css transform: translate transition behaving strangely

this sandbox,我重新制作了经典的滑动益智游戏。

在我的 GameBlock 组件上,我使用了 css transform: translate(x,y)transition: transform 的组合来为滑动的游戏组件设置动画:

const StyledGameBlock = styled.div<{
  index: number;
  isNextToSpace: boolean;
  backgroundColor: string;
}>`
  position: absolute;
  display: flex;
  justify-content: center;
  align-items: center;
  width: ${BLOCK_SIZE}px;
  height: ${BLOCK_SIZE}px;
  background-color: ${({ backgroundColor }) => backgroundColor};
  ${({ isNextToSpace }) => isNextToSpace && "cursor: pointer"};

  ${({ index }) => css`
    transform: translate(
      ${getX(index) * BLOCK_SIZE}px,
      ${getY(index) * BLOCK_SIZE}px
    );
  `}

  transition: transform 400ms;
`;

基本上,我在板上使用块的当前索引来计算它的 xy 值,这些值会在块被打开时更改块的 transform: translate 值感动了。

虽然这确实能够在将方块滑动到顶部、向右和向左时触发平滑过渡 - 但出于某种原因,从上到下滑动方块不会平滑过渡。

知道是什么导致了这个异常吗?

React、列表和键

您看到的是 <GameBlock /> 个组件的 mount/unmount 的结果。
尽管您将 key 道具传递给组件,但 React 不确定您是否仍在渲染相同的元素。

如果我必须猜测为什么 React 不确定,我会把罪魁祸首放在:

  • 更改数组排序方式:
     const previousSpace = gameBlocks[spaceIndex];
     gameBlocks[spaceIndex] = gameBlocks[index];
     gameBlocks[index] = previousSpace;
  • 使用条件 isSpace 具有不同的虚拟 DOM 结果:
 ({ correctIndex, currentIndex, isSpace, isNextToSpace }) => isSpace ? null : ( <GameBlock             ....    

通常在应用程序中,我们不介意 re-mount 因为它非常快。当我们附加动画时,我们不想要任何 re-mount,因为它们会与 css-transitions.
混淆 为了让 React 确定它是同一个节点,不需要 re-mount。我们应该注意这一点;渲染之间;虚拟 dom 基本保持不变。 我们可以实现在列表的渲染中不做任何花哨的事情,并在渲染之间传递相同的键。

向下传递 isSpace

而不是改变呈现的 DOM 节点,我们希望列表呈现总是 return 等量的节点,具有完全 相同的键 对于每个节点,相同的顺序

只需将 'isSpace' 向下传递并设置为 display:none; 的样式即可。

 <GameBlock
      ...
      isSpace={isSpace}
      ...
  >

const StyledGameBlock = styled.div<{ ....}>`
   ...
  display: ${({isSpace})=> isSpace? 'none':'flex'};
   ...  
`;

确保不更改数组排序

React 认为要修改 gameBlocks 数组,键的顺序不同。因此触发渲染的 <GameBlock/> 组件的 unmount/mount。 我们可以确保 React 认为这个数组是未修改的,只需更改列表中项目的属性而不是排序本身。

在您的情况下,我们可以保留所有属性,只更改彼此 moved/swapped 的块的 currentIndex

 const onMove = useCallback(
    (index) => {
      const newSpaceIndex = gameBlocks[index].currentIndex; // the space will get the current index of the clicked block.
      const movedBlockNewIndex = gameBlocks[spaceIndex].currentIndex; // the clicked block will get the index of the space.

      setState({
        spaceIndex: spaceIndex, // the space will always have the same index in the array.
        gameBlocks: gameBlocks.map((block) => {
          const isMovingBlock = index === block.correctIndex; // check if this block is the one that was clicked
          const isSpaceBlock =
            gameBlocks[spaceIndex].currentIndex === block.currentIndex;  // check if this block is the space block. 
          let newCurrentIndex = block.currentIndex; // most blocks will stay in their spot. 
          if (isMovingBlock) {
            newCurrentIndex = movedBlockNewIndex; // the moving block will swap with the space. 
          }
          if (isSpaceBlock) {
            newCurrentIndex = newSpaceIndex; // the space will swap with the moving block
          }
          return {
            ...block,
            currentIndex: newCurrentIndex,
            isNextToSpace: getIsNextToSpace(newCurrentIndex, newSpaceIndex)
          };
        })
      });
    },
    [gameBlocks, spaceIndex]
  );


...
// we have to be sure to call onMove the with the index of the clicked block.
() => onMove(correctIndex) 

我们唯一改变的是点击块的 currentIndex 和 space。

沙盒:

sandbox example 基于您提供的沙箱。

结束语:我认为您的代码易于阅读和理解,做得很好!

除了@Lars 提供的出色回答和解释之外,我还想分享视觉证据,证明某些 <GameBlock /> 组件确实按顺序卸载或更改,导致 CSS 动画出现问题。

如您所见,当聚焦其中一个块并向下滑动时,元素会更改其在 DOM 中的位置。