In this section, we will implement a DoneWidget that performs a checkmark animation upon creation, as shown in Figure:
The implementation code is as follows:
class DoneWidget extends LeafRenderObjectWidget { const DoneWidget({ Key? key, this.strokeWidth = 2.0, this.color = Colors.green, this.outline = false, }) : super(key: key); // Line width final double strokeWidth; // Outline color or fill color final Color color; // If true, there is no fill color; color represents the outline color; if false, color is the fill color final bool outline; @override RenderObject createRenderObject(BuildContext context) { return RenderDoneObject( strokeWidth, color, outline, )..animationStatus = AnimationStatus.forward; // Perform forward animation upon creation } @override void updateRenderObject(context, RenderDoneObject renderObject) { renderObject ..strokeWidth = strokeWidth ..outline = outline ..color = color; } }
The DoneWidget
has two modes: an outline mode, where there is no fill color, and the color represents the outline color; in non-outline mode, the color represents the fill color, and the color of the checkmark is simply set to white.
Next, we need to implement RenderDoneObject
. Since the component does not need to respond to events, we can omit the event handling code. However, the component needs to execute animations, so we can directly use the RenderObjectAnimationMixin
we encapsulated in the previous section. The specific implementation code is as follows:
class RenderDoneObject extends RenderBox with RenderObjectAnimationMixin { double strokeWidth; Color color; bool outline; ValueChanged<bool>? onChanged; RenderDoneObject( this.strokeWidth, this.color, this.outline, ); // Animation duration is 300ms @override Duration get duration => const Duration(milliseconds: 300); @override void doPaint(PaintingContext context, Offset offset) { // Apply a curve to the animation Curve curve = Curves.easeIn; final _progress = curve.transform(progress); Rect rect = offset & size; final paint = Paint() ..isAntiAlias = true ..style = outline ? PaintingStyle.stroke : PaintingStyle.fill // Fill ..color = color; if (outline) { paint.strokeWidth = strokeWidth; rect = rect.deflate(strokeWidth / 2); } // Draw background circle context.canvas.drawCircle(rect.center, rect.shortestSide / 2, paint); paint ..style = PaintingStyle.stroke ..color = outline ? color : Colors.white ..strokeWidth = strokeWidth; final path = Path(); Offset firstOffset = Offset(rect.left + rect.width / 6, rect.top + rect.height / 2.1); final secondOffset = Offset( rect.left + rect.width / 2.5, rect.bottom - rect.height / 3.3, ); path.moveTo(firstOffset.dx, firstOffset.dy); const adjustProgress = .6; // Draw "checkmark" if (_progress < adjustProgress) { // Animate the line from the first point to the second point (the second point continuously changes) Offset _secondOffset = Offset.lerp( firstOffset, secondOffset, _progress / adjustProgress, )!; path.lineTo(_secondOffset.dx, _secondOffset.dy); } else { // Connect the first point and the second point path.lineTo(secondOffset.dx, secondOffset.dy); // The position of the third point changes with the animation final lastOffset = Offset( rect.right - rect.width / 5, rect.top + rect.height / 3.5, ); Offset _lastOffset = Offset.lerp( secondOffset, lastOffset, (progress - adjustProgress) / (1 - adjustProgress), )!; path.lineTo(_lastOffset.dx, _lastOffset.dy); } context.canvas.drawPath(path, paint..style = PaintingStyle.stroke); } @override void performLayout() { // If the parent specifies fixed dimensions, use them; otherwise, default to 25 size = constraints.constrain( constraints.isTight ? Size.infinite : const Size(25, 25), ); } }
The above code is straightforward, but three points should be noted:
We applied the easeIn curve to the animation. It can be observed that applying a curve to the animation in
RenderObject
essentially adds a mapping layer to the animation progress. By using different mapping rules, the speed of the animation at different stages can be controlled.We override the
duration
inRenderObjectAnimationMixin
, which specifies the animation duration.The purpose of
adjustProgress
is mainly to divide the "checkmark" animation into two parts. The first part involves animating the line between the first and second points, which occupies the first 60% of the total animation duration. The second part is the animation connecting the second and third points, which occupies the last 40% of the total duration.