如何在Next.js中实现缩略图转全屏图片的页面过渡效果
Next.js 实现页面跳转时图片过渡效果
核心思路
- 首页点击缩略图时,记录当前图片的位置、尺寸、src,通过路由传递给详情页
- 利用Next.js路由API监听切换状态,控制过渡层的显示/隐藏
- 在过渡层中完成从缩略图到详情页头部的平滑动画,动画结束后销毁过渡层
具体实现步骤
1. 首页(列表页)组件
import { useRouter } from 'next/router'; import { useRef, useState } from 'react'; // 模拟项目数据 const projects = [ { id: 1, title: '项目A', imgSrc: '/images/project-a.jpg' }, { id: 2, title: '项目B', imgSrc: '/images/project-b.jpg' } ]; export default function Home() { const router = useRouter(); const [transitionImg, setTransitionImg] = useState(null); const imgRefs = useRef({}); const handleProjectClick = (e, project) => { // 获取当前图片的DOM位置信息 const imgEl = imgRefs.current[project.id]; const rect = imgEl.getBoundingClientRect(); // 存储过渡所需的图片数据 setTransitionImg({ src: project.imgSrc, width: rect.width, height: rect.height, top: rect.top + window.scrollY, left: rect.left, id: project.id }); // 延迟跳转,确保过渡层已渲染 setTimeout(() => { router.push({ pathname: `/projects/${project.id}`, query: { imgSrc: project.imgSrc } }); }, 50); }; return ( <div className="project-list"> {projects.map(project => ( <div key={project.id} className="project-item" onClick={(e) => handleProjectClick(e, project)} > <img ref={el => imgRefs.current[project.id] = el} src={project.imgSrc} alt={project.title} className="project-thumb" /> <h3>{project.title}</h3> </div> ))} {/* 过渡层 - 跳转时显示 */} {transitionImg && ( <div className="transition-overlay"> <img src={transitionImg.src} alt="过渡图片" className="transition-img" style={{ position: 'fixed', top: transitionImg.top, left: transitionImg.left, width: transitionImg.width, height: transitionImg.height, objectFit: 'cover', zIndex: 9999 }} /> </div> )} </div> ); }
2. 详情页组件
import { useRouter } from 'next/router'; import { useEffect, useRef, useState } from 'react'; export default function ProjectDetail() { const router = useRouter(); const { imgSrc } = router.query; const headerImgRef = useRef(null); const [isTransitioning, setIsTransitioning] = useState(true); useEffect(() => { if (!isTransitioning) return; // 获取详情页头部图片的最终位置 const headerRect = headerImgRef.current.getBoundingClientRect(); const transitionImg = document.querySelector('.transition-img'); if (!transitionImg) { setIsTransitioning(false); return; } // 触发CSS过渡动画 transitionImg.style.transition = 'all 0.6s ease-out'; transitionImg.style.top = `${headerRect.top + window.scrollY}px`; transitionImg.style.left = `${headerRect.left}px`; transitionImg.style.width = `${headerRect.width}px`; transitionImg.style.height = `${headerRect.height}px`; // 动画结束后清理过渡层 const handleTransitionEnd = () => { transitionImg.remove(); setIsTransitioning(false); transitionImg.removeEventListener('transitionend', handleTransitionEnd); }; transitionImg.addEventListener('transitionend', handleTransitionEnd); // 监听浏览器后退,处理返回动画 const handlePopState = () => { setIsTransitioning(true); // 可在此添加返回首页时的反向动画逻辑 }; router.beforePopState(handlePopState); return () => { router.beforePopState(() => true); // 清除监听 }; }, [isTransitioning, router]); return ( <div className="project-detail"> {/* 详情页头部图片 */} <div className="detail-header"> <img ref={headerImgRef} src={imgSrc} alt="项目头部图" className="header-img" style={{ opacity: isTransitioning ? 0 : 1 }} /> </div> {/* 项目详情内容 */} <div className="detail-content"> <h1>项目详情</h1> <p>这里是项目的具体内容...</p> </div> </div> ); }
3. 全局CSS补充
.transition-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; pointer-events: none; z-index: 9999; } .project-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 2rem; padding: 2rem; } .project-thumb { width: 100%; height: 200px; object-fit: cover; cursor: pointer; transition: transform 0.2s ease; } .project-thumb:hover { transform: scale(1.05); } .header-img { width: 100%; height: 60vh; object-fit: cover; transition: opacity 0.3s ease; }
React Router 适配方案
核心逻辑和Next.js一致,仅替换路由API为React Router的useNavigate和useLocation
1. 首页组件
import { useNavigate } from 'react-router-dom'; import { useRef, useState } from 'react'; const projects = [ { id: 1, title: '项目A', imgSrc: '/images/project-a.jpg' }, { id: 2, title: '项目B', imgSrc: '/images/project-b.jpg' } ]; export default function Home() { const navigate = useNavigate(); const [transitionImg, setTransitionImg] = useState(null); const imgRefs = useRef({}); const handleProjectClick = (e, project) => { const imgEl = imgRefs.current[project.id]; const rect = imgEl.getBoundingClientRect(); setTransitionImg({ src: project.imgSrc, width: rect.width, height: rect.height, top: rect.top + window.scrollY, left: rect.left, id: project.id }); setTimeout(() => { navigate(`/projects/${project.id}`, { state: { imgSrc: project.imgSrc, rect } }); }, 50); }; // 其余结构和Next.js首页一致,过渡层写法相同 }
2. 详情页组件
import { useLocation, useNavigate } from 'react-router-dom'; import { useEffect, useRef, useState } from 'react'; export default function ProjectDetail() { const location = useLocation(); const { imgSrc } = location.state || {}; const headerImgRef = useRef(null); const [isTransitioning, setIsTransitioning] = useState(true); useEffect(() => { if (!isTransitioning || !imgSrc) return; const headerRect = headerImgRef.current.getBoundingClientRect(); const transitionImg = document.querySelector('.transition-img'); if (!transitionImg) { setIsTransitioning(false); return; } transitionImg.style.transition = 'all 0.6s ease-out'; transitionImg.style.top = `${headerRect.top + window.scrollY}px`; transitionImg.style.left = `${headerRect.left}px`; transitionImg.style.width = `${headerRect.width}px`; transitionImg.style.height = `${headerRect.height}px`; const handleTransitionEnd = () => { transitionImg.remove(); setIsTransitioning(false); transitionImg.removeEventListener('transitionend', handleTransitionEnd); }; transitionImg.addEventListener('transitionend', handleTransitionEnd); // 监听浏览器后退 const handlePopState = () => { setIsTransitioning(true); // 可添加返回时的反向动画 }; window.addEventListener('popstate', handlePopState); return () => { window.removeEventListener('popstate', handlePopState); }; }, [isTransitioning, imgSrc]); // 其余结构和Next.js详情页一致 }
关键注意事项
- 图片预加载:点击缩略图时提前预加载详情页大图,避免动画过程中图片加载不全
- 响应式适配:处理不同屏幕尺寸下的位置计算,防止动画错位
- 路由状态稳定性:Next.js用
query传递数据更稳定,刷新页面不会丢失;React Router用state时需添加刷新后的 fallback 逻辑 - 动画性能:优先用
transform替代top/left属性,减少页面重排提升流畅度
内容的提问来源于stack exchange,提问作者Vansh
相关产品推荐
相关产品推荐

