深度揭密轮播插件核心代码的实现过程 深度揭密轮播插件核心代码的实现过程 准备工作 搭建程序骨架 这些功能该怎么实现? 思考

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

准备工作

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!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 >
        <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>

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

深度揭密轮播插件核心代码的实现过程
深度揭密轮播插件核心代码的实现过程
准备工作
搭建程序骨架
这些功能该怎么实现?
思考

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

在线预览

搭建程序骨架

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

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
;( 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 : thisfunction( window, noGlobal ){
    "use strict";
         function Slider( selector, options ) {
        return new Slider.init( selector, options );
    }
    Slider.init=function(selector, params){
        next:function(){},<br>             prev:function(){},<br>             move:function(){}
       }<br>     Slider.init.prototype = Slider.prototype = {<br>      <br>       };
       return Slider
});

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

    1. 点击左、右箭头可以控制滚动

    3. 可以向左、向右拖拽滑动 

    4. 返弹效果

    5. 自动轮播效果

    6. 页码指示

这些功能该怎么实现?

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

深度揭密轮播插件核心代码的实现过程
深度揭密轮播插件核心代码的实现过程
准备工作
搭建程序骨架
这些功能该怎么实现?
思考

移动可以用css3的transform:translate3d来做,也可以用js变化left/top来做,用translate3d来做的核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
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

1
2
3
4
5
6
7
8
9
10
11
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(当前显示的图片索引)的值。如果到了最右边就不能再允许滚动了。

1
2
3
var activeIndex = this.lastIndex--;<br>if(activeIndex > this.lastIndex){
     return;
}

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

1
2
3
4
var activeIndex = --this.activeIndex;
if(activeIndex < this.firstIndex){
  return;
}

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

  深度揭密轮播插件核心代码的实现过程
深度揭密轮播插件核心代码的实现过程
准备工作
搭建程序骨架
这些功能该怎么实现?
思考

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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就可以了。关键代码如下:

1
transition(this.wrap,0);

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

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

  或者:

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

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

1
2
3
4
5
6
7
8
9
10
11
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()方法。不过我这里没有这样做,因为考虑到后面我们还要做手势(鼠标)拖动翻页效果,我决定用事件代理来做。让事件统统都冒泡到包装容器上去处理,这也是提升性能的常用技巧之一。

1
2
3
4
5
6
7
8
9
10
11
12
13
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方法,可以被监听函数自动捕获到,这个函数名是固定的,不明白的可以自行搜索这个函数名。

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

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

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

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

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

3. 反弹怎么做?

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

对于第2点,可以把监听mouseup放到document上去,最好加一个移动的距离大小判断,如果超过容器的大小,就当作是释放了,该反弹的反弹,该滑页的滑页。

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

1
2
3
4
5
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   , 在线预览无图片版本

小结一下:

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

<!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>
		<style type="text/css">
			body {
				padding: 0;
				min- 300px;
				max- 640px;
				margin: 0 auto;
			}
			
			ul {
				list-style: none;
			}
			
			ul,
			li {
				margin: 0;
				padding: 0;
			}
			
			.slider-wrapper {
				position: relative;
				 100%;
				height: 220px;
				overflow: hidden;
				background-color: pink;
			}
			
			.slider-items {
				position: relative;
				height: 100%;
				 
			}
			
			.slider-item:nth-child(odd) {
				background-color: red;
			}
			
			.slider-item:nth-child(even) {
				background-color: green;
			}
			
			.slider-item {
				float: left;
				height: 320px;
				line-height: 320px;
				text-align: center;
			}
			
			.slider-item>img {
				 100%;
				-webkit-user-select: none;
				-ms-user-select: none;
				user-select: none;
				pointer-events: none;
			}
			
			.slider-pagination {
				position: absolute;
				bottom: 10px;
				left: 0;
				 100%;
				text-align: center;
				-webkit-transition-duration: .3s;
				-moz-transition-duration: .3s;
				-o-transition-duration: .3s;
				transition-duration: .3s;
			}
			
			.slider-bullet {
				 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%;
				 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;
			}
		</style>
	</head>

	<body>
		<div >
			<ul class="slider-items">
				<li class="slider-item">1</li>
				<li class="slider-item">2</li>
				<li class="slider-item">3</li>
				<li class="slider-item">4</li>
			</ul>
		</div>
		<script>
			window.onload = function() {
				Slider('#slider')
			}
		</script>
		<script type="text/javascript">
			;
			(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";

				var version = '1.0.0';
				var defaults = {
					speed: 500,
					delay: 5000,
					direction: 'horizontal',
					autoplay: true,
					bounceRatio: 0.5,
					pagination: true,
					loop: true,
					buttons: true,
					paginationClass: 'slider-pagination',
					bulletClass: 'slider-bullet',
					bulletActiveClass: 'slider-bullet-active'
				};

				var isObject = function(obj) {
					return Object.prototype.toString.call(obj) === "[object Object]";
				}

				var extend = function(target, source) {
					if(!isObject(source)) {
						source = {};
					}
					for(var i in target) {
						if(source[i] === undefined) {
							source[i] = target[i]
						}
					}
					return source;
				}
				var Device = (function() {
					var ua = navigator.userAgent;
					var android = ua.match(/(Android);?[s/]+([d.]+)?/);
					var ipad = ua.match(/(iPad).*OSs([d_]+)/);
					var ipod = ua.match(/(iPod)(.*OSs([d_]+))?/);
					var iphone = !ipad && ua.match(/(iPhonesOS)s([d_]+)/);
					return {
						ios: ipad || iphone || ipod,
						android: android,
						desktop: !(ipad || iphone || ipod || android)
					};
				})();

				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';
				}

				function Slider(selector, options) {
					options = extend(defaults, options);
					return new Slider.init(selector, options);
				}

				Slider.init = function(selector, params) {
					var container = document.querySelector(selector);
					var wrap = container.children[0];
					var slides = wrap.children;
					var sliderCount = slides.length;
					if(sliderCount === 0) {
						console.warn('Slider children require at least one');
						return this;
					}
					this.container = container;
					this.wrap = wrap;
					this.slides = slides;
					this.params = {};
					extend(params, this.params);
					this.sliderCount = sliderCount;
					this.lastIndex = sliderCount - 1;
					this.firstIndex = 0;
					this.isAnimating = false;
					this.axis = {
						x: 0,
						y: 0
					};
					this.initSlides();
					this.bindEvents();
				}

				var fn = Slider.init.prototype;

				fn.initSlides = function() {
					var width = this.wrap.clientWidth;
					this.slideWidth = width;
					this.wrap.style.width = width * this.sliderCount + 'px';
					this.bounceWidth = this.params.bounceRatio * width;
					this.activeIndex = 0;
					this.slideStack = [];
					for(var i = 0; i < this.sliderCount; i++) {
						this.slides[i].style.width = width + 'px';
						this.slideStack.push(i);
					}

					if(this.params.pagination) {
						this.createPagination();
					}

					if(this.params.loop) {
						this.createLoopItems();
					}

					if(this.params.buttons) {
						this.createButtons();
					}

					if(this.params.autoplay) {
						this.autoPlay();
					}
				}

				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;
				}

				fn.fixedLoop = function(activeIndex) {
					if(!this.params.loop) return;
					translate3d(this.wrap, this.slideWidth * activeIndex);
					transition(this.wrap, 0);
					this.activeIndex = activeIndex;
					this.isAnimating = false;
				}

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

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

				fn.createPagination = function() {
					var ul = document.createElement('ul');
					var bullets = [];
					var bulletClass = this.params.bulletClass;
					ul.className = this.params.paginationClass;
					for(var i = 0; i < this.sliderCount; i++) {
						var li = document.createElement('li');
						li.className = bulletClass;
						ul.appendChild(li);
						bullets.push(li);
					}
					this.container.appendChild(ul);
					this.bullets = bullets;
					this.setActivePagination();
				}

				fn.createButtons = function() {
					var prev = document.createElement('div');
					var next = document.createElement('div');
					prev.className = 'slider-button prev';
					next.className = 'slider-button next';
					prev.innerHTML = '<';
					next.innerHTML = '>';
					this.container.appendChild(prev);
					this.container.appendChild(next);
				}

				fn.setActivePagination = function() {
					var prevIndex = this.activeBulletIndex;
					var activeIndex = this.activeIndex;
					activeIndex = this.slideStack[activeIndex];
					if(prevIndex === activeIndex) {
						return;
					}
					if(prevIndex !== undefined) {
						this.bullets[prevIndex].className = this.params.bulletClass;
					}
					this.bullets[activeIndex].className += ' ' + this.params.bulletActiveClass;
					this.activeBulletIndex = activeIndex;
				}

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

				fn.next = function() {
					var activeIndex = ++this.activeIndex;
					if(activeIndex > this.lastIndex) {
						this.params.autoplay && clearTimeout(this.timeId);
						return;
					}
					this.isAnimating = true;
					translate3d(this.wrap, activeIndex * this.slideWidth);
					transition(this.wrap, this.params.speed);
					this.setActivePagination();
					if(activeIndex === this.lastIndex) {
						this.fixedNextLoop();
					}
				}

				fn.prev = function() {
					var activeIndex = --this.activeIndex;
					if(activeIndex < this.firstIndex) {
						this.params.autoplay && clearTimeout(this.timeId);
						return;
					}
					this.isAnimating = true;
					translate3d(this.wrap, activeIndex * this.slideWidth);
					transition(this.wrap, this.params.speed);
					this.setActivePagination();
					if(activeIndex === this.firstIndex) {
						this.fixedPrevLoop();
					}
				}

				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);
				}

				fn.transitionend = function() {
					this.isAnimating = false;
				}

				fn.start = function(pageX) {
					this.axis.x = parseInt(pageX);
					if(this.params.autoplay) {
						this.params.autoplay = false;
						this.timeId && clearTimeout(this.timeId);
					}
				}

				fn.move = function(pageX) {
					pageX = parseInt(pageX);
					if(this.isAnimating) return;
					if(this.axis.x === 0) return;
					if(pageX > this.slideWidth || pageX < 0) {
						return;
					}
					var distance = this.axis.x - pageX;
					translate3d(this.wrap, distance + this.slideWidth * this.activeIndex);
					transition(this.wrap, 0);
					if(distance > 0) {
						if(distance > this.bounceWidth) {
							this.next();
							this.axis.x = 0;
						}
					} else {
						if(-distance > this.bounceWidth) {
							this.prev();
							this.axis.x = 0;
						}
					}
				}

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

				fn.handleEvent = function(e) {
					var type = e.type;
					switch(type) {
						case 'mousedown':
							this.start(e.pageX);
							break;
						case 'mousemove':
							this.move(e.pageX);
							break;
						case 'mouseup':
							this.stop();
							break;
						case 'touchstart':
							this.start(e.targetTouches[0].pageX);
							break;
						case 'touchmove':
							this.move(e.targetTouches[0].pageX);
							break;
						case 'touchend':
							this.stop();
							break;
						case 'transitionend':
						case 'WebkitTransitionend':
							this.transitionend();
							break;
						case 'click':
							e.stopPropagation();
							var evt = e.target.className.split(' ')[1];
							if(evt === 'prev' || evt === 'next') {
								this[evt]();
							}
							break;
						default:
							break;
					}
				}

				if(!noGlobal) {
					window.Slider = Slider;
				}
				return Slider;
			});
		</script>
	</body>

</html>

  

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

准备工作

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!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 >
        <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>

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

深度揭密轮播插件核心代码的实现过程
深度揭密轮播插件核心代码的实现过程
准备工作
搭建程序骨架
这些功能该怎么实现?
思考

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

在线预览

搭建程序骨架

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

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
;( 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 : thisfunction( window, noGlobal ){
    "use strict";
         function Slider( selector, options ) {
        return new Slider.init( selector, options );
    }
    Slider.init=function(selector, params){
        next:function(){},<br>             prev:function(){},<br>             move:function(){}
       }<br>     Slider.init.prototype = Slider.prototype = {<br>      <br>       };
       return Slider
});

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

    1. 点击左、右箭头可以控制滚动

    3. 可以向左、向右拖拽滑动 

    4. 返弹效果

    5. 自动轮播效果

    6. 页码指示

这些功能该怎么实现?

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

深度揭密轮播插件核心代码的实现过程
深度揭密轮播插件核心代码的实现过程
准备工作
搭建程序骨架
这些功能该怎么实现?
思考

移动可以用css3的transform:translate3d来做,也可以用js变化left/top来做,用translate3d来做的核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
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

1
2
3
4
5
6
7
8
9
10
11
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(当前显示的图片索引)的值。如果到了最右边就不能再允许滚动了。

1
2
3
var activeIndex = this.lastIndex--;<br>if(activeIndex > this.lastIndex){
     return;
}

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

1
2
3
4
var activeIndex = --this.activeIndex;
if(activeIndex < this.firstIndex){
  return;
}

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

  深度揭密轮播插件核心代码的实现过程
深度揭密轮播插件核心代码的实现过程
准备工作
搭建程序骨架
这些功能该怎么实现?
思考

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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就可以了。关键代码如下:

1
transition(this.wrap,0);

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

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

  或者:

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

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

1
2
3
4
5
6
7
8
9
10
11
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()方法。不过我这里没有这样做,因为考虑到后面我们还要做手势(鼠标)拖动翻页效果,我决定用事件代理来做。让事件统统都冒泡到包装容器上去处理,这也是提升性能的常用技巧之一。

1
2
3
4
5
6
7
8
9
10
11
12
13
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方法,可以被监听函数自动捕获到,这个函数名是固定的,不明白的可以自行搜索这个函数名。

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

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

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

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

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

3. 反弹怎么做?

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

对于第2点,可以把监听mouseup放到document上去,最好加一个移动的距离大小判断,如果超过容器的大小,就当作是释放了,该反弹的反弹,该滑页的滑页。

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

1
2
3
4
5
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   , 在线预览无图片版本

小结一下:

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

<!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>
		<style type="text/css">
			body {
				padding: 0;
				min- 300px;
				max- 640px;
				margin: 0 auto;
			}
			
			ul {
				list-style: none;
			}
			
			ul,
			li {
				margin: 0;
				padding: 0;
			}
			
			.slider-wrapper {
				position: relative;
				 100%;
				height: 220px;
				overflow: hidden;
				background-color: pink;
			}
			
			.slider-items {
				position: relative;
				height: 100%;
				 
			}
			
			.slider-item:nth-child(odd) {
				background-color: red;
			}
			
			.slider-item:nth-child(even) {
				background-color: green;
			}
			
			.slider-item {
				float: left;
				height: 320px;
				line-height: 320px;
				text-align: center;
			}
			
			.slider-item>img {
				 100%;
				-webkit-user-select: none;
				-ms-user-select: none;
				user-select: none;
				pointer-events: none;
			}
			
			.slider-pagination {
				position: absolute;
				bottom: 10px;
				left: 0;
				 100%;
				text-align: center;
				-webkit-transition-duration: .3s;
				-moz-transition-duration: .3s;
				-o-transition-duration: .3s;
				transition-duration: .3s;
			}
			
			.slider-bullet {
				 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%;
				 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;
			}
		</style>
	</head>

	<body>
		<div >
			<ul class="slider-items">
				<li class="slider-item">1</li>
				<li class="slider-item">2</li>
				<li class="slider-item">3</li>
				<li class="slider-item">4</li>
			</ul>
		</div>
		<script>
			window.onload = function() {
				Slider('#slider')
			}
		</script>
		<script type="text/javascript">
			;
			(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";

				var version = '1.0.0';
				var defaults = {
					speed: 500,
					delay: 5000,
					direction: 'horizontal',
					autoplay: true,
					bounceRatio: 0.5,
					pagination: true,
					loop: true,
					buttons: true,
					paginationClass: 'slider-pagination',
					bulletClass: 'slider-bullet',
					bulletActiveClass: 'slider-bullet-active'
				};

				var isObject = function(obj) {
					return Object.prototype.toString.call(obj) === "[object Object]";
				}

				var extend = function(target, source) {
					if(!isObject(source)) {
						source = {};
					}
					for(var i in target) {
						if(source[i] === undefined) {
							source[i] = target[i]
						}
					}
					return source;
				}
				var Device = (function() {
					var ua = navigator.userAgent;
					var android = ua.match(/(Android);?[s/]+([d.]+)?/);
					var ipad = ua.match(/(iPad).*OSs([d_]+)/);
					var ipod = ua.match(/(iPod)(.*OSs([d_]+))?/);
					var iphone = !ipad && ua.match(/(iPhonesOS)s([d_]+)/);
					return {
						ios: ipad || iphone || ipod,
						android: android,
						desktop: !(ipad || iphone || ipod || android)
					};
				})();

				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';
				}

				function Slider(selector, options) {
					options = extend(defaults, options);
					return new Slider.init(selector, options);
				}

				Slider.init = function(selector, params) {
					var container = document.querySelector(selector);
					var wrap = container.children[0];
					var slides = wrap.children;
					var sliderCount = slides.length;
					if(sliderCount === 0) {
						console.warn('Slider children require at least one');
						return this;
					}
					this.container = container;
					this.wrap = wrap;
					this.slides = slides;
					this.params = {};
					extend(params, this.params);
					this.sliderCount = sliderCount;
					this.lastIndex = sliderCount - 1;
					this.firstIndex = 0;
					this.isAnimating = false;
					this.axis = {
						x: 0,
						y: 0
					};
					this.initSlides();
					this.bindEvents();
				}

				var fn = Slider.init.prototype;

				fn.initSlides = function() {
					var width = this.wrap.clientWidth;
					this.slideWidth = width;
					this.wrap.style.width = width * this.sliderCount + 'px';
					this.bounceWidth = this.params.bounceRatio * width;
					this.activeIndex = 0;
					this.slideStack = [];
					for(var i = 0; i < this.sliderCount; i++) {
						this.slides[i].style.width = width + 'px';
						this.slideStack.push(i);
					}

					if(this.params.pagination) {
						this.createPagination();
					}

					if(this.params.loop) {
						this.createLoopItems();
					}

					if(this.params.buttons) {
						this.createButtons();
					}

					if(this.params.autoplay) {
						this.autoPlay();
					}
				}

				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;
				}

				fn.fixedLoop = function(activeIndex) {
					if(!this.params.loop) return;
					translate3d(this.wrap, this.slideWidth * activeIndex);
					transition(this.wrap, 0);
					this.activeIndex = activeIndex;
					this.isAnimating = false;
				}

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

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

				fn.createPagination = function() {
					var ul = document.createElement('ul');
					var bullets = [];
					var bulletClass = this.params.bulletClass;
					ul.className = this.params.paginationClass;
					for(var i = 0; i < this.sliderCount; i++) {
						var li = document.createElement('li');
						li.className = bulletClass;
						ul.appendChild(li);
						bullets.push(li);
					}
					this.container.appendChild(ul);
					this.bullets = bullets;
					this.setActivePagination();
				}

				fn.createButtons = function() {
					var prev = document.createElement('div');
					var next = document.createElement('div');
					prev.className = 'slider-button prev';
					next.className = 'slider-button next';
					prev.innerHTML = '<';
					next.innerHTML = '>';
					this.container.appendChild(prev);
					this.container.appendChild(next);
				}

				fn.setActivePagination = function() {
					var prevIndex = this.activeBulletIndex;
					var activeIndex = this.activeIndex;
					activeIndex = this.slideStack[activeIndex];
					if(prevIndex === activeIndex) {
						return;
					}
					if(prevIndex !== undefined) {
						this.bullets[prevIndex].className = this.params.bulletClass;
					}
					this.bullets[activeIndex].className += ' ' + this.params.bulletActiveClass;
					this.activeBulletIndex = activeIndex;
				}

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

				fn.next = function() {
					var activeIndex = ++this.activeIndex;
					if(activeIndex > this.lastIndex) {
						this.params.autoplay && clearTimeout(this.timeId);
						return;
					}
					this.isAnimating = true;
					translate3d(this.wrap, activeIndex * this.slideWidth);
					transition(this.wrap, this.params.speed);
					this.setActivePagination();
					if(activeIndex === this.lastIndex) {
						this.fixedNextLoop();
					}
				}

				fn.prev = function() {
					var activeIndex = --this.activeIndex;
					if(activeIndex < this.firstIndex) {
						this.params.autoplay && clearTimeout(this.timeId);
						return;
					}
					this.isAnimating = true;
					translate3d(this.wrap, activeIndex * this.slideWidth);
					transition(this.wrap, this.params.speed);
					this.setActivePagination();
					if(activeIndex === this.firstIndex) {
						this.fixedPrevLoop();
					}
				}

				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);
				}

				fn.transitionend = function() {
					this.isAnimating = false;
				}

				fn.start = function(pageX) {
					this.axis.x = parseInt(pageX);
					if(this.params.autoplay) {
						this.params.autoplay = false;
						this.timeId && clearTimeout(this.timeId);
					}
				}

				fn.move = function(pageX) {
					pageX = parseInt(pageX);
					if(this.isAnimating) return;
					if(this.axis.x === 0) return;
					if(pageX > this.slideWidth || pageX < 0) {
						return;
					}
					var distance = this.axis.x - pageX;
					translate3d(this.wrap, distance + this.slideWidth * this.activeIndex);
					transition(this.wrap, 0);
					if(distance > 0) {
						if(distance > this.bounceWidth) {
							this.next();
							this.axis.x = 0;
						}
					} else {
						if(-distance > this.bounceWidth) {
							this.prev();
							this.axis.x = 0;
						}
					}
				}

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

				fn.handleEvent = function(e) {
					var type = e.type;
					switch(type) {
						case 'mousedown':
							this.start(e.pageX);
							break;
						case 'mousemove':
							this.move(e.pageX);
							break;
						case 'mouseup':
							this.stop();
							break;
						case 'touchstart':
							this.start(e.targetTouches[0].pageX);
							break;
						case 'touchmove':
							this.move(e.targetTouches[0].pageX);
							break;
						case 'touchend':
							this.stop();
							break;
						case 'transitionend':
						case 'WebkitTransitionend':
							this.transitionend();
							break;
						case 'click':
							e.stopPropagation();
							var evt = e.target.className.split(' ')[1];
							if(evt === 'prev' || evt === 'next') {
								this[evt]();
							}
							break;
						default:
							break;
					}
				}

				if(!noGlobal) {
					window.Slider = Slider;
				}
				return Slider;
			});
		</script>
	</body>

</html>