Flutter 的渲染邏輯及和 Native 通訊
本文首發於RTC 開發者社群,作者劉斯龍, 5年的 Android 程式設計師,從事過 AR ,Unity3D,Weex,Cordova,Flutter 及小程式開發
作者 github: github.com/liusilong
作者 blog:liusilong.github.io/
作者 StackOverflow: stackoverflow.com/users/47233…
作者掘金部落格: juejin.im/user/58eb94…
在這篇文章中,我們主要了解兩個部分的內容,一個是 Flutter 的基本渲染邏輯 另一個是 Flutter 和 Native 互通的方法 ,這裡的 Native 是以 Android 為例。然後使用案例分別進行演示。
Flutter 渲染
在 Android 中,我們所說的 View
的渲染邏輯指的是 onMeasure()
, onLayout()
, onDraw()
, 我們只要重寫這三個方法就可以自定義出符合我們需求的 View
。其實,即使我們不懂 Android 中 View 的渲染邏輯,也能寫出大部分的 App,但是當系統提供的 View 滿足不了我們的需求的時候,這時就需要我們自定義 View 了,而自定義 View 的前提就是要知道 View 的渲染邏輯。
Flutter中也一樣,系統提供的 Widget 可以滿足我們大部分的需求,但是在一些情況下我們還是得渲染自己的 Widget。
和 Android 類似,Flutter 中的渲染也會經歷幾個必要的階段,如下:
- Layout : 佈局階段,Flutter 會確定每一個子 Widget 的大小和他們在螢幕中將要被放置的位置。
- Paint : 繪製階段,Flutter 為每個子 Widget 提供一個 canvas,並讓他們繪製自己。
- Composite : 組合階段,Flutter 會將所有的 Widget 組合在一起,並交由 GPU 處理。
上面三個階段中,比較重要的就是 Layout 階段了,因為一切都始於佈局。
在 Flutter 中,佈局階段會做兩個事情:父控制元件將 約束(Constraints) 向下傳遞到子控制元件;子控制元件將自己的 佈局詳情(Layout Details) 向上傳遞給父控制元件。如下圖:

佈局過程如下:
這裡我們將父 widget 稱為 parent;將子 widget 稱為 child
-
parent 會將某些佈局約束傳遞給 child,這些約束是每個 child 在 layout 階段必須要遵守的。如同 parent 這樣告訴 child :“只要你遵守這些規則,你可以做任何你想做的事”。最常見的就是 parent 會限制 child 的大小,也就是 child 的 maxWidth 或者 maxHeight。
-
然後 child 會根據得到的約束生成一個新的約束,並將這個新的約束傳遞給自己的 child(也就是 child 的 child),這個過程會一直持續到出現沒有 child 的 widget 為止。
-
之後,child 會根據 parent 傳遞過來的約束確定自己的 佈局詳情(Layout Details) 。如:假設 parent 傳遞給 child 的最大寬度約束為 500px,child 可能會說:“好吧,那我就用500px”,或者 “我只會用 100px”。這樣,child 就確定了自己的佈局詳情,並將其傳遞給 parent。
-
parent 反過來做同樣的事情,它根據 child 傳遞回來的 Layout Details 來確定其自身的 Layout Details,然後將這些 Layout Details 向上層的 parent 傳遞,直到到達 root widget (根 widget)或者遇到了某些限制。
那我們上面所提到的 約束(Constraints) 和 佈局詳情(Layout Details) 都是什麼呢?這取決於 佈局協議(Layout protocol) 。Flutter 中有兩種主要的佈局協議: Box Protocol 和 Sliver Protocol ,前者可以理解為類似於盒子模型協議,後者則是和滑動佈局相關的協議。這裡我們以前者為例。
在 Box Protocol 中,parent 傳遞給 child 的約束都叫做BoxConstraints 這些約束決定了每個 child 的 maxWidth 和 maxHeight 以及 minWidth 和 minHeight。如:parent 可能會將如下的BoxConstraints 傳遞給 child。

上圖中,淺綠色的為 parent,淺紅色的小矩形為 child。 那麼,parent 傳遞給 child 的約束就是 150 ≤ width ≤ 300, 100 ≤ height ≤ 無限大
而 child 回傳給 parent 的佈局詳情就是 child 的尺寸(Size)。
有了 child 的 Layout Details ,parent 就可以繪製它們了。
在我們渲染自己的 widget 之前,先來了解下另外一個東西 Render Tree 。
Render Tree
我們在 Android
中會有 View tree ,Flutter 中與之對應的為 Widget tree ,但是 Flutter 中還有另外一種 tree,稱為 Render tree 。
在 Flutter 中 我們常見的 widget 有 StatefulWidget
, StatelessWidget
, InheritedWidget
等等。但是這裡還有另外一種 widget 稱為 RenderObjectWidget
,這個 widget 中沒有 build()
方法,而是有一個 createRenderObject()
方法,這個方法允許建立一個 RenderObject
並將其新增到 render tree 中。
RenderObject 是渲染過程中非常重要的元件,render tree 中的內容都是 RenderObject,每個 RenderObject 中都有許多用來執行渲染的屬性和方法:
- constraints : 從 parent 傳遞過來的約束。
- parentData: 這裡面攜帶的是 parent 渲染 child 的時候所用到的資料。
- performLayout():此方法用於佈局所有的 child。
- paint():這個方法用於繪製自己或者 child。
- 等等...
但是,RenderObject 是一個抽象類,他需要被子類繼承來進行實際的渲染。RenderObject 的兩個非常重要的子類是RenderBox 和RenderSliver 。這兩個類是所有實現 Box Protocol 和 Sliver Protocol 的渲染物件的父類。而且這兩個類還擴充套件了數十個和其他幾個處理特定場景的類,並且實現了渲染過程的細節。
現在我們開始渲染自己的 widget,也就是建立一個 RenderObject。這個 widget 需要滿足下面兩點要求:
- 它只會給 child 最小的寬和高
- 它會把它的 child 放在自己的右下角
如此 “小氣” 的 widget ,我們就叫他 Stingy 吧! Stingy 所屬的樹形結構如下:
MaterialApp |_Scaffold |_Container// Stingy 的 parent |_Stingy// 自定義的 RenderObject |_Container// Stingy 的 child 複製程式碼
程式碼如下:
void main() { runApp(MaterialApp( home: Scaffold( body: Container( color: Colors.greenAccent, constraints: BoxConstraints( maxWidth: double.infinity, minWidth: 100.0, maxHeight: 300, minHeight: 100.0), child: Stingy( child: Container( color: Colors.red, ), ), ), ), )); } 複製程式碼
Stingy
class Stingy extends SingleChildRenderObjectWidget { Stingy({Widget child}) : super(child: child); @override RenderObject createRenderObject(BuildContext context) { // TODO: implement createRenderObject return RenderStingy(); } } 複製程式碼
Stingy
繼承了 SingleChildRenderObjectWidget
,顧名思義,他只能有一個 child 而 createRenderObject(...)
方法建立並返回了一個 RenderObject
為 RenderStingy
類的例項
RenderStingy
class RenderStingy extends RenderShiftedBox { RenderStingy() : super(null); // 繪製方法 @override void paint(PaintingContext context, Offset offset) { // TODO: implement paint super.paint(context, offset); } // 佈局方法 @override void performLayout() { // 佈局 child 確定 child 的 size child.layout( BoxConstraints( minHeight: 0.0, maxHeight: constraints.minHeight, minWidth: 0.0, maxWidth: constraints.minWidth), parentUsesSize: true); print('constraints: $constraints'); // child 的 Offset final BoxParentData childParentData = child.parentData; childParentData.offset = Offset(constraints.maxWidth - child.size.width, constraints.maxHeight - child.size.height); print('childParentData: $childParentData'); // 確定自己(Stingy)的大小 類似於 Android View 的 setMeasuredDimension(...) size = Size(constraints.maxWidth, constraints.maxHeight); print('size: $size'); } } 複製程式碼
RenderStingy
繼承自 RenderShiftedBox
,該類是繼承自 RenderBox
。 RenderShiftedBox
實現了 Box Protocol 所有的細節,並且提供了 performLayout()
方法的實現。我們需要在 performLayout()
方法中佈局我們的 child ,還可以設定他們的偏移量。
我們在使用 child.layout(...)
方法佈局 child 的時候傳遞了兩個引數,第一個為 child 的佈局約束,而另外一個引數是 parentUserSize
, 該引數如果設定為 false
,則意味著 parent 不關心 child 選擇的大小,這對佈局優化比較有用;因為如果 child 改變了自己的大小, parent 就不必重新 layout
了。但是在我們的例子中,我們的需要把 child 放置在 parent 的右下角,這意味著如果 child 的 大小(Size) 一旦改變,則其對應的 偏移量(Offset) 也會改變,這就意味著 parent 需要重新佈局,所以我們這裡傳遞了一個 true
。
當 child.layout(...)
完成了以後, child 就確定了自己的 Layout Details 。然後我們就還可以為其設定偏移量來將它放置到我們想放的位置。在我們的例子中為 右下角 。
最後,和 child 根據 parent 傳遞過來的約束選擇了一個尺寸一樣,我們也需要為 Stingy 選擇一個尺寸,以至於 Stingy 的 parent 知道如何放置它。類似於在 Android 中我們自定義 View
重寫 onMeasure(...)
方法的時候需要呼叫 setMeasuredDimension(...)
一樣。
執行效果如下:

綠色部分為我們定義的 Stingy,紅色小方塊為 Stingy 的 child ,這裡是一個 Container
程式碼中的輸入如下 (iphone 6 尺寸):
flutter: constraints: BoxConstraints(100.0<=w<=375.0, 100.0<=h<=300.0) flutter: childParentData: offset=Offset(275.0, 200.0) flutter: size: Size(375.0, 300.0) 複製程式碼
上述我們自定義 RenderBox
的 performLayout()
中做的事情可大概分為如下三個步驟:
- 使用
child.layout(...)
來佈局 child ,這裡是為 child 根據 parent 傳遞過來的約束選擇一個大小 -
child.parentData.offset
, 這是在為 child 如何擺放設定一個偏移量 - 設定當前 widget 的
size
在我們的例子中, Stingy 的 child 是一個 Container
,並且 Container
沒有 child ,因此他會使用 child.layout(...)
中設定的最大約束。通常,每個 widget 都會以不同的方式來處理提供給他的約束。如果我們使用 RaiseButton
替換 Container
:
Stingy( child: RaisedButton( child: Text('Button'), onPressed: (){} ) ) 複製程式碼
效果如下:

可以看到, RaisedButton
的 width 使用了 parent 給他傳遞的約束值 100,但是高度很明顯沒有 100, RaisedButton
的高度預設為 48 ,由此可見 RaisedButton
內部對 parent 傳遞過來的約束做了一些處理。
我們上面的 Stingy 繼承的是 SingleChildRenderObjectWidget
,也就是隻能有一個 child 。那如果有多個 child 怎麼辦,不用擔心,這裡還有一個 MultiChildRenderObjectWidget
,而這個類有一個子類叫做 CustomMultiChildLayout
,我們直接用這個子類就好。
先來看看 CustomMultiChildLayout
的構造方法如下:
/// The [delegate] argument must not be null. CustomMultiChildLayout({ Key key, @required this.delegate, List<Widget> children = const <Widget>[], }) 複製程式碼
- key: widget 的一個標記,可以起到識別符號的作用
- delegate:這個特別重要,註釋上明確指出這個引數一定不能為空,我們在下會說
- children:這個就很好理解了,他是一個 widget 陣列,也就是我們們需要渲染的 widget
上面的 delegate
引數型別如下:
/// The delegate that controls the layout of the children. final MultiChildLayoutDelegate delegate; 複製程式碼
可以看出 delegate
的型別為 MultiChildLayoutDelegate
,並且註釋也說明了它的作用: 控制 children 的佈局 。也就是說,我們的 CustomMultiChildLayout
裡面要怎麼佈局,完全取決於我們自定義的 MultiChildLayoutDelegate
裡面的實現。所以 MultiChildLayoutDelegate
中也會有類似的 performLayout(..)
方法。
另外, CustomMultiChildLayout
中的每個 child 必須使用 LayoutId
包裹,註釋如下:
/// Each child must be wrapped in a [LayoutId] widget to identify the widget for /// the delegate. 複製程式碼
LayoutId 的構造方法如下:
/// Marks a child with a layout identifier. /// Both the child and the id arguments must not be null. LayoutId({ Key key, @required this.id, @required Widget child }) 複製程式碼
註釋的大概意思說的是:使用一個佈局標識來標識一個 child ;引數 child
和 引數 id
不定不能為空。 我們在佈局 child 的時候會根據 child 的 id
來佈局。
下面我們來使用 CustomMultiChildLayout
實現一個用於展示熱門標籤的效果:
Container( child: CustomMultiChildLayout( delegate: _LabelDelegate(itemCount: items.length, childId: childId), children: items, ), ) 複製程式碼
我們的 _LabelDelegate
裡面接受兩個引數,一個為 itemCount
,還有是 childId
。
_LabelDelegate
程式碼如下:
class _LabelDelegate extends MultiChildLayoutDelegate { final int itemCount; final String childId; // x 方向上的偏移量 double dx = 0.0; // y 方向上的偏移量 double dy = 0.0; _LabelDelegate({@required this.itemCount, @required this.childId}); @override void performLayout(Size size) { // 獲取父控制元件的 width double parentWidth = size.width; for (int i = 0; i < itemCount; i++) { // 獲取子控制元件的 id String id = '${this.childId}$i'; // 驗證該 childId 是否對應一個 非空的 child if (hasChild(id)) { // layout child 並獲取該 child 的 size Size childSize = layoutChild(id, BoxConstraints.loose(size)); // 換行條件判斷 if (parentWidth - dx < childSize.width) { dx = 0; dy += childSize.height; } // 根據 Offset 來放置 child positionChild(id, Offset(dx, dy)); dx += childSize.width; } } } /// 該方法用來判斷重新 layout 的條件 @override bool shouldRelayout(_LabelDelegate oldDelegate) { return oldDelegate.itemCount != this.itemCount; } } 複製程式碼
在 _LabelDelegate
中,重寫了 performLayout(...)
方法。方法中有一個引數 size
,這個 size
表示的是當前 widget 的 parent 的 size
,在我們這個例子中也就表示 Container
的 size
。我們可以看看 performLayout(...)
方法的註釋:
/// Override this method to lay out and position all children given this /// widget's size. /// /// This method must call [layoutChild] for each child. It should also specify /// the final position of each child with [positionChild]. void performLayout(Size size); 複製程式碼
還有一個是 hasChild(...)
方法,這個方法接受一個 childId , childId 是由我們自己規定的,這個方法的作用是 判斷當前的 childId 是否對應著一個非空的 child 。
滿足 hasChild(...)
之後,接著就是 layoutChild(...)
來佈局 child , 這個方法中我們會傳遞兩個引數,一個是 childId ,另外一個是 child 的 約束(Constraints) ,這個方法返回的是當前這個 child 的 Size 。
佈局完成之後,就是如何擺放的問題了,也就是上述程式碼中的 positionChild(..)
了,此方法接受一個 childId
和 一個當前 child 對應的 Offset
, parent 會根據這個 Offset
來放置當前的 child 。
最後我們重寫了 shouldRelayout(...)
方法用於判斷重新 Layout 的條件。
完整原始碼在文章末尾給出。
效果如下:

Flutter 和 Native 的互動
我們這裡說的 Native 指的是 Android 平臺。
那既然要相互通訊,就需要將 Flutter 整合到 Android 工程中來,不清楚的如何整合可以看看這裡
這裡有一點需要注意,就是我們在 Android 程式碼中需要初始化 Dart VM ,不然我們在使用 getFlutterView()
來獲取一個 Flutter View 的時候會丟擲如下異常:
Caused by: java.lang.IllegalStateException: ensureInitializationComplete must be called after startInitialization at io.flutter.view.FlutterMain.ensureInitializationComplete(FlutterMain.java:178) ... 複製程式碼
我們有兩種方式來執行初始化操作:一個是直接讓我們的 Application
繼承 FlutterApplication
,另外一個是需要我們在我們自己的 Application
中手動初始化:
方法一:
public class App extends FlutterApplication { } 複製程式碼
方法二:
public class App extends Application { @Override public void onCreate() { super.onCreate(); // 初始化 Flutter Flutter.startInitialization(this); } } 複製程式碼
其實方法一中的 FlutterApplication
中在其 onCreate()
方法中幹了同樣的事情,部分程式碼如下:
public class FlutterApplication extends Application { ... @CallSuper public void onCreate() { super.onCreate(); FlutterMain.startInitialization(this); } ... } 複製程式碼
如果我們的 App 只是需要使用 Flutter 在螢幕上繪製 UI,那麼沒問題, Flutter 框架能夠獨立完成這些事情。但是在實際的開發中,難免會需要呼叫 Native 的功能,如:定位,相機,電池等等。這個時候就需要 Flutter 和 Native 通訊了。
官網上有一個案例 是使用MethodChannel來呼叫給本地的方法獲取手機電量。
其實我們還可以使用另外一個類進行通訊,叫做BasicMessageChannel,先來看看它如果建立:
// java basicMessageChannel = new BasicMessageChannel<String>(getFlutterView(), "foo", StringCodec.INSTANCE); 複製程式碼
BasicMessageChannel
需要三個引數,第一個是BinaryMessenger;第二個是通道名稱,第三個是互動資料型別的編解碼器,我們接下來的例子中的互動資料型別為 String
,所以這裡傳遞的是 StringCodec.INSTANCE
,Flutter 中還有其他型別的編解碼器 BinaryCodec
, JSONMessageCodec
等,他們都有一個共同的父類 MessageCodec
。 所以我們也可以根據規則建立自己編解碼器。
接下來建立的例子是: Flutter
給 Android
傳送一條訊息, Android
收到訊息之後給 Flutter
回覆一條訊息,反之亦然。
先來看看 Android
端的部分程式碼:
// 接收 Flutter 傳送的訊息 basicMessageChannel.setMessageHandler(new BasicMessageChannel.MessageHandler<String>() { @Override public void onMessage(final String s, final BasicMessageChannel.Reply<String> reply) { // 接收到的訊息 linearMessageContainer.addView(buildMessage(s, true)); scrollToBottom(); // 延遲 500ms 回覆 flutterContainer.postDelayed(new Runnable() { @Override public void run() { // 回覆 Flutter String replyMsg = "Android : " + new Random().nextInt(100); linearMessageContainer.addView(buildMessage(replyMsg, false)); scrollToBottom(); // 回覆 reply.reply(replyMsg); } }, 500); } }); // ---------------------------------------------- // 向 Flutter 傳送訊息 basicMessageChannel.send(message, new BasicMessageChannel.Reply<String>() { @Override public void reply(final String s) { linearMessageContainer.postDelayed(new Runnable() { @Override public void run() { // Flutter 的回覆 linearMessageContainer.addView(buildMessage(s, true)); scrollToBottom(); } }, 500); } }); 複製程式碼
類似的, Flutter
這邊的部分程式碼如下:
// 訊息通道 static const BasicMessageChannel<String> channel = BasicMessageChannel<String>('foo', StringCodec()); // ---------------------------------------------- // 接收 Android 傳送過來的訊息,並且回覆 channel.setMessageHandler((String message) async { String replyMessage = 'Flutter: ${Random().nextInt(100)}'; setState(() { // 收到的android 端的訊息 _messageWidgets.add(_buildMessageWidget(message, true)); _scrollToBottom(); }); Future.delayed(const Duration(milliseconds: 500), () { setState(() { // 回覆給 android 端的訊息 _messageWidgets.add(_buildMessageWidget(replyMessage, false)); _scrollToBottom(); }); }); // 回覆 return replyMessage; }); // ---------------------------------------------- // 向 Android 傳送訊息 void _sendMessageToAndroid(String message) { setState(() { _messageWidgets.add(_buildMessageWidget(message, false)); _scrollToBottom(); }); // 向 Android 端傳送傳送訊息並處理 Android 端給的回覆 channel.send(message).then((value) { setState(() { _messageWidgets.add(_buildMessageWidget(value, true)); _scrollToBottom(); }); }); } 複製程式碼
最後的效果如下:
螢幕的上半部分為 Android,下半部分為 Flutter

原始碼地址: flutter_rendering flutter_android_communicate
參考: Flutter’s Rendering Engine: A Tutorial — Part 1 Flutter's Rendering Pipeline