本文首發於 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
在 Android 中,咱們所說的 View
的渲染邏輯指的是 onMeasure()
, onLayout()
, onDraw()
, 咱們只要重寫這三個方法就能夠自定義出符合咱們需求的 View
。其實,即便咱們不懂 Android 中 View 的渲染邏輯,也能寫出大部分的 App,可是當系統提供的 View 知足不了咱們的需求的時候,這時就須要咱們自定義 View 了,而自定義 View 的前提就是要知道 View 的渲染邏輯。canvas
Flutter 中也同樣,系統提供的 Widget 能夠知足咱們大部分的需求,可是在一些狀況下咱們仍是得渲染本身的 Widget。小程序
和 Android 相似,Flutter 中的渲染也會經歷幾個必要的階段,以下:數組
上面三個階段中,比較重要的就是 Layout 階段了,由於一切都始於佈局。markdown
在 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。
咱們在 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 中都有許多用來執行渲染的屬性和方法:
可是,RenderObject 是一個抽象類,他須要被子類繼承來進行實際的渲染。RenderObject 的兩個很是重要的子類是 RenderBox 和 RenderSliver 。這兩個類是全部實現 Box Protocol 和 Sliver Protocol 的渲染對象的父類。並且這兩個類還擴展了數十個和其餘幾個處理特定場景的類,而且實現了渲染過程的細節。
如今咱們開始渲染本身的 widget,也就是建立一個 RenderObject。這個 widget 須要知足下面兩點要求:
如此 「小氣」 的 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 如何擺放設置一個偏移量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>[],
})
複製代碼
上面的 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 的條件。
完整源碼在文章末尾給出。
效果以下:
咱們這裏說的 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
相關閱讀
推廣:歡迎進入 Github 體驗 Agora Flutter SDK,一個幫助 Flutter 應用實現實時音視頻功能的 plugin。