300行代码揭密轮播插件核心代码的实现过程

  轮播效果在网页中用的不少,swiper是其中最有表明性的做品,它支持水平和竖直滑动,还有反弹效果,兼容移动端和pc端。固然代码量也是至关大的,单是js就有5300行(3.4.0的未缩版本),若不考虑代码利用率和加载速度直接就用了,在移动端比较慎重,好比京东(m.jd.com)的轮播就没有用它,而是本身实现了相似的功能,代码量不多的样子(格式化以后看起来二三百行左右的样子)。那么这个功能若是本身来实现,要怎么作呢?javascript

准备工做

1. 准备几张图片(我这里放了四张)css

2. 搭建目录结构(html+css+images+js)html

3. 编写代码,实现初始状态的显示效果java

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
	<title>轮播</title>
	<link rel="stylesheet" type="text/css" href="slider.css">
	<script src="slider.js"></script>
</head>
<body>
	<div id="slider" class="slider-wrapper">
		<ul class="slider-items">
			<li class="slider-item"><img src="images/pic21.gif" alt="1"></li>
			<li class="slider-item"><img src="images/pic22.gif" alt="2"></li>
			<li class="slider-item"><img src="images/pic23.gif" alt="3"></li>
			<li class="slider-item"><img src="images/pic24.gif" alt="4"></li>
		</ul>
	</div>
	<script>
		Slider('#slider',{});
	</script>
</body>
</html>

写几行样式,先让页面有一种滚动前的初始画面。css3

body {
	padding: 0;
    min-width: 300px;
    max-width: 640px;
    margin: 0 auto; 
}

ul {
	list-style: none;
}

ul,li {
	margin: 0;
	padding: 0;
}

.slider-wrapper {
	position: relative;
	width: 100%;
	height: 220px;
	overflow: hidden;
	-webkit-user-select: none;
    -ms-user-select: none;
    user-select: none;
}

.slider-items {
	position: relative;
	height: 100%;
}

.slider-item {
	float: left;
	text-align: center;
	cursor: pointer;
}

.slider-item > img {
	width: 100%;
    pointer-events: none;
}

.slider-pagination {
	position: absolute;
	bottom: 10px;
	left: 0;
	width: 100%;
	text-align: center;
	-webkit-transition-duration: .3s;
    -moz-transition-duration: .3s;
    -o-transition-duration: .3s;
    transition-duration: .3s;
}

.slider-bullet {
	width: 8px;
    height: 8px;
    margin: 0 5px;
    display: inline-block;
    border-radius: 100%;
    background-color: black;
    opacity: .2;
    cursor: pointer;
}

.slider-bullet-active {
	opacity: 1;
	background-color: #007aff;
}

.slider-button {
	position: absolute;
	top: 50%;
	width: 50px;
	height: 50px;
	text-align: center;
	line-height: 50px;
	margin-top: -25px;
	z-index: 10;
	font-size: 4rem;
	color: gray;
	-webkit-user-select:none;
	user-select:none;
}

.next {
	right: 0px;
}

.prev {
	left: 0px;
}

作好这个静态页,能够帮助咱们在开发过程当中预览效果,方便查找问题,欣赏制做过程带来的乐趣。git

在线预览github

搭建程序骨架

接下来就是写js了,先搭一个程序的架子,我这里仿一下jQ的无new式设计(实际上是本身在内部自动实现new的过程)web

;( function( global, factory ) {
	"use strict";
	if ( typeof module === "object" && typeof module.exports === "object" ) {
		module.exports = global.document ?
			factory( global, true ) :
			function( w ) {
				if ( !w.document ) {
					throw new Error( "Slider requires a window with a document" );
				}
				return factory( w );
			};
	} else {
		factory( global );
	}

// Pass this if window is not defined yet
} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ){
	"use strict";
         function Slider( selector, options ) {
		return new Slider.init( selector, options );
	}
	Slider.init=function(selector, params){
        next:function(){},
prev:function(){},
move:function(){} }
     Slider.init.prototype = Slider.prototype = {
      
}; return Slider });

架子搭好以后,先停下来喝口水,思考一下,最终咱们的这个插件要实现哪些功能。数组

    1. 点击左、右箭头能够控制滚动app

    3. 能够向左、向右拖拽滑动 

    4. 返弹效果

    5. 自动轮播效果

    6. 页码指示

这些功能该怎么实现?

先画几张草图,在纸上模拟一下这个过程,搞明白以后,再用代码来让计算机执行。下图中的蓝框表明显示器的宽度,红框表明图片所在容器的实际宽度。我只要移动红框,那么在屏幕上看起来,就会有轮播滚动的效果。原理看起来是否是很简单?本着先易后难,按部就班的做战方针,先不考虑循环滚动。假设屏幕上显示的是图片1,此时只要把红框往左移一个屏的宽度,那么就会显示2,再移一屏,就能够显示3. 向右滚动正好相反。

移动能够用css3的transform:translate3d来作,也能够用js变化left/top来作,用translate3d来作的核心代码以下:

function translate3d(element,x,y) {
		x = x === undefined ? 0 : x;
		y = y === undefined ? 0 : x;
		element.style['-webkit-transform'] = 'translate3d(-'+x+'px,'+y+'px,0px)';
		element.style['transform'] = 'translate3d(-'+x+'px,'+y+'px,0px)';
	}
	
	function transition(element,time){
		element.style['-webkit-transition-duration'] = time+'ms';
		element.style['transition-duration'] = time+'ms';
	} 

x控制左右移动,y控制竖直移动(本例中暂不考虑),z固定(0px). 当x移动的距离是正数的时候,向左滚动--prev,为负数的时候向右滚动--next

fn.next = function(){
		var activeIndex = ++this.activeIndex;
		translate3d(this.wrap,activeIndex*this.slideWidth);
	 	transition(this.wrap,this.params.speed);
	}

fn.prev = function(){
		var activeIndex = --this.activeIndex;
		translate3d(this.wrap,activeIndex*this.slideWidth);
	 	transition(this.wrap,this.params.speed);
	}

因为图片数量是有限的,不能一直滚动,若是到头了,须要作一些处理。所以须要判断activeIndex(当前显示的图片索引)的值。若是到了最右边就不能再容许滚动了。

var activeIndex = this.lastIndex--;
if(activeIndex > this.lastIndex){ return; }

同理,到了最左边,也不能继续往左滚动了(听起来是一句很是正确的废话)

var activeIndex = --this.activeIndex;
if(activeIndex < this.firstIndex){
  return;
}

如今要考虑自动滚动的状况,若是是自动轮播的状况,那就不能直接return; 要么当到达最右边的时候,activeIndex赋成firstIndex,当达到最左边的时候,activeIndex赋成lastIndex; 这样作的实际效果看起来就是像荡秋千同样,这显然不是咱们想要的效果。要么跳到最前面从新开始,这作出来的实际效果一点也不连续。最后决定去看看swiper是怎么实现的。swiper的作法就是把第一张复制一份放到最后面,把最后一张复制一份插到最前面。 以下图所示:

  

第一张和第四张都被复制了一份。对应的代码以下:

fn.createLoopItems = function(){
		var lastItem = this.slides[this.lastIndex];
		var firstItem = this.slides[this.firstIndex];
		var prevItem = lastItem.cloneNode(true);
		var nextItem = firstItem.cloneNode(true);
		var sliderCount = this.sliderCount+2;
		var slideWidth = this.slideWidth;
		this.slideStack.push(this.firstIndex);
		this.slideStack.unshift(this.lastIndex);
		this.wrap.insertBefore(prevItem,firstItem);
		this.wrap.appendChild(nextItem);
		this.wrap.style.width = slideWidth * sliderCount + 'px';
		translate3d(this.wrap,slideWidth);
		this.activeIndex += 1;
		this.sliderCount = sliderCount;
		this.lastIndex += 2;
	}

不得不认可这个作法很巧妙。随便咱们往哪一个方向翻,都不至于出现空白期。当翻到最未尾(数字4)的时候,还能够再翻一页,即第一张的复制品,虽然不是真的第一张,可是看起来就像是平滑的过渡到了第一张同样。不过这是临时的,咱们须要在过渡完以后,当即回到真正的1的位置上去。由于咱们实际上在未端只补了一张,翻完这一页,若是不进一步处理,仍是会到头。这时,就是问题的关键了。当咱们往右翻,从第四张翻到复制的第一张时,须要悄悄地,人不知,鬼不觉的把红框的位置移到1的真身上来。同时把activeIndex也置为图1的索引。那么问题又来了,怎么才能作到人不知鬼不觉的暗渡陈仓呢?其实很简单,只要把移位动画的时间改为0就能够了。关键代码以下:

transition(this.wrap,0);

不过这一步要在从4变为1,刚停下来的时候,当即执行。作早了,就会感受是直接从4跳到1,没有动画效果,作晚了,就会出现空白,并由于索引溢出而报错。因此这里须要一个修复方法:

fn.fixedPrevLoop = function(){
		var that = this;
		setTimeout(function(){
			that.fixedLoop(that.lastIndex-1)
		},that.params.speed);
	}

  或者:

this.container.addEventListener('transitionend',function(){
//监听动画结速以后再执行判断是否要修复
},false);

作完这一步,就看起来有连续滚动的效果了,在这个基础上实现自动轮播就好办了,只要用定时器,每隔一段时间就执行一下自身就能够了。代码以下:

fn.autoPlay = function(){
		var that = this;
		if(!this.params.autoplay){
			return;
		}
		this.timeId = setTimeout(function(){
			that.next();
			//that.prev();
			that.autoPlay();
		},this.params.delay);
	}

  我注释掉了prev(), 默认都是向右自动轮播。若是要改为是往左轮播,只要在这里加一个一个配置选择就行了。自动循环都作好了,在此基础上点击翻页,也是很容易的事情了,给按钮邦定一个click事件,若是是右边的,就调用next()方法,反之则调用prev()方法。不过我这里没有这样作,由于考虑到后面咱们还要作手势(鼠标)拖动翻页效果,我决定用事件代理来作。让事件通通都冒泡到包装容器上去处理,这也是提高性能的经常使用技巧之一。

fn.bindEvents  = function(){
		if(Device.desktop){
			this.container.addEventListener('mousedown',this,false);
			this.container.addEventListener('mousemove',this,false);
			document.addEventListener('mouseup',this,false);
		}else{
			this.container.addEventListener('touchstart',this,false);
			this.container.addEventListener('touchmove',this,false);
			document.addEventListener('touchend',this,false);
		}
		this.container.addEventListener('transitionend',this,false);
		this.container.addEventListener('click',this,false);
	}

  为何这里的addEventListener为何是邦定this而不是一个函数呢?简单说,是由于上下文中有一个handleEvent方法,能够被监听函数自动捕获到,这个函数名是固定的,不明白的能够自行搜索这个函数名。

fn.handleEvent = function(e){
		var type = e.type;
  //注意这里边的this
}

这样作的好处是能够维持事件邦定的那个函数的上下文。简单说就是不用操心this的指向会变。

作完这一步,就能够作拖动翻页了。在pc上用鼠标,在手机上用手指,处理的方式都是同样的,监听按下,移动,释放这三个事件。按下的时候,记住初始坐标,移动的时候和这个坐标进行对比,计算出移动的距离,而后更新到移动的对象上(红框)。这里边有几个地方须要注意:

1. 若是移动的时候,不是直线,x坐标和y坐标都有改变,是判断成水平拖动仍是垂直拖动?

2. 在pc上,如何判断是拖动,拖出屏幕外了怎么处理?

3. 反弹怎么作?

对于第1点,能够比较两个方向距离的大小,谁大听谁的。若是指向了是水平滚动,那么能够直接忽略竖直方向的变化。

对于第2点,能够把监听mouseup放到document上去,最好加一个移动的距离大小判断,若是超过容器的大小,就看成是释放了,该反弹的反弹,该滑页的滑页。

对于第3点,在拖动释放的时候,判断移动距离,好比拖动的距离小于屏宽的1/3,就反方向translate相应的距离回去,甚至都不用关心这个距离,反正这时的activeIndex没有更新的,直接回到这个activeIndex对应的页就算是反弹了。代码就是这样:

fn.stop = function(){
		this.axis.x = 0;
		translate3d(this.wrap,this.slideWidth*this.activeIndex);
		transition(this.wrap,this.params.speed);
}

接下来就是作页码指示器了,这个简单,翻页成功以后就更新一下对应的小点就是了。因为咱们人为的插了两个页面进去,索引数和页码数就对应不起来了,实际参与滚动的图片有6张,可是只能显示4个点。我作的时候走了一些弯路,如今总结起来无非就是两点:

1. 给小圓点加一个属性用来标记是哪一个页面。用于处理点击滚动的时候,知道是跳到哪一个页面。

2. 用一个数组来保存页面索引,好比【3,0,1,2,3,1】,这样当自动或拖动翻页的时候,能够经过activeIndex的值,肯定要高亮哪一个页码指示器。(小圆点)

也可能还有更好的方法,暂时就先这样实现吧。先把功能作出来,后面有时间,有灵感了再去优化。

到这里,几本上就作完了,只是还要再完善一些边际状况,好比一张图都没有的状况,默认参数的处理等。

思考

除了这个方法以外是否是有其它解决方法呢?好比下图这样,红框中只放三张,多余的叠在屏后面,移动的时候不是移红框,而是真实的移动图片?

 

 这种方法也是能够行的通的,不过我在尝试的时候,发现这种方法在手机上有些卡顿,在电脑上看,容器边框会有闪动,缘由没有深刻去查了。

  咳!咳..,下载源码请上号称全球最大的同性交友网站github.com   , 在线预览无图片版本

小结一下:

写到这里的时候,主体的逻辑差很少就实现了, 只用了大约三百多行代码,虽然很精简,可是这里还有许多东西没有考虑进去,好比竖直方向的滚动,兼容性问题等等。经过画草图的方法来帮助理清思路,遇到困难,能够借鉴别人的实现方法,参考但不是原封不动的复制粘贴。代码组织要考虑扩展性和可读性,先从程序的骨架写起,而后再去写方法,处理细节问题。经过动手实践,能够发现一些看似简单的东西,作起来也不是那么容易的。本身作出来以后,再去看别人写的好的代码,就会知道人家哪些地方比我实现的好,有哪些值得学习的地方。

相关文章
相关标签/搜索