在這篇文章中穆咐,我們主要了解兩個部分的內(nèi)容颤诀,一個是 Flutter 的基本渲染邏輯 另一個是 Flutter 和 Native 互通的方法,這里的 Native 是以 Android 為例对湃。然后使用案例分別進行演示着绊。
Flutter 渲染
在 Android 中,我們所說的 View
的渲染邏輯指的是 onMeasure()
, onLayout()
, onDraw()
, 我們只要重寫這三個方法就可以自定義出符合我們需求的 View
熟尉。其實归露,即使我們不懂 Android 中 View 的渲染邏輯,也能寫出大部分的 App斤儿,但是當系統(tǒng)提供的 View 滿足不了我們的需求的時候剧包,這時就需要我們自定義 View 了,而自定義 View 的前提就是要知道 View 的渲染邏輯往果。
Flutter 中也一樣疆液,系統(tǒng)提供的 Widget 可以滿足我們大部分的需求,但是在一些情況下我們還是得渲染自己的 Widget陕贮。
和 Android 類似堕油,F(xiàn)lutter 中的渲染也會經(jīng)歷幾個必要的階段,如下:
- Layout : 布局階段肮之,F(xiàn)lutter 會確定每一個子 Widget 的大小和他們在屏幕中將要被放置的位置掉缺。
- Paint : 繪制階段,F(xiàn)lutter 為每個子 Widget 提供一個 canvas戈擒,并讓他們繪制自己眶明。
- Composite : 組合階段,F(xiàn)lutter 會將所有的 Widget 組合在一起筐高,并交由 GPU 處理搜囱。
上面三個階段中丑瞧,比較重要的就是 Layout 階段了,因為一切都始于布局蜀肘。
在 Flutter 中绊汹,布局階段會做兩個事情:父控件將 約束(Constraints) 向下傳遞到子控件;子控件將自己的 布局詳情(Layout Details) 向上傳遞給父控件扮宠。如下圖:
布局過程如下:
這里我們將父 widget 稱為 parent西乖;將子 widget 稱為 child
parent 會將某些布局約束傳遞給 child,這些約束是每個 child 在 layout 階段必須要遵守的涵卵。如同 parent 這樣告訴 child :“只要你遵守這些規(guī)則,你可以做任何你想做的事”荒叼。最常見的就是 parent 會限制 child 的大小轿偎,也就是 child 的 maxWidth 或者 maxHeight。
然后 child 會根據(jù)得到的約束生成一個新的約束被廓,并將這個新的約束傳遞給自己的 child(也就是 child 的 child)坏晦,這個過程會一直持續(xù)到出現(xiàn)沒有 child 的 widget 為止。
之后嫁乘,child 會根據(jù) parent 傳遞過來的約束確定自己的布局詳情(Layout Details)昆婿。如:假設 parent 傳遞給 child 的最大寬度約束為 500px,child 可能會說:“好吧蜓斧,那我就用500px”仓蛆,或者 “我只會用 100px”。這樣挎春,child 就確定了自己的布局詳情看疙,并將其傳遞給 parent。
parent 反過來做同樣的事情直奋,它根據(jù) child 傳遞回來的 Layout Details 來確定其自身的 Layout Details能庆,然后將這些 Layout Details 向上層的 parent 傳遞,直到到達 root widget (根 widget)或者遇到了某些限制脚线。
那我們上面所提到的 約束(Constraints) 和 布局詳情(Layout Details) 都是什么呢搁胆?這取決于布局協(xié)議(Layout protocol)。Flutter 中有兩種主要的布局協(xié)議:Box Protocol 和 Sliver Protocol邮绿,前者可以理解為類似于盒子模型協(xié)議渠旁,后者則是和滑動布局相關的協(xié)議。這里我們以前者為例船逮。
在 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防泵,F(xiàn)lutter 中與之對應的為 Widget tree蚀之,但是 Flutter 中還有另外一種 tree,稱為 Render tree捷泞。
在 Flutter 中 我們常見的 widget 有 StatefulWidget
足删,StatelessWidget
,InheritedWidget
等等锁右。但是這里還有另外一種 widget 稱為 RenderObjectWidget
失受,這個 widget 中沒有 build()
方法,而是有一個 createRenderObject()
方法咏瑟,這個方法允許創(chuàng)建一個 RenderObject
并將其添加到 render tree 中拂到。
RenderObject 是渲染過程中非常重要的組件,render tree 中的內(nèi)容都是 RenderObject码泞,每個 RenderObject 中都有許多用來執(zhí)行渲染的屬性和方法:
- constraints : 從 parent 傳遞過來的約束兄旬。
- parentData: 這里面攜帶的是 parent 渲染 child 的時候所用到的數(shù)據(jù)。
- performLayout():此方法用于布局所有的 child余寥。
- paint():這個方法用于繪制自己或者 child辖试。
- 等等...
但是,RenderObject 是一個抽象類劈狐,他需要被子類繼承來進行實際的渲染罐孝。RenderObject 的兩個非常重要的子類是 RenderBox 和 RenderSliver 。這兩個類是所有實現(xiàn) Box Protocol 和 Sliver Protocol 的渲染對象的父類肥缔。而且這兩個類還擴展了數(shù)十個和其他幾個處理特定場景的類莲兢,并且實現(xiàn)了渲染過程的細節(jié)。
現(xiàn)在我們開始渲染自己的 widget续膳,也就是創(chuàng)建一個 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(...)
方法創(chuàng)建并返回了一個 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
實現(xiàn)了 Box Protocol 所有的細節(jié)邻耕,并且提供了 performLayout()
方法的實現(xiàn)。我們需要在 performLayout()
方法中布局我們的 child燕鸽,還可以設置他們的偏移量兄世。
我們在使用 child.layout(...)
方法布局 child 的時候傳遞了兩個參數(shù),第一個為 child 的布局約束啊研,而另外一個參數(shù)是 parentUserSize
御滩, 該參數(shù)如果設置為 false
,則意味著 parent 不關心 child 選擇的大小党远,這對布局優(yōu)化比較有用削解;因為如果 child 改變了自己的大小,parent 就不必重新 layout
了沟娱。但是在我們的例子中氛驮,我們的需要把 child 放置在 parent 的右下角,這意味著如果 child 的大谢ǔ痢(Size)一旦改變柳爽,則其對應的偏移量(Offset) 也會改變媳握,這就意味著 parent 需要重新布局碱屁,所以我們這里傳遞了一個 true
。
當 child.layout(...)
完成了以后蛾找,child 就確定了自己的 Layout Details娩脾。然后我們就還可以為其設置偏移量來將它放置到我們想放的位置。在我們的例子中為 右下角打毛。
最后柿赊,和 child 根據(jù) parent 傳遞過來的約束選擇了一個尺寸一樣,我們也需要為 Stingy 選擇一個尺寸幻枉,以至于 Stingy 的 parent 知道如何放置它碰声。類似于在 Android 中我們自定義 View
重寫 onMeasure(...)
方法的時候需要調(diào)用 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 根據(jù) 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
內(nèi)部對 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:這個特別重要檩帐,注釋上明確指出這個參數(shù)一定不能為空术幔,我們在下會說
- children:這個就很好理解了,他是一個 widget 數(shù)組湃密,也就是我們們需要渲染的 widget
上面的 delegate
參數(shù)類型如下:
/// The delegate that controls the layout of the children.
final MultiChildLayoutDelegate delegate;
可以看出 delegate
的類型為 MultiChildLayoutDelegate
诅挑,并且注釋也說明了它的作用:控制 children 的布局。也就是說泛源,我們的 CustomMultiChildLayout
里面要怎么布局拔妥,完全取決于我們自定義的 MultiChildLayoutDelegate
里面的實現(xiàn)。所以 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;參數(shù) child
和 參數(shù) id
不定不能為空。
我們在布局 child 的時候會根據(jù) child 的 id
來布局攻柠。
下面我們來使用 CustomMultiChildLayout
實現(xiàn)一個用于展示熱門標簽的效果:
Container(
child: CustomMultiChildLayout(
delegate: _LabelDelegate(itemCount: items.length, childId: childId),
children: items,
),
)
我們的 _LabelDelegate
里面接受兩個參數(shù),一個為 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;
}
// 根據(jù) Offset 來放置 child
positionChild(id, Offset(dx, dy));
dx += childSize.width;
}
}
}
/// 該方法用來判斷重新 layout 的條件
@override
bool shouldRelayout(_LabelDelegate oldDelegate) {
return oldDelegate.itemCount != this.itemCount;
}
}
在 _LabelDelegate
中邻辉,重寫了 performLayout(...)
方法溪王。方法中有一個參數(shù) 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 是由我們自己規(guī)定的芒珠,這個方法的作用是判斷當前的 childId 是否對應著一個非空的 child。
滿足 hasChild(...)
之后搅裙,接著就是 layoutChild(...)
來布局 child , 這個方法中我們會傳遞兩個參數(shù)皱卓,一個是 childId裹芝,另外一個是 child 的約束(Constraints),這個方法返回的是當前這個 child 的 Size娜汁。
布局完成之后嫂易,就是如何擺放的問題了,也就是上述代碼中的 positionChild(..)
了掐禁,此方法接受一個 childId
和 一個當前 child 對應的 Offset
怜械,parent 會根據(jù)這個 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)
...
我們有兩種方式來執(zhí)行初始化操作:一個是直接讓我們的 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 框架能夠獨立完成這些事情。但是在實際的開發(fā)中堕义,難免會需要調(diào)用 Native 的功能猜旬,如:定位脆栋,相機倦卖,電池等等。這個時候就需要 Flutter 和 Native 通信了椿争。
官網(wǎng)上有一個案例 是使用 MethodChannel來調(diào)用給本地的方法獲取手機電量怕膛。
其實我們還可以使用另外一個類進行通信,叫做 BasicMessageChannel秦踪,先來看看它如果創(chuàng)建:
// java
basicMessageChannel = new BasicMessageChannel<String>(getFlutterView(), "foo", StringCodec.INSTANCE);
BasicMessageChannel
需要三個參數(shù)褐捻,第一個是 BinaryMessenger;第二個是通道名稱椅邓,第三個是交互數(shù)據(jù)類型的編解碼器柠逞,我們接下來的例子中的交互數(shù)據(jù)類型為 String
,所以這里傳遞的是 StringCodec.INSTANCE
景馁,F(xiàn)lutter 中還有其他類型的編解碼器BinaryCodec
板壮,JSONMessageCodec
等,他們都有一個共同的父類 MessageCodec
合住。 所以我們也可以根據(jù)規(guī)則創(chuàng)建自己編解碼器绰精。
接下來創(chuàng)建的例子是:Flutter
給 Android
發(fā)送一條消息撒璧,Android
收到消息之后給 Flutter
回復一條消息,反之亦然笨使。
先來看看 Android
端的部分代碼:
// 接收 Flutter 發(fā)送的消息
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 發(fā)送消息
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 發(fā)送過來的消息,并且回復
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 發(fā)送消息
void _sendMessageToAndroid(String message) {
setState(() {
_messageWidgets.add(_buildMessageWidget(message, false));
_scrollToBottom();
});
// 向 Android 端發(fā)送發(fā)送消息并處理 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