React中useMotionValueEvent实现滚动容器渐变遮罩效果失效问题求助
React中useMotionValueEvent实现滚动容器渐变遮罩效果失效问题求助
我最近刚接触React Motion(之前叫Framer Motion),正在学习基础用法,现在尝试实现文档里的「跟踪元素滚动偏移」效果,已经让scrollXprogress正常工作了,但遮罩渐变动画的部分一直没搞对——本来应该根据滚动到容器的开头、中间、结尾位置,分别显示不同的左右渐变遮罩,但现在遮罩只会停留在初始设置的样式上。
我看文档里的示例大多用TypeScript,而我用的是React JS,自己写了一段代码,但用maskImage.get()只能拿到初始值,滚动时不会更新。以下是我目前的代码:
import React, { useRef } from "react"; import { animate, motion, useMotionValue, useMotionValueEvent, useScroll, } from "motion/react"; const ScrollXBox = () => { const left = `0%`; const right = `100%`; const leftInset = `20%`; const rightInset = `80%`; const transparent = `#0000`; const opaque = `#000`; function useScrollOverflowMask(scrollXProgress) { const maskImage = useMotionValue( `linear-gradient(90deg, ${opaque}, ${opaque} ${left}, ${opaque} ${rightInset}, ${transparent})` ); useMotionValueEvent(scrollXProgress, "change", (value) => { if (value === 0) { animate( maskImage, `linear-gradient(90deg, ${opaque}, ${opaque} ${left}, ${opaque} ${rightInset}, ${transparent})` ); } else if (value === 1) { animate( maskImage, `linear-gradient(90deg, ${transparent}, ${opaque} ${leftInset}, ${opaque} ${right}, ${opaque})` ); } else if ( scrollXProgress.getPrevious() === 0 || scrollXProgress.getPrevious() === 1 ) { animate( maskImage, `linear-gradient(90deg, ${transparent}, ${opaque} ${leftInset}, ${opaque} ${rightInset}, ${transparent})` ); } }); return maskImage; } const elements = ["A", "B", "C", "D"]; const colors = [ "bg-red-800", "bg-yellow-600", "bg-green-900", "bg-purple-900", ]; const ref = useRef(null); const { scrollXProgress } = useScroll({ container: ref }); const maskImage = useScrollOverflowMask(scrollXProgress); return ( <div className="scroll-x h-screen flex flex-col justify-center items-center gap-4 bg-violet-400"> <div id="example"> <svg id="progress" width="80" height="80" viewBox="0 0 100 100"> <circle cx="50" cy="50" r="40" pathLength="1" style={{ fill: "none", stroke: "red", strokeWidth: 20 }} /> <motion.circle cx="50" cy="50" r="40" style={{ fill: "none", stroke: "yellow", strokeWidth: 10, pathLength: scrollXProgress, }} /> </svg> <div ref={ref} className="element-container overflow-x-scroll w-96 flex gap-4" style={{ WebkitMaskImage: maskImage.get(), maskImage: maskImage.get(), }} > {elements.map((element, index) => ( <motion.ul key={index} className="flex-shrink-0"> <li> <div className={`${colors[index]} w-64 h-64`}></div> </li> </motion.ul> ))} </div> </div> </div> ); }; export default ScrollXBox;
问题分析与修复方案
我帮你排查了下,主要问题出在样式绑定方式和滚动进度的判断逻辑上:
- 你用
maskImage.get()绑定样式,只会在组件初始渲染时获取一次值,后续maskImage更新时不会触发样式重新渲染; - 精确判断
value === 0或value === 1很难触发,因为滚动进度通常是接近0/1的小数,不会完全等于这两个值。
下面是修改后的代码,已经修复了这两个问题:
import React, { useRef } from "react"; import { animate, motion, useMotionValue, useMotionValueEvent, useScroll, } from "motion/react"; const ScrollXBox = () => { // 定义遮罩相关常量 const left = `0%`; const right = `100%`; const leftInset = `20%`; const rightInset = `80%`; const transparent = `#0000`; const opaque = `#000`; // 优化后的滚动遮罩钩子 function useScrollOverflowMask(scrollXProgress) { const maskImage = useMotionValue( `linear-gradient(90deg, ${opaque}, ${opaque} ${left}, ${opaque} ${rightInset}, ${transparent})` ); useMotionValueEvent(scrollXProgress, "change", (value) => { const prevValue = scrollXProgress.getPrevious(); // 接近滚动开头(进度小于0.01) if (value < 0.01) { animate(maskImage, `linear-gradient(90deg, ${opaque}, ${opaque} ${left}, ${opaque} ${rightInset}, ${transparent})`); } // 接近滚动结尾(进度大于0.99) else if (value > 0.99) { animate(maskImage, `linear-gradient(90deg, ${transparent}, ${opaque} ${leftInset}, ${opaque} ${right}, ${opaque})`); } // 从边缘滚动到中间时切换遮罩 else if (prevValue < 0.01 || prevValue > 0.99) { animate(maskImage, `linear-gradient(90deg, ${transparent}, ${opaque} ${leftInset}, ${opaque} ${rightInset}, ${transparent})`); } }); return maskImage; } const elements = ["A", "B", "C", "D"]; const colors = [ "bg-red-800", "bg-yellow-600", "bg-green-900", "bg-purple-900", ]; const ref = useRef(null); const { scrollXProgress } = useScroll({ container: ref }); const maskImage = useScrollOverflowMask(scrollXProgress); return ( <div className="scroll-x h-screen flex flex-col justify-center items-center gap-4 bg-violet-400"> <div id="example"> <svg id="progress" width="80" height="80" viewBox="0 0 100 100"> <circle cx="50" cy="50" r="40" pathLength="1" style={{ fill: "none", stroke: "red", strokeWidth: 20 }} /> <motion.circle cx="50" cy="50" r="40" style={{ fill: "none", stroke: "yellow", strokeWidth: 10, pathLength: scrollXProgress, }} /> </svg> {/* 换成motion.div,直接绑定maskImage,自动监听值更新 */} <motion.div ref={ref} className="element-container overflow-x-scroll w-96 flex gap-4" style={{ WebkitMaskImage: maskImage, maskImage: maskImage, }} > {elements.map((element, index) => ( <motion.ul key={index} className="flex-shrink-0"> <li> <div className={`${colors[index]} w-64 h-64`}></div> </li> </motion.ul> ))} </motion.div> </div> </div> ); }; export default ScrollXBox;
关键修改说明:
- 把滚动容器换成
motion.div,直接将maskImage作为样式属性传递,Motion会自动监听值的变化并更新样式,不用手动调用.get(); - 用范围判断
value < 0.01/value > 0.99代替精确等于0/1,避免因为滚动进度是小数导致判断失效; - 保留了从边缘滚动到中间时的遮罩切换逻辑,确保交互流畅。
你可以试试这个修改后的代码,滚动容器时遮罩应该会随着滚动位置正确切换渐变效果了。
备注:内容来源于stack exchange,提问作者TeDi
相关产品推荐
相关产品推荐

