You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Framer Motion实现React组件间带滑动手势的转场效果?

React + Framer Motion 实现带滑动转场的页面切换

核心方案选择

用 Drag 而非 Swipe:Swipe 是触发一次性动作的快捷方式,而 Drag 支持交互式拖拽(按住时实时切换页面状态,松开后确认结果),完全匹配你的需求。

完整实现代码

以下是调整后的代码,同时支持按钮切换和滑动切换,且仅在首次拖拽时加载目标页面:

// Test.jsx
import { motion, AnimatePresence } from 'framer-motion';
import { useState, useRef } from 'react';
import { Box } from './Box';

export function Test() {
  const [currentPage, setCurrentPage] = useState(1);
  const [isDragging, setIsDragging] = useState(false);
  const currentPageRef = useRef(null);

  // 拖拽结束判断是否切换页面
  const handleDragEnd = (_, info) => {
    const threshold = window.innerWidth * 0.3; // 切换阈值设为屏幕宽度30%
    if (info.offset.x < -threshold) {
      setCurrentPage(prev => prev + 1);
    } else if (info.offset.x > threshold) {
      setCurrentPage(prev => prev - 1);
    }
    setIsDragging(false);
  };

  // 开始拖拽时标记状态,加载目标页面
  const handleDragStart = () => {
    setIsDragging(true);
  };

  // 按钮切换逻辑
  const handleButtonToggle = () => {
    setCurrentPage(prev => prev + 1);
  };

  // 生成页面动画配置
  const getPageMotionProps = (pageNum) => {
    const isNextPage = pageNum > currentPage;
    return {
      style: { position: 'absolute', width: '100%', height: '116px' },
      initial: isNextPage ? { x: '100%', opacity: 0 } : { x: '-100%', opacity: 0 },
      animate: { x: 0, opacity: 1 },
      exit: isNextPage ? { x: '-100%', opacity: 0 } : { x: '100%', opacity: 0 },
      transition: { duration: 0.5 }
    };
  };

  return (
    <>
      {/* 页面容器:相对定位,隐藏溢出内容 */}
      <div style={{ position: 'relative', width: '100%', height: '116px', overflow: 'hidden' }}>
        <AnimatePresence initial={false} mode="popLayout">
          {/* 当前页面:支持横向拖拽 */}
          <motion.div
            key={currentPage}
            {...getPageMotionProps(currentPage)}
            drag="x"
            dragConstraints={{ left: 0, right: 0 }}
            dragElastic={0.1}
            onDragStart={handleDragStart}
            onDragEnd={handleDragEnd}
            ref={currentPageRef}
          >
            <Box n={currentPage} />
          </motion.div>

          {/* 拖拽时才渲染目标页面,实现懒加载 */}
          {isDragging && (
            <motion.div
              key={currentPage + 1}
              {...getPageMotionProps(currentPage + 1)}
              style={{
                ...getPageMotionProps(currentPage + 1).style,
                x: currentPageRef.current ? currentPageRef.current.getBoundingClientRect().width + currentPageRef.current._x : 0
              }}
            >
              <Box n={currentPage + 1} />
            </motion.div>
          )}
        </AnimatePresence>
      </div>

      <br /><br />
      <button type="button" onClick={handleButtonToggle}>
        切换页面
      </button>
    </>
  );
}

关键细节说明

  • 拖拽交互:drag="x" 开启横向拖拽,dragConstraints 限制拖拽边界,dragElastic 调整拖拽时的弹性效果,让交互更自然。
  • 懒加载目标页面:通过 isDragging 状态控制目标页面的渲染,只有用户开始拖拽时才加载,避免预加载隐藏页面。
  • 切换阈值:拖拽距离超过屏幕30%时触发页面切换,可根据需求调整阈值比例。
  • 动画同步:当前页面和目标页面的入场/退场动画方向相反,保证转场过程流畅连贯。

扩展到路由场景

如果要将页面切换和路由绑定(比如使用 react-router),只需将页面状态替换为路由参数:

import { useParams, useNavigate } from 'react-router-dom';

export function Test() {
  const { pageId } = useParams();
  const navigate = useNavigate();
  const currentPage = parseInt(pageId) || 1;
  const [isDragging, setIsDragging] = useState(false);
  const currentPageRef = useRef(null);

  const handleDragEnd = (_, info) => {
    const threshold = window.innerWidth * 0.3;
    if (info.offset.x < -threshold) {
      navigate(`/page/${currentPage + 1}`);
    } else if (info.offset.x > threshold) {
      navigate(`/page/${currentPage - 1}`);
    }
    setIsDragging(false);
  };

  // 其他逻辑保持不变...
}

内容的提问来源于stack exchange,提问作者ephramd

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 02:53:11