上滑跑马灯

前言

产品说,我们做一个转盘活动吧,需要轮播中奖信息。 当然这需求完全没有问题。
产品说,你听我说完。

  1. 是从下往上轮播
  2. 如何数据没有更新,就反复轮播。
  3. 如果数据有更新,要无缝更新。
  4. 进入时间1s,暂停1S,出去时间1s.

没问题吧。
额, 等等,没多大问题。 那个谁,这个任务教你啦。

方案

然后,我的同事开始搜罗实现方案,很多都是匀速走的。
同事甚至和产品讨论要不要换成跑马灯,嘻嘻, 开玩笑。
说个笑话,csdn上有不少的这样代码,但是下载要积分,我可以说,日了*狗么!。

一个下午天气晴,有凉风,心情还好。 于是花了一点时间思考了一种方案。

关于移动端动画,无非是纯js控制,js Animation API(兼容不理想), css动画,canvas, webgl以及杂交方案。 关于本需求,前两种应该比较适合,成本低,容易实现。

纯js实现控制比较传统的方案,要启用定时器setTimeout/setInterval/requestAnimation等等,我很烦这。

采用css3 + js杂交方案,有戏靠谱。 既然有三个阶段,那么我就把你拆成三段动画, 随你去配置,随你去high。 当然你也可以用一段动画,通过设置来控制距离。

整体的思路

  1. 我把每个需要滚动的每个元素独立起来,每个元素有三段动画, in , pause , out. 怎么衔接, 通过animationend事件。
  2. 那么不同元素又怎么衔接,通过animationDelay来延时启动动画, 启动后依旧是走上面的三段动画。
  3. 怎么轮回播放,当然你可以利用原来的节点,重新修改属性,设置延时启动。 我这里采用比较简单的,直接删除了原有,然后重新创建。 当然重新创建是有讲究的,你有很多选择,只要控制好衔接的事件,我这里是在最后一个节点开始第一阶段运动的时候,重新创建新节点,最后节点第三阶段运动结束,清除之前运动完毕的节点
  4. 关于无缝更新,当然要让最后一个运动的元素运动完, 所以我在第二个阶段 pause阶段执行新的节点创建,并设置好相关的延时。
  5. 关于暂停能力, animationPlayState提供这个能力,paused和running。 暂停的手,animationDelay也会停止计时,非常的棒。 因为懒,只实现了PC的暂停能力。移动端嘛,添加touch,tap,pointer事件就好。 因为懒,所以。

改进

  1. 每个元素三个动画,是有点消耗。可以用一个大的容器放好所有的元素,然后animationPlayState来控制,蛮不错的。 如何控制每个阶段的计时呢,当然可以用js,我有个想法,就是起一个三段动画,在这个事件里面去控制animationPlayState。
  2. 节点也没回收利用,是有点浪费。
  3. 移动端的支持呢?
  4. 不太重要的其他……

效果呢?

怎么使用呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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);
})

源码呢?

等等这个有点用, 源码呢
上滑跑马灯源码

再贴出源码,这样文章长一点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
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;
}
}