昨天,有位叫学智的哥们(和我大学一个同学(绰号-法师)同名不同姓)要做一个手机上的页面切换效果,虽然自己的期望和实际的效果相差并不是很大,但法师追求完美,眼睛里揉不得沙子的秉性,把他逼得急躁,惶恐,甚至有些迷失自我,不得不为解决一个bug不惜搞出十个bug,法师快哭了…… 此时,老天有眼,曾经逼迫他的伟大秉性尚且给他留了点智商,学智想起了我们群,对!就是我们群——HTML5移动开发No.1,群号219118183,欢迎加入!(请原谅我这么狗血的开头 -_-‘ )

以下是对话节选:

CodingSerf: 亲,不知道您要什么效果呢?
学智:就是手指往上滑,下一页就会滑上来盖住这页;向下滑就上一页滑下来盖住这页。
CodingSerf: 亲,这不就是个slider么?
学智: 那应该怎么整哇
CodingSerf: 你也不要哭,静下心来好好想想

于是,我静下心来好好想了想,写了一个插件,为了纪念法师,插件就叫touchslider吧。因为学智用到了Zepto,这也是我喜欢用的移动端lib,所以这是个Zepto的插件。当然在我看来这更像是个移动端页面切换的效果,总之逻辑是差不多的。

插件写的很了草,支持的接口也不多,为什么呢?因为自己写插件够用就好,你随便网上找个插件NB的不行,功能提供一大堆,但你在某个项目中用到的就是那么一两个接口,而你却不得不把他们都load下来,所以有时你也会感觉jQuery太臃肿,很多API都不会用到,于是Zepto的模块化就显出了优势。zepto.js主要提供Dom相关的操作,你想用动画,你想用tap事件?好,zepto.js里是没有这些模块的,你需要加载fx.js和touch.js。

touchslider插件里会用到animate方法来执行动画,所以要依赖zepto的fx.js模块。

之前,学智的需求是要上下纵向滑动的,我们可以放一个接口让插件接受左右横向滑动,但是代码却不会增加多少,因为还是用一样的逻辑,只是一个标识位的不同。

此外,我们可以再开放一个接口来设置当用户滑动多少百分比(0.0~1.0)后才会触发slider,否则就还原到滑动前的位置。

下面是两个Demo页面,在PC端查看记得打开控制台的触摸屏模拟器(emulate touch screen):

qrcode_h 左右横向滑动
qrcode_v 上下纵向滑动

插件使用方法如下:

<script type="text/javascript" src="zepto.touchSlider.js"></script>
<script type="text/javascript">
	$(function(){
		$('.main').touchSlider({direction: 'h',itemSelector:'.page',slidePercent:0.3});
	});
</script>

<div class="main">
    <div class="page page_1">page<p>1</p><a href="http://www.codingserf.com" target="_blank">CodingSerf.com</a></div>
    <div class="page page_2">page<p>2</p><a href="http://www.codingserf.com" target="_blank">CodingSerf.com</a></div>
    <div class="page page_3">page<p>3</p><a href="http://www.codingserf.com" target="_blank">CodingSerf.com</a></div>
    <div class="page page_4">page<p>4</p><a href="http://www.codingserf.com" target="_blank">CodingSerf.com</a></div>
    <div class="page page_5">page<p>5</p><a href="http://www.codingserf.com" target="_blank">CodingSerf.com</a></div>
    <div class="page page_6">page<p>6</p><a href="http://www.codingserf.com" target="_blank">CodingSerf.com</a></div>
</div>

以下是插件的源码(你也可以在github上查看最新版本):

//      zepto.touchslider.js
//      v 1.0
//      David
//      http://www.CodingSerf.com

;(function($){
    //干掉页面bounce效果
    $(document).on('touchmove',function(e){
        e.preventDefault();
    });
    $.fn.touchSlider = function(option){
        var opts = $.extend({}, $.fn.touchSlider.defaults, option), //配置选项
            $slider = this,
            $items = $slider.find(opts.itemSelector),    //获取子元素数组
            sliderStylePosition = $slider.css('position'),
            sliderHeight = $slider.height(),
            sliderWidth = $slider.width(),
            step = 0,
            curItemIndex = 0,
            oldPosition = 0,
            backMove = false,
            forwardMove = false,
            backFirst = false,
            forwardLast = false,
            isAnimate = false,
            isDirectionV = (opts.direction == 'v'),
            distance = isDirectionV ? sliderHeight : sliderWidth;
        //初始化
        $slider.css({'background-color':'#000'})
        sliderStylePosition == 'absolute' || sliderStylePosition == 'fixed' || ($slider.css({'position':'relative'}));  //设置slider容器的position值
        $items.each(function(i,o){
            $items.eq(i).css({
                'position':'absolute',
                'top': isDirectionV ? sliderHeight+'px' : 0,
                'left': isDirectionV ? 0 : sliderWidth +'px',
                'z-index': '500'
            });
        }); //遍历子元素设置样式
        $items.eq(0).css(isDirectionV ? 'top' : 'left', 0); //重置第一个子元素的样式

        //事件注册
        $slider.on('touchstart',function(e){
            var touch = e.touches[0];
            oldPosition = isDirectionV ? touch.clientY : touch.clientX;
            step = 0;
            backMove = false;
            forwardMove = false;
            backFirst = false;
            forwardLast = false;
        }).on('touchmove',function(e){
            var touch = e.touches[0],
                curPosition = isDirectionV ? touch.clientY : touch.clientX,
                gap = curPosition - oldPosition,
                moveItemIndex=0,
                newPosition = 0;
            step += gap;
            //向上/左正向滑
            if(gap<0 && !isAnimate){
                forwardLast = curItemIndex == $items.length-1 && !backMove ? true : false;
                if(backMove){//先向下/右逆向滑,过程中不放手再向上/左正向滑
                    newPosition = -distance+step;
                    moveItemIndex = curItemIndex==0 ? curItemIndex : curItemIndex-1;
                    moveItem(moveItemIndex, newPosition);
                }else if(curItemIndex!==$items.length-1 && !backFirst){
                    newPosition = distance+step;
                    moveItemIndex = curItemIndex+1;
                    moveItem(moveItemIndex, newPosition);
                    forwardMove = true;
                }
            }
            //向下/右逆向滑
            if(gap>0 && !isAnimate){
                backFirst = curItemIndex == 0 && !forwardMove ? true : false;
                if(forwardMove){//先向上/左正向滑,过程中不放手再向下/右逆向滑
                    newPosition = distance+step;
                    moveItemIndex = curItemIndex==$items.length-1 ? curItemIndex : curItemIndex+1;
                    moveItem(moveItemIndex, newPosition);
                }else if(curItemIndex!==0 && !forwardLast){
                    newPosition = -distance+step;
                    moveItemIndex = curItemIndex-1;
                    backMove = true; 
                    moveItem(moveItemIndex, newPosition);
                }
            }
            //更新坐标
            oldPosition = curPosition;
        }).on('touchend',function(e){
            if(forwardMove){
                animateItem(1);
            }
            if(backMove){
                animateItem(-1);
            }
        });
        
        //放手后自动滑向目标位置, flag控制滑动方向
        function animateItem(flag){
            isAnimate = true;
            var percent = Math.abs(step)/distance,
                animTarget = percent>opts.slidePercent ? '0px' : distance*flag+'px',
                animProperty = isDirectionV ? {'top': animTarget} : {'left': animTarget},
                oldTarget = -distance*flag+'px',
                oldProperty = isDirectionV ? {'top': oldTarget} : {'left': oldTarget};
            $items.eq(curItemIndex+flag).animate(animProperty,250,'ease-in',function(){
                if(percent>opts.slidePercent){
                    $items.eq(curItemIndex).css(oldProperty).css('opacity','');
                    $items.eq(curItemIndex+flag).css({'transition':'','z-index':500});
                    curItemIndex += flag;
                    opts.onMoveEnd(curItemIndex);
                }else{
                    $items.eq(curItemIndex).css('opacity','');
                }
                isAnimate = false;
            });
        }

        //设置随手被拖动的坐标
        function moveItem(moveItemIndex, newPosition){ 
            $items.eq(curItemIndex).css('opacity',1-Math.abs(step)/distance);
            $items.eq(moveItemIndex).css(isDirectionV ? 'top' : 'left',newPosition+'px').css({'z-index':1000});
        }

        //链式返回
        return this;
    };
    $.fn.touchSlider.defaults = {
        direction: 'h', //'h' 纵向,'v' 横向
        slidePercent: 0.3,  //拖动超过多少百分比后才翻页
        itemSelector: 'div', //子元素选择器
        onMoveEnd: function(index){ //滑动结束后事件
            //console.log(index);
        }
    };
})(Zepto);

Have a nice weekend~!