requestAnimationFrame实现单张图片无缝持续滚动
背景
在很久以前,有写过一个使用 js 实现单张图片持续滚动图片的 代码,但那一版实现会持续操作DOM,向DOM中插入元素,性能较差,最近发现 requestAnimationFrame 通过 动画的方式实现图片滚动更加方便,遂重新实现了一版,效果更赞,性能更好。
效果如下
需求描述
需要单张图片在可视区域内无缝持续向上滚动或向左滚动,由于向下和向右属于反着坐标轴移动图片,和正常DOM元素插入展示顺序也相反,遂不考虑此种场景。
代码实现
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>滚动图片</title> <style> /*竖向滚动*/ #container { width: 300px; height: 150px; overflow: hidden; margin: 100px; } #wrap { width: 100%; height: auto; display: flex; flex-direction: column; align-items: center; transform: translatez(0); } img { width: 100%; height: auto; } /*横向滚动*/ /* #container { width: 300px; height: 150px; overflow: hidden; margin: 100px; } #wrap { width: auto; height: 100%; display: flex; flex-wrap: nowrap; align-items: center; transform: translatez(0); } img { width: auto; height: 100%; } */ </style> </head> <body> <div id="container"> <div id="wrap"> <!-- 横图 --> <!-- <img src="https://img1.baidu.com/it/u=49865366,3040475020&fm=253&fmt=auto&app=138&f=JPEG?w=751&h=500" alt="" /> --> <!-- 竖图 --> <img src="https://img0.baidu.com/it/u=3724390669,1663648862&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=752" alt="" /> </div> </div> <script> "use strict"; // 功能:实现图片无缝向上滚动 // run:运行图片轮播 // pause:暂停图片轮播 // imgWrap:图片容器,放置多张图片,整体进行滚动 // imgView: 图片所展示区域的窗口view // step 每次移动的距离 // direction: 滚动方向,默认 "top" 持续向上滚动,可选值为 "top" 和 "left" function imageScroll(imgWrap, imgView, step = 1, direction = "top") { if (!imgWrap || !imgView) { console.warn("请传入参数形如[图片包裹容器,图片展示容器]"); return false; } // 获取窗口宽度 const containerWidth = parseInt(imgView.clientWidth); // 获取窗口高度 const containerHeight = parseInt(imgView.clientHeight); // 获取图片元素 const imgElem = imgWrap.querySelector("img"); // 获取图片宽度 const imgWidth = parseInt(imgElem.width); // 获取图片高度 const imgHeight = parseInt(imgElem.height); // 初始化移动距离 let distance = 0; // 定义 transform 值名称 let transformV; // 初始化图片移动置为0的边界长度 let boundaryValue = 0; switch (direction) { case "left": // 向左滚动,值为 translateX transformV = "translateX"; // 置为 0 的边界值为图片宽度 boundaryValue = parseFloat(imgWidth); // 克隆的图片个数,至少克隆一张 const num1 = Math.ceil(containerWidth / imgWidth) || 1; for (let index = 0; index < num1; index++) { // 克隆一张图片并插入到图片最后面 imgWrap.appendChild(imgWrap.querySelector("img").cloneNode(true)); } break; default: // 向上滚动,值为 translateY transformV = "translateY"; // 置为 0 的边界值为图片高度 boundaryValue = parseFloat(imgHeight); // 克隆的图片个数,至少克隆一张 const num2 = Math.ceil(containerHeight / imgHeight) || 1; for (let index = 0; index < num2; index++) { // 克隆一张图片并插入到图片最后面 imgWrap.appendChild(imgWrap.querySelector("img").cloneNode(true)); } break; } if ( /iP(ad|hone|od).*OS 13/.test(window.navigator.userAgent) || // iOS6 is buggy !window.requestAnimationFrame || !window.cancelAnimationFrame ) { let lastTime = 0; window.requestAnimationFrame = function (callback) { const now = Date.now(); const nextTime = Math.max(lastTime + 16, now); return setTimeout(function () { callback((lastTime = nextTime)); }, nextTime - now); }; window.cancelAnimationFrame = clearTimeout; } // 执行动画函数 const requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame; // 取消执行动画函数 const cancelAnimationFrame = window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame; // 初始化定义轮播函数 requestId = null; return function () { return { run: () => { // 定义滚动动画回调函数 const scroll = () => { // 移动的距离=已经移动的距离+每步的长度 distance = distance + step; // 设置图片容器的 transform imgWrap.style.transform = `${transformV}(-${distance}px)`; // 关键行:当移动距离大于边界值时,重置 distance=0 if (distance >= boundaryValue) { distance = 0; } // 再次调用滚动动画 requestId = requestAnimationFrame(scroll); }; // 执行滚动动画,传入滚动动画回调函数 requestId = requestAnimationFrame(scroll); }, // 暂停动画 pause: () => { cancelAnimationFrame(requestId); }, }; }; } window.onload = () => { // 向上滚动 const scroll = imageScroll( document.getElementById("wrap"), document.getElementById("container"), 1, "top" ); // 向左滚动 // const scroll = imageScroll( // document.getElementById("wrap"), // document.getElementById("container"), // 0.5, // "left" // ); scroll().run(); // 通过定时器可以实现图片滚动几秒后暂停,如下表示先滚动 4s 后暂停,之后每个隔 2s 再滚动,2秒后再暂停 // setInterval(() => { // scroll().pause(); // setTimeout(() => { // scroll().run(); // }, 2000); // }, 4000); }; </script> </body> </html>
备注
对于向上滚动和向左滚动两种效果,css 样式要同步修改,支持横图、竖图滚动。
代码中用到了百度图片,侵删。
参考链接
如何设计实现无缝轮播 【同时这里其他朋友答案也都很赞,收藏了】
requestAnimationFrame 知多少?【相关知识点与优势可参考这里】
发现的坑
1、非严格模式下,function中定义的变量 ,如果没写 let 或 const 或 var ,会导致 该方法之后都不会执行,也没有报错
"use strict" 严格模式下,会报错该变量未定义。
=====================
2022.03.09 更新
2、发现在有些场景下图片onload事件触发之后,依然获取不到图片宽高,而上面我们图片滚动是依赖图片的宽高的,这里需要再加个定时器,轮询获取图片宽高,当确实可以获取到宽高之后,再设置滚动距离,并允许开始滚动。
更新后代码如下:
function newScroll(imgWrap, imgView, step = 1, direction = 'top') { if (!imgWrap || !imgView) { console.warn('请传入参数形如[图片包裹容器,图片展示容器]'); return false; } // requestAnimationFrame 向下兼容 if ( /iP(ad|hone|od).*OS 13/.test(window.navigator.userAgent) || // iOS6 is buggy !window.requestAnimationFrame || !window.cancelAnimationFrame ) { let lastTime = 0; window.requestAnimationFrame = function (callback) { const now = Date.now(); const nextTime = Math.max(lastTime + 16, now); return setTimeout(function () { callback((lastTime = nextTime)); }, nextTime - now); }; window.cancelAnimationFrame = clearTimeout; } // 执行动画函数 const requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame; // 取消执行动画函数 const cancelAnimationFrame = window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame; // 初始化定义轮播函数 let requestId = null; // 获取图片窗口宽度 const containerWidth = (imgView.clientWidth && parseFloat(imgView.clientWidth)) || document.body.clientWidth;; // 获取图片窗口高度 const containerHeight = (imgView.clientHeight && parseFloat(imgView.clientHeight)) || document.body.clientHeight; // 获取图片元素 const imgElem = imgWrap.querySelector('img'); // 获取图片宽度 let imgWidth = 0; // 获取图片高度 let imgHeight = 0; // 初始化移动距离 let distance = 0; // 定义 transform 值名称 let transformV; // 初始化图片移动置为0的边界长度 let boundaryValue = 0; // 是否可以执行滚动动画,保证获取到图片真实宽高之后再开始滚动,否则获取不到宽高,始终不会滚动 let canScroll = false; // 定时执行获取图片宽高 var checkImg = function () { // 只要任何一方大于0,表示已经服务器已经返回宽高 if (imgElem.width > 0 || imgElem.height > 0) { console.log("获取到真实宽高,清除定时器",imgElem.width,imgElem.height) console.log("定时器获取",new Date().getTime()) clearInterval(set); imgWidth = parseFloat(imgElem.width); // 获取图片高度 imgHeight = parseFloat(imgElem.height); switch (direction) { case 'left': // 向左滚动,值为 translateX transformV = 'translateX'; // 置为 0 的边界值为图片宽度 boundaryValue = imgWidth; // 克隆的图片个数,至少克隆一张 const num1 = Math.ceil(containerWidth / imgWidth) || 1; if(imgElem){ for (let index = 0; index < num1; index++) { // 克隆一张图片并插入到图片最后面 imgWrap.appendChild(imgElem.cloneNode(false)); } } break; default: // 向上滚动,值为 translateY transformV = 'translateY'; // 置为 0 的边界值为图片高度 boundaryValue = imgHeight ; // 克隆的图片个数,至少克隆一张 const num2 = Math.ceil(containerHeight / imgHeight) || 1; if(imgElem){ for (let index = 0; index < num2; index++) { // 克隆一张图片并插入到图片最后面 imgWrap.appendChild(imgElem.cloneNode(false)); } } break; } canScroll = true; } }; // 当图片元素存在时再调用定时器 if(imgElem){ var set = setInterval(checkImg, 40); // 轮询 10 秒,还拿不到就不清除定时器 setTimeout(()=>{ clearInterval(set); },10000) } return function () { return { run: () => { // 保证在获取到图片宽高,可以滚动的时候再滚动 let getCanScrollStatus = setInterval(()=>{ if(canScroll){ clearInterval(getCanScrollStatus); // 定义滚动动画回调函数 const scroll = () => { // 移动的距离=已经移动的距离+每步的长度 distance = distance + step; // 设置图片容器的 transform imgWrap.style.transform = `${transformV}(-${distance}px)`; // 关键行:当移动距离大于边界值时,重置 distance=0 if (distance >= boundaryValue) { distance = 0; } // 再次调用滚动动画 requestId = requestAnimationFrame(scroll); }; // 执行滚动动画,传入滚动动画回调函数 requestId = requestAnimationFrame(scroll); } }, 40); setTimeout(()=>{ clearInterval(getCanScrollStatus); },100000) }, // 暂停动画 pause: () => { if(requestId){ cancelAnimationFrame(requestId); } } }; }; }