移动端效果之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 相关文章推荐
jquery ajax 同步异步的执行示例代码
Jun 23 Javascript
jquery如何把参数列严格转换成数组实现思路
Apr 01 Javascript
JQuery实现动态适时改变字体颜色的方法
Mar 10 Javascript
javascript实现简单的贪吃蛇游戏
Mar 31 Javascript
AngularJs实现分页功能不带省略号的代码
May 30 Javascript
归纳下js面向对象的几种常见写法总结
Aug 24 Javascript
浅谈原型对象的常用开发模式
Jul 22 Javascript
React实践之Tree组件的使用方法
Sep 30 Javascript
微信小程序wx.request实现后台数据交互功能分析
Nov 25 Javascript
Angular2中监听数据更新的方法
Aug 31 Javascript
Makefile/cmake/node-gyp中区分判断不同平台的方法
Dec 18 Javascript
Nest.js 授权验证的方法示例
Feb 22 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中MVC模式的模板引擎开发经验分享
2011/03/23 PHP
解析PHP可变函数的经典用法
2013/06/20 PHP
淘宝ip地址查询类分享(利用淘宝ip库)
2014/01/07 PHP
php中fsockopen用法实例
2015/01/05 PHP
php截取指定2个字符之间字符串的方法
2015/04/15 PHP
YII2框架中excel表格导出的方法详解
2017/07/21 PHP
Laravel学习教程之IOC容器的介绍与用例
2017/08/15 PHP
JQuery Ajax通过Handler访问外部XML数据的代码
2010/06/01 Javascript
解析jQuery的三种bind/One/Live事件绑定使用方法
2013/12/30 Javascript
jquery中常用的函数和属性详细解析
2014/03/07 Javascript
JavaScript编程中的Promise使用大全
2015/07/28 Javascript
JS实现表单中checkbox对勾选中增加边框显示效果
2015/08/21 Javascript
学习JavaScript事件流和事件处理程序
2016/01/25 Javascript
Javascript将数字转化成为货币格式字符串
2016/06/22 Javascript
浅谈js键盘事件全面控制
2016/12/01 Javascript
微信小程序实现自定义modal弹窗封装的方法
2018/06/15 Javascript
vue 使某个组件不被 keep-alive 缓存的方法
2018/09/21 Javascript
Vue表单输入绑定的示例代码
2018/11/01 Javascript
如何通过JS实现日历简单算法
2020/10/14 Javascript
vue封装自定义指令之动态显示title操作(溢出显示,不溢出不显示)
2020/11/12 Javascript
[46:58]完美世界DOTA2联赛PWL S3 Forest vs LBZS 第一场 12.17
2020/12/19 DOTA
Windows下Python2与Python3两个版本共存的方法详解
2017/02/12 Python
python实现机械分词之逆向最大匹配算法代码示例
2017/12/13 Python
对Python 语音识别框架详解
2018/12/24 Python
Python中正反斜杠(‘/’和‘\’)的意义与用法
2019/08/12 Python
Python如何优雅获取本机IP方法
2019/11/10 Python
Python类反射机制使用实例解析
2019/12/30 Python
基于python连接oracle导并出数据文件
2020/04/28 Python
matplotlib基础绘图命令之errorbar的使用
2020/08/13 Python
如何用tempfile库创建python进程中的临时文件
2021/01/28 Python
基于HTML5的WebGL实现json和echarts图表展现在同一个界面
2017/10/26 HTML / CSS
德国百年厨具品牌WMF美国站:WMF美国
2016/09/12 全球购物
学习作风建设心得体会
2014/10/22 职场文书
趣味运动会新闻稿
2015/07/17 职场文书
2019新学期家长会工作计划
2019/08/21 职场文书
Apache Linkis 中间件架构及快速安装步骤
2022/03/16 Servers