flutterflutter-custompaintercustom-painter

Flutter - Center a CustomPaint Shape


I am building a CustomPaint heart shape that has animation on the size. An optimal state is the cirle GIF below. However, what i have been able to achieve so far is the heart GIF below.

Here is the code for my CustomPaint:

class MyPainter extends CustomPainter {
  // The color of the heart
  final Color color;

  ///[double] minimum radius of the painter
  final double minRadius;

  ///[int] number of wave count in the animation
  final int wavesCount;

  ///[Color] of the painter
  final Animation<double>? _animation;

  MyPainter(
    this.color,
    this.minRadius,
    this.wavesCount,
    this._animation,
  ) : super(repaint: _animation);

  @override
  void paint(Canvas canvas, Size size) {
    for (int wave = 0; wave <= wavesCount; wave++) {
      heart(
        canvas,
        size,
        minRadius,
        wave,
        _animation!.value,
        wavesCount,
        color,
      );
    }
  }

  void heart(
    Canvas canvas,
    Size size,
    double minRadius,
    int wave,
    double anim,
    int? length,
    Color heartColor,
  ) {
    Color color = heartColor;

    if (wave != 0) {
      final double opacity =
          (1 - ((wave - 1) / length!) - anim).clamp(0.0, 1.0);
      color = color.withOpacity(opacity);

      final Paint body = Paint();
      body
        ..color = color
        ..style = PaintingStyle.fill
        ..strokeWidth = 0;

      final double width = size.width * (1 + (wave * anim)) * anim;
      final double height = size.height * (1 + (wave * anim)) * anim;

      final Path path = Path();
      path.moveTo(0.5 * width, height * 0.4);
      path.cubicTo(0.2 * width, height * 0.1, -0.25 * width, height * 0.6,
          0.5 * width, height);
      path.moveTo(0.5 * width, height * 0.4);
      path.cubicTo(0.8 * width, height * 0.1, 1.25 * width, height * 0.6,
          0.5 * width, height);

      canvas.drawPath(path, body);
    }
  }

  @override
  bool shouldRepaint(MyPainter oldDelegate) {
    return true;
  }
}

The CustomPaint class is used like so:

class MyRippleState extends State<MyRipple> with TickerProviderStateMixin {
  AnimationController? _controller;

  @override
  void initState() {
    _controller = AnimationController(
      duration: widget.duration,
      vsync: this,
    );

    // repeating or just forwarding the animation once.
    Timer(widget.delay, () {
      widget.repeat ? _controller?.repeat() : _controller?.forward();
    });

    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      size: const Size(150, 150),
      painter: MyPainter(
        vybPrimary,
        widget.minRadius,
        widget.ripplesCount + 2,
        _controller,
      ),
    );
  }

  @override
  void dispose() {
    _controller!.dispose();
    super.dispose();
  }
}

Any help/pointers to assist me in centering the heart shape in the middle of the page just like in the circle?

Current State

Optimal State


Solution

  • Replace your path with this:

     final Path path = Path();
      final startX =(size.width/2) - (width/2);
      final startY =(size.height/2) - height * 0.6;
      path.moveTo(startX + (0.5 * width), startY + (height * 0.4));
      path.cubicTo(startX + (0.2 * width), startY + (height * 0.1), startX +(-0.25 * width), startY + (height * 0.6),
          startX + (0.5 * width), startY + height);
      path.moveTo(startX + (0.5 * width), startY + (height * 0.4));
      path.cubicTo(startX + (0.8 * width), startY + (height * 0.1), startX + (1.25 * width), startY + (height * 0.6),
          startX + (0.5 * width), startY + height);
    
      canvas.drawPath(path, body);