作為系列文章的第七篇,本篇主要在前文的基礎(chǔ)上,再深入了解 Widget 和布局中的一些常識(shí)性問題。
文章匯總地址:
在第六篇中我們知道了 Widget
、Element
、RenderObject
三者之間的關(guān)系,其中我們最為熟知的 Widget
,作為“配置文件”的存在,在 Flutter 中它的功能都是比較單一的,屬于 “顆粒度比較細(xì)的存在” ,寫代碼時(shí)就像拼樂高“積木”,那這“積木”究竟怎么拼的?下面就 深入 去挖挖有意思的東西吧。( ̄▽ ̄)
一、單子元素布局
在 Flutter 單個(gè)子元素的布局 Widget 中,Container
無疑是被用的最廣泛的,因?yàn)樗凇肮δ堋鄙喜⒉粫?huì)如 Padding
等 Widget 那樣功能單一,這是為什么呢?
究其原因,從下圖源碼可以看出,Container
其實(shí)也只是把其他“單一”的 Widget 做了二次封裝,然后通過配置來達(dá)到“多功能的效果”而已。
接著我們先看 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 |
然后我們繼續(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)志位的作用:
所以大部分時(shí)候,我們的 Widget 都是通過實(shí)現(xiàn) RenderBox
實(shí)現(xiàn)布局的 ,那我們可不可拋起 Widget 直接用 RenderBox
呢?答案明顯是可以的,如果你閑的??疼的話!
Flutter 官方為了治療我們“??疼”,提供了一個(gè)叫 CustomSingleChildLayout
的類,它抽象了一個(gè)叫 SingleChildLayoutDelegate
的對(duì)象,讓你可以更方便的操作 RenderBox
來達(dá)到自定義的效果。
如下圖三張?jiān)创a所示,SingleChildLayoutDelegate
的對(duì)象提供以下接口,并且接口 前三個(gè) 是按照順序被調(diào)用的,通過實(shí)現(xiàn)這個(gè)接口,你就可以輕松的控制RenderBox 的 布局位置、大小 等。
二、多子元素布局
事實(shí)上“多子元素布局”和單子元素類似,通過“舉一反三”我們就可以知道它們的關(guān)系了,比如:
-
Row
、Colum
都繼承了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 |
同樣“多子元素布局”也提供了 CustomMultiChildLayout
和 MultiChildLayoutDelegate
滿足你的“??疼”需求。
三、多子元素滑動(dòng)布局
滑動(dòng)布局作為 “多子元素布局” 的另一個(gè)分支,如 ListView
、GridView
、Pageview
,它們?cè)趯?shí)現(xiàn)上要復(fù)雜的多,從下圖一個(gè)的流程上我們大致可以知道它們的關(guān)系:
由上圖我們可以知道,流程最終回產(chǎn)生兩個(gè) RenderObject :
RenderSliver
:Base class for the render objects that implement scroll effects in viewports.RenderViewport
:A 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
了吧。)
再稍微說下上圖的流程:
ListView
、Pageview
、GridView
等都是通過Scrollable
、ViewPort
、Sliver
大家族實(shí)現(xiàn)的效果。這里簡(jiǎn)單不規(guī)范描述就是:一個(gè)“可滑動(dòng)”的控件,嵌套了一個(gè)“視覺窗口”,然后內(nèi)部通過“碎片”展示 children 。不同的是
PageView
沒有繼承SrollView
,而是直接通過NotificationListener
和ScrollNotification
嵌套實(shí)現(xiàn)。
注意
TabBarView
內(nèi)部就是:NotificationListener
+PageView
是不是覺得少了什么?哈哈哈,有的有的,官方同樣提供了解決“??疼”的自定義滑動(dòng) CustomScrollView
,它繼承了 ScrollView
,可通過 slivers 參數(shù)實(shí)現(xiàn)布局,這些 slivers
最終回通過 Scrollable
的 buildViewport
添加到 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é)束了!(///▽///)
資源推薦
- Github : https://github.com/CarGuo/
- 開源 Flutter 完整項(xiàng)目:https://github.com/CarGuo/GSYGithubAppFlutter
- 開源 Flutter 多案例學(xué)習(xí)型: https://github.com/CarGuo/GSYFlutterDemo
- 開源 Fluttre 實(shí)戰(zhàn)電子書項(xiàng)目:https://github.com/CarGuo/GSYFlutterBook