Flutter paint arrow where the tip is rounded

216 views Asked by At

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.

3

There are 3 answers

0
Chris On BEST ANSWER

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);
    }
  }
}
7
k.s poyraz On
  1. First of all, you don't have to draw your custom paint yourself. Indeed, it is nearly impossible to achieve complex drawings. Instead, here you can use this web application in which you can draw your paintings and export their code; Flutter Shape Maker. The more and more beautiful thing is you can import any SVG files inside that application and export its code as a Dart File. That's really perfect!

  2. Here, I share images that you can follow


    then


Note: BE SURE that you selected RESPONSIVE. This way, you can resize any SVG file in CustomPainter widget

  1. Let's come to your the problem. Here I select an SVG file for your needs. (Later, you can change it according to your desire)
<svg width="800px" height="800px" viewBox="0 0 24 24" id="meteor-icon-kit__solid-triangle" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M13.7889 1.57769L23.5528 21.1056C24.0468 22.0935 23.6463 23.2949 22.6584 23.7888C22.3806 23.9277 22.0744 24 21.7639 24H2.23607C1.1315 24 0.236069 23.1046 0.236069 22C0.236069 21.6895 0.308359 21.3833 0.447214 21.1056L10.2111 1.57769C10.7051 0.589734 11.9065 0.189285 12.8944 0.683263C13.2815 0.876791 13.5953 1.19064 13.7889 1.57769Z" fill="#758CA3"/></svg>
  1. I made import and export actions to the above web application as ı stated. Here is the result;

    class ArrowPaint extends CustomPainter {
      final Color color;
    
      ArrowPaint({required this.color});
    
      @override
      void paint(Canvas canvas, Size size) {
        Path path_0 = Path();
        path_0.moveTo(size.width * 0.5745375, size.height * 0.06573708);
        path_0.lineTo(size.width * 0.9813667, size.height * 0.8794000);
        path_0.cubicTo(
            size.width * 1.001950,
            size.height * 0.9205625,
            size.width * 0.9852625,
            size.height * 0.9706208,
            size.width * 0.9441000,
            size.height * 0.9912000);
        path_0.cubicTo(
            size.width * 0.9325250,
            size.height * 0.9969875,
            size.width * 0.9197667,
            size.height,
            size.width * 0.9068292,
            size.height);
        path_0.lineTo(size.width * 0.09316958, size.height);
        path_0.cubicTo(
            size.width * 0.04714583,
            size.height,
            size.width * 0.009836208,
            size.height * 0.9626917,
            size.width * 0.009836208,
            size.height * 0.9166667);
        path_0.cubicTo(
            size.width * 0.009836208,
            size.height * 0.9037292,
            size.width * 0.01284829,
            size.height * 0.8909708,
            size.width * 0.01863392,
            size.height * 0.8794000);
        path_0.lineTo(size.width * 0.4254625, size.height * 0.06573708);
        path_0.cubicTo(
            size.width * 0.4460458,
            size.height * 0.02457225,
            size.width * 0.4961042,
            size.height * 0.007886875,
            size.width * 0.5372667,
            size.height * 0.02846929);
        path_0.cubicTo(
            size.width * 0.5533958,
            size.height * 0.03653296,
            size.width * 0.5664708,
            size.height * 0.04961000,
            size.width * 0.5745375,
            size.height * 0.06573708);
        path_0.close();
    
        Paint paint_0_fill = Paint()..style = PaintingStyle.fill;
        paint_0_fill.color = color;
        canvas.drawPath(path_0, paint_0_fill);
      }
    
      @override
      bool shouldRepaint(covariant CustomPainter oldDelegate) {
        return true;
      }
    }

  1. I created a Widget named as TextCloud;

    class TextCloud extends StatelessWidget {
      final String text; // write here box content
      final Color color; // Set box Color
      final EdgeInsets padding; // Set content padding
      final double width; // Box width
      final double height; // Box Height
      final AxisDirection axisDirection; // Set triangle location up,left,right,down
      final double locationOfArrow; // set between 0 and 1, If 0.5 is set triangle position will be centered
      const TextCloud({
        super.key,
        required this.text,
        this.color = Colors.white,
        this.padding = const EdgeInsets.all(10),
        this.width = 200,
        this.height = 100,
        this.axisDirection = AxisDirection.down,
        this.locationOfArrow = 0.5,
      });
    
      @override
      Widget build(BuildContext context) {
        Size arrowSize = const Size(25, 25);
        return Stack(
          clipBehavior: Clip.none,
          children: [
            Container(
              width: width,
              height: height,
              padding: padding,
              decoration: BoxDecoration(
                color: color,
                borderRadius: BorderRadius.circular(5),
              ),
              child: Text(text),
            ),
            Builder(builder: (context) {
              double angle = 0;
              switch (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;
                default:
                  angle = 0;
              }
              return Positioned(
                left: axisDirection == AxisDirection.left
                    ? -arrowSize.width + 5
                    : (axisDirection == AxisDirection.up ||
                            axisDirection == AxisDirection.down
                        ? width * locationOfArrow - arrowSize.width / 2
                        : null),
                right: axisDirection == AxisDirection.right
                    ? -arrowSize.width + 5
                    : null,
                top: axisDirection == AxisDirection.up
                    ? -arrowSize.width + 5
                    : (axisDirection == AxisDirection.right ||
                            axisDirection == AxisDirection.left
                        ? height * locationOfArrow - arrowSize.width / 2
                        : null),
                bottom: axisDirection == AxisDirection.down
                    ? -arrowSize.width + 5
                    : null,
                child: Transform.rotate(
                  angle: angle,
                  child: CustomPaint(
                    size: arrowSize,
                    painter: ArrowPaint(color: color),
                  ),
                ),
              );
            })
          ],
        );
      }
    }

  1. Then, use it TextCloud(text: "Big Bang Theory is the best TV sitcom ever!")

Result;

TextCloud(
  text: "Big Bang Theory is the best TV sitcom ever!",
  axisDirection: AxisDirection.down,
  locationOfArrow: 0.5,
)

TextCloud(
  text: "Big Bang Theory is the best TV sitcom ever!",
  axisDirection: AxisDirection.left,
  locationOfArrow: 0.25,
)

Good luck

0
Kaleb On

Here is how I have managed to create the above using custom paint

First define a custom painter class like I did below

class buttonBackground extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    double sw = size.width;
    double sh = size.height;
    var paint = Paint();
    Path greyWave = Path();
    greyWave.lineTo((sw * .5) - 20, 0);
    //The value 8 represent the amount of curve you can update to make it more or less rounded
    greyWave.conicTo(sw * .5, -30, (sw * .5) + 20, 0, 8);
    greyWave.close();
    paint.color = Colors.blue.shade200;
    canvas.drawPath(greyWave, paint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return oldDelegate != this;
  }
}

This is how you are going to implement it.

CustomPaint(
      painter: buttonBackground(),
      child: Container(
        height: 250,
        width: 250,
        decoration: BoxDecoration(
            color: Colors.green,
            borderRadius: BorderRadius.all(Radius.circular(20))),
      ),
    ),
  )

The above code should give you a pointer facing upward for any widget wrapped with it.

Here is the output and thank you.

enter image description here