Flutter完整開發(fā)實(shí)戰(zhàn)詳解(七、 深入布局原理)

作為系列文章的第七篇,本篇主要在前文的基礎(chǔ)上,再深入了解 Widget 和布局中的一些常識(shí)性問題。

文章匯總地址:

Flutter 完整實(shí)戰(zhàn)實(shí)戰(zhàn)系列文章專欄

Flutter 番外的世界系列文章專欄

在第六篇中我們知道了 WidgetElementRenderObject 三者之間的關(guān)系,其中我們最為熟知的 Widget ,作為“配置文件”的存在,在 Flutter 中它的功能都是比較單一的,屬于 “顆粒度比較細(xì)的存在” ,寫代碼時(shí)就像拼樂高“積木”,那這“積木”究竟怎么拼的?下面就 深入 去挖挖有意思的東西吧。( ̄▽ ̄)

一、單子元素布局

在 Flutter 單個(gè)子元素的布局 Widget 中,Container 無疑是被用的最廣泛的,因?yàn)樗凇肮δ堋鄙喜⒉粫?huì)如 Padding 等 Widget 那樣功能單一,這是為什么呢?

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

Container源碼

接著我們先看 ConstrainedBox 源碼,從下圖源碼可以看出,它是繼承了 SingleChildRenderObjectWidget,關(guān)鍵是 override 了 createRenderObject 方法,返回了 RenderConstrainedBox

這里體現(xiàn)了第六篇中的 Widget 與 RenderObject 的關(guān)系

是的,RenderConstrainedBox 就是繼承自 RenderBox,從而實(shí)現(xiàn)RenderObject 的布局,這里我們得到了它們的關(guān)系如下 :

Widget RenderObject
ConstrainedBox RenderConstrainedBox
ConstrainedBox

然后我們繼續(xù)對(duì)其他每個(gè) Widget 進(jìn)行觀察,可以看到它們也都是繼承SingleChildRenderObjectWidget ,而“簡(jiǎn)單來說”它們不同的地方就是 RenderObject 的實(shí)現(xiàn)了:

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

所以我們可以總結(jié):真正的布局和大小計(jì)算等行為,都是在 RenderBox 上去實(shí)現(xiàn)的。 不同的 Widget 通過各自的 RenderBox 實(shí)現(xiàn)了“差異化”的布局效果。所以找每個(gè) Widget 的實(shí)現(xiàn),找它的 RenderBox 實(shí)現(xiàn)就可以了。(當(dāng)然,另外還有 RenderSliver,這里暫時(shí)不討論)

這里我們通過 Offstage 這個(gè)Widget 小結(jié)下,Offstage 這個(gè) Widget 是通過 offstage 標(biāo)志控制 child 是否顯示的效果,同樣的它也有一個(gè) RenderOffstage ,如下圖,通過 RenderOffstage 的源碼我們可以“真實(shí)”看到 offstage 標(biāo)志位的作用:

RenderOffstage

所以大部分時(shí)候,我們的 Widget 都是通過實(shí)現(xiàn) RenderBox 實(shí)現(xiàn)布局的 ,那我們可不可拋起 Widget 直接用 RenderBox呢?答案明顯是可以的,如果你閑的??疼的話!

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

image

如下圖三張?jiān)创a所示,SingleChildLayoutDelegate 的對(duì)象提供以下接口,并且接口 前三個(gè) 是按照順序被調(diào)用的,通過實(shí)現(xiàn)這個(gè)接口,你就可以輕松的控制RenderBox 的 布局位置、大小 等。

image
image
image

二、多子元素布局

事實(shí)上“多子元素布局”和單子元素類似,通過“舉一反三”我們就可以知道它們的關(guān)系了,比如:

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

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

三、多子元素滑動(dòng)布局

滑動(dòng)布局作為 “多子元素布局” 的另一個(gè)分支,如 ListViewGridViewPageview ,它們?cè)趯?shí)現(xiàn)上要復(fù)雜的多,從下圖一個(gè)的流程上我們大致可以知道它們的關(guān)系:

image

由上圖我們可以知道,流程最終回產(chǎn)生兩個(gè) 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內(nèi)部是不能直接放置 RenderBox,需要通過 RenderSliver 大家族來完成布局。而從源碼可知:RenderViewport 對(duì)應(yīng)的 Widget Viewport 就是一個(gè) MultiChildRenderObjectWidget (你看,又回到 MultiChildRenderObjectWidget 了吧。)

再稍微說下上圖的流程:

  • ListViewPageviewGridView 等都是通過 ScrollableViewPortSliver大家族實(shí)現(xiàn)的效果。這里簡(jiǎn)單不規(guī)范描述就是:一個(gè)“可滑動(dòng)”的控件,嵌套了一個(gè)“視覺窗口”,然后內(nèi)部通過“碎片”展示 children

  • 不同的是 PageView 沒有繼承 SrollView,而是直接通過 NotificationListenerScrollNotification 嵌套實(shí)現(xiàn)。

注意 TabBarView 內(nèi)部就是:NotificationListener + PageView

是不是覺得少了什么?哈哈哈,有的有的,官方同樣提供了解決“??疼”的自定義滑動(dòng) CustomScrollView ,它繼承了 ScrollView,可通過 slivers 參數(shù)實(shí)現(xiàn)布局,這些 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'),
          );
        },
      ),
    ),
  ],
)

不知道你看完本篇后,有沒有對(duì) Flutter 的布局有更深入的了解呢?讓我們愉悅的堆積木吧!

自此,第七篇終于結(jié)束了!(///▽///)

資源推薦

完整開源項(xiàng)目推薦:
我們還會(huì)再見嗎?
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容