Zero: preface

1. Series of introduction

The first thing you might think about Flutter painting is to use the CustomPaint component to create a custom CustomPainter object. All visible components of a Flutter, such as Text, Image, Switch, Slider, and so on, are drawn. However, most of the components of a Flutter are not drawn using the CustomPaint component. The CustomPaint component is a wrapper around the underlying painting of the framework. This series is an exploration of Flutter drawing, testing, debugging, and source code analysis to reveal things that are overlooked or never known when drawing, and points that can go wrong if omitted.


2. Questions that arise when using CustomPainter

This is the first article to start with CustomPaint. Do you still use State#setState to refresh the artboard in Flutter drawing? Do you have the same question as this guy? Can you only partially refresh a drawing by pulling it out of a new build? After analyzing and studying the source code, we will find that there is a more efficient way to refresh the CustomPainter redraw. This article will share this very important knowledge point.


1. Custom drawing of Flutter

The test case for this article looks like this, using the CustomPaint component to draw a circle and animate it for 3 seconds from red to blue.


1. Customize your artboardShapePainter

Here’s a CustomPainter, passing color in the constructor. We need to duplicate two methods paint and shouldRepaint. The Canvas and Size objects are called back in the paint method for drawing. As follows, draw a circle with color as color.

class ShapePainter extends CustomPainter {
  final Color color;

  ShapePainter({this.color});

  @override
  voidpaint(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;
  }
}
Copy the code

2. Use sketchpad

To display a custom artboard, set the Painter property for it using the CustomPaint component. The following code passes red when instantiating 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( //<-- use the draw component
            size: Size(100.100),
            painter: ShapePainter(color: Colors.red),  //<-- set artboard),),); }}Copy the code

3. Run the program

After running the main program, you can see the effect of drawing.

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()); }}Copy the code

2. Refresh the drawing board in animation

1. Used by higher-level state classessetState(Not recommended)

From the ValueListenableBuilder article, we should know that executing setState in a higher State class results in more Build processes. The following code implements the color change by listening to the AnimationController and setState to update the node under the current build method.

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)), ), ), ); }}Copy the code

2. Settle for second bestLocal refresh(Not recommended)

Then you might say, just lower the refresh node, separate the artboard component, or use ValueListenableBuilder to partially refresh. If you look at the source code of ValueListenableBuilder, you will find that its essence is to extract components, but to encapsulate it, call back Builder to simplify user use. ValueListenableBuilder is used to partially build the component, so that it can be rebuilt without using setState. I want to emphasize the following sentence: ValueListenableBuilder source code is also based on State#setState for reconstruction. It is not a good or bad thing, but also depends on the use of the scenario and timing.

---->[_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)),
        ),
      ),
  );
}
Copy the code

You might be thinking, isn’t it great that now the rebuild is just for CustomPaint, and you have control over the granularity of the rebuild? But the important point is that CustomPaint is rebuilt, and ShapePainter will be rebuilt as well. The following debugging is for two paints during the animation process. As you can see from this, the memory address of the current object is different, indicating that the artboard is different every time it is updated. This is disastrous for animation, as artboards are built every 16 ms, a rate at which even partial refreshes are not optimal. Is there a way to draw silently without triggering any component refactoring? The answer is yes! .

For the first time, The second time

3. Sketchpad redraw based on listeners (recommended)

With a few modifications to the ValueListenableBuilder version, we can do this. ValueListenableBuilder is first removed, and Animation

is passed as a member factor of ShapePainter in the constructor. Member repaint is assigned using super(repaint: factor). Repaint is a member of CustomPainter and is of type Listenable listener. When the repaint value changes, the artboard is notified to repaint.


---->[_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
  voidpaint(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;
  }
}
Copy the code

In this way, debug the paint method breakpoint when clicked, resulting in the following result. As you can see, it feels amazing to complete the color change without reconstructing any components or changing the ShapePainter object.

For the first time, The second time

Some may ask, how do you know all this? When a question is on my mind, I find a way to investigate it, and the best way to investigate it is by constantly testing and analyzing the source code. The target can be the source code for CustomPainter itself, or a place in the source code where CustomPainter is used. In fact, a lot of knowledge has been written in the source code, but few people see. As you can see from CustomPainter’s annotations, the most efficient ways to trigger redraw are implemented based on listeners.

The most efficient way to trigger a redraw is: [1] : inherits the [CustomPainter] class and provides one in the constructor'repaint'Parameter that notifies its listeners when it needs to be redrawn. [2] : inherits [Listenable] (for example through [ChangeNotifier]) and implements [CustomPainter] so that the object itself can provide notifications directly.Copy the code

Three,CustomPainterApplication of Flutter framework

In fact, there are not many applications of CustomPainter in the source code of Flutter framework. There are only 20 in total. These are the uses of CustomPainter in the source code and are relatively formal in the way they represent these uses.


1. _CupertinoActivityIndicatorPainter

The enlightenment for the first time, is in _CupertinoActivityIndicatorPainter source, namely the iOS chrysanthemum turned drawing pad. Position is an Animation

object, and the Animation is a Listenable. Was not found in the CupertinoActivityIndicator using setState can trigger interface refresh, I’m very surprise, through analysis and research on its realization ways, I finally found the CustomPainter repaint the secret.


2. ScrollbarPainter

The second way is by inheriting from Listenable and implementing CustomPainter, such as ScrollbarPainter in the source code. It is used to draw ScrollBar components, which allows ScrollBar Painter to handle both drawing and notifications.

In _CupertinoScrollbarState, you can use ScrollbarPainter as a member variable with the same lifetime as State. And at some appropriate moment, the object triggers the corresponding method to redraw the canvas.


3._GlowingOverscrollIndicatorPainter

There was also confusion about how to listen for multiple properties, such as multiple animations being executed at the same time, if repaint was just passing in a Listenable object. So when it saw _GlowingOverscrollIndicatorPainter be suddenly enlightened. It’s the thing that slides into the top and bottom halo. LeadingController and trailingController can be monitored. In addition, pass repaint.

You can merge multiple listeners using listenable. merge.


4. _PlaceholderPainter

But when I think Repaint is invincible, I still find a lot of classes drawn in the source code that don’t use Repaint, but instead expose properties to the outside world. As _PlaceholderPainter’s rectangle × as _GridPaperPainter’s grid…

The source code of _GridPaperPainter is only exposed to the outside world to draw related properties.

When drawing with animation or sliding, repaint is used to set the listener to trigger the refresh. For static drawings, the properties of the paint are exposed to the outside world. The only way to refresh is by rebuilding the artboard object. This is easy to understand: animations and slides are triggered very frequently, which is why they are redrawn in a particular way.

Does the redrawing of the artboard have to be done only through listeners? Not so — while a canvas refresh can be triggered by a listener — for example ValueNotifier< color > is the color member of _PlaceholderPainter — this adds complexity. For scenes that refresh infrequently, a partial refresh is sufficient, which is probably why repaint is not used in the source code for non-animations and slides. But do use it for frequently triggered drawing, such as animation and sliding.

One last word: nothing is perfect. In the adult world, there is no right or wrong, only suitable and inappropriate. Before all confusion, doubt, refute, you should do is more test, more think, more look. This is the end of this article, it should be clear about the correct refresh posture of CustomPainter, but this is only the tip of the iceberg of painting exploration, there are a lot of things worth exploring behind CustomPainter and CustomPaint, with further exploration, Unfold a fuller Flutter world for you.


@Zhang Fengjietele 2021.01.11 not allowed to transfer my public number: the king of programming contact me – email :[email protected] – wechat :zdl1994328 ~ END ~