Flutter 的渲染邏輯及和 Native 通訊

本文首發於 RTC 開發者社區,做者劉斯龍, 5年的 Android 程序員,從事過 AR ,Unity3D,Weex,Cordova,Flutter 及小程序開發html

做者 github: github.com/liusilongjava

做者 blog:liusilong.github.io/android

做者 StackOverflow:stackoverflow.com/users/47233…git

做者掘金博客:juejin.im/user/58eb94…程序員

在這篇文章中,咱們主要了解兩個部分的內容,一個是 Flutter 的基本渲染邏輯 另外一個是 Flutter 和 Native 互通的方法,這裏的 Native 是以 Android 爲例。而後使用案例分別進行演示。github

Flutter 渲染

Android 中,咱們所說的 View 的渲染邏輯指的是 onMeasure(), onLayout(), onDraw(), 咱們只要重寫這三個方法就能夠自定義出符合咱們需求的 View。其實,即便咱們不懂 Android 中 View 的渲染邏輯,也能寫出大部分的 App,可是當系統提供的 View 知足不了咱們的需求的時候,這時就須要咱們自定義 View 了,而自定義 View 的前提就是要知道 View 的渲染邏輯。canvas

Flutter 中也同樣,系統提供的 Widget 能夠知足咱們大部分的需求,可是在一些狀況下咱們仍是得渲染本身的 Widget。小程序

和 Android 相似,Flutter 中的渲染也會經歷幾個必要的階段,以下:數組

  • Layout : 佈局階段,Flutter 會肯定每個子 Widget 的大小和他們在屏幕中將要被放置的位置。
  • Paint : 繪製階段,Flutter 爲每一個子 Widget 提供一個 canvas,並讓他們繪製本身。
  • Composite : 組合階段,Flutter 會將全部的 Widget 組合在一塊兒,並交由 GPU 處理。

上面三個階段中,比較重要的就是 Layout 階段了,由於一切都始於佈局。markdown

在 Flutter 中,佈局階段會作兩個事情:父控件將 約束(Constraints) 向下傳遞到子控件;子控件將本身的 佈局詳情(Layout Details) 向上傳遞給父控件。以下圖:

佈局過程以下:

這裏咱們將父 widget 稱爲 parent;將子 widget 稱爲 child

  1. parent 會將某些佈局約束傳遞給 child,這些約束是每一個 child 在 layout 階段必需要遵照的。如同 parent 這樣告訴 child :「只要你遵照這些規則,你能夠作任何你想作的事」。最多見的就是 parent 會限制 child 的大小,也就是 child 的 maxWidth 或者 maxHeight。

  2. 而後 child 會根據獲得的約束生成一個新的約束,並將這個新的約束傳遞給本身的 child(也就是 child 的 child),這個過程會一直持續到出現沒有 child 的 widget 爲止。

  3. 以後,child 會根據 parent 傳遞過來的約束肯定本身的佈局詳情(Layout Details)。如:假設 parent 傳遞給 child 的最大寬度約束爲 500px,child 可能會說:「好吧,那我就用500px」,或者 「我只會用 100px」。這樣,child 就肯定了本身的佈局詳情,並將其傳遞給 parent。

  4. parent 反過來作一樣的事情,它根據 child 傳遞回來的 Layout Details 來肯定其自身的 Layout Details,而後將這些 Layout Details 向上層的 parent 傳遞,直到到達 root widget (根 widget)或者遇到了某些限制。

那咱們上面所提到的 約束(Constraints)佈局詳情(Layout Details) 都是什麼呢?這取決於佈局協議(Layout protocol)。Flutter 中有兩種主要的佈局協議:Box ProtocolSliver 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 中 咱們常見的 widgetStatefulWidgetStatelessWidgetInheritedWidget 等等。可是這裏還有另一種 widget 稱爲 RenderObjectWidget,這個 widget 中沒有 build() 方法,而是有一個 createRenderObject() 方法,這個方法容許建立一個 RenderObject 並將其添加到 render tree 中。

RenderObject 是渲染過程當中很是重要的組件,render tree 中的內容都是 RenderObject,每一個 RenderObject 中都有許多用來執行渲染的屬性和方法:

  • constraints : 從 parent 傳遞過來的約束。
  • parentData: 這裏面攜帶的是 parent 渲染 child 的時候所用到的數據。
  • performLayout():此方法用於佈局全部的 child。
  • paint():這個方法用於繪製本身或者 child。
  • 等等...

可是,RenderObject 是一個抽象類,他須要被子類繼承來進行實際的渲染。RenderObject 的兩個很是重要的子類是 RenderBoxRenderSliver 。這兩個類是全部實現 Box ProtocolSliver 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,顧名思義,他只能有一個 childcreateRenderObject(...) 方法建立並返回了一個 RenderObjectRenderStingy 類的實例

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,該類是繼承自 RenderBoxRenderShiftedBox 實現了 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 選擇一個尺寸,以致於 Stingyparent 知道如何放置它。相似於在 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)
複製代碼

上述咱們自定義 RenderBoxperformLayout() 中作的事情可大概分爲以下三個步驟:

  • 使用 child.layout(...) 來佈局 child,這裏是爲 child 根據 parent 傳遞過來的約束選擇一個大小
  • child.parentData.offset , 這是在爲 child 如何擺放設置一個偏移量
  • 設置當前 widgetsize

在咱們的例子中,Stingychild 是一個 Container,而且 Container 沒有 child,所以他會使用 child.layout(...) 中設置的最大約束。一般,每一個 widget 都會以不一樣的方式來處理提供給他的約束。若是咱們使用 RaiseButton 替換 Container

Stingy(  
  child: RaisedButton(  
    child: Text('Button'),
    onPressed: (){}
  )  
)
複製代碼

效果以下:

能夠看到,RaisedButtonwidth 使用了 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 的時候會根據 childid 來佈局。

下面咱們來使用 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 表示的是當前 widgetparentsize,在咱們這個例子中也就表示 Containersize。咱們能夠看看 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(...) 方法,這個方法接受一個 childIdchildId 是由咱們本身規定的,這個方法的做用是判斷當前的 childId 是否對應着一個非空的 child

知足 hasChild(...) 以後,接着就是 layoutChild(...) 來佈局 child , 這個方法中咱們會傳遞兩個參數,一個是 childId,另一個是 child約束(Constraints),這個方法返回的是當前這個 childSize

佈局完成以後,就是如何擺放的問題了,也就是上述代碼中的 positionChild(..) 了,此方法接受一個 childId 和 一個當前 child 對應的 Offsetparent 會根據這個 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 中還有其餘類型的編解碼器BinaryCodecJSONMessageCodec等,他們都有一個共同的父類 MessageCodec。 因此咱們也能夠根據規則建立本身編解碼器。

接下來建立的例子是:FlutterAndroid 發送一條消息,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

相關閱讀

構建你的第一個 Flutter 視頻通話應用


推廣:歡迎進入 Github 體驗 Agora Flutter SDK,一個幫助 Flutter 應用實現實時音視頻功能的 plugin。

相關文章
相關標籤/搜索