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) { // 向右 // to do } else if(offsetX >= 50) { // 向左 // to do } } |
無限輪播
無限輪播要面對的是兩個問題:
- 輪播的資料結構;
- 前端渲染
資料結構
無限輪播筆者聯想到旋轉木馬。
在資料結構中有一個叫迴圈連結串列的結構,可以完美地模擬旋轉木馬。
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() { // queue 迴圈連結串列 // nodelist 圖片列表 for(var i=0; i<5; ++i) { nodelist[queue[i]].style.cssText = css[i]; } } // 層級與補間 css = [ "z-index: 3; other css...", // pic1 "z-index: 2; other css...", // pic2 "z-index: 1; other css...", // pic3 "z-index: 1; other css...", // pic4 "z-index: 2; other css..." // pic5 ]; |
任意圖片數
圖片數可以分成三種情況來討論:count == 5; count > 5; count 。其中 count == 5 是理想條件,上幾節就是圍繞它展開的。本節將分析 count > 5 與 count 的解決思路。
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"); // nodelist n = nodelist.length; queue = [0, 1, 2, ..., n]; // 迴圈連結串列 snapshot = new Array(n); // 對映 nodelist 的快照 // 初始化 nodelist 樣式 for(var i=0; i<n; ++i) { nodelist[i].style.cssText = defaultCssText; } } // 預設樣式 var defaultCssText = "visibility: hidden"; // 層級與補間 css = [ "z-index: 3; other css...", // pic1 "z-index: 2; other css...", // pic2 "z-index: 1; other css...", // pic3 "z-index: 1; other css...", // pic(n-1) "z-index: 2; other css..." // picn ]; // 切換渲染 function swap() { nextSnap = new Array(n); // swiper切換後的快照 for(var i in [0, 1, 2, n-1, n]) { nextSnap[queue[i]] = css[i]; } // 更新 snapshot 與 nodelist for(var i=0; i<n; ++i) { if(snapshot[i] != nextSnap[i]) { // 快照更新 snapshort[i] = nextSnap[i]; // 樣式更新 nodelist[i].style.cssText = snapshort[i] || defaultCssText); } } } |
count
當count >= 5
時,渲染節點是一個穩定的金字塔:
當 count 時,渲染金字塔變得不確定:
count | 金字塔 |
---|---|
1 | |
2 | |
3 | |
4 |
由於只有 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...", // pic1 ] css2 = [ "z-index: 2; other css...", // pic1 "z-index: 1; other css..." // pic2 ] css3 = [ "z-index: 2; other css...", // pic1 "z-index: 1; other css...", // pic2 "z-index: 1; other css..." // pic3 ] css4 = [ "z-index: 3; other css...", // pic1 "z-index: 2; other css...", // pic2 "z-index: 2; other css...", // pic3 "z-index: 1; other css..." // pic4 ] 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() { // queue 迴圈連結串列 // nodelist 圖片列表 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() { // queue 迴圈連結串列 // renderList 渲染列表 while(queue.length > 0 && renderList.length < 5) { renderList.push(renderList.length % 2 ? queue.pop() : queue.shift()); } // nodelist 圖片列表 for(var i=0; i<renderList.length; ++i) { nodelist[queue[i]].style.cssText = css[i]; } } // 層級與補間 css = [ "z-index: 3; other css...", // pic1 "z-index: 2; other css...", // picn "z-index: 2; other css...", // pic2 "z-index: 1; other css...", // pic(n-1) "z-index: 1; other css..." // pic3 ]; |
細節優化
筆者實現的 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 |
// 支援 3d 透視 swiper.style["-webkit-transform-style"] = "preserve-3d"; // 層級與補間 css = [ "z-index: 3; transfomr: translateZ(10px)", // pic1 "z-index: 2; transfomr: translateZ(6px)", // picn "z-index: 2; transfomr: translateZ(6px)", // pic2 "z-index: 1; transfomr: translateZ(2px)", // pic(n-1) "z-index: 1; transfomr: translateZ(2px)" // pic3 ]; |
新增 z-index
後的swiper: https://leeenx.github.io/mobile-swiper/v2.html
再看看 count 的執行效果:
當 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]; } } |
修復後的效果如下:
count | 效果圖 | 地址 |
---|---|---|
2 | https://leeenx.github.io/mobile-swiper/index.html?count=2 | |
4 | https://leeenx.github.io/mobile-swiper/index.html?count=4 |
總結
感謝閱讀完本文章的讀者。本文最終實現的 swiper 筆者託管在 Github 倉庫,有興趣的讀者可以看一下:https://github.com/leeenx/mobile-swiper
希望對你們有幫助。