概要
本文主要介绍一个无缝轮播图的基本设计原理和实现方式,该轮播图不依赖任何第三方插件或组件库,可以在常用的Chrome或Edge浏览器上正常运行。
基本功能
基本功能目标包括:
- 轮播图可以自动播放
- 轮播图可以手工切换到指定页面
- 实现无缝轮播,最后一张播完后,自动切换到第一张
- 用户可以配置轮播图的起始播放页和每页的播放时间
- 用户鼠标进入播放区域,播放暂停
- 用户鼠标离开播放区域,播放开始
实现原理
轮播图的实现原理与胶片放映类似,通过一帧一帧的切换实现轮播效果,具体请见下图:
如上图所示, 假设我们有4张图片进行轮播,虚线右侧Slider Window是播放窗口,我们将要播放的图片排成一行,每次向左移动一个Slider的长度,播放窗口的长度只能显示一张图片,从播放窗口中看,图片就变成一帧一帧连续播放。
在第四张图片播放完成后, 我们需要将整个播放队列重新移动到第一张,这样会降低用户的体验。
我们的解决方式是在程序运行前,将Slider1的内容克隆一份,放到队尾,在Slider4播放完毕后,将克隆的slider1展示出来,同时快速将播放队列重置,这样可以让用户感觉不到播放队列的重置。
设计及实现
在设计和实现上,本文采用数据驱动的方式。即以当前播放中的Slider的页码作为响应式数据。自动播放,向上翻页,向下翻页,指定播放页,这些功能只操作当前播放的页码,不涉及任何DOM操作。
移动播放页面,重置播放队列,修改播放轨道样式等DOM操作全部根据当前播放的页码的变化,而被调用。从而实现数据和DOM操作的分离。
关键代码解析
响应式数据的设置
get currentIndex() {
return this._index;
}
set currentIndex(newValue) {
if (newValue === this._index){
return;
}
if(newValue < 0){
return;
}
this._index = newValue;
this.update();
}
- 响应式数据是currentIndex
- 通过设置currentIndex的get和set方法,使其成为响应式数据。
- 当前播放页码修改后,首先判断当前页是否为克隆页,如果是,重置播放队列,否则移动到播放队列的下一张。
update = ()=> {
if (this.currentIndex === this.$silders.length - 1){
this.reset();
this._index = 0;
}else {
this.moveSlider();
}
}
关键Click事件
鼠标点击播放轨道后,会自动切换到指定的播放页。
采用事件代理方式,为整个轨道设置click事件,而不是给每个spot设置click事件
handleClickTrack = (e) => {
const dot = e.target;
const classList = [...dot.classList];
if (!classList.includes("dot")){
return;
}
this.currentIndex = [].indexOf.call(this.$spliders, dot);
}
附录
class Carousel {
constructor(options){
const {
el, defaultIndex, duration} = options;
this.$carousel = document.querySelector(el);
this.$sliderWindow = this.$carousel.querySelector("div.slider-window");
this._index = 0;
this.defaultIndex = defaultIndex;
this.duration = duration;
this.sliderWidth = this.$carousel.offsetWidth;
this.$silders = this.$carousel.getElementsByClassName("slider");
this.$spliderTrack = this.$carousel.querySelector("div.splide-track");
this.$spliders = this.$carousel.querySelectorAll("i.dot");
this.splideWidth = this.$spliders[0].offsetWidth;
this.$nav = this.$carousel.querySelector("div.nav");
this.autoPlayInerval = null;
this.init();
}
get currentIndex() {
return this._index;
}
set currentIndex(newValue) {
if (newValue === this._index){
return;
}
if(newValue < 0){
return;
}
this._index = newValue;
this.update();
}
init = () => {
this.initDom();
this.initPlay();
this.autoPlay();
this.bindEvent();
}
initPlay = () => {
if ( this.defaultIndex === 0){
return;
}
this.currentIndex = this.defaultIndex;
}
bindEvent = () => {
this.$spliderTrack.addEventListener("click", this.handleClickTrack, false);
this.$carousel.addEventListener("mouseenter", this.handleMouseEnter, false);
this.$carousel.addEventListener("mouseleave", this.handleMouseLeave, false);
this.$nav.addEventListener("click", this.handleClickArrow, false);
}
handleClickTrack = (e) => {
const dot = e.target;
const classList = [...dot.classList];
if (!classList.includes("dot")){
return;
}
this.currentIndex = [].indexOf.call(this.$spliders, dot);
}
handleClickArrow = (e) => {
const arrow = e.target;
const classList = [...arrow.classList];
if (classList.includes("next")){
this.currentIndex ++;
}else if (classList.includes("prev")){
this.currentIndex --;
}
}
handleMouseEnter = () => {
clearInterval(this.autoPlayInerval);
}
handleMouseLeave = () => {
this.autoPlay();
}
initDom = () => {
const firstNode = this.$silders[0].cloneNode(true);
this.$sliderWindow.append(firstNode);
this.$sliderWindow.style.width = this.sliderWidth * this.$silders.length + 'px';
this.$spliderTrack.style.width = this.splideWidth * this.$silders.length + 'px';
}
autoPlay = () => {
this.autoPlayInerval = setInterval(()=> {
this.currentIndex ++;
}, this.duration);
}
moveSlider = () => {
this.$sliderWindow.style.transition = "transform .3s linear";
this.$sliderWindow.style.transform = `translate3d(-${
this.currentIndex * this.sliderWidth}px, 0, 0 )`;
this.$spliders.forEach(s => s.classList.remove("active"));
this.$spliders[this.currentIndex].classList.add("active");
}
reset = () => {
setTimeout(() => {
this.$sliderWindow.style.transition = "none";
this.$sliderWindow.style.transform = `translate3d(0,0,0)`;
this.$spliders.forEach(s => s.classList.remove("active"));
this.$spliders[0].classList.add("active");
}, 500);
}
update = ()=> {
if (this.currentIndex === this.$silders.length - 1){
this.reset();
this._index = 0;
}else {
this.moveSlider();
}
}
}
new Carousel({
el:"#c1",
defaultIndex:0,
duration:3000
});
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.carousel {
position: relative;
margin: 30px auto;
width:768px;
height: 350px;
overflow: hidden;
.slider-window {
position: absolute;
left: 0;
top:0;
width: 3840px;
.slider {
width: 768px;
height: 100%;
float: left;
img {
width: 100%;
height: 100%;
}
}
}
.splide-track {
display:flex;
flex-direction: row;
justify-content: center;
align-items: center;
position: absolute;
left:50%;
transform: translateX(-50%);
border-radius: 10px;
bottom: 30px;
width: 100px;
height: 20px;
background-color:rgba(0,0,0,5);
.dot {
width: 20px;
height: 100%;
cursor: pointer;
&:before{
content: '';
display: block;
width: 10px;
height:10px;
border-radius: 50%;
margin: 5px 5px 5px 5px;
background-color: #ccc;
transition: all .3s linear;
}
&.active:before{
background-color: #fff;
}
}
}
&:hover > .nav label {
opacity: .5;
}
.nav {
width: 100%;
label {
position: absolute;
opacity: 0;
transition: opacity .3 linear;
width: 100px;
height:350px;
line-height: 350px;
text-align: center;
color:#fff;
font-size: 150px;
background-color: rgba($color: #fff, $alpha: 0.3);
text-shadow: 0 0 15px rgb(119,119,119) ;
z-index: 5;
cursor: pointer;
&:hover {
opacity: 1;
}
}
.prev {
left: 0;
}
.next {
right: 0;
}
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Carousel</title>
<link rel="stylesheet" href="style.css">
<script src="./carousel.js" defer></script>
</head>
<body>
<div class="container">
<div class="carousel" id="c1">
<div class="slider-window">
<div class="slider"><img src="images/768-x-350.jpg" /></div>
<div class="slider"><img src="images/768-x-350_1.jpg" /></div>
<div class="slider"><img src="images/768-x-350_3.jpg" /></div>
<div class="slider"><img src="images/BS-Promo-banner-768-350-without-text.jpg" /></div>
<div class="slider"><img src="./images/ae-dxb-scb-mgm-masthdeads-mobile.jpg" alt="" srcset=""></div>
</div>
<div class="splide-track">
<i class="dot active"></i>
<i class="dot"></i>
<i class="dot"></i>
<i class="dot"></i>
<i class="dot"></i>
</div>
<div class="nav">
<label for="" class="prev">‹</label>
<label for="" class="next">›</label>
</div>
</div>
</div>
</body>
</html>