做为系列文章的第九篇,本篇主要深刻了解 Widget 中绘制相关的原理,探索 Flutter 里的 RenderObject 最后是如何走完屏幕上的最后一步,结尾再经过实际例子理解如何设计一个 Flutter 的自定义绘制。node
前文:git
在第6、第七篇中咱们知道了 Widget
、Element
、RenderObject
的关系,同时也知道了Widget
的布局逻辑,最终全部 Widget
都转化为 RenderObject
对象, 它们堆叠出咱们想要的画面。github
因此在 Flutter 中,最终页面的 Layout
、Paint
等都会发生在 Widget 所对应的 RenderObject
子类中,而 RenderObject
也是 Flutter 跨平台的最大的特色之一:全部的控件都与平台无关 ,这里简单的人话就是: Flutter 只要求系统提供的 “Canvas”,而后开发者经过 Widget 生成 RenderObject
“直接” 经过引擎绘制到屏幕上。canvas
ps 从这里开始篇幅略长,可能须要消费您的一点耐心。bash
咱们知道 Widget
最终都转化为 RenderObject
, 因此了解绘制咱们直接先看 RenderObject
的 paint
方法。ide
以下图所示,全部的 RenderObject
子类都必须实现 paint
方法,而且该方法并非给用户直接调用,须要更新绘制时,你能够经过 markNeddsPaint
方法去触发界面绘制。布局
那么,按照“国际流程”,在经历大小和布局等位置计算以后,最终 paint
方法会被调用,该方法带有两个参数: PaintingContext
和 Offset
,它们就是完成绘制的关键所在,那么相信此时你们确定有个疑问就是:post
PaintingContext
是什么?Offset
是什么?经过飞速查阅源码,咱们能够首先了解到有 :测试
PaintingContext
的关键是 A place to paint ,同时它在父类 ClipContext
是包含有 Canvas
,而且 PaintingContext
的构造方法是 @protected
,只在 PaintingContext.repaintCompositedChild
和 pushLayer
时自动建立。动画
Offset
在 paint
中主要是提供当前控件在屏幕的相对偏移值,提供绘制时肯定绘制的坐标。
OK,继续往下走,那么既然 PaintingContext
叫 Context ,那它确定是存在上下文关系,那它是在哪里开始建立的呢?
经过调试源码可知,项目在 runApp
时经过 WidgetsFlutterBinding
启动,而在之前的篇幅中咱们知道, WidgetsFlutterBinding
是一个“胶水类”,它会触发 mixin 的 RendererBinding
,以下图建立出根 node 的 PaintingContext
。
好了,那么Offset
呢?以下图,对于 Offset
的传递,是经过父控件和子控件的 offset 相加以后,一级一级的将须要绘制的坐标结合去传递的。
目前简单来讲,经过 PaintingContext
和 Offset
,在布局以后咱们就能够在屏幕上准确的地方绘制会须要的画面。
这里咱们先作一个有趣的测试。
咱们如今屏幕上经过 Container
限制一个高为 60 的绿色容器,以下图,暂时忽略容器内的 Slider
控件 ,咱们图中绘制了一个 100 x 100 的红色方块,这时候咱们会看到下图右边的效果是:纳尼?为何只有这么小?
事实上,由于正常 Flutter 在绘制 Container
的时候,AppBar
已经帮咱们计算了状态栏和标题栏高度误差,但咱们这里在用 Canvas
时直接粗暴的 drawRect
,绘制出来的红色小方框,左部和顶部起点均为0,实际上是从状态栏开始计算绘制的。
那若是咱们调整位置呢?把起点 top 调整到 300,出现了以下图的效果:纳尼?红色小方块竟然画出去了,明明 Container
只有绿色的大小。
其实这里的问题仍是在于 PaintingContext
,它有一个参数是 estimatedBounds
,而 estimatedBounds
正常是在建立时经过 child.paintBounds
赋值的,可是对于 estimatedBounds
还有以下的描述:原来画出去也是能够。
The canvas will allow painting outside these bounds.
The [estimatedBounds] rectangle is in the [canvas] coordinate system.
复制代码
因此到这里你能够通俗的总结, 对于 Flutter 而言,整个屏幕都是一块画布,咱们经过各类 Offset
和 Rect
肯定了位置,而后经过 PaintingContext
的Canvas
绘制上去,目标是整个屏幕区域,整个屏幕就是一帧,每次改变都是从新绘制。
固然,每次从新绘制并非彻底从新绘制 ,这里面实际上是存在一些规制的。
还记得前面的 markNeedsPaint
方法吗 ?咱们先从 markNeedsPaint()
开始, 总结出其大体流程以下图,能够看到 markNeedsPaint
在 requestVisualUpdate
时确实触发了引擎去更新绘制界面。
接着咱们看源码,如源码所示,当调用 markNeedsPaint()
时,RenderObject
就会往上的父节点去查找,根据 isRepaintBoundary
是否为 true,会决定是否从这里开始去触发重绘。换个说法就是,肯定要更新哪些区域。
因此其实流程应该是:经过isRepaintBoundary
往上肯定了更新区域,经过 requestVisualUpdate
方法触发更新往下绘制。
而且从源码中能够看出, isRepaintBoundary
只有 get
,因此它只能被子类 override
,由子类代表是不是为重绘的边缘,好比 RenderProxyBox
、RenderView
、RenderFlow
等 RenderObject
的 isRepaintBoundary
都是 true。
因此若是一个区域绘制很频繁,且能够不影响父控件的状况下,其实能够将 override isRepaintBoundary
为 true。
上文咱们知道了,当 isRepaintBoundary
为 true 时,那么该区域就是一个可更新绘制区域,而当这个区域造成时, 其实就会新建立一个 Layer
。
不一样的 Layer
下的 RenderObject
是能够独立的工做,好比 OffsetLayer
就在 RenderObject
中用到,它就是用来作定位绘制的。
同时这也引生出了一个结论:不是每一个 RenderObject
都具备 Layer
的,由于这受 isRepaintBoundary
的影响。
其次在 RenderObject
中还有一个属性叫 needsCompositing
,它会影响生成多少层的 Layer
,而这些 Layer
又会组成一棵 Layer Tree 。好吧,到这里又多了一个树,实际上这颗树才是所谓真正去给引擎绘制的树。
到这里咱们大概就了解了 RenderObject
的整个绘制流程,而且这个绘制时机咱们是去“触发”的,而不是主动调用,而且更新是判断区域的。 嗯~有点 React 的味道!
前面咱们讲了那么多绘制的流程,如今让咱们从 Slider
这个控件的源码,去看看一个绘制控件的设计实现吧。
整个 Slider
的实现能够说是很 Flutter
了,大致结构以下图。
在 _RenderSlider
中,除了 手势 和 动画 以外,其他的每一个绘制的部分,都是独立的 Component 去完成绘制,而这些 Component 都是经过 SliderTheme
的 SliderThemeData
提供的。
巧合的是,SliderTheme
自己就是一个 InheritedWidget
。看过之前篇章的同窗应该会知道, InheritedWidget
通常就是用于作状态共享的,因此若是你须要自定义 Slider
,完成能够经过 SliderTheme
嵌套,而后经过 SliderThemeData
选择性的自定义你须要的模块。
而且以下图,在 _RenderSlider
中注册时手势和动画,会在监听中去触发 markNeedsPaint
方法,这就是为何你的触摸可以响应画面的缘由了。
同时能够看到 _SliderRender
内的参数都重写了 get
、 set
方法, 在 set
时也会有 markNeedsPaint()
,或者调用 _updateLabelPainter
去间接调用 markNeedsLayout
。
至于 Slider
内的各类 Shape 的绘制这里就不展开了,都是 Canvas
标准的 pathTo
、drawRect
、translate
、drawPath
等熟悉的操做了。
自此,第九篇终于结束了!(///▽///)
《Flutter完整开发实战详解(1、Dart语言和Flutter基础)》
《Flutter完整开发实战详解(4、Redux、主题、国际化)》
《Flutter完整开发实战详解(6、 深刻Widget原理)》
《Flutter完整开发实战详解(10、 深刻图片加载流程)》
《Flutter完整开发实战详解(11、全面深刻理解Stream)》
《React Native 的将来与React Hooks》