移动端效果之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也能包含文件
Oct 26 Javascript
jquery jqPlot API 中文使用教程(非常强大的图表工具)
Aug 15 Javascript
了不起的node.js读书笔记之例程分析
Dec 22 Javascript
jQuery()方法的第二个参数详解
Apr 29 Javascript
jQuery选择器基础入门教程
May 10 Javascript
jQuery滚动新闻实现代码
Jun 26 Javascript
javaScript给元素添加多个class的简单实现
Jul 20 Javascript
JS中使用mailto实现将用户在网页中输入的内容传递到本地邮件客户端
Oct 08 Javascript
利用Node.js编写跨平台的spawn语句详解
Feb 12 Javascript
Vue.js实现的表格增加删除demo示例
May 22 Javascript
详解Vue项目中实现锚点定位
Apr 24 Javascript
jquery实现吸顶导航效果
Jan 08 jQuery
浅谈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文件中bom的PHP代码
2012/03/13 PHP
细谈php中SQL注入攻击与XSS攻击
2012/06/10 PHP
Thinkphp实现自动验证和自动完成
2015/12/19 PHP
php阳历转农历优化版
2016/08/08 PHP
Laravel框架使用Seeder实现自动填充数据功能
2018/06/13 PHP
PHP开发API接口签名生成及验证操作示例
2020/05/27 PHP
javascript的函数
2007/01/31 Javascript
javascript multibox 全选
2009/03/22 Javascript
让textarea自动调整大小的js代码
2011/04/12 Javascript
js中parseInt函数浅谈
2013/07/31 Javascript
JavaScript语言核心数据类型和变量使用介绍
2013/08/23 Javascript
jquery仿搜索自动联想功能代码
2014/05/23 Javascript
js window对象属性和方法相关资料整理
2015/11/11 Javascript
JS实现动画兼容性的transition和transform实例分析
2016/12/13 Javascript
js实现导航栏中英文切换效果
2017/01/16 Javascript
[01:05:36]VP vs TNC Supermajor小组赛B组 BO3 第二场 6.2
2018/06/03 DOTA
[40:27]完美世界DOTA2联赛PWL S3 PXG vs GXR 第一场 12.19
2020/12/24 DOTA
跟老齐学Python之开始真正编程
2014/09/12 Python
python根据路径导入模块的方法
2014/09/30 Python
一个基于flask的web应用诞生 使用模板引擎和表单插件(2)
2017/04/11 Python
python之消除前缀重命名的方法
2018/10/21 Python
解决每次打开pycharm直接进入项目的问题
2018/10/28 Python
在Python中获取两数相除的商和余数方法
2018/11/10 Python
详解Django模版中加载静态文件配置方法
2019/07/21 Python
python Gunicorn服务器使用方法详解
2019/07/22 Python
全网首秀之Pycharm十大实用技巧(推荐)
2020/04/27 Python
关于Python错误重试方法总结
2021/01/03 Python
python 图像增强算法实现详解
2021/01/24 Python
会计专业毕业生自我评价
2013/09/25 职场文书
小学少先队活动方案
2014/02/18 职场文书
护理工作个人总结
2015/03/03 职场文书
小学班主任培训心得体会
2016/01/07 职场文书
创业计划书详解
2019/07/19 职场文书
三年级作文之趣事作文
2019/11/04 职场文书
祝福语集锦:给满月宝宝的祝福语
2019/11/20 职场文书
Java elasticsearch安装以及部署教程
2021/06/28 Java/Android