ReactJS无滚动div的滚动方向检测与页面切换动画实现
实现鼠标滚轮触发的React + Framer Motion页面切换动画
核心思路
因为页面容器不可滚动,无法通过scroll事件检测滚动行为,所以改用监听**鼠标滚轮事件(wheel)**判断滚动方向,结合状态管理控制页面切换逻辑,再通过Framer Motion实现指定动画效果:
- 检测滚轮方向(向上/向下)
- 根据方向更新当前页面与上一页状态
- 触发动画:当前页上移并透明化,新页同步显示,原页透明后返回原位
- 用动画锁防止滚动触发多次叠加动画
完整代码实现
import { useState, useEffect } from 'react'; import { motion } from 'framer-motion'; // 页面数据示例 const pages = [ { id: 1, content: '首页', bgColor: '#f8f9fa' }, { id: 2, content: '列表页', bgColor: '#e9ecef' }, { id: 3, content: '详情页', bgColor: '#dee2e6' } ]; export default function ScrollPageTransition() { // 状态管理 const [currentPage, setCurrentPage] = useState(0); const [prevPage, setPrevPage] = useState(0); const [isAnimating, setIsAnimating] = useState(false); const [isScrollDirectionUp, setIsScrollDirectionUp] = useState(false); // 监听鼠标滚轮事件 useEffect(() => { const handleWheel = (e) => { // 动画进行中忽略滚轮事件,避免叠加 if (isAnimating) return; // 判断滚动方向:deltaY < 0 为向上滚动,反之向下 const directionUp = e.deltaY < 0; setIsScrollDirectionUp(directionUp); // 页面边界判断 + 切换逻辑 if (directionUp && currentPage > 0) { setIsAnimating(true); setPrevPage(currentPage); setCurrentPage(prev => prev - 1); } else if (!directionUp && currentPage < pages.length - 1) { setIsAnimating(true); setPrevPage(currentPage); setCurrentPage(prev => prev + 1); } }; window.addEventListener('wheel', handleWheel); return () => window.removeEventListener('wheel', handleWheel); }, [currentPage, isAnimating]); // 动画变体定义 const pageExitVariant = { y: '-100%', opacity: 0, transition: { duration: 0.5 } }; const pageReturnVariant = { y: 0, opacity: 1, transition: { duration: 0.5, delay: 0.5 } }; const pageEnterVariant = { opacity: 1, transition: { duration: 0.5 } }; return ( <div style={{ position: 'relative', width: '100vw', height: '100vh', overflow: 'hidden', margin: 0, padding: 0 }}> {/* 上一页(原页面):执行退出动画后返回原位 */} <motion.div key={pages[prevPage].id} style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '3rem', backgroundColor: pages[prevPage].bgColor }} initial={{ y: 0, opacity: 1 }} animate={isAnimating ? pageExitVariant : pageReturnVariant} onAnimationComplete={() => { // 动画完成后解锁,允许下次滚动触发 if (isAnimating) setIsAnimating(false); }} > {pages[prevPage].content} </motion.div> {/* 当前页(新页面):同步显示 */} <motion.div key={pages[currentPage].id} style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '3rem', backgroundColor: pages[currentPage].bgColor }} initial={{ opacity: 0 }} animate={pageEnterVariant} > {pages[currentPage].content} </motion.div> </div> ); }
关键细节说明
- 滚轮事件监听:使用
wheel事件替代scroll事件,解决容器不可滚动时无法检测滚动的问题,通过deltaY值判断滚动方向 - 动画锁:
isAnimating状态确保同一时间只有一组动画执行,避免快速滚动导致的动画混乱 - 动画时序:原页面退出动画(0.5s)完成后,延迟0.5s执行返回原位动画,新页面同步从透明变为可见,符合需求中的动画流程
- 边界处理:限制页面切换范围,避免超出页面数组的索引边界
内容的提问来源于stack exchange,提问作者keshavnischal
相关产品推荐
相关产品推荐

