前言
產品說,我們做一個轉盤活動吧,需要輪播中獎信息。 當然這需求完全沒有問題。
產品說,你聽我說完。
- 是從下往上輪播
- 如何數據沒有更新,就反覆輪播。
- 如果數據有更新,要無縫更新。
- 進入時間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;
}
}