微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

移动端效果之Swiper详解

写在前面

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

代码在这里

1. 说明

父容器页面

2. 核心解析

2.1 页面初始化

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

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) {
  // 给激活的子<a href="https://www.jb51.cc/tag/yemian/" target="_blank" class="keywords">页面</a><a href="https://www.jb51.cc/tag/jiashang/" target="_blank" class="keywords">加上</a>激活class
  child.classList.add('is-active');
}

});

pages = aPages;
}

2.2 容器滑动开始(onTouchStart)

在低版本的性能提升作用,使得滑动起来不是那么卡。

前置工作:

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

滑动开始:

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

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

2.3 容器滑动(onTouchMove)

套用全局

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

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

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

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

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

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

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

重置位移

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,如果是用户自然滚动,那么要做一些清除操作:

rush:js;"> dragging = false; dragState = {};

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

判断是否是tap事件

rush:js;"> // 时间小于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'); } }

判断方向

rush:js;"> // 如果事件间隔小于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;
}

执行动画

<div class="jb51code">
<pre class="brush:js;">
// 当没有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];
  }
}

// 计算<a href="https://www.jb51.cc/tag/shangyiye/" target="_blank" class="keywords">上一页</a>与<a href="https://www.jb51.cc/tag/xiayiye/" target="_blank" class="keywords">下一页</a>之后
// 重置位移
// 参看doOnTouchMove
// 其实这里的options 传与不传也就是<a href="https://www.jb51.cc/tag/huoqu/" target="_blank" class="keywords">获取</a><a href="https://www.jb51.cc/tag/shangyiye/" target="_blank" class="keywords">上一页</a>信息与<a href="https://www.jb51.cc/tag/xiayiye/" target="_blank" class="keywords">下一页</a>信息
if (prevPage) {
  prevPage.style.<a href="https://www.jb51.cc/tag/dis/" target="_blank" class="keywords">dis</a>play = 'block';
  translate(prevPage,-pageWidth);
}

if (nextPage) {
  nextPage.style.<a href="https://www.jb51.cc/tag/dis/" target="_blank" class="keywords">dis</a>play = '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,speed)
}
} else if (towards === 'prev') {
isDone = true;
before(currentPage);
translate(currentPage,callback);
if (prevPage) {
translate(prevPage,speed);
}
} else {
// 如果既不是左滑也不是右滑
isDone = true;
// 当前页面依旧处于视野中
// 上一页和下一页滑出
translate(currentPage,callback);
if (typeof offsetLeft !== 'undefined') {
if (prevPage && offsetLeft > 0) {
translate(prevPage,pageWidth * -1,speed);
}
if (nextPage && offsetLeft < 0) {
translate(nextPage,speed);
}
} else {
if (prevPage) {
translate(prevPage,speed);
}

  if (nextPage) {
   translate(nextPage,speed);
  }
 }
}

},10);
}

后置工作:

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

rush:js;"> dragging = false; dragState = {};

总结

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

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持编程之家。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐