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.

  • Flutter drawing exploration 1 | CustomPainter refresh the correct posture
  • Flutter drawn to explore 2 | comprehensive analysis CustomPainter related classes
  • Flutter draw explore CustomPainter class 3 | in-depth analysis
  • Flutter draw explore 4 | in-depth analysis setState reconstruction and renewal
  • Flutter draw redraw range RepaintBoundary explore 5 | in-depth analysis

2. CustomPainter and listener

We know that we can use the AnimationController to complete the animation requirement, which will trigger a callback every 16.6ms or so. Each callback will evenly change the number it holds from 0 to 1. Interpolation can be achieved through various Tween implementations, and animation curves can be set through Curve to adjust changes. The use of outer State#setState or local component refresh is not recommended for highly triggered drawings such as animations. This drawing in Flutter explore 1 | CustomPainter refresh the correct posture , have said very clearly that Listenable object can be used to inform the canvas re-paint, without any element of the reconstruction. This article builds on the previous articles and looks at how repaint triggers a refresh. This article examines the properties of the CustomPaint component, which we have explored previously around CustomPainter.


I. Test case description

1. The component class

The AnimationController is a Listenable object. In HomePage, pass the AnimationController object to the RunningPainter. I’m not doing any setState here, but I can redraw the artboard.

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

class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
  AnimationController spread;
  @override
  void initState() {
    super.initState();
    spread =
        AnimationController(vsync: this, duration: Duration(milliseconds: 2000))
          ..repeat();
  }
  
  @override
  void dispose() {
    spread.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: CustomPaint(
          size: Size(120.120), painter: ShapePainter(spread: spread), ), ), ); }}Copy the code

2. The drawing class

The only thing special is that the spread object is passed to the super construct to initialize the _repaint member. The drawing operation is very simple, draw a small circle and use the animator to draw a circle with gradually changing radius and decreasing color opacity.

class ShapePainter extends CustomPainter {
  final Animation<double> spread;

  ShapePainter({this.spread}) : super(repaint: spread);

  @override
  void paint(Canvas canvas, Size size) {
    final double smallRadius = size.width / 6;
    final double spreadFactor = 2; Paint paint = Paint().. color = Colors.green; canvas.translate(size.width /2, size.height / 2);
    canvas.drawCircle(Offset(0.0), smallRadius, paint);
    
    if(spread.value ! =0) { paint.. color = Colors.green.withOpacity(1 - spread.value);
      canvas.drawCircle(
          Offset(0.0), smallRadius * (spreadFactor * spread.value), paint); }}@override
  bool shouldRepaint(covariant ShapePainter oldDelegate) {
    return oldDelegate.spread != spread;
  }
}
Copy the code

Explore callbacks when listening to Listenable

1. CustomPainter Listenable

CustomPainter is an abstract class that holds a _repaint object of type Listenable preceded by _ and does not provide get or set methods, which means that the object cannot be set or retrieved directly from the outside world. You can see that the only way to set this up is through the CustomPainter constructor. This is why subclasses can only be set in super.


2. CustomPainter#_repaintAdd or remove listening paths

Since the _repaint object is not exposed to the outside world, how does it work? The CustomPainter class inherits Listenable itself and overwrites addListener and removeListener. In this case, _repaint is wrapped inside the class, with CustomPainter itself as the listener, providing methods to listen on and remove the listener.

abstract class CustomPainter extends Listenable {
  const CustomPainter({ Listenable? repaint }) : _repaint = repaint;

  final Listenable? _repaint;

  / / to monitor
  @override
  voidaddListener(VoidCallback listener) => _repaint? .addListener(listener);// Remove the listener
  @override
  voidremoveListener(VoidCallback listener) => _repaint? .removeListener(listener);/ / a little...
}
Copy the code

3. When CustomPainter was monitored

Paints explore 2 | Flutter in comprehensive analysis CustomPainter related classes in said RenderCustomPaint rendering object will hold CustomPainter, And call _Painter #addListener in the attach method with markNeedsPaint as the method to listen for notification triggers. In the detach method, _painter#removeListener is executed to remove the listener.

---->[RenderCustomPaint#attach]----
@override
void attach(PipelineOwner owner) {
  super.attach(owner); _painter? .addListener(markNeedsPaint); _foregroundPainter? .addListener(markNeedsPaint); }@override
voiddetach() { _painter? .removeListener(markNeedsPaint); _foregroundPainter? .removeListener(markNeedsPaint);super.detach();
}
Copy the code

4. RenderObject# attach timing

Paints to explore Flutter in 2 | CustomPainter related said in class, comprehensive analysis RenderObjectWidget gens components, will create RenderObject in RenderObjectElement# mount. In the following debugging, add a breakpoint before RenderCustomPaint#attach, and you can see that after creating the RenderObject, attach the newly created RenderObject to the render tree by attachRenderObject. RenderObject#attach is called in this process.


3. CustomPaint component analysis

1. Learn about the CustomPaint component

We must first identify CustomPaint status, it inherits from SingleChildRenderObjectWidget is a Widget, means that it is a configuration information, all of its members are as final. Second, it’s a RenderObjectWidget, so you need to create and maintain the RenderObject. Below, CustomPaint has four members in addition to Painter.

attribute introduce type The default value
painter Background sketchpad CustomPainter? null
foregroundPainter Prospects for the sketchpad CustomPainter? null
size size Size Size.zreo
isComplex Is it too complicated to turn on the cache bool false
willChange Whether the cache should be told that the content might change in the next frame bool false
child Child components Widget? null

2. Maintain RenderCustomPaint

The CustomPaint class, which is the property handler, basically creates RenderCustomPaint and updates the render object when updateRenderObject is updated. So the CustomPaint component itself is not complicated, it takes member attributes as input when RenderCustomPaint is instantiated, and those attributes are ultimately used in RenderCustomPaint.

@override
RenderCustomPaint createRenderObject(BuildContext context) {
  return RenderCustomPaint(
    painter: painter,
    foregroundPainter: foregroundPainter,
    preferredSize: size,
    isComplex: isComplex,
    willChange: willChange,
  );
}

@override
voidupdateRenderObject(BuildContext context, RenderCustomPaint renderObject) { renderObject .. painter = painter .. foregroundPainter = foregroundPainter .. preferredSize = size .. isComplex = isComplex .. willChange = willChange; }@override
voiddidUnmountRenderObject(RenderCustomPaint renderObject) { renderObject .. painter =null
    ..foregroundPainter = null;
}
Copy the code

3. CustomPaint Painter, groundPainter and Child

CustomPaint has two artboard objects: painter and foregroundPainter, which are used to paint the background and foreground respectively. Because he is a subclass of SingleChildRenderObjectWidget, so I can package a child component, and the background and foreground is relative to the child. In CustomPaint, child is an icon with a blue circle in the foreground and a red circle in the background.

---->[Artboard usage]---- CustomPaint(size: size (200.200),
  painter: ShapePainter(color: Colors.red,offset: Offset(50.50)),
  foregroundPainter: ShapePainter(color: Colors.blue),
  child: Icon(Icons.android_rounded,size: 50,color: Colors.green,),
),

class ShapePainter extends CustomPainter {
  final Color color;
  final Offset offset;

  ShapePainter({this.color, this.offset = Offset.zero});

  @override
  voidpaint(Canvas canvas, Size size) { Paint paint = Paint().. color = color; canvas.drawCircle(offset,20, paint);
  }

  @override
  bool shouldRepaint(covariant ShapePainter oldDelegate) {
    returnoldDelegate.color ! = color || oldDelegate.offset ! = offset; }}Copy the code

The previous introduction of background painter should be incisively and vividly. The _foregroundPainter is similar, and you can see that in RenderCustomPaint#paint, The background painter is used first, then super.paint is used to draw the child, and finally the foreground is used to draw the _foregroundPainter. This is how the above three attribute hierarchies work.

---->[RenderCustomPaint#paint]----
@override
void paint(PaintingContext context, Offset offset) {
  if(_painter ! =null) { _paintWithPainter(context.canvas, offset, _painter!) ; _setRasterCacheHints(context); }super.paint(context, offset);
  if(_foregroundPainter ! =null) {
    _paintWithPainter(context.canvas, offset, _foregroundPainter!);
    _setRasterCacheHints(context);
  }
}
Copy the code

4. CustomPaint’s isComplex and willChange

These two parameters are probably not well known; they are booleans and default to false. Take a look at their description in the source documentation:

  • isComplex
The synthesizer includes a raster cache that holds layers of bitmaps to avoid the expense of repeatedly rendering those layers on each frame. If this flag is not set, the synthesizer will use its own triggers to determine whether the layer is complex enough to benefit from caching. If both [Painter] and [groundPainter] arenull, this flag cannot be set totrueBecause the flag is ignored in this case.Copy the code
  • willChange
Should the raster cache be told if the painting is likely to change in the next frame? If this flag is not set, the Compositor uses its heuristics to determine whether the current layer is likely to be reused in the future. If [Painter] and [foregroundPainter] are both null, this flag cannot be set to true, as the flag will be ignored in this case.Copy the code

We know that members in CustomPaint are passed in to RenderCustomPaint. After the drawing above, the _setRasterCacheHints method is called to set the properties in the drawing context, and finally the properties are set to _currentLayer. In general, these two booleans are handled internally by the framework if they are not set.

---->[RenderCustomPaint#_setRasterCacheHints]----
void _setRasterCacheHints(PaintingContext context) {
  if (isComplex)
    context.setIsComplexHint();
  if (willChange)
    context.setWillChangeHint();
}

---->[PaintingContext#setIsComplexHint]----
voidsetIsComplexHint() { _currentLayer? .isComplexHint =true;
}
---->[PaintingContext#setWillChangeHint]----
voidsetWillChangeHint() { _currentLayer? .willChangeHint =true;
}
Copy the code

5. CustomPaint size

If you are confused when you use the size object in the CustomPainter#paint callback, why it is sometimes size (0,0), let’s explore what happens to the size callback. Size is a member of CustomPaint, which defaults to size (0,0).

When the RenderCustomPaint object is created, size is taken as the preferredSize parameter, and the _preferredSize member in RenderCustomPaint is initialized.


As follows, the paint callback on the artboard is called back to the size object, which is a member of the RenderBox. RenderCustomPaint is a subclass of RenderBox. In performResize, size is assigned to constraints. Constrain (preferredSize).

---->[RenderCustomPaint#performResize]----
@override
void performResize() {
  size = constraints.constrain(preferredSize);
  markNeedsSemanticsUpdate();
}

void _paintWithPainter(Canvas canvas, Offset offset, CustomPainter painter) {
  late int debugPreviousCanvasSaveCount;
  canvas.save();
  if(offset ! = Offset.zero) canvas.translate(offset.dx, offset.dy); painter.paint(canvas, size);// <----
Copy the code

For example, CustomPaint is used directly in the Scaffold and the size callback is size (0,0).

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    returnScaffold( appBar: AppBar(), body: CustomPaint( painter: ShapePainter(color: Colors.red), ), ); }}Copy the code

Check that size is assigned by constraints. Constrain (preferredSize). The Scaffold’s body property is constrained by BoxConstraints(0.0<= W <=411.4, 0.0<=h<=603.4). The current preferredSize is unset and defaults to Size(0,0), so see what the constrain method does.

Go to the BoxConstraints. Constrain method and create a Size with the width and constrain as follows:

Clamp functions are then used to calculate the incoming width according to minWidth and maxWidth.

So what does this function do? In simple terms, the target value t and the target range [a,b]. If t is inside [a,b], return t; When t < a, return a; When t > b, return b. You can see that if you do not set the size attribute, you get size (0,0) under the BoxConstraints(0.0<=w<=411.4, 0.0<=h<=603.4). When size is specified, within the constraints, the specified size is used.

main(){
  print('--0.clamp(3, 6):-------The ${0.clamp(3.6)}-- -- -- -- -- -- -- ');
  print('--1.clamp(3, 6)-------The ${1.clamp(3.6)}-- -- -- -- -- -- -- ');
  print('--4.clamp(3, 6)-------The ${4.clamp(3.6)}-- -- -- -- -- -- -- ');
  print('--7.clamp(3, 6)-------The ${7.clamp(3.6)}-- -- -- -- -- -- -- '); } Log: -- 0.clamp(3.6) : -- -- -- -- -- -- 3-------
-1.clamp(3.6) -- -- -- -- -- -- 3-------
-- 4.clamp(3.6) -- -- -- -- -- -4 --------
-7.clamp(3.6) -- -- -- -- -- -- 6-------
Copy the code

This is when child is null, add the child property below, and you’ll see that you have the size. If you don’t know how it works, you’ll think it’s too accurate and you’ll be afraid to use it. But when you recognize the principle, you can use more confidence, this is the benefit of looking at the source code, all the strange behavior, there will be its roots behind.

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    returnScaffold( appBar: AppBar(), body: CustomPaint( painter: ShapePainter(color: Colors.red), child: Icon(Icons.android_rounded), ), ); }}Copy the code

If chID = null, if child! =null, the child’s size will be used. This is called the top-down delivery of constraints and the bottom-up setting of dimensions.

Now that all the attributes of CustomPaint are covered, you’ll be able to use it with ease once you know what’s inside. This knowledge will allow you to make the most informed decision when it comes to drawing dynamically and sizing your artboard, rather than messing around with setState refreshes or being afraid to use callback size.


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