设计一个五图的 Swiper,设计稿如下:
Swiper 的功能如下:
- 左右切换
- 无限轮播
- 任意图片数
接下来,详细介绍这三个功能的实现过程:
左右切换
这里指触发左右切换的手指交互,目前主要是以下两种:
方案 | 示意图 |
---|
手指拖拽 | |
手势判断 | |
手指拖拽容易有性能问题并且实现相对麻烦,所以笔者果断采用了手势判断,伪代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| swiper.on("touchstart", startHandle); swiper.on("touchmove", moveHandle); function startHandle(e) { var x0 = e.touch.pageX, y0 = e.touch.pageY; } function moveHandle(e) { var x = e.touch.pageX, y = e.touch.pageY, offsetX = x0 - x, offsetY = y0 - y; if(offsetX <= -50) { } else if(offsetX >= 50) { } }
|
无限轮播
无限轮播要面对的是两个问题:
数据结构
无限轮播笔者联想到旋转木马。
在数据结构中有一个叫循环链表的结构,可以完美地模拟旋转木马。
javascript 没有指针,链表需要由数组来模拟。分析循环链表的两个重点特征:
- 数据项都由头指针访问
- 链表头尾有指针串联
笔者用 pop&unshift/shift&push
APIs 模拟指针的前后移动,解决了链表头尾串联的问题,然后用数组的第一个元素(Arrayy[0])作为头指针。
伪代码如下:
1 2 3 4 5 6 7 8
| if(left) { queue.push(this.queue.shift()); swap("left"); } else { queue.unshift(this.queue.pop()); swap("right"); }
|
前端渲染
swiper 换个角度来看,它其实是一个金字塔:
梳理好层级问题再把过渡补间写上,swiper 的渲染就已经OK了。以下是伪代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function swap() { for(var i=0; i<5; ++i) { nodelist[queue[i]].style.cssText = css[i]; } }
css = [ "z-index: 3; other css...", "z-index: 2; other css...", "z-index: 1; other css...", "z-index: 1; other css...", "z-index: 2; other css..." ];
|
任意图片数
图片数可以分成三种情况来讨论:count == 5; count > 5; count < 5
。其中 count == 5
是理想条件,上几节就是围绕它展开的。本节将分析 count > 5
与 count < 5
的解决思路。
count > 5
将循环链表(5节)扩容:
扩容后的工作过程如下:
- 循环链表指针移动;
- 渲染节点(1, 2, 3, n-1, n);
- 回收节点(4, 5, …, n-2)。
注:这里的回收节点指隐藏节点(display: none/visibility: hidden)
渲染金字塔如下:
为了提高性能笔者在循环链表与节点中间创建了一个快照数组 snapshot,snapshot 映射节点上的属性,循环链表每一次变动都会生成一个新的快照数组 nextSnap,通过 nextSnap 来更新 snapshot 与 节点样式。以下是实现的伪代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| function init() { nodelist = document.querySelectorAll("li"); n = nodelist.length; queue = [0, 1, 2, ..., n]; snapshot = new Array(n); for(var i=0; i<n; ++i) { nodelist[i].style.cssText = defaultCssText; } }
var defaultCssText = "visibility: hidden";
css = [ "z-index: 3; other css...", "z-index: 2; other css...", "z-index: 1; other css...", "z-index: 1; other css...", "z-index: 2; other css..." ];
function swap() { nextSnap = new Array(n); for(var i in [0, 1, 2, n-1, n]) { nextSnap[queue[i]] = css[i]; } for(var i=0; i<n; ++i) { if(snapshot[i] != nextSnap[i]) { snapshort[i] = nextSnap[i]; nodelist[i].style.cssText = snapshort[i] || defaultCssText); } } }
|
count < 5
当count >= 5
时,渲染节点是一个稳定的金字塔:
当 count < 5
时,渲染金字塔变得不确定:
由于只有 count == 1 ~ 4
四种情况,可以直接用个 swith
把状态列表出来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| css1 = [ "z-index: 1; other css...", ] css2 = [ "z-index: 2; other css...", "z-index: 1; other css..." ] css3 = [ "z-index: 2; other css...", "z-index: 1; other css...", "z-index: 1; other css..." ] css4 = [ "z-index: 3; other css...", "z-index: 2; other css...", "z-index: 2; other css...", "z-index: 1; other css..." ] switch(n) { case 4: css = css4, renderList = [1, 4, 2, 3], break; case 3: css = css3, renderList = [1, 3, 2], break; case 2: css = css2, renderList = [1, 2], break; default: css = css1, renderList = [1], break; } function swap() { for(var i in renderList) { nodelist[queue[renderList[i]]].style.cssText = css[i]; } }
|
上面的伪代码显得很冗长,并不是个好实现方式。不过仍能从上面代码获得启发: 渲染列表(renderList) 与循环链表(queue)的对应关系 —— [shift, pop, shift, pop, shift]。于是伪代码可以简化为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| function swap() { while(queue.length > 0 && renderList.length < 5) { renderList.push(renderList.length % 2 ? queue.pop() : queue.shift()); } for(var i=0; i<renderList.length; ++i) { nodelist[queue[i]].style.cssText = css[i]; } }
css = [ "z-index: 3; other css...", "z-index: 2; other css...", "z-index: 2; other css...", "z-index: 1; other css...", "z-index: 1; other css..." ];
|
细节优化
笔者实现的 swiper: https://leeenx.github.io/mobile-swiper/v1.html
(count >= 5)运行效果如下:
仔细观察能看到切换效果上的小瑕疵:
造成这个瑕疵是因为同值 z-index
节点的渲染层级与 DOM 树的出现顺序相关: 后出现的节点层级更高。
解决方案很简单,为 swiper 添加一个 translateZ
。如下伪代码:
1 2 3 4 5 6 7 8 9 10
| swiper.style["-webkit-transform-style"] = "preserve-3d";
css = [ "z-index: 3; transfomr: translateZ(10px)", "z-index: 2; transfomr: translateZ(6px)", "z-index: 2; transfomr: translateZ(6px)", "z-index: 1; transfomr: translateZ(2px)", "z-index: 1; transfomr: translateZ(2px)" ];
|
添加 z-index
后的swiper: https://leeenx.github.io/mobile-swiper/v2.html
再看看 count < 5
的运行效果:
当 count == 2
/ count == 4
时,swiper 向右切换时怪怪的,总感觉有什么不对!!其实问题出在渲染金字塔上,偶数swiper 在视觉在不是一个对称的图形:
由于笔者使用定势渲染的原因造成金字塔底被固定在左侧,当向右侧切换时会觉得很奇怪。这里其实只要加一个方向修正即可,以下是修正的伪代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function swap(orientation) { odd = 1; // 奇偶标记 total = queue.length; // 渲染列表长度 last = total - 1; // renderList 最后一个索引 while(queue.length > 0 && renderList.length < 5) { renderList.push(odd ? queue.pop() : queue.shift()); odd = !odd; // 取反 } // nodelist 图片列表 for(var i=0; i<5; ++i) { // 偶数并且向右切换,将最后一个节点右置 nodelist[queue[i]].style.cssText = (orientation == "right" && !odd && i == last) ? css[i+1] : css[i]; } }
|
修复后的效果如下:
结语
感谢阅读完本文章的读者。本文最终实现的 swiper 笔者托管在 Github 仓库,有兴趣的读者可以看一下:https://github.com/leeenx/mobile-swiper
希望对你们有帮助。
上次更新:2019-04-20 19:28:31