Flutter RenderBox指南——绘制篇

Stella981
• 阅读 975

本文基于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;
  }
}
点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
3年前
java将前端的json数组字符串转换为列表
记录下在前端通过ajax提交了一个json数组的字符串,在后端如何转换为列表。前端数据转化与请求varcontracts{id:'1',name:'yanggb合同1'},{id:'2',name:'yanggb合同2'},{id:'3',name:'yang
待兔 待兔
11个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
4年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
美凌格栋栋酱 美凌格栋栋酱
4个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(