Flutter RenderBox指南——繪製篇

jyau發表於2020-05-17

本文基於1.12.13+hotfix.8版本原始碼分析。

0、大綱

  1. RenderBox的用法
  2. 通過RenderObjectWidget把RenderBox塞進介面

1、RenderBox

在flutter中,我們最常接觸的,莫過於各種各樣的widget了,但是,實際負責渲染的RenderObject是很少接觸的(它們之間的關聯可以看看閒魚的這篇文章:https://www.yuque.com/xytech/flutter/tge705)。而作為一名天天向上的程式設計師,我們自然要去學習一下它的原理,做到知其然且知其所以然。本文會先來看看RenderBox的用法,以此拋磚引玉,便於後面繼續深入flutter的繪製原理。

首先,RenderBox是RenderObject的子類,它將座標系轉換成我們熟知的笛卡爾座標系,更便於使用。使用RenderBox進行繪製(本文暫時不講child),我們需要做三件事:

(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中完成測量,並且performLayout中不能對size賦值! 感興趣的同學也可以兩邊都寫,看看會發生什麼:)。

  @override
  void performLayout() {}
  
  @override
  void performResize() {
    size = Size(
      constraints.constrainWidth(200),
      constraints.constrainHeight(200),
    );
  }

  @override
  bool get sizedByParent => true;
(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。

相信對flutter有所瞭解的同學都對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;
  }
}

相關文章