如何用Framer Motion实现书籍排序/洗牌时的X轴滑动过渡动画
书籍排序/洗牌时X轴平滑滑动过渡异常修复方案
需求背景
实现书籍排序或洗牌操作时,每本书沿X轴平滑滑动的过渡效果,当前使用Framer Motion后出现动画异常。
技术栈
- Next.js
- Tailwind CSS
- Framer Motion
核心问题排查
- Motion模块未导入:Book组件中使用
motion.li但未引入framer-motion,导致Motion标签无效。 - 索引作为key:排序/洗牌时,React因key复用错误识别元素,Motion无法跟踪位置变化,引发异常动画。
- 未启用布局动画:未添加
layout属性,Motion无法自动处理元素位置变更的过渡效果。 - 代码功能缺陷:存在
selectedBook状态未定义、shuffleArray函数缺失、书籍数据未设置到状态、Image加载事件未绑定等问题,间接影响动画表现。
修复后的完整代码
Page.js(父组件)
"use client"; import Book from "./Book"; import { useEffect, useState } from "react"; export default function Home() { const [books, setBooks] = useState([]); const [sortOrder, setSortOrder] = useState({ title: "asc", dateFinished: "asc", year: "asc" }); const [selectedBook, setSelectedBook] = useState(null); useEffect(() => { async function fetchBooks() { try { const response = await fetch("/books.json"); if (!response.ok) throw new Error(`Failed to fetch: ${response.statusText}`); const data = await response.json(); setBooks(data); } catch (error) { console.error("Error fetching books:", error); } } fetchBooks(); }, []); const shuffleArray = (array) => { const shuffled = [...array]; for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } return shuffled; }; const sortBooks = (criteria) => { const order = sortOrder[criteria] === "asc" ? "desc" : "asc"; const sortedBooks = [...books].sort((a, b) => { switch (criteria) { case "title": return order === "asc" ? a.title.localeCompare(b.title) : b.title.localeCompare(a.title); case "year": return order === "asc" ? a.year - b.year : b.year - a.year; default: return 0; } }); setBooks(sortedBooks); setSortOrder({ ...sortOrder, [criteria]: order }); }; const handleSelectBook = (book) => { setSelectedBook(selectedBook === book ? null : book); }; const handleShuffleBooks = () => { setBooks(shuffleArray([...books])); }; return ( <div className="overflow-y-hidden max-h-[100vh] max-w-[100vw] overflow-hidden"> <h1 className="text-center text-2xl font-bold my-4">My Bookshelf</h1> <div className="flex gap-2 px-8 mb-4"> <button onClick={() => sortBooks("title")} className="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200 transition-colors" > Sort Alphabetically ({sortOrder.title === "asc" ? "A-Z" : "Z-A"}) </button> <button onClick={() => sortBooks("year")} className="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200 transition-colors" > Sort by Year ({sortOrder.year === "asc" ? "Oldest First" : "Newest First"}) </button> <button onClick={handleShuffleBooks} className="px-3 py-1 bg-gray-100 rounded hover:bg-gray-200 transition-colors" > 🔀 </button> </div> <ul className="max-h-[calc(100vh-120px)] flex border-b-[40px] border-orange-900 mt-4 items-baseline mx-8 px-8 overflow-x-scroll w-full"> {books.map((book) => ( <Book key={book.id} data={book} /> ))} </ul> </div> ); }
Book.js(子组件)
"use client"; import { useState } from "react"; import Image from "next/image"; import { motion } from "framer-motion"; export default function Book({ data }) { const [imageSize, setImageSize] = useState({ width: 0, height: 0 }); const handleImageLoad = ({ target }) => { setImageSize({ width: target.naturalWidth / 4, height: target.naturalHeight / 4 }); }; return ( <motion.li className="relative flex gap-2 items-end mr-2" layout transition={{ type: "spring", stiffness: 250, damping: 30 }} > <Image src={`/images/${data.route}`} width={imageSize.width} height={imageSize.height} unoptimized={true} onLoad={handleImageLoad} alt={`Cover of ${data.title}`} className="shadow-md hover:shadow-lg transition-shadow" /> </motion.li> ); }
关键修复说明
- 替换元素key:用书籍的唯一ID替代索引,确保React能准确识别每个书籍元素,让Motion可以跟踪元素的位置变化。
- 启用Motion布局动画:给
motion.li添加layout属性,Motion会自动计算元素位置变更的过渡动画,实现平滑滑动效果。 - 配置过渡参数:通过
transition属性设置弹簧动画的刚度和阻尼值,调整动画的快慢和回弹效果,让滑动更自然。 - 修复功能缺陷:补充缺失的状态、函数和事件绑定,确保组件能正常渲染和交互,为动画提供稳定的基础。
内容的提问来源于stack exchange,提问作者Tyler Morales
相关产品推荐
相关产品推荐

