• PaintingContext 是什么?
  • Offset 是什么?

通过飞速查阅源码,我们可以首先了解到有 :

  • PaintingContext 的关键是 A place to paint ,同时它在父类 ClipContext 是包含有 Canvas ,并且 PaintingContext 的构造方法是 @protected,只在 PaintingContext.repaintCompositedChild 和 pushLayer 时自动创建。

  • Offset 在 paint 中主要是提供当前控件在屏幕的相对偏移值,提供绘制时确定绘制的坐标。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nY0vQkgH-1637310321561)(https://user-gold-cdn.xitu.io/2019/11/18/16e7da3e8308faca?imageView2/0/w/1280/h/960/ignore-error/1)]

OK,继续往下走,那么既然 PaintingContext 叫 Context ,那它肯定是存在上下文关系,那它是在哪里开始创建的呢?

通过调试源码可知,项目在 runApp 时通过 WidgetsFlutterBinding 启动,而在以前的篇幅中我们知道, WidgetsFlutterBinding 是一个“胶水类”,它会触发 mixin 的 RendererBinding ,如下图创建出根 node 的 PaintingContext 。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KD7v7FIb-1637310321587)(https://user-gold-cdn.xitu.io/2019/11/18/16e7da3e83556096?imageView2/0/w/1280/h/960/ignore-error/1)]

好了,那么Offset 呢?如下图,对于 Offset 的传递,是通过父控件和子控件的 offset 相加之后,一级一级的将需要绘制的坐标结合去传递的。

目前简单来说,通过 PaintingContext 和 Offset ,在布局之后我们就可以在屏幕上准确的地方绘制会需要的画面。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-keDslO53-1637310321588)(https://user-gold-cdn.xitu.io/2019/11/18/16e7da3e81f066eb?imageView2/0/w/1280/h/960/ignore-error/1)]

1、测试绘制

这里我们先做一个有趣的测试。

我们现在屏幕上通过 Container 限制一个高为 60 的绿色容器,如下图,暂时忽略容器内的 Slider 控件 ,我们图中绘制了一个 100 x 100 的红色方块,这时候我们会看到下图右边的效果是:纳尼?为什么只有这么小?

事实上,因为正常 Flutter 在绘制 Container 的时候,AppBar 已经帮我们计算了状态栏和标题栏高度偏差,但我们这里在用 Canvas 时直接粗暴的 drawRect,绘制出来的红色小方框,左部和顶部起点均为0,其实是从状态栏开始计算绘制的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kyMWC1F3-1637310321589)(https://user-gold-cdn.xitu.io/2019/11/18/16e7da3e832b8a0d?imageView2/0/w/1280/h/960/ignore-error/1)]

那如果我们调整位置呢?把起点 top 调整到 300,出现了如下图的效果:纳尼?红色小方块居然画出去了,明明 Container 只有绿色的大小。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LxXa0yyB-1637310321590)(https://user-gold-cdn.xitu.io/2019/11/18/16e7da3e82c9cabe?imageView2/0/w/1280/h/960/ignore-error/1)]

其实这里的问题还是在于 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 绘制上去,目标是整个屏幕区域,整个屏幕就是一帧,每次改变都是重新绘制。

2、RepaintBoundary

当然,每次重新绘制并不是完全重新绘制 ,这里面其实是存在一些规制的。

还记得前面的 markNeedsPaint 方法吗 ?我们先从 markNeedsPaint() 开始, 总结出其大致流程如下图,可以看到 markNeedsPaint 在 requestVisualUpdate 时确实触发了引擎去更新绘制界面。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9NLE1JEP-1637310321591)(https://user-gold-cdn.xitu.io/2019/11/18/16e7da3ea7ceba13?imageView2/0/w/1280/h/960/ignore-error/1)]

接着我们看源码,如源码所示,当调用 markNeedsPaint() 时,RenderObject 就会往上的父节点去查找,根据 isRepaintBoundary 是否为 true,会决定是否从这里开始去触发重绘。换个说法就是,确定要更新哪些区域。

所以其实流程应该是:**通过isRepaintBoundary 往上确定了更新区域,通过 `re

《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》

开源分享完整内容戳这里

questVisualUpdate` 方法触发更新往下绘制。**

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J99P9vGZ-1637310321591)(https://user-gold-cdn.xitu.io/2019/11/18/16e7da3eb1095d02?imageView2/0/w/1280/h/960/ignore-error/1)]

并且从源码中可以看出, isRepaintBoundary 只有 get ,所以它只能被子类 override ,由子类表明是否是为重绘的边缘,比如 RenderProxyBox 、RenderView 、RenderFlow 等 RenderObject 的 isRepaintBoundary 都是 true。

所以如果一个区域绘制很频繁,且可以不影响父控件的情况下,其实可以将 override isRepaintBoundary 为 true。

3、Layer

上文我们知道了,当 isRepaintBoundary 为 true 时,那么该区域就是一个可更新绘制区域,而当这个区域形成时, 其实就会新创建一个 Layer 。

不同的 Layer 下的 RenderObject 是可以独立的工作,比如 OffsetLayer 就在 RenderObject 中用到,它就是用来做定位绘制的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cC1rSz6f-1637310321592)(https://user-gold-cdn.xitu.io/2019/11/18/16e7da3eb1105853?imageView2/0/w/1280/h/960/ignore-error/1)]

同时这也引生出了一个结论:不是每个 RenderObject 都具有 Layer 的,因为这受 isRepaintBoundary 的影响。

其次在 RenderObject 中还有一个属性叫 needsCompositing ,它会影响生成多少层的 Layer ,而这些 Layer 又会组成一棵 Layer Tree 。好吧,到这里又多了一个树,实际上这颗树才是所谓真正去给引擎绘制的树。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WzNYv77v-1637310321593)(https://user-gold-cdn.xitu.io/2019/11/18/16e7da3e78fb9768?imageView2/0/w/1280/h/960/ignore-error/1)]

到这里我们大概就了解了 RenderObject 的整个绘制流程,并且这个绘制时机我们是去“触发”的,而不是主动调用,并且更新是判断区域的。 嗯~有点 React 的味道!

二、Slider 控件的绘制实现

前面我们讲了那么多绘制的流程,现在让我们从 Slider 这个控件的源码,去看看一个绘制控件的设计实现吧。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d2NjHdjC-1637310321593)(https://user-gold-cdn.xitu.io/2019/11/18/16e7da3e78fb9768?imageView2/0/w/1280/h/960/ignore-error/1)]

整个 Slider 的实现可以说是很 Flutter 了,大体结构如下图。

在 _RenderSlider 中,除了 手势 和 动画 之外,其余的每个绘制的部分,都是独立的 Component 去完成绘制,而这些 Component 都是通过 SliderTheme 的 SliderThemeData 提供的。

巧合的是,SliderTheme 本身就是一个 InheritedWidget 。看过以前篇章的同学应该会知道, InheritedWidget 一般就是用于做状态共享的,所以如果你需要自定义 Slider ,完成可以通过 SliderTheme 嵌套,然后通过 SliderThemeData 选择性的自定义你需要的模块。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4OVl2iym-1637310321594)(https://user-gold-cdn.xitu.io/2019/11/18/16e7da3e78fb9768?imageView2/0/w/1280/h/960/ignore-error/1)]

并且如下图,在 _RenderSlider 中注册时手势和动画,会在监听中去触发 markNeedsPaint 方法,这就是为什么你的触摸能够响应画面的原因了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yZt5Kq6u-1637310321594)(https://user-gold-cdn.xitu.io/2019/11/18/16e7da3e78fb9768?imageView2/0/w/1280/h/960/ignore-error/1)]

注册时手势和动画,会在监听中去触发 markNeedsPaint 方法,这就是为什么你的触摸能够响应画面的原因了。

[外链图片转存中…(img-yZt5Kq6u-1637310321594)]

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐