轮播图的简单实现position绝对定位版

轮播图的简单实现position绝对定位版。

在之前的帖子中尝试以HTML5中的flex布局来实现轮播图。

当然,实现有几个问题:

  • 由于基于整个box进行移动,比较难以实现单方向的无限下一张;
  • 由于基于整个box进行移动,在切换某一张图时可能动画过快;
  • 由于基于整个box进行移动,需要动态计算滑动的距离。

所以本篇实现以绝对定位以及上篇也使用到的translate3d来实现。

本篇实现参考了 B 站首页轮播图的实现(实现细节可能不一致,不过视觉上是一致的)。

使用的HTML结构和上一篇轮播图的HTML结构一样,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<div class="carousel">
<div class="carousel__container">
<div class="carousel__item green">A</div>
<div class="carousel__item red">B</div>
<div class="carousel__item green">C</div>
<div class="carousel__item pink">D</div>
<div class="carousel__item orange">E</div>
</div>
</div>
<!--额外添加个按钮,这个按钮后面会用到,用来模拟下一张-->
<div style="display: flex;align-items: center;justify-content: center">
<button id="btn">下一张</button>
</div>

本篇实现着重点为无限下一张的逻辑,对左右按钮以及下方小点的逻辑省略(最后会有总的实现代码)。

首先我们可以F12查看 B 站首页的轮播图的结构和样式切换。

从右侧的样式上可以看到,每个轮播框都是绝对定位的,然后通过样式来切换,样式主要是transform:translate3d(x, y, z)

从图中几次style切换可以看出,初始定位都是全部放在右侧,当前第一张放在可视窗口内。

我们先编写下相应的css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.carousel {
position: relative;
width: 500px;
height: 300px;
margin: 30px auto;
background-color: #ffdde3;
overflow: hidden;
}

.carousel__container {
width: 100%;
height: 100%;
}

.carousel__item {
/*使用绝对定位*/
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
}

HTML如下:

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
<div class="carousel">
<div class="carousel__container">
<div
class="carousel__item green"
style="transform: translate3d(0px, 0px, 0px); transition: all 0.55s ease 0s;"
>
A
</div>
<div
class="carousel__item red"
style="transform: translate3d(500px, 0px, 0px); transition: all 0.55s ease 0s;"
>
B
</div>
<div
class="carousel__item green"
style="transform: translate3d(500px, 0px, 0px); transition: none 0s ease 0s;"
>
C
</div>
<div
class="carousel__item pink"
style="transform: translate3d(500px, 0px, 0px); transition: none 0s ease 0s;"
>
D
</div>
<div
class="carousel__item orange"
style="transform: translate3d(500px, 0px, 0px); transition: none 0s ease 0s;"
>
E
</div>
</div>
</div>
<!--额外添加个按钮-->
<div style="display: flex;align-items: center;justify-content: center">
<button id="btn">下一张</button>
</div>

效果如下:

现在如果进行一轮轮播,那么需要以下的步骤:

  • 当前的轮播往中心往左移(离开可视窗口);
  • 下一张轮播从右侧往中心移动(进入可视窗口)。

我们通过按钮来实现上面这个下一张的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const eles = document.getElementsByClassName("carousel__item");
// 当前轮播的索引
let curIdx = 0;
document.getElementById("btn").onclick = function () {
// 设置动画的过渡时间
eles[curIdx].style.setProperty("transition", "all 0.5s");
// 中心往左移动(离开可视窗口)
eles[curIdx].style.setProperty("transform", "translate3d(-500px,0,0)");

curIdx++;

// 设置动画的过渡时间
eles[curIdx].style.setProperty("transition", "all 0.5s");
// 从右往左移动(进入可视窗口)
eles[curIdx].style.setProperty("transform", "translate3d(0,0,0)");
};

效果如下:

可以看到效果基本出来了。

但是有个问题,上面的实现中没有对curIdx进行合法性判断(也就是要小于轮播数量的长度)。

如果一直下一张那么会报错,如下:

所以要在最后一张时,重新回到第一张。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const eles = document.getElementsByClassName("carousel__item");
let curIdx = 0;
document.getElementById("btn").onclick = function () {
// 设置动画的过渡时间
eles[curIdx].style.setProperty("transition", "all 0.5s");
// 中心往左移动(离开可视窗口)
eles[curIdx].style.setProperty("transform", "translate3d(-500px,0,0)");

curIdx++;

// 合法性判断
if (curIdx === eles.length) {
curIdx = 0;
}

// 设置动画的过渡时间
eles[curIdx].style.setProperty("transition", "all 0.5s");
// 从右往左移动(进入可视窗口)
eles[curIdx].style.setProperty("transform", "translate3d(0,0,0)");
};

逻辑上应该没问题,但是实际是有问题的,如下:

发现第一轮没问题,但是第二次循环到 A 图的时候,就发现它从左边往中间移动了。

原因就是当前轮播图离开可视窗口之后,它就一直在左侧不可见区域了,而不是在右侧不可见区域了,如下:

所以需要补充的逻辑就是:在当前轮播图离开可视窗口之前,如果左侧不可见区域还有轮播图,那么把它放到右侧不可见区域。

我们可以用一个变量来保存左侧不可见区域轮播图的索引。

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
let eles = document.getElementsByClassName("carousel__item");
let curIdx = 0;
// 对于刚打开页面的轮播图,此时左侧不可见区域没有,所以设置为-1
let recoveryIdx = -1;

document.getElementById("btn").onclick = function () {
// 判断,如果-1表示刚启动轮播图,不用重新放到右侧
if (recoveryIdx !== -1) {
// 注意这里把过渡关了,这样移动到右侧就不会有动画
eles[recoveryIdx].style.setProperty("transition", "none");
eles[recoveryIdx].style.setProperty(
"transform",
`translate3d(500px, 0, 0)`
);
}

// 下一次要重新放到右侧的轮播图索引就是当前索引,它会在下一次被放到右侧。
recoveryIdx = curIdx;

// 当前轮播图离开
eles[curIdx].style.setProperty("transition", "all 0.55s");
eles[curIdx].style.setProperty("transform", `translate3d(-500px, 0, 0)`);

// 另一种重新计算索引的方式,和上面的一样
curIdx = (curIdx + 1) % eles.length;

// 下一张轮播图进入
eles[curIdx].style.setProperty("transition", "all 0.55s");
eles[curIdx].style.setProperty("transform", `translate3d(0, 0, 0)`);
};

看起来真不错~

但是,还是有问题…

回到我们的轮播逻辑,每次操作需要改变三张图片的样式:

  • 原来就在左侧的,重新放到右侧;
  • 当前位于可见区域的,离开进入左侧不可见区域;
  • 当前位于右侧不可见区域的,进入可见区域。

所以轮播的数量必须不小于3张,如果是2张,那么上面的逻辑就不成立,如下:

如果是1张,那么轮播图直接不会动了,如下:

当然,一张图不轮播也没啥大问题,但是2张的时候应该是B后面跟着A(从右侧出来)。

对于每次操作,可以看作是一组绑定的操作,也就是每次必须控制三个节点(三个节点不同)。比较复杂,很容易写漏逻辑。

回归初心,我们要的效果就是下一张从右进入可视窗口,可视窗口的这张往左离开。

那么是否可以每次就控制两个节点来实现这个效果呢?

答案是可以的,之前需要控制三个节点的原因是需要提前把对应的节点搬到右侧(无动画),然后在本次渲染时进行动画过渡。

这得结合浏览器的渲染原理,浏览器会在两个宏任务之间渲染页面,所以我们完全可以先把节点先挪到对应位置,然后在下个宏任务中进行动画操作,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div class="carousel">
<div class="carousel__container">
<div
class="carousel__item"
style="transform: translate3d(0px, 0px, 0px); transition: all 0.55s ease 0s;"
>
<img src="../images/img1.jpg" alt="" />
</div>
<div
class="carousel__item"
style="transform: translate3d(500px, 0px, 0px); transition: all 0.55s ease 0s;"
>
<img src="../images/img2.jpg" alt="" />
</div>
</div>
</div>

PS:HTML 结构上换成图片展示了。

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
const eles = document.getElementsByClassName("carousel__item");
const len = eles.length;
let curIdx = 0;

document.getElementById("btn").onclick = function () {
// 保存离开轮播图的索引
let last = curIdx;
// 计算下一张轮播图索引
curIdx = (curIdx + 1) % len;

// 先把当前这张放右边
eles[curIdx].style.setProperty("transition", "none");
eles[curIdx].style.setProperty("transform", "translate3d(500px, 0, 0)");

// 浏览器会在当前宏任务执行完毕把下一张轮播图渲染到右侧
// 下一个宏任务,进行动画的过渡
// 此时下一张已经在右侧了,直接设置样式进行动画过渡
setTimeout(() => {
// 上一张离开
eles[last].style.setProperty("transition", "all 0.55s");
eles[last].style.setProperty("transform", `translate3d(-500px, 0, 0)`);

// 当前这张进入
eles[curIdx].style.setProperty("transition", "all 0.55s");
eles[curIdx].style.setProperty("transform", "translate3d(0, 0, 0)");
}, 0);
};

效果如下:

这种情况下,对于一张图片的话:

  • 如果不需要实现轮播,那么最简单,在下一张的逻辑前进行长度判断即可;
  • 如果需要实现轮播,那么可以补一张重复的在它的后面,然后要处理小圆点的逻辑前进行长度判断即可。

效果如下(补一张的效果):

到现在,核心逻辑基本上就结束了,下面是一个完全实现的代码段,包括了左右按钮和小圆点逻辑。

HTML 结构简单,用 js 来生成结构。

1
<div id="myCarousel"></div>

css 结构和之前没啥不同。

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
.carousel {
position: relative;
width: 500px;
height: 350px;
margin: 30px auto;
background-color: #ffdde3;
overflow: hidden;
}

.carousel__container {
height: 100%;
}

.carousel__item {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
}

.carousel__item > img {
display: block;
width: 100%;
height: 100%;
}

.carousel__trigger__left,
.carousel__trigger__right {
position: absolute;
bottom: 0;
top: 0;
margin: auto 0;
width: 50px;
height: 50px;
border-radius: 50%;
background-color: rgba(204, 204, 204, 0.5);
cursor: pointer;
}

.carousel__trigger__left {
left: 0;
}

.carousel__trigger__right {
right: 0;
}

.carousel__trigger__points {
position: absolute;
left: 0;
right: 0;
bottom: 10px;
margin: 0 auto;
padding: 0;
}

.carousel__trigger__point__item {
width: 10px;
height: 10px;
background-color: rgba(204, 204, 204, 0.5);
border-radius: 50%;
margin: 0 10px;
cursor: pointer;
}

.carousel__trigger__point__item.active {
background-color: yellow;
}

.carousel__trigger__point__item:hover {
background-color: yellow;
}

js代码如下:

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
function createCarousel(urls, config) {
// 创建整个dom树,返回之后需要用到的部分
function createDom(urls) {
function createElement(tag, classList) {
const el = document.createElement(tag);
if (!Array.isArray(classList)) {
classList = [classList];
}
el.classList.add(...classList);
return el;
}

const carousel = createElement("div", "carousel");
const carouselContainer = createElement("div", "carousel__container");
const carouselTriggerContainer = createElement("div", "carousel__trigger");
const carouselTriggerLeft = createElement("div", "carousel__trigger__left");
const carouselTriggerRight = createElement(
"div",
"carousel__trigger__right"
);
const carouselTriggerPointContainer = createElement(
"div",
"carousel__trigger__points"
);
carousel.append(carouselContainer);
carousel.append(carouselTriggerContainer);
carouselTriggerContainer.append(carouselTriggerLeft);
carouselTriggerContainer.append(carouselTriggerRight);
carouselTriggerContainer.append(carouselTriggerPointContainer);

// 这里表示最少2张
const len = Math.max(2, urls.length);

const carouselItemList = [];
const carouselPointList = [];

for (let i = 0; i < len; i++) {
const item = createElement("div", "carousel__item");
const img = createElement("img");
// 如果是1张,那么取余就是两种一样的。
img.src = urls[i % urls.length];
item.append(img);
carouselContainer.append(item);
carouselItemList.push(item);
}

for (let i = 0; i < urls.length; i++) {
const item = createElement("div", "carousel__trigger__point__item");
carouselPointList.push(item);
carouselTriggerPointContainer.append(item);
}

return {
// 整个轮播图的root
carousel,
// 轮播项目的dom节点数组
carouselItemList,
// 左按钮
carouselTriggerLeft,
// 右按钮
carouselTriggerRight,
// 小点dom节点数组
carouselPointList,
};
}

// 设置索引对应的样式,没有动画过渡的
function setPos(idx, mode) {
dom.carouselItemList[idx].style.setProperty("transition", "none");
dom.carouselItemList[idx].style.setProperty(
"transform",
`translate3d(${mode === "LEFT" ? -100 : 100}%, 0, 0)`
);
}

// 设置索引对应的样式,有动画过渡
function go(idx, pos) {
dom.carouselItemList[idx].style.setProperty("transition", "all 0.55s");
dom.carouselItemList[idx].style.setProperty(
"transform",
`translate3d(${pos}%, 0, 0)`
);
}

// 一个动画函数,先设置到对应位置,然后在渲染之后立即进行动画过渡
function animate(__curIdx, __nextIdx, dir) {
// 右向左
if (dir === "RIGHT_TO_LEFT") {
setPos(__nextIdx, "RIGHT");
setTimeout(() => {
go(__curIdx, -100);
go(__nextIdx, 0);
});
} else {
setPos(__nextIdx, "LEFT");
setTimeout(() => {
go(__curIdx, 100);
go(__nextIdx, 0);
});
}
// 设置小圆点样式,如果只有一张就不改样式了,没意义
if (realLen !== 1) {
dom.carouselPointList[__curIdx].classList.remove("active");
dom.carouselPointList[__nextIdx].classList.add("active");
}
}

const {
mount: root = document.getElementsByTagName("body")[0],
defaultIdx = 0,
} = config;
let dom = createDom(urls);
let curIdx = defaultIdx;
const len = dom.carouselItemList.length;
const realLen = dom.carouselPointList.length;

// 向左切换
function __triggerLeft() {
let last = curIdx;
// 计算当前的左边的一张
curIdx = (len + curIdx - 1) % len;
animate(last, curIdx, "LEFT_TO_RIGHT");
}

// 向右切换
function __triggerRight() {
let last = curIdx;
// 计算当前的右边的一张
curIdx = (curIdx + 1) % len;
animate(last, curIdx, "RIGHT_TO_LEFT");
}

// 小点切换
function __triggerPoint(index) {
return function () {
if (realLen === 1 || index === curIdx) {
return;
}
let last = curIdx;
curIdx = index;
animate(last, curIdx, last < curIdx ? "RIGHT_TO_LEFT" : "LEFT_TO_RIGHT");
};
}

// 事件绑定
dom.carouselTriggerLeft.addEventListener("click", __triggerLeft);
dom.carouselTriggerRight.addEventListener("click", __triggerRight);
const events = {};
dom.carouselPointList.forEach((pointEl, index) => {
pointEl.addEventListener("click", (events[index] = __triggerPoint(index)));
});

// 初始化样式,主要是对当前的索引进行样式设定
function init() {
dom.carouselItemList.forEach((item, index) => {
if (index !== curIdx) {
setPos(index, 500);
}
});
dom.carouselPointList[curIdx].classList.add("active");
}

// 初始化
init();
// 挂载
root.append(dom.carousel);

return {
// 返回一个对象,销毁这个轮播图,主要是解除事件监听
destroy: function () {
dom.carouselTriggerLeft.removeEventListener("click", __triggerLeft);
dom.carouselTriggerRight.removeEventListener("click", __triggerRight);
dom.carouselPointList.forEach((pointEl, index) => {
pointEl.removeEventListener("click", events[index]);
});
dom = null;
},
};
}

2 张以上的demo

js代码

1
2
3
4
5
6
7
8
9
10
11
12
const carousel = createCarousel(
[
"../images/img1.jpg",
"../images/img2.jpg",
"../images/img3.jpg",
"../images/img4.jpg",
],
{
mount: document.getElementById("myCarousel"),
defaultIdx: 0,
}
);

效果如下:

1 张的demo

js代码

1
2
3
4
const carousel = createCarousel(["../images/img1.jpg"], {
mount: document.getElementById("myCarousel"),
defaultIdx: 0,
});

后记

还有点不足就是没有实现定时器的部分,就交给看到帖子的你啦~

思路就是监听mouseentermouseleave来设置和清除定时器。

定时器就是右边按钮的逻辑。

在返回的销毁函数中销毁即可。

demo如下: