前言
Youky ... 2021-11-27 About 4 min
# 前言
最近在使用Swiper轮播图时,发现其官网给出的demo竟然运行不了。于是乎想到自己造个轮子试试。 最终的实现为封装的React组件。完整代码:项目源码 (opens new window)
# 功能分析
首先分析一下,一个轮播图组件需要哪些功能:
- 基础轮播显示
- 自定义宽高
- 显示当前位置的分页器
- 点击分页器跳转页面
- 上/下一个按钮
- 自动播放
- 循环播放
- 监听点击事件
- 移动端支持
然后,对于上述功能一一进行实现。
# 结构拆分
首先对整个轮播图组件进行拆分,主要分为四部分:
- 最外层容器container
- 容纳图片的包装容器
- 底部的分页器
- 上/下一个按钮,位于两侧
为了结构的清晰,对于不同部分,拆分成了不同的JSX。 因此最后返回的JSX为:
<div className={style.swiper_container} ref={containerRef} style={{width,height}}>
<div className={style.swiper_wrapper} style={wrapperStyle}>
{ swiperItems }
</div>
{slideButton && slideButtons}
{pagination && paginationBar}
</div>
1
2
3
4
5
6
7
2
3
4
5
6
7
# 轮播功能
对于轮播功能的实现,我的思路是:
- 最外层容器设置宽为width,并设置overflow:hidden
.swiper_container {
position: relative;
height: 100%;
width: 100%;
border-bottom: 1px solid rgb(231, 225, 225);
overflow: hidden;
}
1
2
3
4
5
6
7
2
3
4
5
6
7
- 每张图片宽度也为width,因此当前视口中只会显示一张图。根据传入的props.list循环生成图片列表
// 轮播图内容
const swiperItems = list.map((item, index) => (
<div
key={index}
className={style.swiper_item}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
onDoubleClick={() => handleDoubleClick(index)}
style={{backgroundImage: `url(${item})`, width:containerWidth}}
></div>
))
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
- 定义当前显示图片的下标currentPage,每次滑动后更改currentPage
const [currentPage, setCurrentPage] = useState(0);
1
- 图片包装容器宽度为N*100% ,N为图片数量。并设置flex布局,即所有图片在一横排排列。通过translateX移动wrapper容器,来控制当前显示的图片
const wrapperStyle = {
width: `${list.length}00%`,
transform: `translateX(-${containerWidth * currentPage}px)`
}
1
2
3
4
2
3
4
- 监听滑动,在mouseDown时记录起始坐标,在mouseUp时记录终点坐标,依次判断是左滑还是右滑
// 切换页面函数
const handleSlide = (isNext) => {
if(isNext) {
if(currentPage < list.length-1){
setCurrentPage(currentPage + 1);
}
else if(loop) {
setCurrentPage(0);
}
}
else{
if(currentPage > 0){
setCurrentPage(currentPage - 1);
}
else if(loop){
setCurrentPage(list.length-1);
}
}
}
// 判断滑动情况
let currentX = null;
const handleMouseDown = e => {
currentX = e.clientX
}
const handleMouseUp = e => {
const end = e.clientX;
if( end - currentX > 20) {
handleSlide(false);
}
else if( currentX - end > 20) {
handleSlide(true);
}
}
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
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
- 对于移动端,则监听touchStart和touchEnd事件
// 移动端监听滑动
const handleTouchStart = e => {
currentX = e.changedTouches[0].clientX;
}
const handleTouchEnd = e => {
const end = e.changedTouches[0].clientX;
if( end - currentX > 20) {
handleSlide(false);
}
else if( currentX - end > 20) {
handleSlide(true);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
至此,基本的滑动效果已经完成了。
# 分页器
# 需求分析
- 固定显示在底部
- 通过颜色来区分当前显示的图片的下标
- 点击分页器会翻页到对应的图片
# 实现
- 首先,定义分页器的JSX
const paginationBar = (
<div className={style.swiper_pagination}>
{list.map((item, index) =>
<div
key={index}
className={paginationItemClass(index)}
onClick={() => handlePaginationClick(index)}
></div>
)}
</div>
)
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
- 从上一步可以看到,分页器每个元素的class是通过一个函数来返回的,目的是区分是否是当前显示的图片
const paginationItemClass = (index) => {
const isActive = index === currentPage ? ' ' + style.pagination_active : '';
return style.pagination_item + isActive;
}
1
2
3
4
2
3
4
- 然后定义分页器样式
- 通过translate将分页器容器固定到底部
- 设置不同的背景色来体现是否当前图片
.swiper_pagination {
width: 100%;
height: 40px;
transform: translate(0,-40px);
display: flex;
justify-content: center;
align-items: center;
}
.pagination_item {
width: 8px;
height: 8px;
border-radius: 4px;
background-color: #eee;
margin: 0 10px;
cursor: pointer;
}
.pagination_item:hover {
background-color: #999;
}
.pagination_active {
background-color: #999;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- 定义点击函数,来完成跳转翻页的功能,其实也就是修改currentPage
const handlePaginationClick = index => {
console.log(`${index} clicked`);
setCurrentPage(index);
}
1
2
3
4
2
3
4
- 通过props.pagination判断是否要显示
{props.pagination && paginationBar}
1
# 上/下一个按钮
# 需求分析
- 上下居中,分别靠左、右侧
- 点击后进行翻页
- 为防止遮挡图片,默认透明度为0.5,悬浮是恢复1
# 实现
- 通过absolute定位,将按钮固定到垂直居中的位置。再通过left和right来分别置于左右
.slideButton {
height: 40px;
width: 40px;
border-radius: 20px;
position: absolute;
background-color: #eee;
top: 50%;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
opacity: 0.5;
}
.slideButton:hover{
opacity: 1;
}
.arrow_right {
right: 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- 定义按钮的JSX。翻页使用的回调函数仍是前面定义的handleSlide:
// 上/下一个按钮
const slideButtons = (
<>
<div
className={style.slideButton}
onClick={() => handleSlide(false)}
>{'<'}</div>
<div
className={style.slideButton + ' ' + style.arrow_right}
onClick={() => handleSlide(true)}
>{'>'}</div>
</>
)
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
- 通过props.slideButton 判断是否显示
{props.slideButton && slideButtons}
1
# 循环播放
# 需求分析
- 当位于最后一张图时,触发向后翻页会跳回第一张
- 当位于第一张图时,触发向前翻页会跳回最后一张
# 实现
props
接收loop
属性,表示是否允许循环播放,默认为true- 在触发向翻页时,根据
loop
做特殊处理
// 切换页面函数
const handleSlide = (isNext) => {
if(isNext) {
if(currentPage < list.length-1){
setCurrentPage(currentPage + 1);
}
else if(loop) { // 特殊处理
setCurrentPage(0);
}
}
else{
if(currentPage > 0){
setCurrentPage(currentPage - 1);
}
else if(loop){ // 特殊处理
setCurrentPage(list.length-1);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 自动播放
# 需求分析
autoPlay
属性,表示是否进行自动播放,默认为false
intrval
属性,表示自动播放的间隔,默认为3000ms
# 实现
- 在
useEffect
Hook中设置定时器,并在返回的清除函数中清理定时器
// 设置自动播放
useEffect(() => {
if(autoPlay){
const auto = setInterval(() => {
handleSlide(true);
}, interval)
return () => {
clearInterval(auto);
}
}
})
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11