flutterdartflutter-custompainter

Flutter paint arrow where the tip is rounded


I want to a CustomPainter that paints a triangle where the top edge is a bit rouned, like this:

enter image description here

I was able to paint a triangle with this:

class CustomStyleArrow extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final Paint paint = Paint()
      ..color = Colors.white
      ..strokeWidth = 1
      ..style = PaintingStyle.fill;
    final double triangleH = 10;
    final double triangleW = 25.0;
    final double width = size.width;
    final double height = size.height;

    final Path trianglePath = Path()
      ..moveTo(width / 2 - triangleW / 2, height)
      ..lineTo(width / 2, triangleH + height)
      ..lineTo(width / 2 + triangleW / 2, height)
      ..lineTo(width / 2 - triangleW / 2, height);
    canvas.drawPath(trianglePath, paint);
    final BorderRadius borderRadius = BorderRadius.circular(15);
    final Rect rect = Rect.fromLTRB(0, 0, width, height);
    final RRect outer = borderRadius.toRRect(rect);
    canvas.drawRRect(outer, paint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => false;
}

But I can not get the rounded corner. How can I do that?

Also, I the whole triangle should be as dynamic as possible so I can put it on top of any container and best case, also pass the location, where exactly the triangle should be.


Solution

  • With inspiration from k.s poyraz I got a perfect solution, where you can wrap any widget with an ArrowIndicator and place the arrow where ever you want:

    import 'dart:math';
    
    import 'package:bling_ui/extensions/build_context.dart';
    import 'package:flutter/material.dart';
    
    class ArrowIndicator extends StatefulWidget {
      final Widget child;
    
      /// Set triangle location up,left,right,down
      final AxisDirection axisDirection;
    
      /// Position of the arrow between 0 and 1, where 0.5 is centered.
      final double fractionalPosition;
    
      /// Height of the arrow when axisDirection is AxisDirection.up or AxisDirection.down.
      final double height;
    
      final Color? color;
    
      const ArrowIndicator({
        super.key,
        required this.child,
        this.axisDirection = AxisDirection.down,
        this.fractionalPosition = 0.5,
        this.height = 30,
        this.color,
      });
    
      @override
      State<ArrowIndicator> createState() => _ArrowIndicatorState();
    }
    
    class _ArrowIndicatorState extends State<ArrowIndicator> {
      // Without this the arrow would be right on the edge of its child and since all corners of the arrow
      // are rounded, it looks cleaner if the child slightly overlaps with the arrow.
      late double extraSmoothness;
      // This is taken from the triangle_rounded_corners_up.svg height and width.
      final double arrowAspectRatio = 51 / 30;
    
      final key = GlobalKey();
      Size childSize = const Size(0, 0);
    
      late double angle;
    
      @override
      void initState() {
        extraSmoothness = widget.height * 0.2;
    
        WidgetsBinding.instance.addPostFrameCallback((_) {
          setState(() {
            childSize = getChildSize(key.currentContext!);
          });
        });
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        final arrowSize = Size(
          arrowAspectRatio * widget.height,
          widget.height,
        );
        initAngle();
    
        return Stack(
          children: [
            _buildArrow(arrowSize, context),
            Container(
              color: Colors.transparent,
              key: key,
              padding: childPaddingToMakeArrowVisible(),
              child: widget.child,
            ),
          ],
        );
      }
    
      Positioned _buildArrow(Size arrowSize, BuildContext context) {
        return Positioned(
          left: widget.axisDirection == AxisDirection.left
              // You can not simply take 0 here since the rotation messes up the width
              ? -(arrowSize.width - arrowSize.height) / 2 + extraSmoothness
              : (widget.axisDirection == AxisDirection.up ||
                      widget.axisDirection == AxisDirection.down
                  ? childSize.width * widget.fractionalPosition -
                      arrowSize.width / 2
                  : null),
          right: widget.axisDirection == AxisDirection.right
              // You can not simply take 0 here since the rotation messes up the width
              ? -(arrowSize.width - arrowSize.height) / 2 + extraSmoothness
              : null,
          top: widget.axisDirection == AxisDirection.up
              ? extraSmoothness
              : (widget.axisDirection == AxisDirection.right ||
                      widget.axisDirection == AxisDirection.left
                  ? childSize.height * widget.fractionalPosition -
                      arrowSize.width / 2
                  : null),
          bottom:
              widget.axisDirection == AxisDirection.down ? extraSmoothness : null,
          child: Transform.rotate(
            angle: angle,
            child: context.icons.triangleRoundedCornersUpSVG.copyWith(
              color: widget.color,
              height: arrowSize.height,
              width: arrowSize.width,
            ),
          ),
        );
      }
    
      Size getChildSize(BuildContext context) {
        final box = context.findRenderObject() as RenderBox;
        return box.size;
      }
    
      void initAngle() {
        switch (widget.axisDirection) {
          case AxisDirection.left:
            angle = pi * -0.5;
            break;
          case AxisDirection.up:
            angle = pi * -2;
            break;
          case AxisDirection.right:
            angle = pi * 0.5;
            break;
          case AxisDirection.down:
            angle = pi;
            break;
        }
      }
    
      EdgeInsets childPaddingToMakeArrowVisible() {
        switch (widget.axisDirection) {
          case AxisDirection.up:
            return EdgeInsets.only(top: widget.height);
          case AxisDirection.right:
            return EdgeInsets.only(right: widget.height);
          case AxisDirection.down:
            return EdgeInsets.only(bottom: widget.height);
          case AxisDirection.left:
            return EdgeInsets.only(left: widget.height);
        }
      }
    }