前言
一段时间没写文,感觉差点什么。正好接到一个需求需要写一个组件,就记一下思路吧
需求如下:
组件能自动循环播放一个长图,播放速度平滑一致,并且在鼠标移动移入移出时停止播放。需求很简单,实际代码量也不是很多,花了大概1个半小时完成
最终效果
实现
1、requestAnimationFrame
首先我们需要创建平滑的视觉动画效果,为此我们需要使用requestAnimationFrame
函数
requestAnimationFrame
是一个浏览器提供的API,用于在下一次重绘之前执行指定的动画帧。它确保了代码在显示器刷新新内容时执行,能创建基于视觉的非常流畅的动画。
相对于setTiemout
的优点也很多
- 调用时机:
requestAnimationFrame
在浏览器准备重新渲染页面之前调用提供的回调函数,这通常发生在浏览器的刷新周期内。 - 优化性能:与使用
setTimeout
或setInterval
不同,requestAnimationFrame
会自动调整帧率以适应浏览器和设备的能力,比如在节能模式或移动设备上降低帧率,从而节省资源。 - 回调函数:传递给
requestAnimationFrame
的函数会在浏览器的渲染队列中添加一个动画帧。这个回调函数接收一个参数,通常表示从上一次动画帧到当前动画帧的时间戳。 - 链式调用:为了持续动画,你需要在回调函数内部再次调用
requestAnimationFrame
。这样,每次动画帧完成后,浏览器都会重复该过程,直到你取消它。 - 取消动画:要停止动画,可以使用
cancelAnimationFrame
函数并传入requestAnimationFrame
返回的唯一标识符(一个长整数)。
function step(timestamp) {
// 动画逻辑,如更新元素的位置
// ...
// 如果还需要继续动画,再次调用
requestAnimationFrame(step);
}
// 启动动画
requestAnimationFrame(step);
对上面的用法进行补充,然后来创建自己的平滑移动动画
//...
/**
* 线性匀速运动函数
* @param t 当前时间戳差值
* @param b 开始进度
* @param c 结束进度
* @param d 时长
* */
function linear(t: number, b: number, c: number, d: number): number {
return (c * t) / d + b;
}
/**
* 平滑移动
* @param targetY 目标元素
* @param duration 动画市场
* @param scrollW 移动距离
*/
function smoothScrollTo(
targetY: HTMLDivElement,
duration: number,
scrollW: number,
) {
const start = performance.now();
const left = targetY.scrollLeft;
function step(timestamp: number) {
if (timestamp < start + duration) {
const progress = timestamp - start;
const linearProgress = linear(progress, 0, 1, duration);
targetY.scrollTo({
left: left + scrollW * linearProgress,
});
requestAnimationFrame(step);
} else {
targetY.scrollTo({ left: left + scrollW });
}
}
requestAnimationFrame(step);
}
//...
2、setInterval
循环执行每一段距离的移动,并且在移动到头的时候重置播放进度
使用playingStatus
状态值来开启或结束动画
const [playingStatus, setPlayingStatus] = useState(false);
useEffect(() => {
let intervalId!: NodeJS.Timeout;
if (!ref.current) {
return;
}
const viewBoxW = ref.current?.clientWidth;
const maxW = (ref.current?.firstChild as HTMLImageElement)?.clientWidth;
// 进度条速度
const speed = 200;
if (playingStatus) {
// 开始计时任务
intervalId = setInterval(() => {
if ((ref.current as HTMLDivElement).scrollLeft + viewBoxW === maxW) {
ref.current?.scrollTo({ left: 0, behavior: 'instant' });
return;
}
smoothScrollTo(ref.current as HTMLDivElement, 500, speed);
}, 500);
} else {
// 清除定时器
clearInterval(intervalId);
}
// 在组件卸载时清除定时器
return () => {
clearInterval(intervalId);
};
}, [playingStatus]);
3、添加移入移出控制
const ref = useRef<HTMLDivElement>(null);
const [playingStatus, setPlayingStatus] = useState(false);
//...
<div
className={styles.imgBox}
ref={ref}
onMouseMove={() => {
setPlayingStatus(false);
}}
onMouseLeave={() => {
setPlayingStatus(true);
}}
>
<img
src={config.imgUrl ?? demoImg}
alt=""
onLoad={() => {
setPlayingStatus(true);
}}
></img>
</div>
//...
4、感谢阅读
完整代码:
const LongImagePreview:React.FC = () => {
const ref = useRef<HTMLDivElement>(null);
const demoImg = require('./demo.jpg');
const [playingStatus, setPlayingStatus] = useState(false);
useEffect(() => {
setPlayingStatus(false);
return () => {
ref.current?.scrollTo(0, 0);
};
}, []);
useEffect(() => {
let intervalId!: NodeJS.Timeout;
if (!ref.current) {
return;
}
const viewBoxW = ref.current?.clientWidth;
const maxW = (ref.current?.firstChild as HTMLImageElement)?.clientWidth;
// 进度条速度
const speed = 200;
if (playingStatus) {
// 开始计时任务
intervalId = setInterval(() => {
if ((ref.current as HTMLDivElement).scrollLeft + viewBoxW === maxW) {
ref.current?.scrollTo({ left: 0, behavior: 'instant' });
return;
}
smoothScrollTo(ref.current as HTMLDivElement, 500, speed);
}, 500);
} else {
// 清除定时器
clearInterval(intervalId);
}
// 在组件卸载时清除定时器
return () => {
clearInterval(intervalId);
};
}, [playingStatus]);
/**
* 线性匀速运动函数
* @param t 当前时间戳差值
* @param b 开始进度
* @param c 结束进度
* @param d 时长
* */
function linear(t: number, b: number, c: number, d: number): number {
return (c * t) / d + b;
}
/**
* 平滑移动
* @param targetY 目标元素
* @param duration 动画市场
* @param scrollW 移动距离
*/
function smoothScrollTo(
targetY: HTMLDivElement,
duration: number,
scrollW: number,
) {
const start = performance.now();
const left = targetY.scrollLeft;
function step(timestamp: number) {
if (timestamp < start + duration) {
const progress = timestamp - start;
const linearProgress = linear(progress, 0, 1, duration);
targetY.scrollTo({
left: left + scrollW * linearProgress,
});
requestAnimationFrame(step);
} else {
targetY.scrollTo({ left: left + scrollW });
}
}
requestAnimationFrame(step);
}
return (
<div className={styles.container}>
<div className={styles.imgContainer}>
<div
className={styles.imgBox}
ref={ref}
onMouseMove={() => {
setPlayingStatus(false);
}}
onMouseLeave={() => {
setPlayingStatus(true);
}}
>
<img
src={demoImg}
alt=""
onLoad={() => {
setPlayingStatus(true);
}}
></img>
</div>
</div>
</div>
);
};