Search code examples
flutterdartflutter-custompainter

How can I create a circle bubble with border in Flutter CustomPainter?


The following is a reference. How can I create a border-only bubble with CustomPainter?

But what I want to achieve is a balloon for the circle. The image will be as follows.

enter image description here

If implemented as follows, they will be separated and drawn as shown in the example.

final path = Path();

// create circle
final center = Offset(size.width / 2, size.height / 2 - 10);
final radius = size.width / 2;
path.addOval(Rect.fromCircle(center: center, radius: radius));

// create tip
path.moveTo(center.dx - 10, center.dy + radius);
path.lineTo(center.dx, center.dy + radius + 12);
path.lineTo(center.dx + 10, center.dy + radius);
path.close();

// draw path
canvas.drawPath(path, paint);
canvas.drawPath(path, borderPaint);

enter image description here

This may be a rudimentary question, but please answer.


Solution

  • I was able to implement it in my own way and share it with you.Better.I'm sure there is a better way.If you have a better way, please let me know.

    class BorderBubblePainter extends CustomPainter {
      BorderBubblePainter({
        this.color = Colors.red,
      });
    
      final Color color;
    
      @override
      void paint(Canvas canvas, Size size) {
        final width = size.width;
        // Equivalent to width since it is circular.
        // Define a variable with a different name for easier understanding.
        final height = width;
        const strokeWidth = 1.0;
    
        final paint = Paint()
          ..isAntiAlias = true
          ..color = color
          ..strokeWidth = strokeWidth
          ..style = PaintingStyle.stroke;
    
        final triangleH = height / 10;
        final triangleW = width / 8;
    
        // NOTE: Set up a good beginning and end.
        const startAngle = 7;
        // NOTE: The height is shifted slightly upward to cover the circle.
        final heightPadding = triangleH / 10;
    
        final center = Offset(width / 2, height / 2);
        final radius = (size.width - strokeWidth) / 2;
    
        final trianglePath = Path()
          ..moveTo(width / 2 - triangleW / 2, height - heightPadding)
          ..lineTo(width / 2, triangleH + height)
          ..lineTo(width / 2 + triangleW / 2, height - heightPadding)
          ..addArc(
            Rect.fromCircle(center: center, radius: radius),
            // θ*π/180=rad
            (90 + startAngle) * pi / 180,
            (360 - (2 * startAngle)) * pi / 180,
          );
    
        canvas.drawPath(trianglePath, paint);
      }
    
      @override
      bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
    }
    
    

    usage

    class BubbleWidget extends StatelessWidget {
      const BubbleWidget({
        super.key,
      });
    
      static const double _width = 100.0;
      static const double _height = 108.0;
    
      @override
      Widget build(BuildContext context) {
        return Stack(
          clipBehavior: Clip.none,
          alignment: Alignment.center,
          children: [
            SizedBox(
              width: _width,
              height: _height,
              child: CustomPaint(
                painter: BorderBubblePainter(),
              ),
            ),
            Transform.translate(
              offset: const Offset(
                0,
                -(_height - _width) / 2,
              ),
              child: Icon(
                Icons.check,
                color: Theme.of(context).colorScheme.primary,
                size: 16,
              ),
            ),
          ],
        );
      }
    }