前言
产品说,我们做一个转盘活动吧,需要轮播中奖信息。 当然这需求完全没有问题。
产品说,你听我说完。
- 是从下往上轮播
- 如何数据没有更新,就反复轮播。
- 如果数据有更新,要无缝更新。
- 进入时间1s,暂停1S,出去时间1s.
没问题吧。
额, 等等,没多大问题。 那个谁,这个任务教你啦。
方案
然后,我的同事开始搜罗实现方案,很多都是匀速走的。
同事甚至和产品讨论要不要换成跑马灯,嘻嘻, 开玩笑。
说个笑话,csdn上有不少的这样代码,但是下载要积分,我可以说,日了*狗么!。
一个下午天气晴,有凉风,心情还好。 于是花了一点时间思考了一种方案。
关于移动端动画,无非是纯js控制,js Animation API(兼容不理想), css动画,canvas, webgl以及杂交方案。 关于本需求,前两种应该比较适合,成本低,容易实现。
纯js实现控制比较传统的方案,要启用定时器setTimeout/setInterval/requestAnimation等等,我很烦这。
采用css3 + js杂交方案,有戏靠谱。 既然有三个阶段,那么我就把你拆成三段动画, 随你去配置,随你去high。 当然你也可以用一段动画,通过设置来控制距离。
整体的思路
- 我把每个需要滚动的每个元素独立起来,每个元素有三段动画, in , pause , out. 怎么衔接, 通过animationend事件。
- 那么不同元素又怎么衔接,通过animationDelay来延时启动动画, 启动后依旧是走上面的三段动画。
- 怎么轮回播放,当然你可以利用原来的节点,重新修改属性,设置延时启动。 我这里采用比较简单的,直接删除了原有,然后重新创建。 当然重新创建是有讲究的,你有很多选择,只要控制好衔接的事件,我这里是在最后一个节点开始第一阶段运动的时候,重新创建新节点,最后节点第三阶段运动结束,清除之前运动完毕的节点
- 关于无缝更新,当然要让最后一个运动的元素运动完, 所以我在第二个阶段 pause阶段执行新的节点创建,并设置好相关的延时。
- 关于暂停能力, animationPlayState提供这个能力,paused和running。 暂停的手,animationDelay也会停止计时,非常的棒。 因为懒,只实现了PC的暂停能力。移动端嘛,添加touch,tap,pointer事件就好。 因为懒,所以。
改进
- 每个元素三个动画,是有点消耗。可以用一个大的容器放好所有的元素,然后animationPlayState来控制,蛮不错的。 如何控制每个阶段的计时呢,当然可以用js,我有个想法,就是起一个三段动画,在这个事件里面去控制animationPlayState。
- 节点也没回收利用,是有点浪费。
- 移动端的支持呢?
- 不太重要的其他......
效果呢?
怎么使用呢?
const contents = [ "队列1:春天 - first", "队列1:夏天", "队列1:秋天", "队列1:冬天", "队列1:夏夏湉", "队列1:求七天", "队列1:Who are You - last" ]; const contents2 = [ "队列2:这是怎么回事 - first", "队列2:谁是最可赖的人", "队列2:壮士一去不复返", "队列2:谁来拯救你", "队列2:家福乐团购有没有 - last" ] const el = document.querySelector("#box"); let upSlide = new UpSlide({ el }); upSlide.start(contents); document.getElementById('btnChange').addEventListener("click", () => { upSlide.start(contents); }) document.getElementById('btnChange2').addEventListener("click", () => { upSlide.start(contents2); })
源码呢?
等等这个有点用, 源码呢
上滑跑马灯源码
再贴出源码,这样文章长一点
const DEFAULT_OPTION = { inTime: 1000, pauseTime: 1500, outTime: 1000, className: "upslide-item", animationClass: "upslide-item-animation", animationInClass: "slideup-animation-in", animationPauseClass: "slideup-animation-pause", animationOutClass: "slideup-animation-out", pauseOnFocus: false }; const DELETING_CLASS_NAME = "__deleting__"; function clearSiblings(el) { const parent = el.parentElement; // 移除前面节点 while (el.previousElementSibling) { parent.removeChild(el.previousElementSibling); } // 移除后面的节点 while (el.nextElementSibling) { parent.removeChild(el.nextElementSibling); } } class UpSlide { constructor(options) { this.el = options.el; this.options = Object.assign({}, DEFAULT_OPTION, options); this.changeStatus = 0; this.currentContents = null; const { inTime, pauseTime, outTime } = this.options; this.totalTime = inTime + pauseTime + outTime; this.inPausePercent = (inTime + pauseTime) / this.totalTime; this.animationstartEvent = this.animationstartEvent.bind(this); this.animationendEvent = this.animationendEvent.bind(this); this.mouseenterEvent = this.mouseenterEvent.bind(this); this.mouseleaveEvent = this.mouseleaveEvent.bind(this); this.init(); } createItems(datas, baseDelay = 0) { const { className, animationInClass, animationClass, inTime } = this.options; const { totalTime, inPausePercent } = this; const fragment = document.createDocumentFragment(); datas.forEach((c, i) => { const newEl = document.createElement("div"); newEl.dataset.isLast = i === datas.length - 1 ? 1 : 0; newEl.innerText = c; newEl.className = className + " " + animationClass; newEl.style.animationName = animationInClass; newEl.style.animationDelay = baseDelay + i * totalTime * inPausePercent + "ms"; newEl.style.animationDuration = inTime + "ms"; fragment.appendChild(newEl); }); return fragment; } animationstartEvent(e) { const { totalTime, inPausePercent } = this; const { animationInClass } = this.options; // 开启新的轮回 if (e.animationName === animationInClass && e.target.dataset.isLast == 1) { this.innerStart(this.currentContents, totalTime * inPausePercent); } } animationendEvent(e) { const { animationInClass, animationPauseClass, animationOutClass, className, animationClass, pauseTime, outTime } = this.options; const { changeStatus } = this; const el = e.target; const parent = el.parentElement; const animationName = e.animationName; switch (animationName) { case animationInClass: el.style.animationName = animationPauseClass; el.style.animationDuration = pauseTime + "ms"; el.style.animationDelay = "0ms"; break; case animationPauseClass: el.style.animationName = animationOutClass; el.style.animationDuration = outTime + "ms"; el.style.animationDelay = "0ms"; // 切换 if (changeStatus === 1) { clearSiblings(el); // 标记 el.classList.add(DELETING_CLASS_NAME); // 切换 this.innerStart(this.currentContents, 0); this.changeStatus = 0; } break; case animationOutClass: e.target.classList.remove(animationClass); e.target.style.animationDelay = ""; if (el.classList.contains(DELETING_CLASS_NAME)) { parent.removeChild(el); } // 轮回结束-清除节点 if (e.target.dataset.isLast == 1) { const parent = e.target.parentElement; const delItems = parent.querySelectorAll( `.${className}:not(.${animationClass})` ); if (delItems.length > 0) { for (let i = delItems.length - 1; i >= 0; i--) { parent.removeChild(delItems[i]); } } } break; default: break; } } mouseenterEvent() { const { className } = this.options; this.el.querySelectorAll("." + className).forEach(el => { el.style.animationPlayState = "paused"; }); } mouseleaveEvent() { const { className } = this.options; this.el.querySelectorAll("." + className).forEach(el => { el.style.animationPlayState = "running"; }); } init() { const { el } = this; el.addEventListener("animationstart", this.animationstartEvent); el.addEventListener("animationend", this.animationendEvent); const { pauseOnFocus } = this.options; if (pauseOnFocus === true) { el.addEventListener("mouseenter", this.mouseenterEvent); el.addEventListener("mouseleave", this.mouseleaveEvent); } } innerStart(content, delay = 0) { this.currentContents = content; const c = this.createItems(content, delay); this.el.appendChild(c); } start(content, delay = 0) { if (this.currentContents != null) { this.changeStatus = 1; this.currentContents = content; return; } this.innerStart(content, delay); } destroy() { this.el.removeEventListener("animationstart", this.animationstartEvent); this.el.removeEventListener("animationend", this.animationendEvent); const { pauseOnFocus } = this.options; if (pauseOnFocus === true) { el.removeEventListener("mouseenter", this.mouseoverEvent); el.removeEventListener("mouseleave", this.mouseleaveEvent); } this.el.innerHTML = null; this.el = null; this.options = null; } }