这个消息气泡可以用于社交的聊天气泡,或者其他长按弹出的效果,
应用场景挺多,主要是用于学习。
先上效果图

1.BubbleWidget封装
-
通过系统的
Canvas
绘制
/// 气泡组件封装 /// /// created by hujintao /// created at 2019-10-21 // import 'dart:math'; import 'package:flutter/material.dart'; enum BubbleArrowDirection { top, bottom, right, left, topLeft } class BubbleWidget extends StatelessWidget { // 尖角位置 final position; // 尖角高度 var arrHeight; // 尖角角度 var arrAngle; // 圆角半径 var radius; // 宽度 final width; // 高度 final height; // 边距 double length; // 颜色 Color color; // 边框颜色 Color borderColor; // 边框宽度 final strokeWidth; // 填充样式 final style; // 子 Widget final child; // 子 Widget 与起泡间距 var innerPadding; BubbleWidget( this.width, this.height, this.color, this.position, { Key key, this.length = 1, this.arrHeight = 12.0, this.arrAngle = 60.0, this.radius = 10.0, this.strokeWidth = 4.0, this.style = PaintingStyle.fill, this.borderColor, this.child, this.innerPadding = 6.0, }) : super(key: key); @override Widget build(BuildContext context) { if (style == PaintingStyle.stroke && borderColor == null) { borderColor = color; } if (arrAngle < 0.0 || arrAngle >= 180.0) { arrAngle = 60.0; } if (arrHeight < 0.0) { arrHeight = 0.0; } if (radius < 0.0 || radius > width * 0.5 || radius > height * 0.5) { radius = 0.0; } if (position == BubbleArrowDirection.top || position == BubbleArrowDirection.bottom) { if (length < 0.0 || length >= width - 2 * radius) { length = width * 0.5 - arrHeight * tan(_angle(arrAngle * 0.5)) - radius; } } else { if (length < 0.0 || length >= height - 2 * radius) { length = height * 0.5 - arrHeight * tan(_angle(arrAngle * 0.5)) - radius; } } if (innerPadding < 0.0 || innerPadding >= width * 0.5 || innerPadding >= height * 0.5) { innerPadding = 2.0; } Widget bubbleWidget; if (style == PaintingStyle.fill) { bubbleWidget = Container( width: width, height: height, child: Stack(children: <Widget>[ CustomPaint( painter: BubbleCanvas(context, width, height, color, position, arrHeight, arrAngle, radius, strokeWidth, style, length)), _paddingWidget() ])); } else { bubbleWidget = Container( width: width, height: height, child: Stack(children: <Widget>[ CustomPaint( painter: BubbleCanvas( context, width, height, color, position, arrHeight, arrAngle, radius, strokeWidth, PaintingStyle.fill, length)), CustomPaint( painter: BubbleCanvas( context, width, height, borderColor, position, arrHeight, arrAngle, radius, strokeWidth, style, length)), _paddingWidget() ])); } return bubbleWidget; } Widget _paddingWidget() { return Padding( padding: EdgeInsets.only( top: (position == BubbleArrowDirection.top) ? arrHeight + innerPadding : innerPadding, right: (position == BubbleArrowDirection.right) ? arrHeight + innerPadding : innerPadding, bottom: (position == BubbleArrowDirection.bottom) ? arrHeight + innerPadding : innerPadding, left: (position == BubbleArrowDirection.left) ? arrHeight + innerPadding : innerPadding), child: Center(child: this.child)); } } class BubbleCanvas extends CustomPainter { BuildContext context; final position; final arrHeight; final arrAngle; final radius; final width; final height; final length; final color; final strokeWidth; final style; BubbleCanvas( this.context, this.width, this.height, this.color, this.position, this.arrHeight, this.arrAngle, this.radius, this.strokeWidth, this.style, this.length); @override void paint(Canvas canvas, Size size) { Path path = Path(); path.arcTo( Rect.fromCircle( center: Offset( (position == BubbleArrowDirection.left) ? radius + arrHeight : radius, (position == BubbleArrowDirection.top) ? radius + arrHeight : radius), radius: radius), pi, pi * 0.5, false); if (position == BubbleArrowDirection.top) { path.lineTo(length + radius, arrHeight); path.lineTo( length + radius + arrHeight * tan(_angle(arrAngle * 0.5)), 0.0); path.lineTo(length + radius + arrHeight * tan(_angle(arrAngle * 0.5)) * 2, arrHeight); } path.lineTo( (position == BubbleArrowDirection.right) ? width - radius - arrHeight : width - radius, (position == BubbleArrowDirection.top) ? arrHeight : 0.0); path.arcTo( Rect.fromCircle( center: Offset( (position == BubbleArrowDirection.right) ? width - radius - arrHeight : width - radius, (position == BubbleArrowDirection.top) ? radius + arrHeight : radius), radius: radius), -pi * 0.5, pi * 0.5, false); if (position == BubbleArrowDirection.right) { path.lineTo(width - arrHeight, length + radius); path.lineTo( width, length + radius + arrHeight * tan(_angle(arrAngle * 0.5))); path.lineTo(width - arrHeight, length + radius + arrHeight * tan(_angle(arrAngle * 0.5)) * 2); } path.lineTo( (position == BubbleArrowDirection.right) ? width - arrHeight : width, (position == BubbleArrowDirection.bottom) ? height - radius - arrHeight : height - radius); path.arcTo( Rect.fromCircle( center: Offset( (position == BubbleArrowDirection.right) ? width - radius - arrHeight : width - radius, (position == BubbleArrowDirection.bottom) ? height - radius - arrHeight : height - radius), radius: radius), pi * 0, pi * 0.5, false); if (position == BubbleArrowDirection.bottom) { path.lineTo(width - radius - length, height - arrHeight); path.lineTo( width - radius - length - arrHeight * tan(_angle(arrAngle * 0.5)), height); path.lineTo( width - radius - length - arrHeight * tan(_angle(arrAngle * 0.5)) * 2, height - arrHeight); } path.lineTo( (position == BubbleArrowDirection.left) ? radius + arrHeight : radius, (position == BubbleArrowDirection.bottom) ? height - arrHeight : height); path.arcTo( Rect.fromCircle( center: Offset( (position == BubbleArrowDirection.left) ? radius + arrHeight : radius, (position == BubbleArrowDirection.bottom) ? height - radius - arrHeight : height - radius), radius: radius), pi * 0.5, pi * 0.5, false); if (position == BubbleArrowDirection.left) { path.lineTo(arrHeight, height - radius - length); path.lineTo(0.0, height - radius - length - arrHeight * tan(_angle(arrAngle * 0.5))); path.lineTo( arrHeight, height - radius - length - arrHeight * tan(_angle(arrAngle * 0.5)) * 2); } path.lineTo((position == BubbleArrowDirection.left) ? arrHeight : 0.0, (position == BubbleArrowDirection.top) ? radius + arrHeight : radius); path.close(); canvas.drawPath( path, Paint() ..color = color ..style = style ..strokeCap = StrokeCap.round ..strokeWidth = strokeWidth); } @override bool shouldRepaint(CustomPainter oldDelegate) { return true; } } double _angle(angle) { return angle * pi / 180; }
2.气泡组件使用
注意事项
-
必填参数
- 宽度 ScreenUtil().setWidth(326),
- 高度 ScreenUtil().setWidth(64),
- 背景颜色 Color(0xff333333),
-
位置
BubbleArrowDirection.bottom
-
可选参数
-
箭头宽度
length: ScreenUtil().setWidth(20) -
箭头高度
arrHeight : ScreenUtil().setWidth(12) -
箭头读书
arrAngle: 75.0, -
子Widget与起泡间距
innerPadding
-
import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:fpdxapp/components/bubble/bubble_widget.dart'; class BubblePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: ListView(children: <Widget>[ SizedBox( height: 20, ), ///1- 复制删除,撤回消息-气泡BottomRight Padding( padding: EdgeInsets.all(4.0), child: BubbleWidget( ScreenUtil().setWidth(326), ScreenUtil().setWidth(64), Color(0xff333333), BubbleArrowDirection.bottom, length: ScreenUtil().setWidth(20), child: Row( mainAxisSize: MainAxisSize.max, children: <Widget>[ // 复制按钮 GestureDetector( onTap: () {}, child: Container( child: Center( child: Text( '复制', style: TextStyle( color: Color(0xffE4E4E4), fontSize: ScreenUtil().setSp(20)), ), ), width: ScreenUtil().setWidth(108), height: ScreenUtil().setWidth(64), ), ), // line Container( width: ScreenUtil().setWidth(1), color: Color(0xff707070)), // 删除按钮 GestureDetector( onTap: () {}, child: Container( child: Center( child: Text( '删除', style: TextStyle( color: Color(0xffE4E4E4), fontSize: ScreenUtil().setSp(20)), ), ), width: ScreenUtil().setWidth(108), height: ScreenUtil().setWidth(64), ), ), // line Container( width: ScreenUtil().setWidth(1), color: Color(0xff707070)), // 撤回按钮 GestureDetector( onTap: () {}, child: Container( child: Center( child: Text( '撤回', style: TextStyle( color: Color(0xffE4E4E4), fontSize: ScreenUtil().setSp(20)), ), ), width: ScreenUtil().setWidth(108), height: ScreenUtil().setWidth(64), ), ), ], ), arrHeight: ScreenUtil().setWidth(12), arrAngle: 75.0, innerPadding: 0.0, )), SizedBox( height: 5, ), ///2- 复制删除,撤回消息-气泡BottomLeft Padding( padding: EdgeInsets.all(4.0), child: BubbleWidget( ScreenUtil().setWidth(326), ScreenUtil().setWidth(64), Color(0xff333333), BubbleArrowDirection.bottom, length: ScreenUtil().setWidth(250), child: Row( mainAxisSize: MainAxisSize.max, children: <Widget>[ // 复制按钮 GestureDetector( onTap: () {}, child: Container( child: Center( child: Text( '复制', style: TextStyle( color: Color(0xffE4E4E4), fontSize: ScreenUtil().setSp(20)), ), ), width: ScreenUtil().setWidth(108), height: ScreenUtil().setWidth(64), ), ), // line Container( width: ScreenUtil().setWidth(1), color: Color(0xff707070)), // 删除按钮 GestureDetector( onTap: () {}, child: Container( child: Center( child: Text( '删除', style: TextStyle( color: Color(0xffE4E4E4), fontSize: ScreenUtil().setSp(20)), ), ), width: ScreenUtil().setWidth(108), height: ScreenUtil().setWidth(64), ), ), // line Container( width: ScreenUtil().setWidth(1), color: Color(0xff707070)), // 撤回按钮 GestureDetector( onTap: () {}, child: Container( child: Center( child: Text( '撤回', style: TextStyle( color: Color(0xffE4E4E4), fontSize: ScreenUtil().setSp(20)), ), ), width: ScreenUtil().setWidth(108), height: ScreenUtil().setWidth(64), ), ), ], ), arrHeight: ScreenUtil().setWidth(12), arrAngle: 75.0, innerPadding: 0.0, ), ), SizedBox( height: 5, ), ///3- 复制删除,撤回消息-气泡TopLeft Padding( padding: EdgeInsets.all(4.0), child: BubbleWidget( ScreenUtil().setWidth(326), ScreenUtil().setWidth(64), Color(0xff333333), BubbleArrowDirection.top, length: ScreenUtil().setWidth(20), child: Row( mainAxisSize: MainAxisSize.max, children: <Widget>[ // 复制按钮 GestureDetector( onTap: () {}, child: Container( child: Center( child: Text( '复制', style: TextStyle( color: Color(0xffE4E4E4), fontSize: ScreenUtil().setSp(20)), ), ), width: ScreenUtil().setWidth(108), height: ScreenUtil().setWidth(64), ), ), // line Container( width: ScreenUtil().setWidth(1), color: Color(0xff707070)), // 删除按钮 GestureDetector( onTap: () {}, child: Container( child: Center( child: Text( '删除', style: TextStyle( color: Color(0xffE4E4E4), fontSize: ScreenUtil().setSp(20)), ), ), width: ScreenUtil().setWidth(108), height: ScreenUtil().setWidth(64), ), ), // line Container( width: ScreenUtil().setWidth(1), color: Color(0xff707070)), // 撤回按钮 GestureDetector( onTap: () {}, child: Container( child: Center( child: Text( '撤回', style: TextStyle( color: Color(0xffE4E4E4), fontSize: ScreenUtil().setSp(20)), ), ), width: ScreenUtil().setWidth(108), height: ScreenUtil().setWidth(64), ), ), ], ), arrHeight: ScreenUtil().setWidth(12), arrAngle: 75.0, innerPadding: 0.0, )), SizedBox( height: 5, ), ///4- 复制删除,撤回消息-气泡TopRight Padding( padding: EdgeInsets.all(4.0), child: BubbleWidget( ScreenUtil().setWidth(326), ScreenUtil().setWidth(64), Color(0xff333333), BubbleArrowDirection.top, length: ScreenUtil().setWidth(250), child: Row( mainAxisSize: MainAxisSize.max, children: <Widget>[ // 复制按钮 GestureDetector( onTap: () {}, child: Container( child: Center( child: Text( '复制', style: TextStyle( color: Color(0xffE4E4E4), fontSize: ScreenUtil().setSp(20)), ), ), width: ScreenUtil().setWidth(108), height: ScreenUtil().setWidth(64), ), ), // line Container( width: ScreenUtil().setWidth(1), color: Color(0xff707070)), // 删除按钮 GestureDetector( onTap: () {}, child: Container( child: Center( child: Text( '删除', style: TextStyle( color: Color(0xffE4E4E4), fontSize: ScreenUtil().setSp(20)), ), ), width: ScreenUtil().setWidth(108), height: ScreenUtil().setWidth(64), ), ), // line Container( width: ScreenUtil().setWidth(1), color: Color(0xff707070)), // 撤回按钮 GestureDetector( onTap: () {}, child: Container( child: Center( child: Text( '撤回', style: TextStyle( color: Color(0xffE4E4E4), fontSize: ScreenUtil().setSp(20)), ), ), width: ScreenUtil().setWidth(108), height: ScreenUtil().setWidth(64), ), ), ], ), arrHeight: ScreenUtil().setWidth(12), arrAngle: 75.0, innerPadding: 0.0, ), ), SizedBox( height: 5, ), // 气泡右 Padding( padding: EdgeInsets.all(4.0), child: Container( alignment: Alignment.centerRight, child: BubbleWidget(200.0, 40.0, Colors.blue.withOpacity(0.7), BubbleArrowDirection.right, child: Text('你好,我是BubbleWidget!', style: TextStyle(color: Colors.white, fontSize: 14.0))))), Padding( padding: EdgeInsets.all(4.0), child: Container( alignment: Alignment.bottomLeft, child: BubbleWidget(300.0, 40.0, Colors.red.withOpacity(0.7), BubbleArrowDirection.top, length: 20, child: Text('你好,你有什么特性化?', style: TextStyle(color: Colors.white, fontSize: 14.0))))), Padding( padding: EdgeInsets.all(4.0), child: Container( alignment: Alignment.centerRight, child: BubbleWidget(300.0, 90.0, Colors.blue.withOpacity(0.7), BubbleArrowDirection.right, child: Text('我可以自定义:\n尖角方向,尖角高度,尖角角度,\n距圆角位置,圆角大小,边框样式等!', style: TextStyle(color: Colors.white, fontSize: 16.0))))), Padding( padding: EdgeInsets.all(4.0), child: Container( alignment: Alignment.centerLeft, child: BubbleWidget(140.0, 40.0, Colors.cyan.withOpacity(0.7), BubbleArrowDirection.left, child: Text('你有什么不足?', style: TextStyle(color: Colors.white, fontSize: 14.0))))), Padding( padding: EdgeInsets.all(4.0), child: Container( alignment: Alignment.centerRight, child: BubbleWidget(350.0, 60.0, Colors.green.withOpacity(0.7), BubbleArrowDirection.right, child: Text('我现在还不会动态计算高度,只可用作背景!', style: TextStyle(color: Colors.white, fontSize: 16.0))))), Padding( padding: EdgeInsets.all(4.0), child: Container( alignment: Alignment.centerLeft, child: BubbleWidget( 105.0, 60.0, Colors.deepOrange.withOpacity(0.7), BubbleArrowDirection.left, child: Text('继续加油!', style: TextStyle(color: Colors.white, fontSize: 16.0))))), ]), appBar: AppBar( centerTitle: true, leading: GestureDetector( child: Icon(Icons.arrow_back_ios, size: 20, color: Color(0xff333333)), onTap: () { Navigator.of(context).maybePop(); }, ), title: Text( '气泡合集', style: TextStyle(color: Colors.black), ), ), ); } }
作者:StevenHu
发表评论