React中使用Framer Motion实现元素位置过渡的问题排查
问题
尝试用Framer Motion实现类似多邻国选词谜题的位移动画:点击下方单词按钮,元素从自身位置过渡到上方选中区域。但通过动态refs获取DOM坐标时得到异常值,导致动画起始位置严重偏离。
已尝试的实现逻辑:
- 获取元素X/Y坐标并存入全局状态
- 将坐标作为props传递给选中项组件,作为动画
initial属性的起始值 - 动画目标设为
{x:0, y:0}
试过的坐标获取方式:createRef+getBoundingClientRect、事件clientX/clientY、offsetTop/offsetLeft,均未解决问题。
原因分析
- 共享状态覆盖:用全局的
currX/currY存储坐标,选中多个单词时,新坐标会覆盖旧值,导致所有已选中项的动画起始位置都变成最后一次点击的位置。 - 坐标体系不匹配:
getBoundingClientRect()返回的是相对于视口的绝对坐标,而Framer Motion的x/y属性默认是相对于元素自身布局位置的偏移量,直接套用会出现位置偏差。 - DOM更新时序问题:点击后更新
selected状态触发重渲染,新选中项尚未挂载时传递坐标,可能存在计算偏差。
解决方案
核心优化点
- 为每个选中项存储独立的起始坐标,避免状态覆盖
- 将视口绝对坐标转换为目标容器的相对偏移量,匹配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
相关产品推荐
相关产品推荐

