打造絲般順滑的 H5 翻頁庫

發表於2017-10-31

背景

隨著近幾年移動營銷頁的火爆,催生了一箇中國式的名詞「H5」。而 H5 最常見的形態就是類似幻燈片翻頁效果。

我們需要製作 H5 的時候,最快的辦法就是使用一些滑動外掛庫,如 iDangero.us 出品的 Swiper,百度 BE-FE 出品的 iSlider。通過這些翻頁庫提供的強大的配置功能,我們就能實現很酷炫的翻頁效果。當然,這些庫還支援自動播放,點選切換和當前頁面指示等配置,所以還能用在網頁上,實現一些 web carousel 的效果。

百度 H5 也先後使用了 Swiper 和 iSlider 作為 H5 執行時的翻頁框架,隨著使用者越來越多,也遇到了一些問題:
1. H5 平臺和這些庫不能很好貼合,有些配置項用不上,而有些必要的功能需要「Hack」的方式實現。
2. 一些 H5 的元素多、動畫多,在低端機型上翻頁時,翻頁時會有「卡頓感」和「粘滯感」,使用者體驗不好。

而我們希望的 H5 翻頁庫能和平臺本身的功能完美貼合,在保持體積小的同時,在翻頁的時候能做到「絲般順滑」。於是我們就開始了研(zao)究(lun)之(zi)旅。

開始

H5 滑屏框架的開發,第一個問題就是:頁面是否跟隨手指滑動?這也是騰訊 ISUX 團隊的《滑屏 H5 開發實踐九問》的第一問(這篇文章原文出處現在是 404 ,大家可以在其他的轉載網站看到),這裡用這篇文章的圖片說明一下這個問題。

不跟隨手指滑動     跟隨手指滑動

左圖:不跟隨手指滑動,右圖:跟隨手指滑動。

左邊的是不跟隨手指滑動,只需要關注手指觸碰開始和離開兩個時間點,中間過程不用考慮。所以實現起來比較簡單。但是使用者的操作沒有實時的反饋,體驗不夠好。因此,儘管實現起來更復雜,我們仍然決定實現前一種「跟隨手指滑動」的效果。

起步

下圖是跟隨手指滑動的 H5 最直觀的版本,所有的「頁面」依次從上到下,首尾相接。需要說明一下,這裡的「頁面」打引號,是因為實際上他們都是 div,後文說的頁面都指這些 div。同時,我們這裡以最常見的豎直方向滑動為例,水平方向同理。

swiper-basic.png

基本原理圖

這些 div 的寬度和高度都是 100% 的容器高度,可視區域是中間的部分,我們監聽 touchstart, touchmove, touchend 事件,跟滑鼠拖拽的原理類似:
1. touchstart 時,記下起點位置;
2. touchmove 實時計算滑動的距離,讓所有頁面一起沿著 Y 軸 translate 這段距離。
3. touchend 時,能得到最終的滑動距離,跟設定的閾值比較。進入到頁面自動控制階段:大於閾值則讓頁面滑動到下一頁,小於閾值則恢復到起始位置。

深入探究

簡單的版本在上一部分很容易就實現了,如果其他需求不多,頁面上元素和動畫比較少,基本上就夠用了。但是本文要探究的是如何能做到「絲般順滑」,其實就是兩個字:效能。

效能的瓶頸是什麼呢?

我們的目標是:在「三多一低」(頁面多、元素多、動畫多,配置低)的情況下,滑動翻頁時,儘可能不產生卡頓。

我們分成兩部分來看這問題:手指離開螢幕前和手指離開螢幕後。

手指離開螢幕前

此時比較耗費效能的操作是:當 touchmove 觸發時,計算出了要移動的距離,所有的頁面都需要沿著 Y 軸移動相同的距離。此時必然免不了進行 DOM 操作,而 DOM 操作是非常「昂貴」的,再加上 touchmove 事件的頻繁觸發,效能處理不夠好的話,很容易出現卡頓。

為了優化效能,我們很自然的想到一個策略:減少 DOM 操作

這裡麵包含兩部分:減少 DOM 操作的元素和減少 DOM 操作的屬性。前者比如,看不到的頁面不參與動畫。後者比如,只改變元素的 css 屬性的一個或幾個。

減少 DOM 操作的元素

最開始簡易的版本的例子中,touchmove 觸發時,所有的頁面都沿著 Y 軸移動。其實沒有必要,因為相當一部分頁面是看不見的。那一般情況下,我們最少需要操作幾個頁面呢?答案是兩個。可以回想一下,我們滑動的時候,最多能同時看到兩個頁面。這個方法相對於所有的頁面一起移動,成倍地提升效能。

減少 DOM 操作的屬性

這個方法的主要意思是,只需要操作一次 DOM 能達到的效果,絕不用兩次。實際上,對於 slide 動畫,我們只需要改變頁面的 transform 的值,其他的 DOM 操作(增加 class,修改元素的 innerHTML)等能不做就不做。

我們得到了一個初步的方案:初始化時,所有的頁面一次性全部置入 container,除了我們用到的兩頁,display 屬性都設定為 nonetouchmove 的時候,只有這兩頁的 transform 屬性有變化。

touchmove 的過程,我們可以寫成數學表示式:

表示手指滑動的距離, 表示頁面滑動距離, 是當前滑動邊的長度,如果是沿 y 軸滑動,則是頁面高度。寫到這裡,就跟時下很流行的「資料驅動」的概念很類似了。我們要實現的就只有一個 render 函式,輸入是使用者的互動資料,輸出是頁面表現。

手指離開螢幕後

當手指離開螢幕時,我們就已經知道了這次滑動的結果(向上還是向下?翻頁還是回彈?),要實現的只是動畫效果,我們有兩個選擇:
方案一:複用 touchmove 的 render 邏輯,按照手指滑動的速度,使用 requestAnimationFrame 控制動畫;
方案二:使用 css3 transition 動畫;

方案一的優點在於:可以在手指滑動和動畫過程使用同樣的 render 函式,最大限度複用了程式碼,邏輯統一;同時可以精確控制動畫的每一幀,動畫曲線會比較流暢。 缺點就是可能存在的效能問題。方案二跟方案一剛好相反。其實說到底還是 js 動畫 vs css 動畫的問題。

動畫效能實驗

為了比較兩個方案在 H5 翻頁動畫上的效能優劣,我們取一個稍微複雜點的例子:

H5:百度無人車招聘的 H5
動畫:從第 1 頁翻到第 2 頁
CPU: 6 * slowdown
瀏覽器:Chrome 61.0.3163.100(64 位)

js 動畫方案:點選這裡
css 動畫方案:點選這裡

js 動畫

js 翻頁動畫方案,Profile 結果

css 動畫

css 翻頁動畫方案,Profile 結果

通過實驗我們可以看到,js 的動畫過程中,幀率大多維持在 30 fps 上下。而 css 動畫,基本都在 60 fps 上下。而且在動畫過程中,明顯感覺 js 動畫有卡頓。這種情況在一些 CPU 和顯示卡配置相對低的 Android 機型上尤為明顯。對於這個問題有興趣的同學,可以看一下 swiper 庫的 raf 分支,這是本次對比測試所用到的 js 。

所以,儘管 js 的動畫方案看起來比較「優雅」,能用「資料驅動」的理念,統一解決滑動過程和動畫過程的問題。實際上效能有瓶頸,我們只能在手指離開螢幕後,採用 css 的動畫方案以保證效能。正應了一句話「能用 css 做的,絕對不要用 js 解決」。

實施方案

下圖形象地展示了我們實施的基本思路,只有兩頁:
currentPage :當前頁面
activePage:即將要翻到的下一頁

其餘的頁面都是初始化的時候載入進 DOM 結構,但是 displaynone 並且 z-index 都是 0。這裡展示「層疊」的狀態是為了更形象的展示。

swiper-basic.png

swiper 原理圖

為了方便獲取頁面,我們採用雙向連結串列儲存頁面結構。每個 page 具有 prevnext 分別指向上一個和下一個 page
我們重點要關注的是,怎麼樣確定 activePage ?即下一個要去到的頁面。答案很簡單,其實,當使用者開始觸碰螢幕,並且滑動的時候,就能確定了:
1. 滑動距離 x < 0,表示頁面向上滑動,此時 activepage = currentPage.next
2. 滑動距離 x > 0,表示頁面向下滑動,此時 activepage = currentPage.prev

擴充套件

翻頁效果

我們舉的例子中的翻頁效果是最普通的滑動效果。怎麼樣擴充套件支援立方體、翻轉等效果呢?可以回頭看看「手指離開螢幕前」部分,我們提出了 , 是使用者滑動距離, 是頁面滑動距離。我們把 擴充套件一下,變成「頁面翻轉角度」或「頁面縮放比率」,就可以支援其他的效果了。

事實上,我們在滑動的時候,本身就是使用 css3 的 transform 屬性,將其中的 translate, rotate, scale 適當的組合就能做出千變萬化的翻頁效果了。

更令人愉悅的動畫

這裡指的是 animation-timing-function,拿最簡單的滑動效果舉例。如果是線性的函式,使用者滑動的速度始終等於頁面滑動速度。而「感覺上」更流暢、更靈敏的應該是:剛開始頁面滑動速度大於使用者滑動速度,隨著翻頁的進行,兩者趨於相同,過了某個點後,單位時間內,頁面滑動速度開始逐漸小於使用者滑動速度,將速度表示為距離,就可以得到 和 之間的關係如下圖:

swiper-basic.png
和 的關係圖(橫軸為 ,縱軸為 )

在這裡,不得不再提起兩種動畫方案: js 動畫和 css 動畫。

js 動畫方案的一個優點是,可以精確控制動畫的程式,而 css 無法做到。比如使用者在 x = 0.8 的時候手指離開螢幕,因為採用的同一個 render,js 可以知道手指離開螢幕的瞬間 x 處於 0.8 的位置,接下來的動畫由 requestAnimationFrame 完成,整個過程流暢且完整。

而 css 動畫則不同,css 動畫只有在動畫開始之前設定 animation-timing-function,當使用者在 x = 0.8 手指離開螢幕時,原本的 js 控制滑動過程中斷,由 css 來完成剩餘的動畫,css 無法根據手指離開螢幕的瞬間動態計算 animation-timing-function ,所以在銜接的那個點,兩者速度不匹配,會影響整體動畫效果。

但遺憾的是,js 的動畫方案有效能問題,我們在使用者手指離開螢幕後的那一部分只能採取 css 動畫方案。這個「更令人愉悅的動畫」也只能用在手指滑動期間。

總結和展望

本文講述了一個「絲般順滑」的 H5 翻頁庫的開發過程中遇到的一些問題和對應的解決方法。基本的滑動翻頁模型建立之後,重點關注了效能的問題,分為手指離開螢幕前手指離開螢幕後兩個階段。前一階段主要聚焦於減少 DOM 操作。後一階段聚焦於動畫的效能,並且對比了 js 動畫和 css 動畫的效能資料,最後得出了在手指離開螢幕後使用 css 動畫的結論。此外,我們還基於「資料驅動」的思想,在翻頁效果動畫函式兩部分進行了擴充套件,增強了翻頁庫的功能,也豐富了 H5 的展現效果。

本文中嘗試用「資料驅動」的思想去解釋整個過程,但是因為效能問題只能暫時放棄,希望在未來能找到更好的方案。由於水平所限,文中難免會出現紕漏,歡迎大家批評指正,共同學習進步。感謝 Swiperislider 翻頁庫的啟發,特別感謝和 @Ronny 的熱烈討論。

本文所述的 swiper 庫地址:https://github.com/fex-team/swiper。master 分支所用的程式碼是目前百度 H5 線上使用的。raf 分支是文中提到的使用 js 動畫方案。

相關文章