Framer Motion布局过渡导致兄弟元素异常问题求助
食谱卡片应用中Framer Motion标题弹跳问题解决
问题描述
刚学习React和Framer Motion,开发食谱卡片应用时遇到以下问题:打开第2张及之后的卡片(下称SUB卡片)并关闭后,再打开任意靠前的卡片,之前打开过又关闭的SUB卡片的{recipe.title}标题会脱离卡片背景,跟着靠前卡片的缩放动作弹跳。
相关代码
RecipeTable.jsx 组件
export default function RecipeTable({ recipes }) { return ( <div className="container-fluid"> <div className="row"> <LayoutGroup> {recipes.map((recipe, index) => ( <RecipeCard recipe={recipe} key={index} /> ))} </LayoutGroup> </div> </div> ); }
RecipeCard.jsx 组件
export default function RecipeCard({ recipe, key }) { const [isOpen, setIsOpen] = useState(false); const controls = useAnimation(); useEffect(() => { if (isOpen) { controls.start({ scale: [1, 1.2, 1,], rotate: [0, 270, 0], transition: { duration: 0.7 }, }); } }, [isOpen, controls]); return ( <motion.div className="card-container"> <motion.div transition={{ layout: { type: "spring", duration: 0.7 }}} layout onClick={() => ! isOpen && setIsOpen(true)} className="card" style={isOpen ? {} : {cursor: "pointer"}} whileHover={ isOpen ? {} : { scale: 1.05 }} > { isOpen && ( <motion.button className="close-button" onClick={() => setIsOpen(false)} initial={{scale: 1, rotate: 0}} animate={controls} whileHover={{scale: 1.3, rotate: 0}} > × </motion.button> )} <motion.h2 layout="position"> {recipe.title} </motion.h2> {isOpen && ( <motion.div className="card-expand" initial={{opacity: 0}} animate={{opacity: 1}} transition={{duration: 0.5}} > <p>{recipe.ingredients}</p> <p>{recipe.instructions}</p> </motion.div> )} </motion.div> </motion.div> ) }
my.css 样式文件
* { margin: 0; padding: 0; box-sizing: border-box; } html { font-family: sans-serif; } body { background: #ADA996; /* fallback for old browsers */ background: -webkit-linear-gradient(to right, #EAEAEA, #DBDBDB, #F2F2F2, #ADA996); /* Chrome 10-25, Safari 5.1-6 */ background: linear-gradient(to right, #EAEAEA, #DBDBDB, #F2F2F2, #ADA996); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ } .card-container { display: flex; justify-content: center; margin-top: 2rem; margin-bottom: 2rem; } .card { background: #fff; padding: 3rem 5rem; border-radius: 1rem; } .card p { line-height: 150%; } .close-button { position: absolute; top: -1rem; right: -1rem; border: none; background-color: black; width: 2rem; height: 2rem; border-radius: 50%; color: #fff; font-size: 20px; } .card-expand { max-width: 60vw; } @media (max-width: 768px) { .card-expand { max-width: 100vw; } }
已尝试的解决方案
- 给外层
<motion.div>设置layout属性,修复了标题弹跳,但破坏了其他卡片为打开卡片腾空间的动画效果。 - 给标题添加空过渡
transition={{}}后略有改善,但标题仍会在卡片内晃动,且卡片打开动画不再流畅:
<motion.h2 layout="position" transition={{}}> {recipe.title} </motion.h2>
问题根源与修复方案
问题根源
标题弹跳是因为layout="position"让标题在LayoutGroup的布局计算中保留了之前打开状态的位置缓存,当其他卡片触发布局变化时,错误触发了标题的位置动画;同时卡片容器的layout和标题的layout="position"之间存在动画冲突,导致异常行为。
修复步骤
- 移除标题的
layout="position"
让标题作为卡片容器的子元素,继承容器的布局变化即可。若需要标题的展开动画,可基于isOpen状态设置显式动画,而非依赖layout:
<motion.h2 initial={{ y: 0 }} transition={{ duration: 0.3 }} > {recipe.title} </motion.h2>
- 给卡片添加相对定位
关闭按钮使用了position: absolute,父元素需要设置相对定位才能正确放置按钮,同时避免布局偏移:
.card { background: #fff; padding: 3rem 5rem; border-radius: 1rem; position: relative; /* 添加此行 */ }
- 替换列表key为唯一标识
避免用index作为key,改用recipe.id这类稳定唯一值,防止列表更新时的key冲突导致布局异常:
{recipes.map((recipe) => ( <RecipeCard recipe={recipe} key={recipe.id} // 改用recipe的唯一id /> ))}
- 简化关闭按钮动画逻辑
直接用animate属性绑定状态,无需单独使用useAnimation:
{isOpen && ( <motion.button className="close-button" onClick={() => setIsOpen(false)} initial={{ scale: 1, rotate: 0 }} animate={{ scale: [1, 1.2, 1], rotate: [0, 270, 0] }} transition={{ duration: 0.7 }} whileHover={{ scale: 1.3, rotate: 0 }} > × </motion.button> )}
完整修改后的RecipeCard.jsx
export default function RecipeCard({ recipe }) { const [isOpen, setIsOpen] = useState(false); return ( <motion.div className="card-container"> <motion.div transition={{ layout: { type: "spring", duration: 0.7 }}} layout onClick={() => !isOpen && setIsOpen(true)} className="card" style={isOpen ? {} : { cursor: "pointer" }} whileHover={!isOpen ? { scale: 1.05 } : {}} > {isOpen && ( <motion.button className="close-button" onClick={() => setIsOpen(false)} initial={{ scale: 1, rotate: 0 }} animate={{ scale: [1, 1.2, 1], rotate: [0, 270, 0] }} transition={{ duration: 0.7 }} whileHover={{ scale: 1.3, rotate: 0 }} > × </motion.button> )} <motion.h2 initial={{ opacity: 1 }} transition={{ duration: 0.3 }} > {recipe.title} </motion.h2> {isOpen && ( <motion.div className="card-expand" initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: "auto" }} exit={{ opacity: 0, height: 0 }} transition={{ duration: 0.5 }} > <p>{recipe.ingredients}</p> <p>{recipe.instructions}</p> </motion.div> )} </motion.div> </motion.div> ) }
内容的提问来源于stack exchange,提问作者user19610670
相关产品推荐
相关产品推荐

