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

React中使用Framer Motion实现元素位置过渡的问题排查

问题

尝试用Framer Motion实现类似多邻国选词谜题的位移动画:点击下方单词按钮,元素从自身位置过渡到上方选中区域。但通过动态refs获取DOM坐标时得到异常值,导致动画起始位置严重偏离。

已尝试的实现逻辑:

  • 获取元素X/Y坐标并存入全局状态
  • 将坐标作为props传递给选中项组件,作为动画initial属性的起始值
  • 动画目标设为{x:0, y:0}

试过的坐标获取方式:createRef+getBoundingClientRect、事件clientX/clientY、offsetTop/offsetLeft,均未解决问题。


原因分析

  1. 共享状态覆盖:用全局的currX/currY存储坐标,选中多个单词时,新坐标会覆盖旧值,导致所有已选中项的动画起始位置都变成最后一次点击的位置。
  2. 坐标体系不匹配:getBoundingClientRect()返回的是相对于视口的绝对坐标,而Framer Motion的x/y属性默认是相对于元素自身布局位置的偏移量,直接套用会出现位置偏差。
  3. DOM更新时序问题:点击后更新selected状态触发重渲染,新选中项尚未挂载时传递坐标,可能存在计算偏差。

解决方案

核心优化点

  1. 为每个选中项存储独立的起始坐标,避免状态覆盖
  2. 将视口绝对坐标转换为目标容器的相对偏移量,匹配Framer Motion的坐标体系

修改后的完整代码

WordSelectionList.tsx

import { useRef, useState } from "react";
import WordSelectedItem from "./WordSelectedItem";

const words = [
  "brown",
  "The",
  "fox",
  "dog",
  "over",
  "the",
  "jumps",
  "lazy",
  "quick",
];

const WordSelectionList = () => {
  // 存储每个选中项的单词+对应起始坐标
  const [selected, setSelected] = useState<Array<{word: string, x: number, y: number}>>([]);
  const wordRefs = useRef<(HTMLButtonElement | null)[]>([]);
  const targetContainerRef = useRef<HTMLDivElement>(null);

  const handleWordSelect = (index: number, word: string) => {
    if (selected.some(item => item.word === word)) return;
    
    const wordEl = wordRefs.current[index];
    const targetContainer = targetContainerRef.current;
    if (!wordEl || !targetContainer) return;

    // 获取元素和目标容器的视口坐标
    const wordRect = wordEl.getBoundingClientRect();
    const targetRect = targetContainer.getBoundingClientRect();

    // 计算相对于目标容器的偏移量(兼容滚动)
    const relativeX = wordRect.left - targetRect.left + window.scrollX;
    const relativeY = wordRect.top - targetRect.top + window.scrollY;

    setSelected(prev => [...prev, { word, x: relativeX, y: relativeY }]);
  };

  const handleWordUnSelect = (word: string) => {
    setSelected(prev => prev.filter(item => item.word !== word));
  };

  return (
    <div className="flex justify-center h-full">
      <div className="w-2/3 border p-6">
        <div className="flex flex-col space-y-4">
          <div className="flex border p-2">
            {/* 绑定目标容器的ref用于坐标计算 */}
            <div 
              ref={targetContainerRef}
              className="flex space-x-2 h-11"
            >
              {selected.map((item, i) => (
                <WordSelectedItem
                  key={i}
                  x={item.x}
                  y={item.y}
                  word={item.word}
                  onSelect={handleWordUnSelect}
                />
              ))}
            </div>
          </div>
          <div className="flex gap-2">
            {words.map((word, i) => (
              <button
                ref={el => wordRefs.current[i] = el}
                className="flex p-2 rounded-lg border"
                key={`word-${i}`}
                onClick={() => handleWordSelect(i, word)}
              >
                {word}
              </button>
            ))}
          </div>
        </div>
      </div>
    </div>
  );
};

export default WordSelectionList;

WordSelectedItem.tsx

import { motion } from "framer-motion";

const WordSelectedItem = ({
  x,
  y,
  word,
  onSelect,
}: {
  x: number;
  y: number;
  word: string;
  onSelect: (word: string) => void;
}) => {
  return (
    <motion.button
      className="flex border rounded-lg p-2"
      initial={{ x, y, opacity: 0 }}
      animate={{ x: 0, y: 0, opacity: 1 }}
      transition={{ duration: 0.5 }}
      onClick={() => onSelect(word)}
    >
      <span>{word}</span>
    </motion.button>
  );
};

export default WordSelectedItem;

额外优化建议

  • 引入AnimatePresence组件,为选中项添加移除时的退场动画
  • 点击后隐藏原单词按钮,避免重复点击(可通过过滤已选中单词实现)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 19:02:12