滑动切换卡片组件

阅读 625
收藏 29
2017-07-13
原文链接:github.com

滑动组件

左鹏飞 2017.07.13

关键功能点

  • 手指拖动时整体跟随手指滑动;松开手指时切换到上/下一张卡片或者归位
  • 拖动第一张和最后一张卡片时,模拟阻力;松开手指时弹性归位
  • 快速左(右)滑时,切换上/下一张卡片
  • 支持回调

关键技术点

  • touch事件中的touches、targetTouches和changedTouches
touches: 当前屏幕上所有触摸点的列表;
targetTouches: 当前对象上所有触摸点的列表;
changedTouches: 涉及当前(引发)事件的触摸点的列表

通过一个例子来区分一下触摸事件中的这三个属性:

1. 用一个手指接触屏幕,触发事件,此时这三个属性有相同的值。

2. 用第二个手指接触屏幕,此时,touches有两个元素,每个手指触摸点为一个值。当两个手指触摸相同元素时,
targetTouches和touches的值相同,否则targetTouches 只有一个值。changedTouches此时只有一个值,
为第二个手指的触摸点,因为第二个手指是引发事件的原因

3. 用两个手指同时接触屏幕,此时changedTouches有两个值,每一个手指的触摸点都有一个值

4. 手指滑动时,三个值都会发生变化

5. 一个手指离开屏幕,touches和targetTouches中对应的元素会同时移除,而changedTouches仍然会存在元素。

6. 手指都离开屏幕之后,touches和targetTouches中将不会再有值,changedTouches还会有一个值,
此值为最后一个离开屏幕的手指的接触点。

touchstart和trouchmove时使用touches和targetTouches都可以;touchend时使用changedTouches

  • 触点坐标选取
touchstart和touchmove使用: e.targetTouches[0].pageX 或 (jquery)e.originalEvent.targetTouches[0].pageX

touchend使用: e.changedTouches[0].pageX 或 (jquery)e.originalEvent.changedTouches[0].pageX
  • 关键的几个初始化值
    // 父容器
    this.parentCon = parentCon;
    // 滚动容器
    this.scrollCon = scrollCon;
    // 每个item的宽度
    this.swipeW = swipeW;
    // 切换成功后的回调
    this.swipeCb = swipeCb;
    // 卡片个数
    this.cardSize = this.scrollCon.children().size();
    // 父容器宽度
    this.wrapW = parentCon.width();
    // 滚动容器宽度
    this.scrollerWidth = this.scrollCon.width();
    // 滚动最大距离
    this.maxScrollX = this.wrapW - this.scrollerWidth - 10 * 2;
    // 滚动容器左边距
    this.paddingLeft = this.scrollCon.offset().left;
    // 是否跟随手指滑动中
    this.isMoving = false;
    // 卡片是否在过渡动画中
    this.isInTransition = false;
    // 当前卡片index
    this.curCardIndex = 0;
  • 滑动的区间的确定: 0 ~ scrollerWidth
  • 当前卡片index, 左滑+1,右滑-1
  • 是否跟随手指滑动中,卡片是否在过渡动画中(这个变量的原因是:手指滑动过程中会反复触发touchend,从而再次触发touchstart;为了节流甚至导致不可预期的错误,最好在touchstart的过程中阻止掉滑动行为)
  • 记录上次最终的偏移量(this.offsetX = 0)
  • 最开始是0;
	init: function () {
        var oThis = this;
        // 记录上次的水平偏移量
        this.offsetX = 0;
        oThis.scrollCon.on('touchstart', function (e) {
        ....
  • 在拖动过程中直接改滑动容器的tanslatex属性,而不要更改this.offsetX,因为拖动只是实现跟随手指动的效果,手指离开时要归位或者翻卡,并不是最终的偏移量
  • 在切换卡片后要记录下当前的偏移量;
	scrollTo: function (x, time) {
        this.isInTransition = true;
        var oThis = this;
        setTimeout(function () {
            oThis.isInTransition = false;
        }, time);
        this.scrollCon.css({
            '-webkit-transform': 'translate3d(' + x + 'px, 0, 0)',
            '-webkit-transition-timing-function': 'cubic-bezier(0.1, 0.3, 0.5, 1)',
            '-webkit-transition-duration': time
        }).attr('x', x);
        this.offsetX = x;
        $.isFunction(oThis.swipeCb) && oThis.swipeCb();

    },
  • 跟随手指滑动

跟随手指滑动的效果即:手指滑动到哪儿容器就跟着到哪儿。主要原理就是计算滑动偏移量,不断的修改tanslateX

	oThis.scrollCon.on('touchmove', function (e) {
            // 滑动中
            oThis.isMoving = true;
            var touches = e.touches ? e.touches[0] : e;
            var timestamp = oThis.getTime();
            oThis.movingX = touches.screenX;
            oThis.movingY = touches.screenY;

            // 水平偏移量
            var diffScreenX = oThis.movingX - oThis.startX;
            // scoller容器应该移动的距离
            var moveScreenX = oThis.offsetX + diffScreenX;

            // 滑动方向
            var moveDirection = oThis.swipeDirection(oThis.startX, oThis.movingX, oThis.startY, oThis.movingY);

            // 不能阻止页面的纵向滚动
            if (moveDirection === 'Left' || moveDirection === 'Right') {
                e.preventDefault();
                e.stopPropagation();
            }

            // 超过边缘滑动有阻力
            if (moveScreenX > 0 || moveScreenX < oThis.maxScrollX) {
                diffScreenX = diffScreenX * 2 / 3;
                moveScreenX = oThis.offsetX + diffScreenX;
            }

            oThis.scrollCon.css({
                '-webkit-transform': 'translate3d(' + moveScreenX + 'px, 0, 0)',
                '-webkit-transition-duration': 'none'
            });
        });
        

注意点

  • 不管左滑(偏移量为负)右滑(偏移量为正),新的trnaslateX值都是加法处理
  • 跟随手指滑动时,不能有动画所以需要:'-webkit-transition-duration': 'none'
  • 滑动方向判断
	// 滑动方向
    swipeDirection: function (x1, x2, y1, y2) {
        return Math.abs(x1 - x2) >= Math.abs(y1 - y2)
            ? (x1 - x2 > 0 ? 'Left' : 'Right')
            : (y1 - y2 > 0 ? 'Up' : 'Down');
    },

横滑不能影响页面上下滚动,所以需要判断是左右滑动才阻止页面滚动行为,上下滑动不能阻止页面滚动

 	// 不能阻止页面的纵向滚动
   if (moveDirection === 'Left' || moveDirection === 'Right') {
         e.preventDefault();
         e.stopPropagation();
   }

  • 超过边缘滑动有阻力效果模拟
if (moveScreenX > 0 || moveScreenX < oThis.maxScrollX) {
       diffScreenX = diffScreenX * 2 / 3;
       moveScreenX = oThis.offsetX + diffScreenX;
}

原理就是在左边和最后边拖动时,把偏移量改为diffScreenX * 2 / 3

  • 如何判断是翻卡行为
  • 触摸动作时间很短 小于300m时
  • 滑动偏移量大于10小于30切到下一张卡或前一张卡
if (duration < 300 || (Math.abs(diffScreenX) > 10 && Math.abs(diffScreenX) < 30)) {
     scrollToFun(moveDirection);
}
  • 拖动后该切换卡片还是归位
if (Math.abs(diffScreenX) >= (oThis.swipeW / 3)) {
       scrollToFun(moveDirection);
}
else {// 归位
       oThis.scrollTo(oThis.offsetX, '300ms');
}

原理:如果拖动的距离大于卡片宽度三分之一时就翻卡;否则就归位

  • 切换卡片还是归位处理函数
				// 切换卡片还是归位
            function scrollToFun(moveDirection) {
                var x;
                if (moveDirection === 'Right') {// 右滑
                    oThis.curCardIndex = oThis.curCardIndex - 1 < 0 ? 0 : oThis.curCardIndex - 1;
                    if (oThis.curCardIndex === 0) {
                        oThis.scrollTo(0, '300ms');
                    }
                    else {
                        if (oThis.curCardIndex === oThis.cardSize - 2) {
                            x = oThis.offsetX
                                 + (oThis.swipeW - ($(window).width() - oThis.swipeW - 10
                                 - oThis.paddingLeft)) + oThis.paddingLeft;
                        }
                        else {
                            x = oThis.offsetX + (oThis.swipeW + 10);
                        }
                        oThis.scrollTo(x, '300ms');
                    }

                }
                else if (moveDirection === 'Left') {// 左滑
                    oThis.curCardIndex = (oThis.curCardIndex + 1) >= oThis.cardSize
                        ? oThis.cardSize - 1 : oThis.curCardIndex + 1;
                    if (oThis.curCardIndex === oThis.cardSize - 1) {
                        oThis.scrollTo(oThis.maxScrollX, '300ms');
                    }
                    else {
                        if (oThis.curCardIndex === 1) {
                            x = oThis.offsetX - oThis.paddingLeft - oThis.swipeW;
                        }
                        else {
                            x = oThis.offsetX - oThis.swipeW - 10;
                        }
                        oThis.scrollTo(x, '300ms');
                    }
                }
            }
  • 向右滑动时如果是第一张卡,就复位到0
  • 向左滑动时如果最后一张卡,就复位到最大偏移量
  • 正常的思路就是如果没有padding和margin以及一个卡片撑满整屏幕的话就好办了, 只需要再上次的位置加一个item卡片的宽度就可以了:(oThis.swipeW + 10);
  • 其他需要注意margin和padding之类的

总结

  • 此次做的功能类似幻灯片的翻页效果,所以没有考虑滚动加速度
  • 其实可以根据角度来判断滑动方向
评论