Flutter (75): Self-drawn components: DoneWidget

Time: Column:Mobile & Frontend views:239

In this section, we will implement a DoneWidget that performs a checkmark animation upon creation, as shown in Figure:

Flutter (75): Self-drawn components: DoneWidget

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:

  1. 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.

  2. We override the duration in RenderObjectAnimationMixin, which specifies the animation duration.

  3. 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.