移动端效果之Swiper详解


Posted in Javascript onOctober 09, 2017

写在前面

最近在做移动端方面运用到了饿了么的vue前端组件库,因为不想单纯用组件而使用它,故想深入了解一下实现原理。后续将会继续研究一下其他的组件实现原理,有兴趣的可以关注下。

移动端效果之Swiper详解

代码在这里:戳我

1. 说明

父容器overflow:hidden;,子页面transform:translateX(-100%);width:100%;

2. 核心解析

2.1 页面初始化

由于所有页面都在手机屏幕左侧一个屏幕宽度的位置,因此最开始的情况是页面中看不到任何一个子页面,所以第一步应该设置应该显示的子页面,默认情况下defaultIndex:0

function reInitPages() {
  // 得出页面是否能够被滑动
  // 1. 子页面只有一个
  // 2. 用户手动设置不能滑动 noDragWhenSingle = true
  noDrag = children.length === 1 && noDragWhenSingle;

  var aPages = [];
  var intDefaultIndex = Math.floor(defaultIndex);
  var defaultIndex = (intDefaultIndex >= 0 && intDefaultIndex < children.length) 
    ? intDefaultIndex : 0;
  
  // 得到当前被激活的子页面索引
  index = defaultIndex;

  children.forEach(function(child, index) {
    aPages.push(child);
    // 所有页面移除激活class
    child.classList.remove('is-active');

    if (index === defaultIndex) {
      // 给激活的子页面加上激活class
      child.classList.add('is-active');
    }
  });

  pages = aPages;
}

2.2 容器滑动开始(onTouchStart)

在低版本的android手机上,设置event.preventDefault()会起到一定的性能提升作用,使得滑动起来不是那么卡。

前置工作:

  • 如果用户设置了 prevent:true, 滑动时阻止默认行为
  • 如果用户设置了stopPropagation:true, 滑动时阻止事件向上传播
  • 如果动画尚未结束,阻止滑动
  • 设置dragging:true,滑动开始
  • 设置用户滚动为false

滑动开始:

使用一个全局对象记录信息,这些信息包括:

dragState = {
  startTime      // 开始时间
  startLeft      // 开始的X坐标
  startTop      // 开始的Y坐标(相对于整个页面viewport pageY)
  startTopAbsolute  // 绝对Y坐标(相对于文档顶部 clientY)
  pageWidth      // 一个页面宽度
  pageHeight     // 一个页面的高度
  prevPage      // 上一个页面
  dragPage      // 当前页面
  nextPage      // 下一个页面
};

2.3 容器滑动(onTouchMove)

套用全局dragState,记录新的信息

dragState = {
  currentLeft     // 开始的X坐标
  currentTop     // 开始的Y坐标(相对于整个页面viewport pageY)
  currentTopAbsolute // 绝对Y坐标(相对于文档顶部 clientY)
};

那么我们就可以通过开始和滑动中的信息来计算出一些东西:

滑动的水平位移(offsetLeft = currentLeft - startLeft)

滑动的垂直位移(offsetTop = currentTopAbsolute - startTopAbsolute)

是否是用户的自然滚动,这里的自然滚动说的是用户并不是想滑动swiper,而是想滑动页面

// 条件
// distanceX = Math.abs(offsetLeft);
// distanceY = Math.abs(offsetTop);
distanceX < 5 || ( distanceY >= 5 && distanceY >= 1.73 * distanceX )

判断是左移还是右移(offsetLeft < 0 左移,反之,右移)

重置位移

// 如果存在上一个页面并且是左移
if (dragState.prevPage && towards === 'prev') {
  // 重置上一个页面的水平位移为 offsetLeft - dragState.pageWidth
  // 由于 offsetLeft 一直在变化,并且 >0
  // 那么也就是说 offsetLeft - dragState.pageWidth 的值一直在变大,但是仍未负数
  // 这就是为什么当连续属性存在的时候左滑会看到上一个页面会跟着滑动的原因
  // 这里的 translate 方法其实很简单,在滑动的时候去除了动画效果`transition`,单纯改变位移
  // 而在滑动结束的时候,加上`transition`,使得滑动到最后释放的过渡更加自然
  translate(dragState.prevPage, offsetLeft - dragState.pageWidth);
}

// 当前页面跟着滑动
translate(dragState.dragPage, offsetLeft);

// 后一个页面同理
if (dragState.nextPage && towards === 'next') {
  translate(dragState.nextPage, offsetLeft + dragState.pageWidth);
}

2.4 滑动结束(onTouchEnd)

前置工作:

在滑动中,我们是可以实时地来判断到底是不是用户的自然滚动userScrolling,如果是用户自然滚动,那么swiper的滑动信息就不算数,因此要做一些清除操作:

dragging = false;
dragState = {};

当然如果userScrolling:false,那么就是滑动子页面,执行doOnTouchEnd方法

判断是否是tap事件

// 时间小于300ms,click事件延迟300ms触发
// 水平位移和垂直位移栋小于5像素
if (dragDuration < 300) {
  var fireTap = Math.abs(offsetLeft) < 5 && Math.abs(offsetTop < 5);
  if (isNaN(offsetLeft) || isNaN(offsetTop)) {
    fireTap = true;
  }
  if (fireTap) {
    console.log('tap');
  }
}

判断方向

// 如果事件间隔小于300ms但是滑出屏幕,直接返回
if (dragDuration < 300 && dragState.currentLeft === undefined) return;

// 如果事件间隔小于300ms 或者 滑动位移超过屏幕宽度 1/2, 根据位移判断方向
if (dragDuration < 300 || Math.abs(offsetLeft) > pageWidth / 2) {
  towards = offsetLeft < 0 ? 'next' : 'prev';
}

// 如果非连续,当处于第一页,不会出现上一页,当处于最后一页,不会出现下一页
if (!continuous) {
  if ((index === 0 && towards === 'prev') 
    || (index === pageCount - 1 && towards === 'next')) {
    towards = null;
  }
}

// 子页面数量小于2时,不执行滑动动画
if (children.length < 2) {
  towards = null;
}

执行动画

// 当没有options的时候,为自然滑动,也就是定时器滑动
function doAnimate(towards, options) {
  if (children.length === 0) return;
  if (!options && children.length < 2) return;

  var prevPage, nextPage, currentPage, pageWidth, offsetLeft;
  var pageCount = pages.length;

  // 定时器滑动
  if (!options) {
    pageWidth = element.clientWidth;
    currentPage = pages[index];
    prevPage = pages[index - 1];
    nextPage = pages[index + 1];
    if (continuous && pages.length > 1) {
      if (!prevPage) {
        prevPage = pages[pages.length - 1];
      }

      if (!nextPage) {
        nextPage = pages[0];
      }
    }

    // 计算上一页与下一页之后
    // 重置位移
    // 参看doOnTouchMove
    // 其实这里的options 传与不传也就是获取上一页信息与下一页信息
    if (prevPage) {
      prevPage.style.display = 'block';
      translate(prevPage, -pageWidth);
    }

    if (nextPage) {
      nextPage.style.display = 'block';
      translate(nextPage, pageWidth);
    }
  } else {
    prevPage = options.prevPage;
    currentPage = options.currentPage;
    nextPage = options.nextPage;
    pageWidth = options.pageWidth;
    offsetLeft = options.offsetLeft;
  }

  var newIndex;
  var oldPage = children[index];

  // 得到滑动之后的新的索引
  if (towards === 'prev') {
    if (index > 0) {
      newIndex = index - 1;
    }
    if (continuous && index === 0) {
      newIndex = pageCount - 1;
    }
  } else if (towards === 'next') {
    if (index < pageCount - 1) {
      newIndex = index + 1;
    }
    if (continuous && index === pageCount - 1) {
      newIndex = 0;
    }
  }

  // 动画完成之后的回调
  var callback = function() {
    // 得到滑动之后的激活页面,添加激活class
    // 重新赋值索引
    if (newIndex !== undefined) {
      var newPage = children[newIndex];
      oldPage.classList.remove('is-active');
      newPage.classList.add('is-active');
      index = newIndex
    }

    if (isDone) {
      end();
    }

    if (prevPage) {
      prevPage.style.display = '';
    }

    if (nextPage) {
      nextPage.style.display = '';
    }
  }

  setTimeout(function() {
    // 向后滑动
    if (towards === 'next') {
      isDone = true;
      before(currentPage);
      // 当前页执行动画,完成后执行callback
      translate(currentPage, -pageWidth, speed, callback);
      if (nextPage) {
        // 下一面移动视野中
        translate(nextPage, 0, speed)
      }
    } else if (towards === 'prev') {
      isDone = true;
      before(currentPage);
      translate(currentPage, pageWidth, speed, callback);
      if (prevPage) {
        translate(prevPage, 0, speed);
      }
    } else {
     // 如果既不是左滑也不是右滑
     isDone = true;
     // 当前页面依旧处于视野中
     // 上一页和下一页滑出
     translate(currentPage, 0, speed, callback);
     if (typeof offsetLeft !== 'undefined') {
       if (prevPage && offsetLeft > 0) {
          translate(prevPage, pageWidth * -1, speed);
       }
       if (nextPage && offsetLeft < 0) {
          translate(nextPage, pageWidth, speed);
       }
     } else {
      if (prevPage) {
       translate(prevPage, pageWidth * -1, speed);
      }

      if (nextPage) {
       translate(nextPage, pageWidth, speed);
      }
     }
    }
  }, 10);
}
​

后置工作:

清除一次滑动周期中保存的状态信息

dragging = false;
dragState = {};

总结

整体来说实现原理还是比较简单的,滑动开始记录初始位置,计算上一页与下一页的应该展示的页面;滑动中计算位移,计算上一页下一页的位移;滑动结束根据位移结果执行相应的动画。

有一个细节就是,在滑动中transition的效果置为空,是为了防止在滑动中上一页与下一页因为过渡存在而位移得不自然,在滑动结束后再给他们加上动画效果。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
Javascript 获取LI里的内容
Dec 17 Javascript
7个JS基础知识总结
Mar 05 Javascript
如何在MVC应用程序中使用Jquery
Nov 17 Javascript
AngularJS入门教程(二):AngularJS模板
Dec 06 Javascript
jQuery实现只允许输入数字和小数点的方法
Mar 02 Javascript
jQuery插件EasyUI获取当前Tab中iframe窗体对象的方法
Aug 05 Javascript
详解微信小程序 通过控制CSS实现view隐藏与显示
May 24 Javascript
微信JS SDK接入的几点注意事项(必看篇)
Jun 23 Javascript
JavaScript代码判断输入的字符串是否含有特殊字符和表情代码实例
Aug 17 Javascript
JavaScript中Require调用js的实例分享
Oct 27 Javascript
vue-router结合vuex实现用户权限控制功能
Nov 14 Javascript
vue学习笔记之作用域插槽实例分析
Feb 01 Javascript
浅谈node的事件机制
Oct 09 #Javascript
JS实现中文汉字按拼音排序的方法
Oct 09 #Javascript
ES6中的Promise代码详解
Oct 09 #Javascript
js+html5生成自动排列对话框实例
Oct 09 #Javascript
jQuery实现获取table中鼠标click点击位置行号与列号的方法
Oct 09 #jQuery
详解如何让Express支持async/await
Oct 09 #Javascript
jQuery插件artDialog.js使用与关闭方法示例
Oct 09 #jQuery
You might like
php图片验证码代码
2008/03/27 PHP
php发送post请求的三种方法
2014/02/11 PHP
php无限遍历目录示例
2014/02/21 PHP
Symfony2实现在doctrine中内置数据的方法
2016/02/05 PHP
php处理静态页面:页面设置缓存时间实例
2017/06/22 PHP
使用php自动备份数据库表的实现方法
2017/07/28 PHP
Aster vs KG BO3 第二场2.19
2021/03/10 DOTA
javascript 面向对象全新理练之数据的封装
2009/12/03 Javascript
jquery 定位input元素的几种方法小结
2013/07/28 Javascript
node.js中的fs.chown方法使用说明
2014/12/16 Javascript
js获取内联样式的方法
2015/01/27 Javascript
jquery实现简单合拢与展开网页面板的方法
2015/09/01 Javascript
JavaScript如何实现组合列表框中元素移动效果
2016/03/01 Javascript
防止Node.js中错误导致进程阻塞的办法
2016/08/11 Javascript
AngularJS中$http服务常用的应用及参数
2016/08/22 Javascript
详解Angular.js指令中scope类型的几种特殊情况
2017/02/21 Javascript
vuejs2.0运用原生js实现简单的拖拽元素功能示例
2017/02/24 Javascript
AngularJs 最新验证手机号码的实例,成功测试通过
2017/11/26 Javascript
微信小程序-API接口安全详解
2019/07/16 Javascript
Layui tree 下拉菜单树的实例代码
2019/09/21 Javascript
使用p5.js临摹动态图形
2019/10/23 Javascript
vue移动端模态框(可传参)的实现
2019/11/20 Javascript
js对象属性名驼峰式转下划线的实例代码
2020/09/17 Javascript
Python如何import文件夹下的文件(实现方法)
2017/01/24 Python
浅谈python中的__init__、__new__和__call__方法
2017/07/18 Python
python的Crypto模块实现AES加密实例代码
2018/01/22 Python
一看就懂得Python的math模块
2018/10/21 Python
Python数据类型之String字符串实例详解
2019/05/08 Python
5 分钟读懂Python 中的 Hook 钩子函数
2020/12/09 Python
Canvas中设置width与height的问题浅析
2018/11/01 HTML / CSS
潘多拉珠宝英国官方网上商店:PANDORA英国
2018/06/12 全球购物
护理自荐信范文
2013/10/05 职场文书
学校献爱心活动总结
2014/07/08 职场文书
幼儿教师年度个人总结
2015/02/05 职场文书
win10+anaconda安装yolov5的方法及问题解决方案
2021/04/29 Python
Redis官方可视化工具RedisInsight安装使用教程
2022/04/19 Redis