滑动轮播
-- 全屏幕滑动轮播图
课程概要
本课程主要讲解 JavaScript 轮播图的原理和开发,在主站首页及营销页中,都会涉及到轮播图的使用。概括了元素的拷贝、回调函数、动画的知识点,还将讲解定时器与清除定时器的方法实现自动轮播。通过本课程,完成一个全屏幕滑动轮播图应用,大家将对轮播图的原理和开发有更深入的了解。
主要包含以下功能:
- 点击下一个箭头,滑动到下一张卡片图
- 点击上一个箭头,滑动到上一张卡片图
- 点击对应的小圆点,滑动到对应的位置
- 当屏幕容器发生变化时候,重新计算不影响显示效果
实现原理
克隆项目的第一个和最后一个分别放在最后一项和第一项,保证在第一项时点击上一个会平滑的展示最后一项的内容。同理在最后一项时候点击下一项会平滑展示第一项的内容。在滑动结束后,瞬间定位到实际位置,例如从第一张图往上滑动到展示克隆的最后一张图,然后定位到实际的最后一张图的实际位置上。
思路:
- 首先必须的及需要存储的值有 每个项目的宽度,有多少个项目(len),当前显示哪个项目
- 头尾添加各一个项目后,重设偏移距离
- 当前显示哪个项目的 index( n ) 值和偏移量 translateX 的关系 -( n + 1 )* x
- 点击获取当前的 index ,然后计算出新的 index ,再交给滚动事件去处理。
- 当页面宽度发生变化时候需要重新计算项目宽度和偏移量,因此需要监听浏览器的尺寸变化
- 当点击的 index 为 -1 时,及会滚动到我们克隆的项目中,在滚动完毕之后需要重新设置偏移距离,瞬间定位到第 len 个项目中去。
- 同理当点击的 index 为 原本项目的数量值时,会滚动到我们克隆的项目中,在滚动完毕之后需要重新设置偏移距离,瞬间定位到初始项目中去。
- 锁,如果连续点击的话,会不断的触发事件,因此我们要加把锁,在开始滚动时把锁关闭,等滚动完毕之后把锁打开。
知识点
本课程涉及到的主要知识点有:
- cloneNode 拷贝元素
<p id="para">text demo</p>
let p = document.getElementById("para");
let p_prime = p.cloneNode(true);
- prepend 从开始位置插入元素
```js
let parentElem = document.createElement('div');
let childElem = document.createElement('p');
parentElem.prepend(childElem);
// 把 p 元素添加到 div 元素的开始位置
- liner 匀速运动
/*
* time 当前时间
* begin 开始位移
* end 结束位移
* duration 全程时间
* return 当前位移
*/
function linear(time, begin, end, duration) {
// 公式:当前时间/全程时间 = 发生位移/全程位置
// 发生位移 = 全程位置 * 当前时间 / 全程时间
// 当前位移 = 发生位移 + 开始位移
return ( end - begin ) * time / duration + begin;
}
- requestAnimationFrame 针频动画
浏览器在下一次重绘之前调用指定的函数来更新动画
function startCircle(el){
let startTime = Date.now(); // 获取启动时间
let cycle = 4000; // 多少秒转一周
requestAnimationFrame(function update(){
let currentTime = Date.now(); // 获取当前时间
let p = (currentTime - startTime) / cycle; // 计算转动多少周
el.style.transform = `rotate(${360 * p }deg)` // 转化为角度
requestAnimationFrame(update) // 下一次渲染继续执行
})
}
var ball = document.getElementById('ball');
startCircle(ball);
function animateTo(begin,end,duration,changeCallback,finishCallback){
// 动画开始的时间
let startTime = Date.now();
requestAnimationFrame(function update(){
// 当前执行的时间
let dataNow = Date.now();
// 当前所消耗的时间
let time = dataNow - startTime;
// 当前发生的位移
let value = PAGE.linear(time,begin,end,duration);
// 执行变化事件回调
typeof changeCallback === 'function' && changeCallback(value)
// 如果动画结束时间大于当前执行时间
if(startTime + duration > dataNow){
// 在执行一次动画渲染
requestAnimationFrame(update)
}else{
// 执行结束回调
typeof finishCallback === 'function' && finishCallback(end)
}
})
}
实现步骤
- 创建单例对象
- 克隆项目
- 滑动到显示位置
- 绑定上一项滑动事件
- 绑定下一项滑动事件
- 加锁与解锁
- 标签切换与高亮
- 位置重置
- 监听浏览器窗口变化
创建单例对象
- 创建单例对象 PAGE
- 定义 PAGE.data 需要存放的数据,
- index 当前第几个
- duration 滑动时长
- isLock 锁
- translateX 偏移量
- defaultLenght 默认的项目数量
- itemWidth 单个项目的长度
- 创建 init 、bind 方法,并在 init 中调用 bind
- 调用 PAGE.init
// 1. 创建单例对象 PAGE
const PAGE = {
// 2. 定义 PAGE.data 需要存放的数据
data: {
index: 0,
duration: 500,
isLock: false,
translateX: 0,
defaultLenght: null,
itemWidth: null
},
// 3. 创建 init 、bind 方法,并在 init 中调用 bind
init: function() {
this.bind();
},
bind: function() {
}
}
// 4. 调用 PAGE.init
PAGE.init();
克隆项目
- 创建 PAGE.clone 克隆项目方法
- 在 init 中调用 clone
- 获取 swiper-item 元素,并克隆第一项和最后一项
- 更新 PAGE.data 中 defaultLenght 、itemWidth、translateX 的值。
- 把克隆的第一项放到 swiper-list 的最后,把克隆的最后一项放在 swiper-list 的第一位
const PAGE = {
...,
init: function() {
// 2. 在 init 中调用 clone
this.clone();
this.bind();
},
...,
// 1. 创建 PAGE.clone 克隆项目方法
clone: function() {
// 3. 获取 swiper-item 元素,并克隆第一项和最后一项
let swiperItem = document.getElementsByClassName('swiper-item');
let firstItem = swiperItem[0].cloneNode();
let lastItem = swiperItem[ swiperItem.length - 1].cloneNode();
// 4. 更新 PAGE.data 中 defaultLenght 、itemWidth、translateX 的值。
let swiperList = document.getElementById('swiper-list');
let index = PAGE.data.index;
let swiperItemWidth = swiperList.offsetWidth;
PAGE.data.defaultLenght = swiperItem.length;
PAGE.data.itemWidth = swiperItemWidth;
PAGE.data.translateX = - (swiperItemWidth + swiperItemWidth * index);
// 5. 把克隆的第一项放到 swiper-list 的最后,把克隆的最后一项放在 swiper-list 的第一位
swiperList.appendChild(firstItem);
swiperList.prepend(lastItem);
},
}
PAGE.init();
滑动到显示位置
- 创建 Page.animateTo 动画函数和 linear 匀速函数
- 创建滑动函数 Page.goIndex,接收一个参数,为滑动到第几项
- 在 PAGE.data 中获取动画时间长、项目宽度、偏移量( duration、itemWidth、translateX )
- 根据传入的 index 计算出滑动结束的目标位置
- 调用 PAGE.animateTo 动画
- 在滑动过程回调中,设置 swiper-list 的偏移值
- 在滑动完毕的回调中,设置借结束位置及更新 PAGE.data 中的当前显示索引以及当前偏移量 ( index、translateX )
- 在 PAGE.clone 方法完毕后调用 Page.goIndex 并传入 PAGE.data.index 默认索引
const PAGE = {
...,
clone: function() {
...,
// 8. 在 PAGE.clone 方法完毕后调用 Page.goIndex 并传入 PAGE.data.index 默认索引
PAGE.goIndex(index);
},
// 2. 创建滑动函数 Page.goIndex,接收一个参数,为滑动到第几项
goIndex: function(index){
// 3. 在 PAGE.data 中获取动画时间长、项目宽度、偏移量( duration、itemWidth、translateX )
let swiperDuration = PAGE.data.duration;
let swiperItemWidth = PAGE.data.itemWidth;
let beginTranslateX = PAGE.data.translateX;
// 4. 根据传入的 index 计算出滑动结束的目标位置
let endTranslateX = - (swiperItemWidth + swiperItemWidth * index);
// 5. 调用 PAGE.animateTo 动画,在滑动过程渲染回调中设置 swiper-list 的偏移值
let swiperList = document.getElementById('swiper-list');
PAGE.animateTo(beginTranslateX,endTranslateX,swiperDuration,function(value){
// 6. 在滑动过程回调中,设置 swiper-list 的偏移值
swiperList.style.transform = `translateX(${value}px)`;
},function(value){
// 7. 在滑动完毕的回调中,设置借结束位置及更新 PAGE.data 中的当前显示索引以及当前偏移量
swiperList.style.transform = `translateX(${value}px)`;
PAGE.data.index = index;
PAGE.data.translateX = value;
})
},
// 1. 创建 Page.animateTo 动画函数和 linear 匀速函数
animateTo:function(begin,end,duration,changeCallback,finishCallback){
let startTime = Date.now();
requestAnimationFrame(function update(){
let dataNow = Date.now();
let time = dataNow - startTime;
let value = PAGE.linear(time,begin,end,duration);
typeof changeCallback === 'function' && changeCallback(value)
if(startTime + duration > dataNow){
requestAnimationFrame(update)
}else{
typeof finishCallback === 'function' && finishCallback(end)
}
})
},
linear: function(time, begin, end, duration) {
return ( end - begin ) * time / duration + begin;
}
}
PAGE.init();
绑定上一项滑动事件
- 获取上一项元素 swiper-prev 并绑定 swpierPrev 在点击时触发
- 创建 swpierPrev 事件
- 获取 PAGE.data 中的当前索引值 index
- 调用 PAGE.goIndex 并传入 index - 1
const PAGE = {
...,
bind: function() {
// 1. 获取上一项元素 swiper-prev 并绑定 swpierPrev 在点击时触发
let swiperPrev = document.getElementById('swiper-prev');
swiperPrev.addEventListener('click',this.swiperPrev);
},
...,
// 2. 创建 swpierPrev 事件
swiperPrev: function() {
// 3. 获取 PAGE.data 中的当前索引值 index
let index = PAGE.data.index;
// 4. 调用 PAGE.goIndex 并传入 index - 1
PAGE.goIndex(index - 1);
},
...,
}
绑定下一项滑动事件
和绑定上一项滑动事件原理及流程一直,在调用 goIndex 的时候,传入下一个索引即可
- 获取上一项元素 swiper-next 并绑定 swpierNext 在点击时触发
- 创建 swpierNext 事件
- 获取 PAGE.data 中的当前索引值 index
- 调用 PAGE.goIndex 并传入 index + 1
const PAGE = {
...,
bind: function() {
...,
// 1. 获取下一项元素 swiper-next 并绑定 swpierNext 在点击时触发
let swpierNext = document.getElementById('swiper-next');
swpierNext.addEventListener('click',this.swpierNext);
},
...,
// 2. 创建 swpierNext 事件
swpierNext: function() {
// 3. 获取 PAGE.data 中的当前索引值 index
let index = PAGE.data.index;
// 4. 调用 PAGE.goIndex 并传入 index + 1
PAGE.goIndex(index + 1);
},
...,
}
加锁与解锁
我们发现在不断点击的时候,会出现 BUG,原因是在滑动的过程中点击,重新触发事件,在交替重复执行。
- 在 goIndex 事件中,调用动画之前获取 PAGE.data.isLock 判断
- 如果已经锁了,就直接返回,不进行滑动事件的触发
- 如果没锁,把锁锁上
- 在滑动动画完成的回调中,解锁
const PAGE = {
...,
goIndex: function(index){
let swiperDuration = PAGE.data.duration;
let swiperItemWidth = PAGE.data.itemWidth;
let beginTranslateX = PAGE.data.translateX;
let endTranslateX = - (swiperItemWidth + swiperItemWidth * index);
let swiperList = document.getElementById('swiper-list');
// 1. 在 goIndex 事件中,调用动画之前获取 PAGE.data.isLock 判断
let isLock = PAGE.data.isLock;
// 2. 如果已经锁了,就直接返回,不进行滑动事件的触发
if(isLock){
return
}
// 3. 如果没锁,把锁锁上
PAGE.data.isLock = true;
PAGE.animateTo(beginTranslateX,endTranslateX,swiperDuration,function(value){
swiperList.style.transform = `translateX(${value}px)`;
},function(value){
swiperList.style.transform = `translateX(${value}px)`;
PAGE.data.index = index;
PAGE.data.translateX = value;
// 4. 在滑动动画完成的回调中,解锁
PAGE.data.isLock = false;
})
},
...,
}
标签切换与高亮
- 获取所有 swiper-pagination-switch 元素
- 循环为其添加 data-index 属性并绑定 swiperSwitch 事件
- 新建 swiperSwitch 方法
- 获取当前点击元素的 dataset 中 index 的值,为对应的索引
- 调用 goIndex 并传入 index
- 新建 hightlight 标签高亮事件,接收第几项高亮的索引 index
- 去除所有 swiper-pagination-switch 的高亮
- 为索引下的 swiper-pagination-switch 添加高亮
- 在 goIndex 滑动结束的回调中调用
const PAGE = {
...,
bind: function() {
...,
// 1. 获取所有 swiper-pagination-switch 元素
let swiperSwitch = document.getElementsByClassName('swiper-pagination-switch');
// 2. 循环为其添加 data-index 属性并绑定 swiperSwitch 事件
for (let i = 0; i < swiperSwitch.length; i++) {
swiperSwitch[i].setAttribute('data-index', i);
swiperSwitch[i].addEventListener('click',this.swiperSwitch);
}
},
...,
// 3. 新建 swiperSwitch 方法
swiperSwitch: function(e) {
// 4. 获取当前点击元素的 dataset 中 index 的值,为对应的索引
let index = e.target.dataset.index;
// 5. 调用 goIndex 并传入 index
index = Number(index);
PAGE.goIndex(index);
},
// 6. 新建 hightlight 标签高亮事件,接收第几项高亮的索引 index
hightlight: function(index) {
// 7. 去除所有 swiper-pagination-switch 的高亮
let swiperItem = document.getElementsByClassName('swiper-pagination-switch');
for (let i = 0; i < swiperItem.length; i++) {
swiperItem[i].className = 'swiper-pagination-switch';
}
// 8. 为索引下的 swiper-pagination-switch 添加高亮
swiperItem[index].className = 'swiper-pagination-switch active';
},
goIndex: function(index) {
...
PAGE.animateTo(beginTranslateX,endTranslateX,swiperDuration,function(value){
...
},function(value){
...
// 9. 在 goIndex 滑动结束的回调中调用
PAGE.hightlight(index);
})
},
}
位置重置
在 goIndex 方法中判断,在什么时候,瞬间重置到哪个位置。
- 在动画回调完成时候,获取默认的项目长度
- 如果当前索引为 -1 时,也是就滑动到我们 clone 的最后一项
- 重设索引为最后一项,并重新计算偏移量
- 如果当前索引为项目长度,也就是滑动到我们 clone 的第一项
- 重设索引为第 0 项目,并重新计算偏移量
const PAGE = {
...,
goIndex: function(index){
let swiperDuration = PAGE.data.duration;
let swiperItemWidth = PAGE.data.itemWidth;
let beginTranslateX = PAGE.data.translateX;
let endTranslateX = - (swiperItemWidth + swiperItemWidth * index);
let swiperList = document.getElementById('swiper-list');
let isLock = PAGE.data.isLock;
if(isLock){
return
}
PAGE.data.isLock = true;
PAGE.animateTo(beginTranslateX,endTranslateX,swiperDuration,function(value){
swiperList.style.transform = `translateX(${value}px)`;
},function(value){
// 1. 在动画回调完成时候,获取默认的项目长度
let swiperLength = PAGE.data.defaultLenght;
// 2. 如果当前索引为 -1 时,也是就滑动到我们 clone 的最后一项
if(index === -1){
// 3. 重设索引为最后一项,并重新计算偏移量
index = swiperLength - 1;
value = - (swiperItemWidth + swiperItemWidth * index);
}
// 4. 如果当前索引为项目长度,也就是滑动到我们 clone 的第一项
if(index === swiperLength){
// 5. 重设索引为第 0 项目,并重新计算偏移量
index = 0;
value = - (swiperItemWidth + swiperItemWidth * index);
}
swiperList.style.transform = `translateX(${value}px)`;
PAGE.data.index = index;
PAGE.data.translateX = value;
PAGE.data.isLock = false;
PAGE.hightlight(index);
})
},
...,
}
监听浏览器窗口变化
- 在 bind 中绑定浏览器 resize 变化事件 swiperReset
- 定义 swiperReset 方法
- 获取 swiper-list 变化后的宽度和当前所在索引
- 计算变化后的偏移量
- 重设 PAGE.data 中的偏移量和宽度
- 设置页面的偏移量
const PAGE = {
...,
bind: function() {
...,
// 1. 在 bind 中绑定浏览器 resize 变化事件 swiperReset
window.addEventListener('resize',this.swiperReset)
},
// 2. 定义 swiperReset 方法
swiperReset: function() {
// 3. 获取 swiper-list 变化后的宽度和当前所在索引
let swiperList = document.getElementById('swiper-list');
let swiperItemWidth = swiperList.offsetWidth;
let index = PAGE.data.index;
// 4. 计算变化后的偏移量
let translateX = - (swiperItemWidth + swiperItemWidth * index);
// 5. 重设 PAGE.data 中的偏移量和宽度
PAGE.data.itemWidth = swiperItemWidth;
PAGE.data.translateX = translateX;
// 6. 设置页面的偏移量
swiperList.style.transform = `translateX(${translateX}px)`;
}
}
代码示例
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>FullSwiper</title>
<style type="text/css">
*{
margin: 0;
padding: 0;
}
.swiper-wrapper{
position: relative;
height: 360px;
overflow: hidden;
}
.swiper-list{
position: relative;
display: flex;
height: 100%;
}
.swiper-item{
width: 100%;
flex: none;
}
.swiper-pagination{
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
}
.swiper-pagination-switch{
display: inline-block;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: #F2EFE6;
}
.swiper-pagination-switch.active{
background-color: #E03636;
}
.swiper-arrow{
position: absolute;
width: 50px;
height: 80px;
top: 50%;
transform: translateY(-50%);
background-color: #EDD0BE;
line-height: 80px;
text-align: center;
color: #666;
cursor: pointer;
opacity: 0;
transition: all .2s ease;
}
.swiper-arrow-left{
left: 100px;
}
.swiper-arrow-right{
right: 100px;
}
.swiper-wrapper:hover .swiper-arrow{
opacity: 1;
}
</style>
</head>
<body>
<div class="swiper-wrapper" id="swiper-wrapper">
<div class="swiper-list" id="swiper-list">
<div class="swiper-item" style="background-color: #82A6F5;">1</div>
<div class="swiper-item" style="background-color: #EAF048;">2</div>
<div class="swiper-item" style="background-color: #9FF048;">3</div>
<div class="swiper-item" style="background-color: #2A5200;">4</div>
<div class="swiper-item" style="background-color: #F6D6FF;">5</div>
</div>
<div class="swiper-pagination">
<span class="swiper-pagination-switch active"></span>
<span class="swiper-pagination-switch"></span>
<span class="swiper-pagination-switch"></span>
<span class="swiper-pagination-switch"></span>
<span class="swiper-pagination-switch"></span>
</div>
<a class="swiper-arrow swiper-arrow-left" id="swiper-prev"><</a>
<a class="swiper-arrow swiper-arrow-right" id="swiper-next">></a>
</div>
<script type="text/javascript">
const PAGE = {
data: {
index: 0,
duration: 500,
isLock: false,
translateX: 0,
defaultLenght: null,
itemWidth: null
},
init: function() {
this.clone();
this.bind();
},
bind: function() {
let swiperPrev = document.getElementById('swiper-prev');
swiperPrev.addEventListener('click',this.swiperPrev);
let swiperNext = document.getElementById('swiper-next');
swiperNext.addEventListener('click',this.swiperNext);
let swiperSwitch = document.getElementsByClassName('swiper-pagination-switch');
for (let i = 0; i < swiperSwitch.length; i++) {
swiperSwitch[i].setAttribute('data-index', i);
swiperSwitch[i].addEventListener('click',this.swiperSwitch);
}
window.addEventListener('resize',this.swiperReset)
},
swiperReset: function(e) {
let swiperList = document.getElementById('swiper-list');
let swiperItemWidth = swiperList.offsetWidth;
let index = PAGE.data.index;
let translateX = - (swiperItemWidth + swiperItemWidth * index);
PAGE.data.itemWidth = swiperItemWidth;
PAGE.data.translateX = translateX;
swiperList.style.transform = `translateX(${translateX}px)`;
},
clone: function() {
let swiperItem = document.getElementsByClassName('swiper-item');
let firstItem = swiperItem[0].cloneNode();
let lastItem = swiperItem[ swiperItem.length - 1].cloneNode();
let swiperList = document.getElementById('swiper-list');
let index = PAGE.data.index;
let swiperItemWidth = swiperList.offsetWidth;
PAGE.data.defaultLenght = swiperItem.length;
PAGE.data.itemWidth = swiperItemWidth;
PAGE.data.translateX = - (swiperItemWidth + swiperItemWidth * index);
swiperList.appendChild(firstItem);
swiperList.prepend(lastItem);
PAGE.goIndex(index);
},
swiperPrev: function() {
let index = PAGE.data.index;
PAGE.goIndex(index - 1);
},
swiperNext: function() {
let index = PAGE.data.index;
PAGE.goIndex(index + 1);
},
swiperSwitch: function(e) {
let index = e.target.dataset.index;
index = Number(index);
PAGE.goIndex(index);
},
goIndex: function(index){
let swiperDuration = PAGE.data.duration;
let swiperItemWidth = PAGE.data.itemWidth;
let beginTranslateX = PAGE.data.translateX;
let endTranslateX = - (swiperItemWidth + swiperItemWidth * index);
let swiperList = document.getElementById('swiper-list');
let isLock = PAGE.data.isLock;
if(isLock){
return
}
PAGE.data.isLock = true;
PAGE.animateTo(beginTranslateX,endTranslateX,swiperDuration,function(value){
swiperList.style.transform = `translateX(${value}px)`;
},function(value){
let swiperLength = PAGE.data.defaultLenght;
if(index === -1){
index = swiperLength - 1;
value = - (swiperItemWidth + swiperItemWidth * index);
}
if(index === swiperLength){
index = 0;
value = - (swiperItemWidth + swiperItemWidth * index);
}
swiperList.style.transform = `translateX(${value}px)`;
PAGE.data.index = index;
PAGE.data.translateX = value;
PAGE.data.isLock = false;
PAGE.hightlight(index);
})
},
hightlight: function(index) {
let swiperItem = document.getElementsByClassName('swiper-pagination-switch');
for (let i = 0; i < swiperItem.length; i++) {
swiperItem[i].className = 'swiper-pagination-switch';
}
swiperItem[index].className = 'swiper-pagination-switch active';
},
animateTo:function(begin,end,duration,changeCallback,finishCallback){
let startTime = Date.now();
requestAnimationFrame(function update(){
let dataNow = Date.now();
let time = dataNow - startTime;
let value = PAGE.linear(time,begin,end,duration);
typeof changeCallback === 'function' && changeCallback(value)
if(startTime + duration > dataNow){
requestAnimationFrame(update)
}else{
typeof finishCallback === 'function' && finishCallback(end)
}
})
},
linear: function(time, begin, end, duration) {
return ( end - begin ) * time / duration + begin;
}
}
PAGE.init();
</script>
</body>
</html>