I want to a CustomPainter
that paints a triangle where the top edge is a bit rouned, like this:
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.
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);
}
}
}