Flutter的RenderBox使用說明書&原理淺析
阿新 • • 發佈:2020-05-21
本文基於1.12.13+hotfix.8版本原始碼分析。
[TOC]
# 一、RenderBox的用法
## 1、RenderBox的使用基本流程
在flutter中,我們最常接觸的,莫過於各種各樣的widget了,但是,實際負責渲染的RenderObject是很少接觸的(它們之間的關聯可以看看閒魚的這篇文章:https://www.yuque.com/xytech/flutter/tge705)。而作為一名天天向上的程式設計師,我們自然要去學習一下它的原理,做到知其然且知其所以然。本文會先來看看RenderBox的用法,以此拋磚引玉,便於後面繼續深入flutter的繪製原理。
使用RenderBox進行繪製,我們需要做三件事:
### (1)測量
第一步,我們需要確定檢視大小,並賦值給父類的size屬性。測量有兩種情況,第一種是size由自身決定,第二種是由parent決定。
首先,由自身決定size的情況,需要在performLayout方法中完成測量,通過父類的constraints可得到滿足約束的值:
```
@override
void performLayout() {
size = Size(
constraints.constrainWidth(200),
constraints.constrainHeight(200),
);
}
```
第二種情況,size由parent決定,這種情況下檢視大小應該完全通過parent提供的constraints測量,不存在其它因素。這種情況下,只要parent的約束不發生變化,就不會重新測量。
這種情況需要重寫sizedByParent並返回true,然後在performResize中完成測量。
```
@override
void performResize() {
size = Size(
constraints.constrainWidth(200),
constraints.constrainHeight(200),
);
}
@override
bool get sizedByParent => true;
```
看到這裡,你可能會疑惑了,這兩個方法什麼時候會被呼叫?順序是怎樣的?答案在RenderObject的layout方法中:
```
void layout(Constraints constraints, { bool parentUsesSize = false }) {
//計算relayoutBoundary
......
//layout
_constraints = constraints;
if (sizedByParent) {
performResize();
}
performLayout();
......
}
}
```
### (2)繪製
RenderBox的繪製與android原生的view繪製非常相似,同樣是Paint+Canvas的組合,而且api也非常接近,會非常容易上手。
```
@override
void paint(PaintingContext context, Offset offset) {
Paint paint = Paint()
..color = _color
..style = PaintingStyle.fill;
context.canvas.drawRect(
Rect.fromLTRB(
0,
0,
size.width,
size.height,
),
paint);
}
```
這樣是不是就萬事大吉了呢?如果通過上面的程式碼進行繪製,你會發現,不管在外層怎麼設定位置,繪製出來的矩形都是固定在螢幕左上角的!怎麼回事?
這裡就是flutter中繪製與android的最大不同:**在這裡繪製的座標系是全域性座標系,即原點在螢幕左上角,而非檢視左上角。**
細心的同學可能已經發現,paint方法中還有一個offset引數,這就是經過parent的約束後,當前檢視的偏移量,繪製時應該將它考慮進去:
```
@override
void paint(PaintingContext context, Offset offset) {
Paint paint = Paint()
..color = _color
..style = PaintingStyle.fill;
context.canvas.drawRect(
Rect.fromLTRB(
offset.dx,
offset.dy,
offset.dx + size.width,
offset.dy + size.height,
),
paint);
}
```
### (3)更新
在flutter中,是由Widget的配置發生變更而引起的rebuild,而這就是我們要實現的第三步:當檢視屬性發生變更時,標記重新佈局或重新繪製,當螢幕重新整理時就會做相應的重新整理。
這裡涉及到兩個方法:markNeedsLayout、markNeedsPaint。顧名思義,前者標記重佈局,後者標記重繪。
我們需要做的,就是根據屬性的影響範圍,在更新屬性時,呼叫合適的標記方法,例如color變化時呼叫markNeedsPaint,width變化時呼叫markNeedsLayout。另外,**兩者都需要更新的情況下,只調用markNeedsLayout即可,不需要兩個方法都調。**
```
set width(double width) {
if (width != _width) {
_width = width;
markNeedsLayout();
}
}
set color(Color color) {
if (color != _color) {
_color = color;
markNeedsPaint();
}
}
```
## 2、RenderObjectWidget
### (1)簡介
上面講了一大堆RenderBox的用法,但是,這玩意兒怎麼用到我們熟知的Widget裡面去?
按照正常流程,我們得實現一個Element和一個Widget,然後在Widget中建立Element,在Element中建立和更新RenderObject,另外還得管理一大堆狀態,處理非常繁瑣。所幸flutter為我們封裝了這一套邏輯,即RenderObjectWidget。
相信看到這裡的同學都對StatelessWidget和StatefulWidget不會陌生,但其實,StatelessWidget和StatefulWidget僅負責屬性、生命週期等的管理,在它們的build方法實現中都會建立RenderObjectWidget,通過它來實現與RenderObject的關聯。
舉個栗子,我們經常使用的Image是個StatefulWidget,對應的state的build方法中實際返回了一個RawImage物件,而這個RawImage是繼承自LeafRenderObjectWidget的,這正是RenderObjectWidget的一個子類;再比如Text,它build方法中建立的RichText是繼承自MultiChildRenderObjectWidget,這同樣是RenderObjectWidget的一個子類。
我們再看看RenderObjectWidget頂部的註釋即可明白:
```
RenderObjectWidgets provide the configuration for [RenderObjectElement]s,
which wrap [RenderObject]s, which provide the actual rendering of the
application.
```
大概意思就是RenderObject才是實際負責渲染應用的,而RenderObjectWidget提供包裝了RenderObject的配置,方便我們使用。
另外,flutter還分別實現了幾個子類,進一步封裝了RenderObjectWidget,它們分別是LeafRenderObjectWidget、SingleChildRenderObjectWidget、MultiChildRenderObjectWidget。其中,LeafRenderObjectWidget是葉節點,不含子Widget;SingleChildRenderObjectWidget僅有一個child;而MultiChildRenderObjectWidget則是含有children列表。這幾個子類根據child的情況分別建立了對應的Element,所以通過這幾個子類,我們只需要關注RenderObject的建立和更新。
### (2)用法
以最簡單的LeafRenderObjectWidget為例,我們需要實現createRenderObject、updateRenderObject兩個方法:
```
class CustomRenderWidget extends LeafRenderObjectWidget {
CustomRenderWidget({
this.width = 0,
this.height = 0,
this.color,
});
final double width;
final double height;
final Color color;
@override
RenderObject createRenderObject(BuildContext context) {
return CustomRenderBox(width, height, color);
}
@override
void updateRenderObject(BuildContext context, RenderObject renderObject) {
CustomRenderBox renderBox = renderObject as CustomRenderBox;
renderBox
..width = width
..height = height
..color = color;
}
}
```
## 3、非容器控制元件的hitTest
通過上面的內容,我們已經可以實現自定義控制元件並用到介面開發中,但是距離一個完整的控制元件還差最後一步:命中測試。當用戶使用手勢,flutter會將手勢資訊交由控制元件進行檢查是否命中。
RenderBox中命中測試的方法有仨:hitTest、hitTestSelf、hitTestChildren,其中hitTest預設實現是呼叫另外兩個方法的:
```
bool hitTest(BoxHitTestResult result, { @required Offset position }) {
if (_size.contains(position)) {
// 從這裡也能看到,當命中children時,不會再進行自身的命中測試
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
}
return false;
}
```
所以重寫命中測試方法有兩個方案,一是重寫hitTest,這種方法需要將命中測試的資訊加到BoxHitTestResult中;二是重寫hitTestSelf和hitTestChildren,這種方法就簡單地返回是否命中即可。
非容器型別的控制元件,只需要重寫hitTestSelf,返回true即命中,例如RawImage中:
```
@override
bool hitTestSelf(Offset position) => true;
```
# 二、容器型別的RenderBox
## 1、介紹
在繪製篇中,我們已經瞭解到如何使用RenderObjectWidget和RenderBox進行基礎的繪製,在本篇中,我們將繼續學習RenderBox如何管理子物件。首先,我們來看看RenderBox頂部的一段註釋:
```
For render objects with children, there are four possible scenarios:
* A single [RenderBox] child. In this scenario, consider inheriting from
[RenderProxyBox] (if the render object sizes itself to match the child) or
[RenderShiftedBox] (if the child will be smaller than the box and the box
will align the child inside itself).
* A single child, but it isn't a [RenderBox]. Use the
[RenderObjectWithChildMixin] mixin.
* A single list of children. Use the [ContainerRenderObjectMixin] mixin.
* A more complicated child model.
```
從上面我們可以瞭解到,帶有子物件的情況有四種:
(1)子物件只有一個,並且是RenderBox的子類。如果當前檢視需要根據子物件調整大小,則繼承RenderProxyBox;如果子物件小於當前檢視,且在當前檢視內部對齊,則繼承RenderShiftedBox(想一下Align會好理解一點);
(2)子物件只有一個,且非RenderBox子類,這種情況使用RenderObjectWithChildMixin;
(3)有多個子物件則使用ContainerRenderObjectMixin;
(4)更復雜的情況。
第四種情況是要用非連結串列的children結構時需要考慮的,比如children要用map或list等結構,這種情況需要繼承RenderObject去實現一套繪製協議,我們這裡暫且先不討論。
而前三種情況其實註釋裡的描述不夠明確,其實情況只有兩種,第一是帶有單一的child,第二是帶有一個children列表,上面的第一第二兩種情況其實可以合併為一種,為什麼這麼說呢?看下去吧~
## 2、單個子物件
### (1)RenderProxyBox
這種情況其實就是當前容器沒有跟大小相關的屬性,size由子類決定,具體邏輯flutter已經在RenderProxyBoxMixin實現了,我們來看看:
```
void performLayout() {
if (child != null) {
child.layout(constraints, parentUsesSize: true);
size = child.size;
} else {
performResize();
}
}
```
邏輯非常簡單,如果有child,則直接使用child的size;如果沒有,就走performResize,而這裡並沒有實現performResize,即走RenderBox的預設實現,取約束的最小值:
```
void performResize() {
size = constraints.smallest;
assert(size.isFinite);
}
```
而繪製方法中,通過PaintingContext的paintChild方法,即可繪製child:
```
@override
void paint(PaintingContext context, Offset offset) {
if (child != null)
context.paintChild(child, offset);
}
```
### (2)RenderShiftedBox
這種情況則與RenderProxyBox相反,即當前容器有跟大小相關的屬性,比如padding。接下來就以非常常見的Padding為例,看看RenderPadding的佈局方法:
```
@override
void performLayout() {
// 將padding的值按照語言方向解析
_resolve();
assert(_resolvedPadding != null);
if (child == null) {
// 如果沒有child,就按照垂直、水平方向的padding值計算得出size
size = constraints.constrain(Size(
_resolvedPadding.left + _resolvedPadding.right,
_resolvedPadding.top + _resolvedPadding.bottom,
));
return;
}
// 如果有child,則將當前約束減去padding值以後,再傳給child進行測量
final BoxConstraints innerConstraints = constraints.deflate(_resolvedPadding);
child.layout(innerConstraints, parentUsesSize: true);
// 測量完畢以後,計算出座標偏移量,提供給child繪製時使用
// parentData是RenderObject的屬性,提供給父佈局使用,用來存取child在父佈局中的一些資訊,包括位置等
final BoxParentData childParentData = child.parentData;
childParentData.offset = Offset(_resolvedPadding.left, _resolvedPadding.top);
// 最後得出大小是padding加上child的大小
size = constraints.constrain(Size(
_resolvedPadding.left + child.size.width + _resolvedPadding.right,
_resolvedPadding.top + child.size.height + _resolvedPadding.bottom,
));
}
```
可以看到,這裡有三個關鍵步驟:第一,根據屬性將約束減去需要額外佔用的寬高,然後傳給child進行測量;第二,測量完畢後計算出child需要用到的繪製偏移量;第三,根據屬性和child的size得出總寬高。
另外,RenderShiftedBox的paint方法邏輯與RenderProxyBox稍微有點不同,會對offset進行處理:
```
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
final BoxParentData childParentData = child.parentData;
context.paintChild(child, childParentData.offset + offset);
}
}
```
### (3)RenderObjectWithChildMixin
回到上面的問題,為什麼說RenderBox和非RenderBox的單一子物件是一樣的呢?其實,RenderProxyBox和RenderShiftedBox是專門為RenderBox的子類再封裝了一層便於使用,它們本身還是with了RenderObjectWithChildMixin:
```
class RenderProxyBox extends RenderBox with RenderObjectWithChildMixin