前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Flutter 绘制探索 1 | CustomPainter 正确刷新姿势 | 七日打卡

Flutter 绘制探索 1 | CustomPainter 正确刷新姿势 | 七日打卡

作者头像
张风捷特烈
发布2021-01-14 11:13:49
1.4K0
发布2021-01-14 11:13:49
举报

@charset "UTF-8";.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:15px;overflow-x:hidden;color:#333}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{line-height:1.5;margin-top:35px;margin-bottom:10px;padding-bottom:5px}.markdown-body h1:first-child,.markdown-body h2:first-child,.markdown-body h3:first-child,.markdown-body h4:first-child,.markdown-body h5:first-child,.markdown-body h6:first-child{margin-top:-1.5rem;margin-bottom:1rem}.markdown-body h1:before,.markdown-body h2:before,.markdown-body h3:before,.markdown-body h4:before,.markdown-body h5:before,.markdown-body h6:before{content:"#";display:inline-block;color:#3eaf7c;padding-right:.23em}.markdown-body h1{position:relative;font-size:2.5rem;margin-bottom:5px}.markdown-body h1:before{font-size:2.5rem}.markdown-body h2{padding-bottom:.5rem;font-size:2.2rem;border-bottom:1px solid #ececec}.markdown-body h3{font-size:1.5rem;padding-bottom:0}.markdown-body h4{font-size:1.25rem}.markdown-body h5{font-size:1rem}.markdown-body h6{margin-top:5px}.markdown-body p{line-height:inherit;margin-top:22px;margin-bottom:22px}.markdown-body strong{color:#3eaf7c}.markdown-body img{max-width:100%;border-radius:2px;display:block;margin:auto;border:3px solid rgba(62,175,124,.2)}.markdown-body hr{border:none;border-top:1px solid #3eaf7c;margin-top:32px;margin-bottom:32px}.markdown-body code{word-break:break-word;overflow-x:auto;padding:.2rem .5rem;margin:0;color:#3eaf7c;font-weight:700;font-size:.85em;background-color:rgba(27,31,35,.05);border-radius:3px}.markdown-body code,.markdown-body pre{font-family:Menlo,Monaco,Consolas,Courier New,monospace}.markdown-body pre{overflow:auto;position:relative;line-height:1.75;border-radius:6px;border:2px solid #3eaf7c}.markdown-body pre>code{font-size:12px;padding:15px 12px;margin:0;word-break:normal;display:block;overflow-x:auto;color:#333;background:#f8f8f8}.markdown-body a{font-weight:500;text-decoration:none;color:#3eaf7c}.markdown-body a:active,.markdown-body a:hover{border-bottom:1.5px solid #3eaf7c}.markdown-body a:before{content:"⇲"}.markdown-body table{display:inline-block!important;font-size:12px;width:auto;max-width:100%;overflow:auto;border:1px solid #3eaf7c}.markdown-body thead{background:#3eaf7c;color:#fff;text-align:left}.markdown-body tr:nth-child(2n){background-color:rgba(62,175,124,.2)}.markdown-body td,.markdown-body th{padding:12px 7px;line-height:24px}.markdown-body td{min-width:120px}.markdown-body blockquote{color:#666;padding:1px 23px;margin:22px 0;border-left:.5rem solid;border-color:#42b983;background-color:#f8f8f8}.markdown-body blockquote:after{display:block;content:""}.markdown-body blockquote>p{margin:10px 0}.markdown-body details{outline:none;border:none;border-left:4px solid #3eaf7c;padding-left:10px;margin-left:4px}.markdown-body details summary{cursor:pointer;border:none;outline:none;background:#fff;margin:0 -17px}.markdown-body details summary::-webkit-details-marker{color:#3eaf7c}.markdown-body ol,.markdown-body ul{padding-left:28px}.markdown-body ol li,.markdown-body ul li{margin-bottom:0;list-style:inherit}.markdown-body ol li .task-list-item,.markdown-body ul li .task-list-item{list-style:none}.markdown-body ol li .task-list-item ol,.markdown-body ol li .task-list-item ul,.markdown-body ul li .task-list-item ol,.markdown-body ul li .task-list-item ul{margin-top:0}.markdown-body ol ol,.markdown-body ol ul,.markdown-body ul ol,.markdown-body ul ul{margin-top:3px}.markdown-body ol li{padding-left:6px}.markdown-body ol li::marker{color:#3eaf7c}.markdown-body ul li{list-style:none}.markdown-body ul li:before{content:"•";margin-right:4px;color:#3eaf7c}@media (max-width:720px){.markdown-body h1{font-size:24px}.markdown-body h2{font-size:20px}.markdown-body h3{font-size:18px}}

零:前言
1. 系列引言

可能说起 Flutter 绘制,大家第一反应就是用 CustomPaint 组件,自定义 CustomPainter 对象来画。Flutter 中所有可以看得到的组件,比如 Text、Image、Switch、Slider 等等,追其根源都是画出来的,但通过查看源码可以发现,Flutter 中绝大多数组件并不是使用 CustomPaint 组件来画的,其实 CustomPaint 组件是对框架底层绘制的一层封装。这个系列便是对 Flutter 绘制的探索,通过测试调试源码分析来给出一些在绘制时被忽略从未知晓的东西,而有些要点如果被忽略,就很可能出现问题。


2. 使用 CustomPainter 容易出现的疑问

本文是第一篇,就先从 CustomPaint 开始说起。你在 Flutter 绘制中,还在使用 State#setState 来刷新画板吗?你会不会也有和下面这位哥们相同的疑惑?你是不是只能将绘制抽离一个新组建来局部刷新?通过对源码的分析和研究后,会发现对于 CustomPainter 的重绘,有一个更高效的刷新方式。本文就来分享一下这个非常重要的知识点。


一、Flutter 中自定义绘制的方式

本文的测试案例效果如下,使用 CustomPaint 组件绘制一个圆,让其执行 3 秒红转蓝 的动画。


1.自定义画板 ShapePainter

如下自定义一个 CustomPainter,构造函数中传入颜色 color。需要复写两个方法 paintshouldRepaint。在 paint 方法中会回调 CanvasSize 对象,以供绘制使用。如下代码,绘制一个颜色为 color 的圆。

class ShapePainter extends CustomPainter {
  final Color color;

  ShapePainter({this.color});

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()..color = color;
    canvas.drawCircle(
        Offset(size.width / 2, size.height / 2), size.width / 2, paint);
  }

  @override
  bool shouldRepaint(covariant ShapePainter oldDelegate) {
    return oldDelegate.color!=color;
  }
}
复制代码

2. 使用画板

自定义的画板想要展示出来,需要使用 CustomPaint 组件,为其设置 painter 属性。如下代码,在实例化 ShapePainter 时传入红色。

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body:  Padding(
        padding: const EdgeInsets.all(20.0),
        child: CustomPaint( //<--- 使用绘制组件
            size: Size(100, 100),
            painter: ShapePainter(color: Colors.red),  //<--- 设置画板
          ),
      ),
    );
  }
}
复制代码

3.运行程序

将主程序运行后,就可以看到绘制的效果。

import 'package:flutter/material.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());
  }
}
复制代码

二、动画中画板的刷新
1. 较高层状态类使用的 setState (不推荐)

通过 ValueListenableBuilder 篇,我们应该知道在较上级的 State 类中执行 setState 会导致更多的 Build 过程。如下代码中通过监听 AnimationController ,并 setState 对当前 build 方法下的节点进行更新,从而实现颜色的变化。

class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
  AnimationController _ctrl;
  @override
  void initState() {
    super.initState();
    _ctrl = AnimationController(vsync: this, duration: Duration(seconds: 3))
      ..addListener(_update);
    _ctrl.forward();
  }
  
  @override
  void dispose() {
    _ctrl.dispose();
    super.dispose();
  }
  
  void _update() {
    setState(() {});
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Padding(
        padding: EdgeInsets.all(20),
        child: CustomPaint(
          size: Size(100, 100),
          painter: ShapePainter(
              color: Color.lerp(Colors.red, Colors.blue, _ctrl.value)),
        ),
      ),
    );
  }
}
复制代码

2.退而求其次的局部刷新 (不推荐)

那也许你会说,只要降低刷新的节点,将 画板组件 单独抽离出去,或使用 ValueListenableBuilder 局部刷新不就好了吗?如果看了 ValueListenableBuilder 的源码就会发现,其实它的本质就是 组件抽离,只不过对其进行封装,回调出 builder 简化用户使用。如下是使用 ValueListenableBuilder 局部构建的组件,这样可以不使用 setState 实现组件的重建,我还是想要着重强调一句:并不是说 setState 不好,而是看它重建的范围,ValueListenableBuilder 源码中也是基于 State#setState 进行重构的,并不是一个东西非好即坏,还需要看使用的场景和时机。

---->[_HomePageState#build]----
@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(),
    body:  ValueListenableBuilder(
        valueListenable: _ctrl,
        builder:(ctx,value,child) => CustomPaint(
          size: Size(100, 100),
          painter: ShapePainter(color: Color.lerp(Colors.red, Colors.blue, value)),
        ),
      ),
  );
}

也许你会觉得,现在不是很好吗,现在重建只是对于 CustomPaint 而言了,已经控制了重建的粒度。但重要的一点是 CustomPaint 被重建了,ShapePainter 也会随之重建,如下的调试,是动画过程中两次 paint 时情况。通过下面的 this 可以看出,当前对象的内存地址是不一样,说明每次更新画板都是不同的。这对于动画来说是灾难性的,每 16 ms 都会构建一次画板,这样的频率,即使是局部刷新,也不是最佳选择。那有没有一种方式,可以悄无声息的地进行绘制,而不会触发任何组件的重构?答案是 有的!

第一次

第二次

第一次
第一次
第二次
第二次

3.画板基于监听器的重绘 (推荐)

在刚才 ValueListenableBuilder 版的基础上稍作修改,我们就可以完成这个需求。首先,剔除掉 ValueListenableBuilder,然后将 Animation 作为 ShapePainter 的成员 factor,在构造函数中传入。并使用 super(repaint: factor) 为成员 repaint 赋值。repaint 是 CustomPainter 的成员,类型为 Listenable 可监听对象,当 repaint 值变化时,会通知画板进行 paint 重绘。

---->[_HomePageState#build]----
@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(),
    body: CustomPaint(
      size: Size(100, 100),
      painter: ShapePainter(factor: _ctrl),
    ),
  );
}

class ShapePainter extends CustomPainter {
  final Animation<double> factor;
  ShapePainter({this.factor}) : super(repaint: factor);
  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
      ..color = Color.lerp(Colors.red, Colors.blue, factor.value);
    canvas.drawCircle(
        Offset(size.width / 2, size.height / 2), size.width / 2, paint);
  }
  @override
  bool shouldRepaint(covariant ShapePainter oldDelegate) {
    return oldDelegate.factor != factor;
  }
}
复制代码

通过这种方式,点击时在 paint 方法断点调试,结果如下。可以看出,在完成颜色变化的同时,没有任何组件的重建,ShapePainter 对象也没有变化,是不是感觉非常神奇。

第一次

第二次

第一次
第一次
第二次
第二次

也许有人会问,这些你是怎么知道的?当一个疑问一直萦绕心头时,我就会想办法去研究它,而研究它最好的途径就是不断测试分析源码。目标可以是 CustomPainter 的源码本身,也可以是源码中使用到CustomPainter的地方。 其实很多知识,一直都写在源码中,只是很少人看到。通过 CustomPainter 的注释可以发现,触发重绘最高效的方式都是基于可监听对象 实现的。

触发重绘的最高效方式是:
[1]:继承 [CustomPainter] 类,并在构造函数提供一个 'repaint' 参数,
     当需要重新绘制时,该对象会进行通知它的监听者。
[2]:继承 [Listenable] (比如通过 [ChangeNotifier])并实现 [CustomPainter],
			这样对象本身就可以直接提供通知。

三、CustomPainter 在 Flutter 框架中的应用

其实 CustomPainter 在 Flutter 框架源码中的应用并不是非常多,一共也就下面的 20 处。这些都是源码中对 CustomPainter 的使用,就表示这些使用的方式相对而言是 最正规 的。


1. _CupertinoActivityIndicatorPainter

第一次的 悟道 ,是在 _CupertinoActivityIndicatorPainter 源码中,也就是那个 iOS 的菊花转的绘制画板。 position 是一个 Animation 类型的对象,Animation 也是一个 Listenable 。当时发现 CupertinoActivityIndicator 中没有使用 setState 却可以触发界面的刷新,我是非常惊喜的,经过分析和研究它的实现方式,我终于发现了 CustomPainterrepaint 秘密。


2. ScrollbarPainter

上面说的第二种是通过继承自 Listenable 并实现 CustomPainter 的方式,如源码中的 ScrollbarPainter。它是用来绘制 ScrollBar 组件的,通过这种方式可以让 ScrollbarPainter 即处理绘制,又处理通知。

这样,在 _CupertinoScrollbarState 中就可以将 ScrollbarPainter 作为成员变量,和 State 拥有同样的生命长度。并在某些恰当的时刻,使用该对象触发相应方法进行画布重绘。


3._GlowingOverscrollIndicatorPainter

当时还有一个疑惑是,repaint 中只是传入一个 Listenable 对象,那么多个属性如何去监听呢,比如多个动画同时执行。于是看到 _GlowingOverscrollIndicatorPainter 时便豁然开朗。它画的是滑动到顶底光晕的那个东西。 其中传入的 leadingControllertrailingController 两个可监听对象。除此之外,额外传入 repaint

可以通过 Listenable.merge 将多个可监听对象融合。


4. _PlaceholderPainter

但当我觉得 repaint 无敌之时,仍会发现,源码中有很多绘制的类并没使用 repaint,而是向外界暴露属性进行设置。比如 _PlaceholderPainter 的矩形×,_GridPaperPainter 的网格,于是陷入沉思。

_GridPaperPainter 的源码,只是向外界暴露绘制相关属性。

最终发现了一个共性:当绘制中含有动画和滑动处理时,都会使用 repaint 设置监听对象来触发刷新,对于仅是静态的绘制,则使用时将绘制属性暴露出去,交由外界处理,需要刷新的话,只能通过重建画板对象。其实这也很容易理解: 动画滑动 的触发频率非常高,所以才会用特殊的方式进行重绘。

那么画板的重绘必须只是通过 可监听对象 吗?并非如此,虽然可以通过可监听对象来触发画布刷新,比如_PlaceholderPainter 中 color 成员变为 ValueNotifier ,但这样就会增加用户使用的复杂性。对于非频繁刷新的场景,局部刷新也就够了,这应该就是源码中,在非 动画和滑动 中不使用 repaint 的原因。但对于频繁触发的绘制,如 动画滑动 一定要用。

最后想说一句:任何东西都不会完美无缺。成年人的世界,没有对错,只有适合与不适合。在一切的困惑、质疑、反驳之前,你应做的是 多测、多想、多看。本文就到这里,应该算是说清楚了 CustomPainter 正确的刷新姿势,但这也仅是 绘制探索 的冰山一角,CustomPainterCustomPaint 背后还有很多值得探寻的东西,会随着之后的探索,为你展开一个更加丰满的 Flutter 世界。


@张风捷特烈 2021.01.11 未允禁转 我的公众号:编程之王 联系我--邮箱:1981462002@qq.com -- 微信:zdl1994328 ~ END ~

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 零:前言
    • 1. 系列引言
      • 2. 使用 CustomPainter 容易出现的疑问
      • 一、Flutter 中自定义绘制的方式
        • 1.自定义画板 ShapePainter
          • 2. 使用画板
            • 3.运行程序
            • 二、动画中画板的刷新
              • 1. 较高层状态类使用的 setState (不推荐)
                • 2.退而求其次的局部刷新 (不推荐)
                  • 3.画板基于监听器的重绘 (推荐)
                  • 三、CustomPainter 在 Flutter 框架中的应用
                    • 1. _CupertinoActivityIndicatorPainter
                      • 2. ScrollbarPainter
                        • 3._GlowingOverscrollIndicatorPainter
                        • 4. _PlaceholderPainter
                        领券
                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档