import 'package:flutter/cupertino.dart'; // normally, bubble view width is longer than its height, // so make the triangle smaller const Size _kBubbleTriangleSizeH = Size(9.0, 16.0); const Size _kBubbleTriangleSizeV = Size(18.0, 9.0); const BorderRadius _kBubbleBorderRadius = BorderRadius.all(Radius.circular(7.5)); /// triangle position enum FLBubbleFrom { bottom, top, left, right } class FLBubble extends StatelessWidget { FLBubble( {Key? key, this.backgroundColor = CupertinoColors.white, this.from = FLBubbleFrom.bottom, this.padding = const EdgeInsets.all(8), @required this.child}) : super(key: key); final Color backgroundColor; final FLBubbleFrom from; final Widget? child; final EdgeInsetsGeometry padding; @override Widget build(BuildContext context) { FLBubbleFrom _from = from; final TextDirection textDirection = Directionality.of(context); final bool isRtl = textDirection == TextDirection.rtl; final bool isHorizontal = (_from == FLBubbleFrom.left || _from == FLBubbleFrom.right); if (isRtl && isHorizontal) { _from = _from == FLBubbleFrom.left ? FLBubbleFrom.right : FLBubbleFrom.left; } // triangle final Size triangleSize = isHorizontal ? _kBubbleTriangleSizeH : _kBubbleTriangleSizeV; final Widget triangle = SizedBox.fromSize( size: triangleSize, child: CustomPaint( painter: _FLBubbleNotchPainter(pos: _from, backgroundColor: backgroundColor), ), ); // main rect final Widget rect = ClipRRect( borderRadius: _kBubbleBorderRadius, child: DecoratedBox( decoration: BoxDecoration( color: backgroundColor, borderRadius: _kBubbleBorderRadius, border: Border.all(color: backgroundColor, width: 0), ), child: Container( padding: padding, child: child, ), ), ); // layout use original from, Row will auto change direction. if (from == FLBubbleFrom.bottom) { return Column( mainAxisSize: MainAxisSize.min, children: [ rect, triangle, const Padding(padding: EdgeInsets.only(bottom: 8.0)) ], ); } else if (from == FLBubbleFrom.top) { return Column( mainAxisSize: MainAxisSize.min, children: [ const Padding(padding: EdgeInsets.only(top: 8.0)), triangle, rect ], ); } else if (from == FLBubbleFrom.left) { return Row( mainAxisSize: MainAxisSize.min, children: [ const Padding(padding: EdgeInsets.only(left: 8.0)), triangle, rect ], ); } else { // FLBubbleFrom.right return Row( mainAxisSize: MainAxisSize.min, children: [ rect, triangle, const Padding(padding: EdgeInsets.only(right: 8.0)) ], ); } } } class _FLBubbleNotchPainter extends CustomPainter { _FLBubbleNotchPainter({this.pos, this.backgroundColor}); final FLBubbleFrom? pos; final Color? backgroundColor; @override void paint(Canvas canvas, Size size) { // paint final Paint paint = Paint() ..color = backgroundColor! ..style = PaintingStyle.fill; // triangle Path triangle; if (pos == FLBubbleFrom.bottom) { triangle = Path() ..lineTo(size.width / 2, 0.0) ..lineTo(0.0, size.height) ..lineTo(-(size.width / 2), 0.0) ..close(); } else if (pos == FLBubbleFrom.left) { triangle = Path() ..lineTo(size.width, size.height / 2) ..lineTo(size.width, -(size.height / 2)) ..close(); } else if (pos == FLBubbleFrom.top) { triangle = Path() ..lineTo(size.width / 2, size.height) ..lineTo(-(size.width / 2), size.height) ..close(); } else { // FLBubbleFrom.right triangle = Path() ..lineTo(0.0, size.height / 2) ..lineTo(size.width, 0.0) ..lineTo(0.0, -(size.height / 2)) ..close(); } // draw canvas.drawPath(triangle, paint); } @override bool shouldRepaint(_FLBubbleNotchPainter oldPainter) => false; }