Flutter完整開發實戰詳解(七、 深入佈局原理)

戀貓de小郭發表於2019-03-16

作為系列文章的第七篇,本篇主要在前文的基礎上,再深入瞭解 Widget 和佈局中的一些常識性問題。

前文:

在第六篇中我們知道了 WidgetElementRenderObject 三者之間的關係,其中我們最為熟知的 Widget ,作為“配置檔案”的存在,在 Flutter 中它的功能都是比較單一的,屬於 “顆粒度比較細的存在” ,寫程式碼時就像拼樂高“積木”,那這“積木”究竟怎麼拼的?下面就 深入 去挖挖有意思的東西吧。( ̄▽ ̄)

一、單子元素佈局

在 Flutter 單個子元素的佈局 Widget 中,Container 無疑是被用的最廣泛的,因為它在“功能”上並不會如 Padding 等 Widget 那樣功能單一,這是為什麼呢?

究其原因,從下圖原始碼可以看出,Container 其實也只是把其他“單一”的 Widget 做了二次封裝,然後通過配置來達到“多功能的效果”而已。

Container原始碼

接著我們先看 ConstrainedBox 原始碼,從下圖原始碼可以看出,它是繼承了 SingleChildRenderObjectWidget,關鍵是 override 了 createRenderObject 方法,返回了 RenderConstrainedBox

這裡體現了第六篇中的 Widget 與 RenderObject 的關係

是的,RenderConstrainedBox 就是繼承自 RenderBox,從而實現RenderObject 的佈局,這裡我們得到了它們的關係如下 :

Widget RenderObject
RenderConstrainedBox RenderConstrainedBox

ConstrainedBox

然後我們繼續對其他每個 Widget 進行觀察,可以看到它們也都是繼承SingleChildRenderObjectWidget ,而“簡單來說”它們不同的地方就是 RenderObject 的實現了:

Widget RenderBox (RenderObject)
Align RenderPositionedBox
Padding RenderPadding
Transform RenderTransform
Offstage RenderOffstage

所以我們可以總結:真正的佈局和大小計算等行為,都是在 RenderBox 上去實現的。 不同的 Widget 通過各自的 RenderBox 實現了“差異化”的佈局效果。所以找每個 Widget 的實現,找它的 RenderBox 實現就可以了。

這裡我們通過 Offstage 這個Widget 小結下,Offstage 這個 Widget 是通過 offstage 標誌控制 child 是否顯示的效果,同樣的它也有一個 RenderOffstage ,如下圖,通過 RenderOffstage 的原始碼我們可以“真實”看到 offstage 標誌位的作用:

RenderOffstage

所以大部分時候,我們的 Widget 都是通過實現 RenderBox 實現佈局的 ,那我們可不可拋起 Widget 直接用 RenderBox呢?答案明顯是可以的,如果你閒的?疼的話!

Flutter 官方為了治療我們“?疼”,提供了一個叫 CustomSingleChildLayout 的類,它抽象了一個叫 SingleChildLayoutDelegate 的物件,讓你可以更方便的操作 RenderBox 來達到自定義的效果。

Flutter完整開發實戰詳解(七、 深入佈局原理)

如下圖三張原始碼所示,SingleChildLayoutDelegate 的物件提供以下介面,並且介面 前三個 是按照順序被呼叫的,通過實現這個介面,你就可以輕鬆的控制RenderBox 的 佈局位置、大小 等。

Flutter完整開發實戰詳解(七、 深入佈局原理)

Flutter完整開發實戰詳解(七、 深入佈局原理)

Flutter完整開發實戰詳解(七、 深入佈局原理)

二、多子元素佈局

事實上“多子元素佈局”和單子元素類似,通過“舉一反三”我們就可以知道它們的關係了,比如:

  • RowColum 都繼承了 Flex,而 Flex 繼承了MultiChildRenderObjectWidget 並通過 RenderFlex 建立了 RenderBox
  • Stack 同樣繼承 MultiChildRenderObjectWidget 並通過 RenderStack 建立了 RenderBox
Widget RenderBox (RenderObject)
Row/Colum/Flex RenderFlex
Stack RenderStack
Flow RenderFlow
Wrap RenderWrap

同樣“多子元素佈局”也提供了 CustomMultiChildLayoutMultiChildLayoutDelegate 滿足你的“?疼”需求。

三、多子元素滑動佈局

滑動佈局作為 “多子元素佈局” 的另一個分支,如 ListViewGridViewPageview ,它們在實現上要複雜的多,從下圖一個的流程上我們大致可以知道它們的關係:

Flutter完整開發實戰詳解(七、 深入佈局原理)

由上圖我們可以知道,流程最終回產生兩個 RenderObject

  • RenderSliverBase class for the render objects that implement scroll effects in viewports.

  • RenderViewportA render object that is bigger on the inside.

/// [RenderViewport] cannot contain [RenderBox] children directly. Instead, use
/// a [RenderSliverList], [RenderSliverFixedExtentList], [RenderSliverGrid], or
/// a [RenderSliverToBoxAdapter], for example.
複製程式碼

並且從 RenderViewport的說明我們知道,RenderViewport內部是不能直接放置 RenderBox,需要通過 RenderSliver 大家族來完成佈局。而從原始碼可知:RenderViewport 對應的 Widget Viewport 就是一個 MultiChildRenderObjectWidget (你看,又回到 MultiChildRenderObjectWidget 了吧。)

再稍微說下上圖的流程:

  • ListViewPageviewGridView 等都是通過 ScrollableViewPortSliver大家族實現的效果。這裡簡單不規範描述就是:一個“可滑動”的控制元件,巢狀了一個“視覺視窗”,然後內部通過“碎片”展示 children

  • 不同的是 PageView 沒有繼承 SrollView,而是直接通過 NotificationListenerScrollNotification 巢狀實現。

注意 TabBarView 內部就是:NotificationListener + PageView

是不是覺得少了什麼?哈哈哈,有的有的,官方同樣提供瞭解決“?疼”的自定義滑動 CustomScrollView ,它繼承了 ScrollView,可通過 slivers 引數實現佈局,這些 slivers 最終回通過 ScrollablebuildViewport 新增到 ViewPort 中,如下程式碼所示:

CustomScrollView(
  slivers: <Widget>[
    const SliverAppBar(
      pinned: true,
      expandedHeight: 250.0,
      flexibleSpace: FlexibleSpaceBar(
        title: Text('Demo'),
      ),
    ),
    SliverGrid(
      gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
        maxCrossAxisExtent: 200.0,
        mainAxisSpacing: 10.0,
        crossAxisSpacing: 10.0,
        childAspectRatio: 4.0,
      ),
      delegate: SliverChildBuilderDelegate(
        (BuildContext context, int index) {
          return Container(
            alignment: Alignment.center,
            color: Colors.teal[100 * (index % 9)],
            child: Text('grid item $index'),
          );
        },
        childCount: 20,
      ),
    ),
    SliverFixedExtentList(
      itemExtent: 50.0,
      delegate: SliverChildBuilderDelegate(
        (BuildContext context, int index) {
          return Container(
            alignment: Alignment.center,
            color: Colors.lightBlue[100 * (index % 9)],
            child: Text('list item $index'),
          );
        },
      ),
    ),
  ],
)
複製程式碼

不知道你看完本篇後,有沒有對 Flutter 的佈局有更深入的瞭解呢?讓我們愉悅的堆積木吧!

自此,第七篇終於結束了!(///▽///)

資源推薦

完整開源專案推薦:
文章

《Flutter完整開發實戰詳解(一、Dart語言和Flutter基礎)》

《Flutter完整開發實戰詳解(二、 快速開發實戰篇)》

《Flutter完整開發實戰詳解(三、 打包與填坑篇)》

《Flutter完整開發實戰詳解(四、Redux、主題、國際化)》

《Flutter完整開發實戰詳解(五、 深入探索)》

《Flutter完整開發實戰詳解(六、 深入Widget原理)》

《Flutter完整開發實戰詳解(七、 深入佈局原理)》

《Flutter完整開發實戰詳解(八、 實用技巧與填坑)》

《Flutter完整開發實戰詳解(九、 深入繪製原理)》

《Flutter完整開發實戰詳解(十、 深入圖片載入流程)》

《Flutter完整開發實戰詳解(十一、全面深入理解Stream)》

《跨平臺專案開源專案推薦》

《移動端跨平臺開發的深度解析》

《React Native 的未來與React Hooks》

我們還會再見嗎?

相關文章