主题列表:juejin, github, smartblue, cyanosis, channing-cyan, fancy, hydrogen, condensed-night-purple, greenwillow, v-green, vue-pro, healer-readable, mk-cute, jzman, geek-black, awesome-green, qklhk-chocolate
贡献主题:https://github.com/xitu/juejin-markdown-themes
theme: juejin
highlight:
零:前言
1. 系列引言
可能说起 Flutter 绘制,大家第一反应就是用 CustomPaint
组件,自定义 CustomPainter
对象来画。Flutter 中所有可以看得到的组件,比如 Text、Image、Switch、Slider 等等,追其根源都是画出来
的,但通过查看源码可以发现,Flutter 中绝大多数组件并不是使用 CustomPaint
组件来画的,其实 CustomPaint
组件是对框架底层绘制的一层封装。这个系列便是对 Flutter 绘制的探索,通过测试
、调试
及源码分析
来给出一些在绘制时被忽略
或从未知晓
的东西,而有些要点如果被忽略,就很可能出现问题。
- Flutter 绘制探索 1 | CustomPainter 正确刷新姿势
- Flutter 绘制探索 2 | 全面分析 CustomPainter 相关类
- Flutter 绘制探索 3 | 深入分析 CustomPainter 类
- Flutter 绘制探索 4 | 深入分析 setState 重建和更新
2. shouldRepaint 无法控制的重绘
前面说过,由于 shouldRepaint
只会在RenderCustomPaint 渲染对象
重新设置画板时而触发。所以它控制画布刷新的场景仅限于上层 element#rebuild
,最常见的场景是 State#setState
。经过测试,发现仍存在一些莫名的 paint
被重绘的场景。本文就来深入探究一下这些情况,已及对应的解决方案。
一、滑动中的莫名重绘
1. 测试案例
如下,通过一个 SingleChildScrollView
包含一个自定义的画板组件。并在 ShapePainter#paint
中打印绘制日志,页面中并未涉及任何
的刷新逻辑。可以发现,随着滑动,ShapePainter#paint
在一直执行。想当年 FlutterUnit 的 CustomPaint
详情页就是这个问题,滑动时非常卡顿。那么为什么会发生这么不可思议的事呢?又该怎样解决呢?
```dart void main() => runApp(MyApp());
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: HomePage()); } }
class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(), body: SingleChildScrollView( child: Column( children: [ Container( height: 150, width: MediaQuery.of(context).size.width, child: CustomPaint( painter: ShapePainter(color: Colors.red))) ), Container( height: 900, color: Colors.green,) ], ), ), ); } }
class ShapePainter extends CustomPainter { final Color color; ShapePainter({this.color});
@override void paint(Canvas canvas, Size size) { print('-------paint----${color.value}---${DateTime.now()}---'); Paint paint = Paint()..color = color; canvas.drawCircle(Offset(80, 80), 50, paint); }
@override bool shouldRepaint(covariant ShapePainter oldDelegate) { return oldDelegate.color!=color; } } ```
2. 案例调试
既然是触发了ShapePainter#paint
,那么必然冤有头,债有主
,肯定有哪里执行了 RenderCustomPaint#paint
。所以分析的最好方法就是打个断点,调试一下。从 RendererBinding.drawFrame
开始看,执行到 ShapePainter#paint
方法栈情况如下:
目前待渲染列表中,只有 _RenderSingleChildViewport
。它是由 SingleChildScrollView
间接创建的,在它的绘制中,会触发绘制孩子。
它的 child 属性是 RenderFlex
,是由 Colunm
创建的。
最后在 PaintingContext.paintChild
中 RenderCustomPaint
作为孩子被绘制。而引发 ShapePainter#paint
绘制的执行。
3.解决方案
代码处理起来非常简单,在 CustomPaint
之上添加 RepaintBoundary
即可。这样滑动时,就不会触发 ShapePainter#paint
的重绘,这时,你的心里肯定会有一个大大的问号,Why? 下面就来一起探索吧。
dart child: RepaintBoundary( <--- 添加 RepaintBoundary child: CustomPaint( painter: ShapePainter(color: Colors.red), ), ),
二、重绘范围 RepaintBoundary
1.绘制的上界
既然是范围,那必然会有上界
和下界
。我们回想一下 Flutter 绘制探索 3 | 深入分析 CustomPainter 类 中,一个 RenderObject
对象被收录到待重绘列表中的情景。事情发生在 RenderObject#markNeedsPaint
。每个 RenderObject
对象都会有一个 isRepaintBoundary
的布尔属性,默认为 false ,其作用就是用于判断是否是绘制的边界。那么绘制的边界到底是什么意思呢?
下面代码可以看出:当一个 RenderObject
对象执行 markNeedsPaint
时,如果自身 isRepaintBoundary
为 false,会向上寻找父级,直到有 isRepaintBoundary=true
为止。然后该父级节点被加入 _nodesNeedingPaint
列表中。
```dart ---->[RenderObject#markNeedsPaint]---- void markNeedsPaint() { if (needsPaint) return; _needsPaint = true; if (isRepaintBoundary) { if (owner != null) { owner!.nodesNeedingPaint.add(this); //<--- 自己被加入 待渲染列表 owner!.requestVisualUpdate(); } } else if (parent is RenderObject) { final RenderObject parent = this.parent as RenderObject; parent.markNeedsPaint(); } else { if (owner != null) owner!.requestVisualUpdate(); } }
bool get isRepaintBoundary => false; ```
如下图,如果 4
节点执行了 markNeedsPaint
,由于它的 isRepaintBoundary=false
,就会执行 parent.markNeedsPaint
,同理向上追溯发现 2
节点的 isRepaintBoundary=true
所以,就会将 2
加入_nodesNeedingPaint
列表中。 如果 3
执行 markNeedsPaint
,也是 2
加入_nodesNeedingPaint
列表中。如果是 5
执行 markNeedsPaint
,其本身是 isRepaintBoundary
, 则 5
加入_nodesNeedingPaint
列表中。这也就是渲染对象的上界
需要是一个 isRepaintBoundary=true
的可渲染对象。
2.绘制的下界
在 RenderObject#paintChild
中可以发现,只有当 child.isRepaintBoundary
成立时,才不会继续绘制绘制孩子,这就是说,如果 2
被加入 _nodesNeedingPaint
列表,在 2
节点触发绘制时,会绘制孩子,如果此时 5
是 isRepaintBoundary
,那么就不会向下绘制,这样 6
就不会绘制,这就是 绘制的下界
。
dart ---->[RenderObject#paintChild]---- void paintChild(RenderObject child, Offset offset) { if (child.isRepaintBoundary) { stopRecordingIfNeeded(); _compositeChild(child, offset); } else { child._paintWithContext(this, offset); } }
唯鹿
兄在 说说Flutter中的RepaintBoundary 也介绍过 RepaintBoundary
,但感觉没有点出绘制上下界的概念。不过他可能是最早分享 RepaintBoundary
使用的人吧,很感谢他的分享。这里通过这个探索系列,相信大家能对此有一个更深刻的认识。
4.RepaintBoundary 组件的原理
其实原理超级简单,比如在旧版的里面,在 2
节点绘制时,会触发 5
的重绘。 想要不让 5
绘制,只要在 5
之前加个挡箭牌
就行了,RepaintBoundary
就是干这个事的,其创建的 RenderRepaintBoundary
对象的 isRepaintBoundary
为 true
。就这么简单。
```dart class RepaintBoundary extends SingleChildRenderObjectWidget { /// Creates a widget that isolates repaints. const RepaintBoundary({ Key key, Widget child }) : super(key: key, child: child); @override RenderRepaintBoundary createRenderObject(BuildContext context) => RenderRepaintBoundary(); // 略... }
class RenderRepaintBoundary extends RenderProxyBox { /// Creates a repaint boundary around [child]. RenderRepaintBoundary({ RenderBox? child }) : super(child); @override bool get isRepaintBoundary => true; ```
5.为什么不全加 RepaintBoundary
有人也许有疑问,既然如此,所有节点都加 RepaintBoundary ,自己负责绘制自己,别牵连别人不好吗?我们来看一下,如果 isRepaintBoundary
成立,虽然之后的节点不会绘制,但会发生什么。
dart ---->[RenderObject#paintChild]---- void paintChild(RenderObject child, Offset offset) { if (child.isRepaintBoundary) { stopRecordingIfNeeded(); _compositeChild(child, offset); <--- } else { child._paintWithContext(this, offset); } }
会进行 _compositeChild
,最终将 child._layer
添加到 _containerLayer
中。如果 RepaintBoundary
非常多,就会导致非常多的 Layer
。所以是药三分毒, RepaintBoundary
也不是来瞎用的。最常见的就是用于 滑动时
,让自己绘制的复杂画板不频繁刷新。
```dart void compositeChild(RenderObject child, Offset offset) { if (child.needsPaint) { repaintCompositedChild(child, debugAlsoPaintedParent: true); } else { final OffsetLayer childOffsetLayer = child.layer as OffsetLayer; childOffsetLayer.offset = offset; appendLayer(child.layer!); }
@protected void appendLayer(Layer layer) { assert(!_isRecording); layer.remove(); _containerLayer.append(layer); } ```
三、盘点源码中 RepaintBoundary 的使用
俗话说,以史为镜,可正衣冠
。 看源码是最正的,我们最信任的应该是源码,但也要保留一分质疑。下面就来看一下,源码中对于 RepaintBoundary
的使用,以此借鉴。
1. _CupertinoScrollbarState
这个组件是 CupertinoScrollbar
,和滑动相关, 在使用 ScrollbarPainter
时,将 CustomPaint
夹在了两个 RepaintBoundary
之间。
2._ScrollbarState
这个对于的组件是 Scrollbar
,和滑动相关, 在使用 ScrollbarPainter
时,将 CustomPaint
夹在了两个 RepaintBoundary
之间。
3._TextFieldState
和 _CupertinoTextFieldState
分别是 TextField
和 CupertinoTextField
,由于输入框的游标频闪,使用需要加 RepaintBoundary
进行限制。
4. _GlowingOverscrollIndicatorState
滑动到顶底的指示器,也是和滑动相关, 在使用 _GlowingOverscrollIndicatorPainter
时,将 CustomPaint
夹在了两个 RepaintBoundary
之间。
5. Sliver
相关
ListView
、GridView
的本质都是 Sliver
相关的组件。在 SliverChildBuilderDelegate
中都默认会套上 RepaintBoundary
,因为 addRepaintBoundaries
默认为 true
。从这可以看出这是列表类滑动组件的默认行为,RepaintBoundary
并没有那么昂贵。
你可以做一个测试,将 SingleChildScrollView
替换成 ListView
。这样在滑动时也不会触发画板的频繁绘制,原因就在于 SliverChildBuilderDelegate
中的 RepaintBoundary
处理。
6. Flow 中
在 Flow
中,其传入的 children ,会通过 RepaintBoundary.wrapAll
对每个组件进行包裹。
四、其他需要注意的组件
1. 水波纹系列
RawMaterialButton
系列的组件,底层都依赖于 InkWell
,在测试中发现水波纹效果会触发自定义画板的不断重绘。如下:
dart class HomePage extends StatelessWidget{ @override Widget build(BuildContext context) { return Scaffold( floatingActionButton: FloatingActionButton( child: Icon(Icons.add), onPressed: () { // debugDumpRenderTree(); }, ), body: CustomPaint( painter: ShapePainter(color: Colors.red), ), ); } } class ShapePainter extends CustomPainter { final Color color; ShapePainter({this.color}); @override void paint(Canvas canvas, Size size) { print("----paint--------${DateTime.now()}-------"); Paint paint = Paint()..color = color; canvas.drawCircle(Offset(100, 100), 50, paint); } @override bool shouldRepaint(covariant ShapePainter oldDelegate) { return oldDelegate.color != color; } }
调试一下可以看到,上界如下,不知道是官方少加了 RepaintBoundary
下界,还是另有考虑。解决方案是在绘制的组件上套一个 RepaintBoundary
。
2.输入框系列
在输入框收起打开时,会触发自定义画板的绘制,而且随着打开次数的增加,绘制越多,感觉像是 bug 。同样解决方案是在绘制的组件上套一个 RepaintBoundary
,就不会出现重绘现象。目前版本,最新稳定版 Flutter 1.22.5
。
dart class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CustomPaint( size: Size(300,150), painter: ShapePainter(color: Colors.red), ), TextField(), ], ), ); } }
当你在通过 CustomPaint
组件自定义绘制时,需要注意这几类组件:1、滑动类型
; 2、InkWell 相关
;3、 输入框
。当然这些只是我遇到的,当你自定义的绘制出现卡顿或频繁重绘时,也要注意一下。
通过本文,你应该对 Flutter
中的绘制范围有了更深的认识。如果你的绘制中出现了频繁触发的异常重绘,那么 RepaintBoundary
一定会帮助你。本文就到这里,下一篇将会讲解另一个 shouldRepaint
无法控制的画板重绘,不过这个无法控制是我们的需求,那就是基于 repaint
对画板绘制的原理。前面虽然有所涉及,但我觉得有必要用一篇文章详述一下可监听对象与画板的关系,再对 CustomPaint
组件的其他属性进行探索。
@张风捷特烈 2021.01.15 未允禁转
我的公众号:编程之王
联系我--邮箱:1981462002@qq.com -- 微信:zdl1994328
~ END ~