文章目录
轮播图实现方法
常见形式
淡进淡出(如京东)
实现
(不需要开头放最后一张图片,结尾放第一张图片)。
给图片盒子添加定位,改变z-index 决定哪张图片显示(在最上面),通过opacity和过渡实现淡进淡出
从左到右一张一张地滑
实现
利用transform:translate3d实现,这个可以不用前后各多放一张图片
我的实现效果
文字描述要实现的效果
最基础轮播图功能
从第一张往后移,1 2 3 1 2 3 1…这样子,并且是有“缓缓”移动的效果。最后一张与第一张无缝连接。
进阶功能
- 鼠标移入停止轮播,鼠标移出继续轮播
- 点击切换上下一张
- 底部小圆点(或其他)随着图片变化而样式发生变化
- 点击底部小圆点(或其他)可切换至对应图片
- 可拖拽;拖拽超过特定距离切换上下一张,不超过特定距离返回原来的图片。
- 点击切换和拖拽切换需要防连续触发(需要用到防抖和节流,防抖和节流又需要用到闭包)
实现
涉及到的知识
-
轮播图的布局,之前说过了。(基础,容易)(配合css动画可让轮播图动起来)
-
过渡。(基础,用于实现图片切换轮播时的动画效果,极其容易)
-
定时器的开始与清除。用于一直轮播图片。(基础,容易)
-
基础的DOM操作,用于获取图片盒子和移动图片的位置(基础,杂,可能需要计算,容易)(1-4可实现js实现轮播图动起来)
-
定位。(难,必需)(用于实现上下一张按钮和底部小圆点的摆放;不过这个照葫芦画瓢不难)。
-
事件的监听与移除监听。鼠标移入停止移出继续轮播、上下一张,拖拽都需要用到。这个用得最多。(基础,杂,容易)
1-6除拖拽都可以实现了。不过最开始实现的是鼠标移入暂停移出继续轮播
-
防抖和节流,闭包(用于理解和实现防抖和节流)。用于防止点击切换上下一张和拖拽切换上下一张连续触发而导致的一些问题。(进阶,难)
-
拖拽。(进阶,难)
-
另外上下一张按钮还使用了阿里巴巴矢量图标库,即字体图标,不过这个不难,并且可以用别的替代。
10.事件委托。非必须
实现过程
(轮播图动起来----最后一张与第一张无缝衔接)----鼠标移入暂停鼠标移出继续轮播-----上一张下一张按钮布局------点击切换上下一张-----拖拽切换上下一张等功能-------底部小圆点可明确表明当前显示第几张图片-----点击底部小圆点切换图片
为什么底部小圆点放最后边呢,因为它几乎跟其他所有的都有关系,而又不是必须的。
轮播图布局
具体为啥这样子不说了,上一篇讲过了
为什么开头要放最后一张图片呢,因为要考虑到当显示第一张图片用户点击上一张,这时候应该是整体往右移动即把在第一张左边的最后一张图片显示出来,然后移动结束之后迅速把位置移动到真实的第三张图片。
为了方便表述可观看,开头的第三张照片就叫做头部第三张照片,尾部的第一张照片就叫做尾部第一张照片。其他的就是第一张照片这样子。
HTML结构
<div class="banner">
<!-- 上一张按钮 -->
<span class="iconfont icon-next prev" id="bannerPrev"></span>
<ul id="banner">
<li><img src="../images/banner3.jpg"></li>
<li><img src="../images/banner1.jpg"></li>
<li><img src="../images/banner2.jpg"></li>
<li><img src="../images/banner3.jpg"></li>
<li><img src="../images/banner1.jpg"></li>
</ul>
<!-- 下一张按钮 -->
<span class="iconfont icon-previous next" id="bannerNext"></span>
<!-- 底部小圆点 -->
<div class="dotButton" id="dotButton">
<div class="dot" id="dot1"></div>
<div class="dot" id="dot2"></div>
<div class="dot" id="dot3"></div>
</div>
</div>
轮播图自动滚动函数
这个是重点,因为后面所有的功能都跟它有关。
不管用户通过点击上下一张按钮切换图片,还是点击底部小圆点切换图片,但是拖拽切换图片,触发它的时候,它应该都能正常地轮播下去;即不管当前显示第几张图片,它都应该正常轮播。那么它跟我上一篇最大的区别就是,它需要获取当前图片位置,并且轮播到下一张。
下面是这个函数
// 自动滚动函数
// 它能根据轮播图的位置去自动滚动,即任何情况下调用它都没问题
// 只要是 3 1 2 3 1这样子头部多一张最后一张图片 尾部多一张第一张图片的情况 都能用
function changebanner() {
// 获取轮播图数量,为了以后改动不用改这个函数
let sum = document.querySelectorAll('#banner li').length; //长度 3+2=5
timer = setInterval(function () {
let element = document.querySelector('#banner');
let currentPosition = element.style.marginLeft;
//把当前位置转换为数字imgIndex,当imgIndex为0时,图片为“头部最后一张”(即实际位置的第一张)
//当imgIndex为sum-1时,图片”尾部第一张“,实际位置倒数第一张
let imgIndex = Number(currentPosition.replace('vw', '').replace('-', '')) / 100;
// marginLeft 将要移动到的位置
// 初始值应该由当前位置决定,
let marginLeft = (imgIndex * -100) - 100;
// 当是显示的是真实倒数第二张图片(非实际位置),
//先按过渡效果移动到下一张(即“第一张”),然后取消过渡效果,移动到第一张
if (imgIndex == sum - 2) {
element.style.marginLeft = marginLeft + 'vw';
// 监听过渡结束,取消过渡效果并移动到第一张,并且执行完后解除监听
//https://www.51xuediannao.com/html5/visibilitychange.html
element.addEventListener('transitionend', function () {
// 但是有问题,这里面的代码在页面在后台时不会执行
element.style.marginLeft = '-100vw';
element.style.transition = "none";
}, { once: true });
marginLeft = -200;
} else {
// 移动之前添加过渡效果
element.style.transition = "margin-left 0.4s linear";
element.style.marginLeft = marginLeft + 'vw';
imgIndex = imgIndex + 1;
marginLeft = marginLeft - 100;
}
}, 3000)
}
需要注意的是,当tag==sum-2时,即已经移动到第三张图片了(我这里就是三张图片),这个时候需要额外处理,先往后正常移动即移动到3后面的尾部第一张图片,并且监听过渡结束,当过渡结束时取消过渡效果并且把位置移动到第一张照片。
上一篇文章里我用的是setTimeout,有缺点,但程序复杂时不能保证它的执行时间。
这个后面需要多次用到,用于处理最后一张与第一张的无缝衔接。需要理解。
上面提到,当页面不在电脑屏幕可视区域内(比如切换页面)时,过渡结束事件里面的代码不会执行,等到页面再次在电脑屏幕可视区域内才执行,这会导致当我们看不到它的时候,轮播图是不正常的。这个不解决不影响体验,但是京东等都没有这个问题。解决方法就是监听页面状态,当不在电脑屏幕可视区域内时暂停轮播图,回来的时候再启动。(这个可以不用看,并且第二种方法更容易理解一些,没有用到闭包)
// 解决页面在后台时过渡结束事件无法执行
// 在后台时暂停轮播图滚动
document.addEventListener('visibilitychange', leaveStop());
function leaveStop() {
let tag = false;
return function () {
let isHidden = document.hidden;
if (isHidden) {
// 暂停
clearInterval(timer);
timer = null;
tag = true;
} else {
// 判断是否已经暂停了,暂停了再启动,用了闭包
// 其实也可以判断timer定时器是否存在,不存在则启动,如下面
if (tag) {
changebanner();
}
}
}
}
/* document.addEventListener('visibilitychange',function(){
let isHidden = document.hidden;
if(isHidden){
clearInterval(timer);
timer = null;
console.log("定时器?"+timer);
}else{
if(!timer){
console.log(123);
changebanner();
}
}
}) */
鼠标移入暂停移出继续轮播
这个很简单,因为前面的函数设置得好,需要暂停就清除定时器就好,需要继续就调用函数。
不过需要注意的是,定时器清除之后在控制台打印它,依然是个数字,为了避免后面改动程序需要判断定时器有没有被清除,这里认为把它变为null,后面需要判断定时器是否被清除时只需要if(timer==null)
// 悬浮停止
function hoverStop() {
let element = document.querySelector('.banner');
element.addEventListener('mouseover', function () {
clearInterval(timer);
timer = null;
})
}
// 离开开始
function leaveStart() {
let element = document.querySelector('.banner');
element.addEventListener('mouseout', function () {
changebanner();
})
}
上下一张按钮与样式
我这里用到了阿里巴巴矢量图标库,其实你搞个丑一点的按钮,就button也行的。这里应该结合我前面的HTML结构去看。
这里需要用到定位的知识,需要你们额外去了解了。
给父盒子设置绝对定位
.banner {
position: relative;
}
给上下一张按钮设置相对定位,并且使它垂直居中
/* 上下一张按钮 */
.banner span {
position: absolute;
top: 50%;
transform: translateY(-50%);
}
上一张在左边,下一张在右边
/* 上一张 */
.banner .prev {
left: -6px;
}
/* 下一张 */
.banner .next {
right: -6px;
}
点击上下一张按钮切换图片
先给按钮监听点击事件
document.getElementById('bannerNext').addEventListener('click', next());
document.getElementById('bannerPrev').addEventListener('click', prev());
点击下一张按钮切换下一张图片,这个用了节流处理连续点击了,你们可以去搜一下防抖和节流。
我这里尝试解释一下,不懂的可以直接跳过。
点击下一张切换下一张
// 点击下一张,已处理连续点击
function next() {
let tag1 = false;
return function () {
if (!tag1) {
tag1 = true;
let element = document.querySelector('#banner');
let currentPosition = element.style.marginLeft;
let imgIndex = Number(currentPosition.replace('vw', '').replace('-', '')) / 100;
let targetPosition = Number(currentPosition.replace('vw', '')) - 100 + 'vw';
element.style.transition = "margin-left 0.4s linear";
element.style.marginLeft = targetPosition;
if (imgIndex == 3) {
// 监听过渡结束,取消过渡效果并移动到第一张,并且执行完后解除监听
element.addEventListener('transitionend', function () {
element.style.marginLeft = '-100vw';
element.style.transition = "none";
}, { once: true });
}
setTimeout(function () {
imgIndex1 = false;
}, 500)
}
}
}
上面的代码也可以改写成这样,不用闭包,而用全局变量
document.getElementById('bannerNext').addEventListener('click', next);
let tag1 = false;
function next() {
if (!tag1) {
tag1 = true;
let sum = document.querySelectorAll('#banner li').length; //长度 3+2=5
let element = document.querySelector('#banner');
let currentPosition = element.style.marginLeft;
let imgIndex = Number(currentPosition.replace('vw', '').replace('-', '')) / 100;
let targetPosition = Number(currentPosition.replace('vw', '')) - 100 + 'vw';
element.style.transition = "margin-left 0.4s linear";
element.style.marginLeft = targetPosition;
if (imgIndex == sum-2) {
// 监听过渡结束,取消过渡效果并移动到第一张,并且执行完后解除监听
element.addEventListener('transitionend', function () {
element.style.marginLeft = '-100vw';
element.style.transition = "none";
}, { once: true });
}
setTimeout(function () {
tag1 = false;
}, 500)
}
}
我们捋一下业务:当用户点击下一张按钮时,应该是希望第一次点击马上生效并移动,然后接着点的时候不能生效不能移动,应该至少要等到图片移动完成(即过渡完成)时再点击才能生效。
还记得我们前面怎么实现最后一张与第一张无缝衔接的吗,当是到最后一张图片时,先正常往后移动到尾部第一张图片,然后监听过渡结束,过渡结束后马上取消过渡效果并把位置移动到第一张图片(我们这里也是这样子处理的)。如果我们不限制,那么图片还没移动完,就继续往后移动了,过渡完成事件里面的代码不会执行,因为那时候imgIndex已经不是3了。
那么怎么限制呢
我在这里斗胆尝试解释一下。懂的就可以跳过
document.getElementById('bannerNext').addEventListener('click', next());
函数后面加()代表执行,next()执行的结果是,声明了一个变量tag1并赋值为 false,然后返回 function(){}。这里的意思是给下一张按钮监听了点击事件,当点击时就执行function(){}里面的内容。因为function(){}里面用到了tag1,所以tag1并没有马上被销毁。
function(){}里面的代码的意思是,只有当tag1为false的时候代码才会执行,并且一开始就把tag1的值变为true,接着是正常执行代码,再声明个定时器,500微妙后把tag1的值赋为false。所以连续点击下一张按钮,第一次相关代码会执行,然后等到500微妙后再点击才能触发。因为过渡是400微妙,所以肯定能保证过渡结束后再触发。这里的时间可以根据情况而修改,但是不能小于过渡的时间。
后面拖拽上下一张也用到类似的处理。但是又不完全一样。
如果不能理解,就先这个没处理连续点击的吧
function next() {
let element = document.querySelector('#banner');
let currentPosition = element.style.marginLeft;
let imgIndex = Number(currentPosition.replace('vw', '').replace('-', '')) / 100;
let targetPosition = Number(currentPosition.replace('vw', '')) - 100 + 'vw';
element.style.transition = "margin-left 0.4s linear";
element.style.marginLeft = targetPosition;
if (imgIndex == 3) {
// 监听过渡结束,取消过渡效果并移动到第一张,并且执行完后解除监听
element.addEventListener('transitionend', function () {
element.style.marginLeft = '-100vw';
element.style.transition = "none";
}, { once: true });
}
}
并且监听也需要改
document.getElementById('bannerNext').addEventListener('click', next);
点击上一张切换上一张
// 点击上一张,已处理连续点击
function prev() {
let tag1 = false;
return function () {
if (!tag1) {
tag1 = true;
let element = document.querySelector('#banner');
let sum = document.querySelectorAll('#banner li').length;
let currentPosition = element.style.marginLeft;
let imgIndex = Number(currentPosition.replace('vw', '').replace('-', '')) / 100;
let targetPosition = Number(currentPosition.replace('vw', '')) + 100 + 'vw';
element.style.transition = "margin-left 0.4s linear";
element.style.marginLeft = targetPosition;
if (imgIndex == 1) {
// 监听过渡结束,取消过渡效果并移动到第一张,并且执行完后解除监听
element.addEventListener('transitionend', function () {
element.style.marginLeft = `-${sum-2}00vw`;
element.style.transition = "none";
}, { once: true });
}
setTimeout(function () {
tag1 = false;
}, 500)
}
}
}
同理,不再解释。
拖拽切换上下一张等功能
(这个功能我看了很多网站都没有,可以不看)
首先你得知道怎么实现元素拖拽。
这是很好的一篇帖子,里面的代码没有一句是没有用的。
这里简单讲一下,HTML5有拖拽事件,但不好用。拖拽包含了三个鼠标事件,鼠标按下,鼠标移动,鼠标松开。给需要拖拽的元素监听鼠标按下事件,并且在事件函数里监听document的鼠标移动和鼠标松开事件,拖拽在鼠标移动事件里写,鼠标松开事件里应该移除鼠标移动和鼠标松开的监听。
当拖拽鼠标移动的时候,拖拽元素的目标位置是由拖拽元素上一次的位置和鼠标移动的距离决定的。比如拖拽元素上一次是在x轴10px处,鼠标往右移动了10px,那么拖拽元素的目标位置就是10+10 (px)。当然拖拽是实时发生的。 鼠标移动的距离(记为dx)就是当前鼠标位置-上一次鼠标位置。
我们这里只需要x方向的移动。我们要判断当鼠标移动超过一定距离后,切换图片;不够则返回原来的图片,所以我们要记录鼠标移动总距离(即位移),它的值就是dx的累加和。
我们这里是这样。给元素(包裹着图片的外面的盒子)监听鼠标按下事件,当鼠标按下时,给document监听鼠标移动和鼠标松开事件并且记录鼠标的位置,记录元素当前位置;当鼠标移动时,计算鼠标移动的距离(dx)并实时更新元素的位置(不需要过渡效果),也需要计算鼠标总移动距离也就是位移;当鼠标松开时,添加过渡效果,根据鼠标总移动距离判断是要返回原来的位置,还是切换上下一张图片,并且要移除鼠标移动和鼠标松开的监听,鼠标总移动距离需要清零。
这里面防连续触发的处理也用到了节流。不过我这里的业务逻辑是当拖拽完成后(包括切换图片),就能触发拖拽,所以我声明了个dragging变量,代表是否正在拖拽,是否能触发拖拽由它的值决定。
代码如下:
// 初始化鼠标位置
let x = 0;
// 鼠标总移动距离 鼠标位移
let total_dx = 0;
// 拖拽之前图片位置,用于拖拽距离不足弹回
let lastMarginLeft = '';
let element = document.querySelector('#banner');
function addmousedown() {
element.addEventListener('mousedown', mousedown1());
}
// dragging 是否正在拖拽
/* 当正在拖拽时,拖拽不能生效;当拖拽完成后,拖拽才能生效
鼠标按下并移动,拖拽开始,dragging为true;到图片移动过渡完成,拖拽结束,dragging为false。
*/
let dragging;
function mousedown1(e) {
dragging = false;
return function (e) {
if (!dragging) {
lastMarginLeft = element.style.marginLeft;
//记录鼠标的位置
x = e.clientX;
document.addEventListener('mousemove', imgUlMove);
document.addEventListener('mouseup', imgUlUp);
element.addEventListener('transitionend', function () {
dragging = false;
}, { once: true });
}
}
}
// 鼠标移动
function imgUlMove(e) {
// 拖拽开始
dragging = true;
// 鼠标移动距离
let dx = e.clientX - x;
//鼠标总移动距离 也就是位移
total_dx = total_dx + dx;
// 把当前margin-left转换为px单位,因为vw只有100,精度不高
let marginLeft_px = 0;
if (element.style.marginLeft.indexOf('vw') != -1) {
marginLeft_px = Number((element.style.marginLeft.replace('vw', '')) / 100) * window.innerWidth;
} else {
marginLeft_px = Number(element.style.marginLeft.replace('px', ''));
}
// 拖拽时不需要过渡效果,切换图片才需要
element.style.transition = "none";
element.style.marginLeft = marginLeft_px + dx + 'px';
x = e.clientX;
}
// 鼠标松开
function imgUlUp() {
element.style.transition = "margin-left 0.4s linear";
let sum = document.querySelectorAll('#banner li').length;
// 鼠标位移小于 120px 图片弹回去
if (total_dx > -120 && total_dx < 120) {
element.style.marginLeft = lastMarginLeft;
} else {
let imgIndex = Number(lastMarginLeft.replace('vw', '').replace('-', '')) / 100;
// 向左 正常的轮播图滚动方向
if (total_dx <= -120) {
element.style.marginLeft = Number(lastMarginLeft.replace('vw', '')) - 100 + 'vw';
if (imgIndex == sum-2) {
// 监听过渡结束,取消过渡效果并移动到第一张,并且执行完后解除监听
element.addEventListener('transitionend', function () {
element.style.marginLeft = '-100vw';
element.style.transition = "none";
}, { once: true });
}
// 向右
} else {
element.style.marginLeft = Number(lastMarginLeft.replace('vw', '')) + 100 + 'vw';
if (imgIndex == 1) {
// 监听过渡结束,取消过渡效果并移动到第三张,并且执行完后解除监听
element.addEventListener('transitionend', function () {
element.style.marginLeft = `-${sum-2}00vw`;
element.style.transition = "none";
}, { once: true });
}
}
}
document.removeEventListener('mousemove', imgUlMove);
document.removeEventListener('mouseup', imgUlUp);
// 鼠标松开,鼠标位移(最后移动距离)应该清零
total_dx = 0;
}
底部小圆点样式
这里不介绍了,因为每个人想要的样式不一样。直接贴出代码了。需要结合前面HTML结构来看
/* 底部小圆点按钮 */
.banner .dotButton {
position: absolute;
bottom: 6%;
left: 50%;
transform: translateX(-50%);
height: 20px;
display: flex;
align-items: center;
}
.banner .dotButton .dot {
box-sizing: content-box;
width: 12px;
margin-right: 20px;
height: 12px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.4);
}
.banner .dotButton .dotactive {
background-color: white;
transform: scale(1.3);
}
.banner .dotButton .dot:hover {
background-color: white;
transform: scale(1.3);
}
底部小圆点可明确表明当前显示第几张图片
每次切换图片都会涉及过渡,我们可以监听过渡结束事件,然后判断当前显示第几张照片,修改相应的小圆点样式。
我这种实现方法,有三种情况,一种是当前显示的是第一张图片(imgIndex = 1或imgIndex=n+1)n指的是多少张轮播的图片(这里是3张);一种是最后一张(imgIndex = 0或imgIndex = n);最后一种情况当n是几,就是显示第几张图片了。
// 下面小圆点按钮跟随图片变色 通过监听过渡结果事件来实现
// 它能根据轮播图的数量来变色
// 只要是 3 1 2 3 1这样子头部多一张最后一张图片 尾部多一张第一张图片的情况 都能用
function changeDotStyle() {
element.addEventListener('transitionend', function () {
// 就是图片排列的位置
// 0 1 2 3 4
// 第三张 第一张 第二张 第三张 第一张
// 如果是4张图片呢
// 0 1 2 3 4 5
// 第四张 第一张 第二张 第三张 第四张 第一张
// n张图片 1-n都是对应第几张图片 0是第n张图片 n+1是第一张图片
let imgIndex = Number(element.style.marginLeft.replace('vw', '').replace('-', '')) / 100;
let sum = document.querySelectorAll('#banner li').length - 2;
let dotEles = document.querySelectorAll('#dotButton .dot');
// 当前显示图片是最后一张
if (imgIndex == 0 || imgIndex == sum) {
for (let i = 0; i < dotEles.length - 1; i++) {
dotEles[i].classList.remove('dotactive');
}
dotEles[dotEles.length-1].classList.add('dotactive');
// 当前显示图片是第一张
} else if (imgIndex == 1 || imgIndex == sum + 1) {
for (let i = 1; i < dotEles.length; i++) {
dotEles[i].classList.remove('dotactive');
}
dotEles[0].classList.add('dotactive');
// 其他情况 非第一张非最后一张
} else {
for (let i = 0; i < dotEles.length; i++) {
dotEles[i].classList.remove('dotactive');
}
dotEles[imgIndex-1].classList.add('dotactive');
}
})
}
点击底部小圆点切换图片
这个啥都不需要考虑,比较简单。这里用到了实践委托,你直接用onclick也行。
// 点击下面的小圆点跳转图片
function clickChangeBanner() {
let dotElement = document.querySelector('.banner .dotButton');
dotElement.addEventListener('click', function (e) {
let target = e.target || e.srcElement;
if (target.className == 'dot') {
let num = Number(target.id.replace('dot', ''));
element.style.transition = "margin-left 0.4s linear";
element.style.marginLeft = `${num * -100}vw`;
}
})
}
好了,已经写完了。
源码:
轮播图
https://www.aliyundrive.com/s/7Wc1eZgKLF7
提取码: 7y4n
点击链接保存,或者复制本段内容,打开「阿里云盘」APP ,无需下载极速在线查看,视频原画倍速播放。
你直接修改图片路径数组里面的数据就可以改成你的了