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
RenderObjectessentially 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
durationinRenderObjectAnimationMixin, which specifies the animation duration.The purpose of
adjustProgressis 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.