Flipboard web移動端-打造每秒60幀的流暢體驗

 

在智能手機和平板電腦的黎明時期, Flipboard 推出「移動先行」的體驗,使咱們能夠從新思考頁面中內容佈局的原則,以及與觸摸屏相關的,如何得到更好的用戶體驗的因素。css

爲了創建完整的體驗,咱們將 Flipboard 帶到 web 端。咱們在 Flipboard 所作的,在每臺用戶使用的設備上都擁有獨立的價值:整理那些來自你最關心的主題,來源以及人的最好的故事。所以把咱們的服務帶到web端,也是一個合乎邏輯的延伸。html

當咱們開始這個項目後,認識到咱們須要把源自移動體驗的思考搬到 web 端,試圖提高 web 端的內容佈局和交互。咱們想達到原生應用般的精緻和性能,且仍能感知到真實的瀏覽器。html5

早些時間,通過測試大量的產品原型後,咱們決定使用滾動的方式做爲 web 端體驗。咱們的移動應用被你們所熟知的是相似翻書般的體驗,在觸摸屏上這很直觀。但一系列的緣由代表,滾動在 web 端的體驗更加天然。react

爲了優化滾動的性能, 咱們知道咱們須要保證頁面渲染的頻率低於16ms,同時限制迴流(reflow)和重繪(repaints)。這在動畫中尤爲重要。爲了不動畫中從新渲 染,有兩個屬性你能夠安全地做用於動畫上: CSS transform 和 opacity。但這樣選擇餘地過小了。ios

當你想實現元素上寬度動畫效果怎麼辦?css3

指尖的流暢體驗

一幀幀的滾動動畫如何處理?git

指尖的流暢體驗

注意,在上圖中,頂部的圖標從白色到黑色。這裏使用了兩個單獨的元素相互覆蓋,根據下面的內容來互相裁剪)。    這些類型的動畫一直在網上遭受詬病,特別是在移動設備上,只由於一個簡單的緣由:DOM 太慢了github

開始使用 <canvas>

大多數現代移動設備都擁有硬件加速的 canvas,咱們爲何不利用起來呢?HTML5 遊戲已經作到了。咱們能真正在 canvas 上開發應用界面麼?web

當即模式與保留模式

Canvas 是一種當即模式的繪圖 API,這意味着繪製時不保留所繪製對象的信息。與其相反的是保留模式,這是一種聲明性的 API,維護所繪製對象的層次結構。算法

保留模式api的優勢是,對於你的應用程序,他們一般更容易構建複雜的場景,例如 DOM。一般這都會帶來性能成本,須要額外的內存來保存場景和更新場景,這可能會很慢。

Canvas 受益於當即模式,容許直接發送繪圖命令到 GPU。但若用它來構建用戶界面,須要進行一個更高層次的抽象。例如一些簡單的處理,好比當繪製一個異步加載的資源到一個元素上時會出現問題,如在圖片上 繪製文本。在HTML中,因爲元素存在順序,以及 CSS 中存在 z-index,所以是很容易實現的。

<canvas>元素中創建UI

相比 HTML+CSS,canvas 則有些先天不足,缺乏很是多在 HTML + CSS 中理所固然的特性。

文本

canvas有一個很簡單的 API 用於繪製文字:fillText(text, x, y [, maxWidth])這個函數接受三個參數:文字自己以及繪製起點的x,y座標。但 canvas 只能一次繪製一行文字。若是你須要讓文字換行,須要本身寫函數。

圖片

你可使用drawImage()函數在 canvas 上繪製圖片。這是個可變參數函數,你能夠指定更多參數,從而控制定位和裁切。可是canvas不在意圖像是否加載,或不能肯定只在圖像加載事件後調用函數。

元素層疊

經過 DOM 元素的順序或使用 CSS 的 z-index屬性,在 HTML 和 CSS 指定一個元素是否在另外一個上應該很容易。但請記住,canvas 是當即模式的繪圖 API。當元素重疊或者其中一個須要重繪時,都必須以同一順序從新繪製(或至少局部重繪)。

譯者注 關於局部重繪提升性能的文章你們能夠參考:《提升HTML5 canvas性能的幾種方法(轉)

自定義字體

須要使用一個自定義 web 字體嗎? canvas 的文本 API 並不在意字體是否加載。你須要一種方法來知道一個字體是否加載,並繪製任何依賴此字體的區域。幸運的是,現代瀏覽器有一個基於promise的API。不幸的是, iOS WebKit (iOS 8 時)不支持它。

<canvas>的優勢

鑑於全部這些缺點,人們開始質疑 canvas 來代替 DOM 這一選擇。最終,咱們的討論由一個很簡單的真理來決定:你不能基於 dom 創建一個60 fps的滾動列表視圖

許多人(包括咱們)已經嘗試過,但都失敗了。可滾動的元素能夠在純 HTML 和 CSS 中經過 overflow:scroll 實現:(結合 IOS 上的 -webkit-overflow-scrolling:touch ),但這些不能在滾動動畫中給予你逐幀控制,同時移動瀏覽器很難處理又長又複雜的內容。

爲了構建一個內含至關複雜的內容的無限滾動列表,咱們須要在 web 端實現一個UITableView

與 DOM 相比,今天的大多數設備都有基於硬件加速的 canvas 實現,能夠直接發送繪圖命令到 GPU。這意味着咱們能夠很是快的渲染元素;在許多狀況下,咱們所說的是毫秒級的範圍。

相比 HTML + CSS , canvas 也是一個很是「苗條」的 API ,這減小了界面上的 bug 或瀏覽器之間的不一致性。有一個理由更加直接,canvas 沒有 Can I Use?。

譯者注 UITableView 是 IOS 控件

更快的 DOM 抽象

如前所述,爲了有點效果,咱們須要一個更高層次的抽象,而不是簡單地繪製矩形、文本和圖像。咱們構建了一個很是小的抽象,容許開發人員處理一個節點樹,而不是處理一個嚴格的繪圖命令序列。

渲染層

渲染層( RenderLayer )是基本節點,其餘節點創建在其上。常見的屬性如 top,left,width,height,backgroundColorzIndex 在這個層展示。 RenderLayer 只不過是一個普通的 JavaScript 對象,包含這些屬性和一個子元素數組。   

圖像

咱們使用圖像層的附加屬性來指定圖像 URL 和信息。你沒必要擔憂圖像加載事件的監聽,圖像層會處理後將一個信號發送到繪圖引擎來表示圖片須要更新。   

文本

文本層能夠顯示多行文本截斷,這在 DOM 裏處理成本很是高。文本層還支持自定義字體,也會處理當字體加載完畢後更新的動做。

合成

這些層能夠合成起來以便構建複雜的界面。下面是一個渲染層樹

{
  frame: [0, 0, 320, 480],
  backgroundColor: '#fff',
  children: [
    {
      type: 'image',
      frame: [0, 0, 320, 200],
      imageUrl: 'http://lorempixel.com/360/420/cats/1/'
    },
    {
      type: 'text',
      frame: [10, 210, 300, 260],
      text: 'Lorem ipsum...',
      fontSize: 18,
      lineHeight: 24
    }
  ]
}

無效層

當一個層須要重繪,例如一個圖像加載後,它發送一個信號到繪圖引擎表示其框架是髒( dirty )的。這些修改使用 requestAnimationFrame來批量處理,避免佈局抖動,以後在下一幀畫布重繪。   

達到60fps的滾動

對於 web 端,也許咱們最理所固然關注的,是瀏覽器如何來滾動網頁。瀏覽器廠商已經不遺餘力提升滾動性能。

這實際上是一個妥協。爲了達到60 fps 的滾動指標,移動瀏覽器在執行滾動期間,中止 JavaScript 的執行,這是怕 DOM 修改致使迴流。最近, IOS 和 Android 暴露了 onScroll 事件,他們的工做過程更像桌面瀏覽器了,但若是你試圖在滾動時保持 DOM 元素的位置同步,具體的實現可能會有差異。

幸運的是,瀏覽器廠商已經意識到這個問題。特別是 Chrome 團隊已經開放了爲了改善手機端這種狀況所作的工做。

回到 canvas ,簡短的回答是你必須用 JavaScript 實現滾動。

你首先須要的是一種計算滾動程度的算法。若是你不想研究數學,Zynga 開源的純滾動實現,適合任何相似此佈局的狀況。

咱們使用一個 canvas 元素來完成滾動。在每個觸摸事件時,根據當前的滾動程度去更新渲染樹。以後,整個渲染樹使用新的座標來從新渲染。

這聽起來使人難以置信的慢,在 canvas 上可使用一個重要的優化技術--畫布上繪圖操做的結果能夠在離屏層(off-screen)canvas 被緩存。離屏層(off-screen)以後能夠用來從新繪製層。

這種技術不只能夠用於圖像層,文本和圖形也適用。兩個成本最高的繪圖操做是填充文本和圖像。可是一旦這些層繪製一次之後,接下來使用離屏層從新繪製他們是很是快的。

指尖的流暢體驗

在上面的演示中,每一個頁面的內容分爲兩層:圖像層和文本層。文本層包含多個元素組合在一塊兒。每一幀滾動動畫中,這兩層都使用位圖緩存來重繪。   

對象池

在一個無限列表的滾動過程當中,大量的 RenderLayers 會被創建和銷燬。這會在內存中建立大量的垃圾,當進行垃圾收集時將中止主線程。

爲了不產生大量垃圾, RenderLayers 與相關對象聚集到一個池中。這意味着只有相對較少的層對象被建立。當再也不須要時,它會被釋放回池中,以後能夠重用。

極速快照

緩存複合層的特性能夠帶來另外一個優點:可以將渲染的部分結構做爲一個位圖。你有沒有創建部分 DOM 結構快照的需求?當你將這些結構渲染在 canvas 時,速度會快得使人難以置信。這個將一個項目放入一本雜誌的界面,利用了這種特性來執行一個時間軸維度的平穩過渡。快照包含去掉頂部和底部的整個項目。

指尖的流暢體驗

一個聲明式的 API

如今咱們已經擁有了構建一個應用程序的磚塊。然而,經過命令來構建 RenderLayers 多是乏味的。若是咱們有個相似於DOM工做方式的聲明式 API 不是很好麼?   

React

咱們是 React 框架的忠實粉絲。它的單必定向的數據流和聲明式API已經改變了人們構建應用程序的方式。react最引人注目的特徵就是虛擬 DOM (virtual DOM)。呈現爲HTML容器只是它在瀏覽器中的一個簡單實現。最近引入的 React Native 證實了這一點。

若是咱們將咱們的 canvas 佈局引擎與 react 組件結合起來會咋樣?

React Canvas簡介

React Canvas 使React組件擁有了渲染到canvas的能力。

第一個版本的 canvas 佈局引擎看上去很像命令式的代碼。若是你作過 JavaScript DOM 操做你可能會運行過這樣的代碼:

// Create the parent layer
var root = RenderLayer.getPooled();
root.frame = [0, 0, 320, 480];

// Add an image
var image = RenderLayer.getPooled('image');
image.frame = [0, 0, 320, 200];
image.imageUrl = 'http://lorempixel.com/360/420/cats/1/';
root.addChild(image);

// Add some text
var label = RenderLayer.getPooled('text');
label.frame = [10, 210, 300, 260];
label.text = 'Lorem ipsum...';
label.fontSize = 18;
label.lineHeight = 24;
root.addChild(label);

固然,這樣能完成效果,可是誰想這樣寫代碼?除了容易出錯,也很難想象出渲染結果

使用React Canvas則變成下面這樣:

var MyComponent = React.createClass({
  render: function () {
    return (
      <Group style={styles.group}>
        <Image style={styles.image} src='http://...' />
        <Text style={styles.text}>
          Lorem ipsum...
        </Text>
      </Group>
    );
  }
});

var styles = {
  group: {
    left: 0,
    top: 0,
    width: 320,
    height: 480
  },

  image: {
    left: 0,
    top: 0,
    width: 320,
    height: 200
  },

  text: {
    left: 10,
    top: 210,
    width: 300,
    height: 260,
    fontSize: 18,
    lineHeight: 24
  }
};

您可能會注意到,一切都彷佛是絕對定位實現的。確實是這樣。咱們的canvas渲染引擎的誕生,就伴隨着驅動像素級佈局,實現多行文本過長省略的使命。傳統的CSS不能作到這一點,因此絕對定位的方式對咱們來講很適合。然而,這種方法並不適合於全部應用程序。   

css佈局

Facebook 最近開源的CSS的JavaScript實現。他支持 CSS 的一些子集,包括 marginpadding,position 和最重要的 flexbox

將 css 佈局整合到 React Canvas 只是一個時間問題。看看這個例子,看看咱們是如何改變組件樣式的。

聲明式的無限滾動

你如何在 React Canvas 中建立一個達到60 fps ,無限,分頁的滾動列表?事實證實這實現起來很是容易,由於 react 會作虛擬 DOM 的 diff 。在render() 函數中只有當前可見的元素被返回,React負責更新滾動期間所需的虛擬 DOM 樹。

var ListView = React.createClass({
  getInitialState: function () {
    return {
      scrollTop: 0
    };
  },

  render: function () {
    var items = this.getVisibleItemIndexes().map(this.renderItem);
    return (
      <Group
        onTouchStart={this.handleTouchStart}
        onTouchMove={this.handleTouchMove}
        onTouchEnd={this.handleTouchEnd}
        onTouchCancel={this.handleTouchEnd}>
        {items}
      </Group>
    );
  },

  renderItem: function (itemIndex) {
    // Wrap each item in a <Group> which is translated up/down based on
    // the current scroll offset.
    var translateY = (itemIndex * itemHeight) - this.state.scrollTop;
    var style = { translateY: translateY };
    return (
      <Group style={style} key={itemIndex}>
        <Item />
      </Group>
    );
  },

  getVisibleItemIndexes: function () {
    // Compute the visible item indexes based on `this.state.scrollTop`.
  }
});

爲了勾住(捕獲)滾動,咱們在列表視圖組件中,調用Scroller(滾動組件)的 setState() 方法。

...

// Create the Scroller instance on mount.
componentDidMount: function () {
  this.scroller = new Scroller(this.handleScroll);
},

// This is called by the Scroller at each scroll event.
handleScroll: function (left, top) {
  this.setState({ scrollTop: top });
},

handleTouchStart: function (e) {
  this.scroller.doTouchStart(e.touches, e.timeStamp);
},

handleTouchMove: function (e) {
  e.preventDefault();
  this.scroller.doTouchMove(e.touches, e.timeStamp, e.scale);
},

handleTouchEnd: function (e) {
  this.scroller.doTouchEnd(e.timeStamp);
}

...

儘管這是一個簡化版本,但展現了 React 一些最優秀的特性。觸摸事件被聲明式綁定在 render()函數中。每一個 touchmove 事件被轉發到 Scroller 中來計算當前滾動的偏移。每一個 Scroller 發出的滾動事件則用於更新狀態列表視圖組件,只對當前屏幕可見的元素進行渲染。全部這一切發生在16ms如下,由於 react 的 diff 算法很是快

你能夠查看這個滾動列表完整實現的源代碼。

實際應用

React Canvas 並不能徹底取代 DOM。咱們在咱們的移動 web app 中,性能要求最關鍵的地方去使用,主要是滾動時間軸視圖這部分。

當渲染性能不是問題的時候, Dom 多是一個更好的方法。事實上,對某些元素輸入字段和音頻/視頻等,這是惟一的方法

從某種意義上說, Flipboard 的移動 web 是一個混合( hybird )的應用程序。相比傳統的原生應用和網絡技術結合的方式, Flipboard 的內容所有是 web 內容。它的 UI 基於 dom 實現,並在適當的地方使用 canvas 渲染。

可訪問性

這個領域須要進一步探索。使用降級內容( canvas 的 DOM 子樹)應該容許 VoiceOver 這樣的屏幕閱讀器與內容交互。咱們在測試的設備上看到了不一樣的結果。另外,關於焦點的管理也有標準,但目前暫時不被瀏覽器支持。

Bespin在2009年提出的一種方法,是元素渲染到 canvas 時,同時維護一個平行Dom,用於元素同步。咱們正在繼續研究實現可訪問性的正確方法。

結論

在追求60 fps 的過程當中,咱們有時會採起極端措施。 Flipboard 爲研究移動網絡的侷限性提供了一個案例。雖然這種方法可能並不適用於全部應用程序,咱們將應用的交互和性能水平提高到能夠與本地應用相競爭。咱們但願經過開放咱們在 React Canvas 中所作的工做,可讓其餘引人注目的例子出現。

用手機訪問flipboard.com,體驗一下。或者若是你沒有 Flipboard 帳戶,體驗一下 Flipboard 上一系列雜誌。請讓咱們得到你的想法。

特別感謝 Charles, Eugene 和 Anh 的編輯和建議。

http://jcodecraeer.com/a/qianduankaifa/css3/2015/0305/2549.html

相關文章
相關標籤/搜索