The indicator implementation of the Banner in the previous article takes the form of a custom view. In this article, we will focus on how to implement a custom view. If you want to customize a view in Android, you need to inherit the view and use the onDraw method to draw the corresponding content on the Canvas. If you want to trigger a redraw, you only need to call invalidate. Also here we need to inherit from CustomPainter and draw the content in its paint method, using the return value of shouldRepaint to determine if we need to redraw. Let’s take a look at its use in detail. The content of this article will be shown in the following figure as an example

CustomPainter is used as a child Widget of CustomPaint. So let’s first look at the constructor of CustomPaint

const CustomPaint({
  Key? key,
  this.painter,
  this.foregroundPainter,
  this.size = Size.zero,
  this.isComplex = false,
  this.willChange = false,
  Widget? child,
})
Copy the code

Here, both painter and foregroundPainter are CustomPainter. They represent the background and foreground of child respectively, while Size represents the Size. Note that if both child and Size are specified, the final Size is the Size of the child. For custom view we can be divided into the following steps:

  • Create a class that inherits CustomPainter. Like this one right hereclass ProgressRingPainter extends CustomPainter
  • Paint: In this method we get the canvas and the Size of the control. All we need to do is paint the desired content on the canvas.
  • The shouldRepaint method in the class returns a condition that needs to be redrawn: if it returns true the redraw is triggered when the corresponding value changes.
  • If you want to animate, add super(repaint: XXX) to the constructor of your custom class to automatically add listeners. This will automatically trigger redraw when the value changes. The specific reasons are as follows
    • The structure of the CustomPainter class is as follows: You can see that if we add the super method we’re actually assigning repaint here. It is a ListEnable object

- When we specify Painter in CustomPaint, we call Painter's set method when we create the CustomPaint.Copy the code

- This is mainly the _didUpdatePainter methodCopy the code

- The addListenter method is called, which is defined in the CustomPainter class, and you can see that we added the listener with the Repaint object we just passed in. So we can update the page like this in CustomPainter.Copy the code

So that’s the principle. Next we will introduce some basic knowledge, in fact, custom view can be summed up in the canvas with a brush to draw the desired graphics. So three more keystrokes, canvas, graphics.

  1. Brush: Paint, this is what we have to use. Common attributes are color (to set the brush color), style (stroke or fill corresponds to paintingstyle. stroke and paintingstyle. fill), strokeWidth (stroke width), isAntiAlias(anti-aliasing or not), Shader (draw the commonly used LinearGradient gradient, SweepGradient scan gradient, RadialGradient RadialGradient gradient). I’m not going to go into the details here, but if you’re interested you can set these properties to the brush and see what they look like.

  2. Drawxxx (XXX,paint) : Canvas, draw the content on the Canvas using the paintbrush

    For example, drawPath is to draw a specified Path, drawCircle is to draw a circle, drawLine is to draw a line, etc. A canvas can not only be painted, it can also be transformed and cropped. For example, commonly used translation canvas.translate, rotation canvas.rotate, and clipping canvas.clipRect. In order to ensure that the content drawn does not exceed the Size of the control, the canvas Size is usually cut at the beginning of the drawing, so that the canvas Size is set to Size, and then the origin of the coordinate system is moved to the middle of the view (the system coordinate system is in the upper left corner, right and down are x and Y axis positive respectively).

  3. Graph: Path, which is called graph, is actually not accurate. From above, we can know that we can draw circles, squares, points, lines and so on directly through canvas. But if you’re doing something more complicated like drawing a Bezier curve, you need a Path, so I’m going to focus on Path. Canvas. Drawxxx implementation of the graph Path can be implemented. For example, canvas.drawLine can be implemented with path.lineTo, and Canvas. drawCircle can be implemented with path.addOval. There are a lot of things you can do with Path

    • Close: connect the ends to form a closed path (path.close())
    • Reset: Resets the path and clears the contents (path.reset())
    • Shift: Shift the path and return a new path. For example, if only one path is a triangle on the canvas, execute path.shift(40,0) to draw an identical triangle 40 to the right of the original triangle, there are two triangles on the canvas so far
    • Contains: Determines whether a point is in the path, which can be used for contact detection and collision detection (path.contains(Offset(20, 20)))
    • GetBounds: The rectangular area of the current path, which returns a Rect. (Rect bounds = path.getbounds ();)
    • Transform: path transformation. For symmetric patterns, when there are already partial monomer paths, the paths can be transformed according to a 4 by 4 matrix. You can use Matrix4 objects to aid in matrix generation. It is very convenient for rotation, translation, scaling, bevel and other transformation effects.
    • Combine: Path union, which can be used to generate complex paths.canvas.drawPath(Path.combine(PathOperation.xor, path1, path2), paint);The thing to notice here is the value of the PathOperation.xorIs to draw path1 and path2 without drawing the overlap between them;differenceDraw only the content that coincides with path1 and does not draw;reverseDifferenceIs to draw only path2 and not the part that overlaps with path1;intersectDraw the intersection of Path1 and Path2,unionDraws the union of path1 and path2
    • ComputeMetrics:path.computeMetrics()You can get an iterable PathMetrics class object that iterates out the PathMetric object, which is the measurement information for each path. That is, with path.computeMetrics() you get a set of measurements for a path. The information includes the length of the path, contourIndex of the path, and isClosed whether the path isClosed.
    • Get position information based on the measured path: for example, I want to draw a ball halfway down the path, which is very difficult to do by myself. Fortunately, it’s very easy to do with path measurements. You can even get information about the Angle and speed of the change point. The following throughpm.length* 0.5 represents information at a point 50% of the path length.pm.getTangentForOffsetWe get the tangent of the path somewhere, and we get the coordinates of the change point and the angular velocity information. For example, you can update the position of the point on the path according to the progress of the animation, that is, pm.length*progress below.
    • Draw the trajectory according to the progress of the animation: this uses the extractPath method in PathMetrics,Path extractPath(double start, double end, {bool startWithMoveTo = true})

    At this point, the preparatory work has been completed. Let’s actually start drawing our progress bar. So let’s analyze it first

  • The outermost layer has a gray circle. This is easier to achieve. In fact, the brush uses stroke to draw a circle, the width of the circle is the width of the stroke. So I need a paint brush, and it’s going to fill it with stroke
  • The outer layer is a pink circle that is drawn as you progress, which is essentially a circle, just a matter of how many are drawn. Here we are drawing the corresponding length according to the progress of the animation or the progress we are given. So here we use the measurement and interception of the path we mentioned above, so we need a PathMetric to capture the current path and plot it bit by bit, and that bit by bit is the progress. We know from the initial analysis that we need a Listenable object, which we use hereAnimation<double>Of course, ValueNotifier can also be used.
  • All that remains is to draw the text. Here we use the Child implementation of CustomPaint (to simplify the drawing of custom views and to understand the Child property of CustomPaint).

Following the custom View steps described above, we create the ProgressRingPainter class that inherits the CustomPainter class. And define the parameters we need (of course, the background color, the color of the progress bar, the width of the ring, etc., should be selected as variables to support configuration, here is a lazy).

Class ProgressRingPainter extends CustomPainter {// Paint _paint; // Final Animation<double> progress; // Path measurement PathMetric PathMetric; ProgressRingPainter(this.progress) : super(repaint: progress) { Path _path = Path(); _path.addOval(Rect.fromCenter(center: Offset(0, 0), width: 90, height: 90)); pathMetric = _path.computeMetrics().first; _paint = Paint() .. color = Colors.black38 .. strokeWidth = 10 .. style = PaintingStyle.stroke; } @override void paint(Canvas canvas, Size size) { ... } @override bool shouldRepaint(covariant ProgressRingPainter oldDelegate) { ... }}Copy the code

You can see that super (repaint: Progress) has been added to the constructor, meaning that a redraw will be attempted if this value changes. In the constructor we draw a circle in path.addoval and measure the path to get a PathMetric object. Next, we will implement the condition that triggers the redraw. In fact, the last progress is inconsistent with the current progress

@override bool shouldRepaint(covariant ProgressRingPainter oldDelegate) { return progress.value ! = oldDelegate.progress.value; }Copy the code

Next comes the important paint method, which goes directly to the code

@override void paint(Canvas canvas, Size size) { canvas.clipRect(Offset.zero & size); canvas.translate(size.width / 2, size.height / 2); canvas.drawCircle(Offset(0, 0), size.width / 2 - 5, _paint); canvas.drawPath( pathMetric.extractPath( 0, pathMetric.length * progress.value, ), Paint() .. color = Colors.pinkAccent .. strokeWidth = 10 .. style = PaintingStyle.stroke); }Copy the code

As we said before, we will first crop the canvas to a rectangle of Size to prevent it from going out of range (if we do not crop the canvas to a rectangle of Size, it will show if you are interested, you can try it). We can not only crop the canvas to a rectangle, we can also use clipRRect to crop the canvas to a rounded rectangle, You can also use CilpPath to crop to specific shapes. Then we move the origin of the frame to the center of the view; Then we draw a static ring, and then we draw variable progress through drawPath, which of course takes the length of the entire path, as a percentage. For the variable ring we are done drawing, the complete code is as follows

Class ProgressRingPainter extends CustomPainter {// Paint _paint; // Final Animation<double> progress; // Path measurement PathMetric PathMetric; ProgressRingPainter(this.progress) : super(repaint: progress) { Path _path = Path(); _path.addOval(Rect.fromCenter(center: Offset(0, 0), width: 90, height: 90)); pathMetric = _path.computeMetrics().first; _paint = Paint() .. color = Colors.black38 .. strokeWidth = 10 .. style = PaintingStyle.stroke; } @override void paint(Canvas canvas, Size size) { canvas.clipRect(Offset.zero & size); canvas.translate(size.width / 2, size.height / 2); canvas.drawCircle(Offset(0, 0), size.width / 2 - 5, _paint); canvas.drawPath( pathMetric.extractPath( 0, pathMetric.length * progress.value, ), Paint() .. color = Colors.pinkAccent .. strokeWidth = 10 .. style = PaintingStyle.stroke); } @override bool shouldRepaint(covariant ProgressRingPainter oldDelegate) { return progress.value ! = oldDelegate.progress.value; }}Copy the code

The rest is the text part. As the progress changes, the text will be redrawn constantly. To achieve the whole effect, either the setState reconstruction of the StatefullWidget is called, or the text control itself is refreshed. Because the Animation (abstract class Animation

extends Listenable implements ValueListenable

) extends Listenable, Just receive his notification and you can rebuild the view. Here we use ValueListenableBuilder to create the Text

class ValueListenableBuilder<T> extends StatefulWidget { /// Creates a [ValueListenableBuilder]. /// /// The [valueListenable] and [builder] arguments must not be null. /// The [child] is optional but is good practice to use if part of the widget /// subtree does not depend on the value of the [valueListenable]. const ValueListenableBuilder({ Key? key, required this.valueListenable, required this.builder, this.child, }) : assert(valueListenable ! = null), assert(builder ! = null), super(key: key); /// The [ValueListenable] whose value you depend on in order to build. /// /// This widget does not ensure that the [ValueListenable]'s value is not /// null, therefore your [builder] may need to handle null values. /// /// This [ValueListenable] itself must not be null. final ValueListenable<T> valueListenable; . }Copy the code

ValueListenable passes in a variable that is exactly the progress of the animation, and Builder builds the widget based on that variable. The Builder is implemented as follows

Widget buildText(BuildContext context, double value, Widget child) {
    return Text("${(value * 100).toInt()}%");
  }
Copy the code

The implementation of each part is now complete, so the complete code used is as follows

class ProgressRing extends StatefulWidget { @override State<StatefulWidget> createState() => _StateProgressRing(); } class _StateProgressRing extends State<ProgressRing> with SingleTickerProviderStateMixin { AnimationController controller; @override void initState() { super.initState(); controller = AnimationController( vsync: this, duration: Duration(seconds: 10), ).. repeat(reverse: true); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('CustomPaint'), ), body: Center( child: CustomPaint( size: Size(100, 100), painter: ProgressRingPainter(controller), child: Container( width: 100, height: 100, child: Center( child: ValueListenableBuilder( valueListenable: controller, builder: buildText, ), ), ), ), ), ); } @override void dispose() { controller.dispose(); super.dispose(); } Widget buildText(BuildContext context, double value, Widget child) { return Text("${(value * 100).toInt()}%"); }} class ProgressRingPainter extends CustomPainter {// Paint _paint; // Final Animation<double> progress; // Path measurement PathMetric PathMetric; ProgressRingPainter(this.progress) : super(repaint: progress) { Path _path = Path(); _path.addOval(Rect.fromCenter(center: Offset(0, 0), width: 90, height: 90)); pathMetric = _path.computeMetrics().first; _paint = Paint() .. color = Colors.black38 .. strokeWidth = 10 .. style = PaintingStyle.stroke; } @override void paint(Canvas canvas, Size size) { canvas.clipRect(Offset.zero & size); canvas.translate(size.width / 2, size.height / 2); canvas.drawCircle(Offset(0, 0), size.width / 2 - 5, _paint); // canvas.rotate(-pi / 2); canvas.drawPath( pathMetric.extractPath( 0, pathMetric.length * progress.value, ), Paint() .. color = Colors.pinkAccent .. strokeWidth = 10 .. style = PaintingStyle.stroke); } @override bool shouldRepaint(covariant ProgressRingPainter oldDelegate) { return progress.value ! = oldDelegate.progress.value; }}Copy the code

Custom view has been implemented, but also please criticize.