image.png

封装气泡组件

  1. /// 气泡组件封装
  2. ///
  3. /// created by hujintao
  4. /// created at 2019-10-21
  5. //
  6. import 'dart:math';
  7. import 'package:flutter/material.dart';
  8. enum BubbleArrowDirection { top, bottom, right, left, topLeft }
  9. class BubbleWidget extends StatelessWidget {
  10. // 尖角位置
  11. final position;
  12. // 尖角高度
  13. var arrHeight;
  14. // 尖角角度
  15. var arrAngle;
  16. // 圆角半径
  17. var radius;
  18. // 宽度
  19. final width;
  20. // 高度
  21. final height;
  22. // 边距
  23. double length;
  24. // 颜色
  25. Color color;
  26. // 边框颜色
  27. Color borderColor;
  28. // 边框宽度
  29. final strokeWidth;
  30. // 填充样式
  31. final style;
  32. // 子 Widget
  33. final child;
  34. // 子 Widget 与起泡间距
  35. var innerPadding;
  36. BubbleWidget(
  37. this.width,
  38. this.height,
  39. this.color,
  40. this.position, {
  41. Key key,
  42. this.length = 1,
  43. this.arrHeight = 12.0,
  44. this.arrAngle = 60.0,
  45. this.radius = 10.0,
  46. this.strokeWidth = 4.0,
  47. this.style = PaintingStyle.fill,
  48. this.borderColor,
  49. this.child,
  50. this.innerPadding = 6.0,
  51. }) : super(key: key);
  52. @override
  53. Widget build(BuildContext context) {
  54. if (style == PaintingStyle.stroke && borderColor == null) {
  55. borderColor = color;
  56. }
  57. if (arrAngle < 0.0 || arrAngle >= 180.0) {
  58. arrAngle = 60.0;
  59. }
  60. if (arrHeight < 0.0) {
  61. arrHeight = 0.0;
  62. }
  63. if (radius < 0.0 || radius > width * 0.5 || radius > height * 0.5) {
  64. radius = 0.0;
  65. }
  66. if (position == BubbleArrowDirection.top ||
  67. position == BubbleArrowDirection.bottom) {
  68. if (length < 0.0 || length >= width - 2 * radius) {
  69. length = width * 0.5 - arrHeight * tan(_angle(arrAngle * 0.5)) - radius;
  70. }
  71. } else {
  72. if (length < 0.0 || length >= height - 2 * radius) {
  73. length =
  74. height * 0.5 - arrHeight * tan(_angle(arrAngle * 0.5)) - radius;
  75. }
  76. }
  77. if (innerPadding < 0.0 ||
  78. innerPadding >= width * 0.5 ||
  79. innerPadding >= height * 0.5) {
  80. innerPadding = 2.0;
  81. }
  82. Widget bubbleWidget;
  83. if (style == PaintingStyle.fill) {
  84. bubbleWidget = Container(
  85. width: width,
  86. height: height,
  87. child: Stack(children: <Widget>[
  88. CustomPaint(
  89. painter: BubbleCanvas(context, width, height, color, position,
  90. arrHeight, arrAngle, radius, strokeWidth, style, length)),
  91. _paddingWidget()
  92. ]));
  93. } else {
  94. bubbleWidget = Container(
  95. width: width,
  96. height: height,
  97. child: Stack(children: <Widget>[
  98. CustomPaint(
  99. painter: BubbleCanvas(
  100. context,
  101. width,
  102. height,
  103. color,
  104. position,
  105. arrHeight,
  106. arrAngle,
  107. radius,
  108. strokeWidth,
  109. PaintingStyle.fill,
  110. length)),
  111. CustomPaint(
  112. painter: BubbleCanvas(
  113. context,
  114. width,
  115. height,
  116. borderColor,
  117. position,
  118. arrHeight,
  119. arrAngle,
  120. radius,
  121. strokeWidth,
  122. style,
  123. length)),
  124. _paddingWidget()
  125. ]));
  126. }
  127. return bubbleWidget;
  128. }
  129. Widget _paddingWidget() {
  130. return Padding(
  131. padding: EdgeInsets.only(
  132. top: (position == BubbleArrowDirection.top)
  133. ? arrHeight + innerPadding
  134. : innerPadding,
  135. right: (position == BubbleArrowDirection.right)
  136. ? arrHeight + innerPadding
  137. : innerPadding,
  138. bottom: (position == BubbleArrowDirection.bottom)
  139. ? arrHeight + innerPadding
  140. : innerPadding,
  141. left: (position == BubbleArrowDirection.left)
  142. ? arrHeight + innerPadding
  143. : innerPadding),
  144. child: Center(child: this.child));
  145. }
  146. }
  147. class BubbleCanvas extends CustomPainter {
  148. BuildContext context;
  149. final position;
  150. final arrHeight;
  151. final arrAngle;
  152. final radius;
  153. final width;
  154. final height;
  155. final length;
  156. final color;
  157. final strokeWidth;
  158. final style;
  159. BubbleCanvas(
  160. this.context,
  161. this.width,
  162. this.height,
  163. this.color,
  164. this.position,
  165. this.arrHeight,
  166. this.arrAngle,
  167. this.radius,
  168. this.strokeWidth,
  169. this.style,
  170. this.length);
  171. @override
  172. void paint(Canvas canvas, Size size) {
  173. Path path = Path();
  174. path.arcTo(
  175. Rect.fromCircle(
  176. center: Offset(
  177. (position == BubbleArrowDirection.left)
  178. ? radius + arrHeight
  179. : radius,
  180. (position == BubbleArrowDirection.top)
  181. ? radius + arrHeight
  182. : radius),
  183. radius: radius),
  184. pi,
  185. pi * 0.5,
  186. false);
  187. if (position == BubbleArrowDirection.top) {
  188. path.lineTo(length + radius, arrHeight);
  189. path.lineTo(
  190. length + radius + arrHeight * tan(_angle(arrAngle * 0.5)), 0.0);
  191. path.lineTo(length + radius + arrHeight * tan(_angle(arrAngle * 0.5)) * 2,
  192. arrHeight);
  193. }
  194. path.lineTo(
  195. (position == BubbleArrowDirection.right)
  196. ? width - radius - arrHeight
  197. : width - radius,
  198. (position == BubbleArrowDirection.top) ? arrHeight : 0.0);
  199. path.arcTo(
  200. Rect.fromCircle(
  201. center: Offset(
  202. (position == BubbleArrowDirection.right)
  203. ? width - radius - arrHeight
  204. : width - radius,
  205. (position == BubbleArrowDirection.top)
  206. ? radius + arrHeight
  207. : radius),
  208. radius: radius),
  209. -pi * 0.5,
  210. pi * 0.5,
  211. false);
  212. if (position == BubbleArrowDirection.right) {
  213. path.lineTo(width - arrHeight, length + radius);
  214. path.lineTo(
  215. width, length + radius + arrHeight * tan(_angle(arrAngle * 0.5)));
  216. path.lineTo(width - arrHeight,
  217. length + radius + arrHeight * tan(_angle(arrAngle * 0.5)) * 2);
  218. }
  219. path.lineTo(
  220. (position == BubbleArrowDirection.right) ? width - arrHeight : width,
  221. (position == BubbleArrowDirection.bottom)
  222. ? height - radius - arrHeight
  223. : height - radius);
  224. path.arcTo(
  225. Rect.fromCircle(
  226. center: Offset(
  227. (position == BubbleArrowDirection.right)
  228. ? width - radius - arrHeight
  229. : width - radius,
  230. (position == BubbleArrowDirection.bottom)
  231. ? height - radius - arrHeight
  232. : height - radius),
  233. radius: radius),
  234. pi * 0,
  235. pi * 0.5,
  236. false);
  237. if (position == BubbleArrowDirection.bottom) {
  238. path.lineTo(width - radius - length, height - arrHeight);
  239. path.lineTo(
  240. width - radius - length - arrHeight * tan(_angle(arrAngle * 0.5)),
  241. height);
  242. path.lineTo(
  243. width - radius - length - arrHeight * tan(_angle(arrAngle * 0.5)) * 2,
  244. height - arrHeight);
  245. }
  246. path.lineTo(
  247. (position == BubbleArrowDirection.left) ? radius + arrHeight : radius,
  248. (position == BubbleArrowDirection.bottom)
  249. ? height - arrHeight
  250. : height);
  251. path.arcTo(
  252. Rect.fromCircle(
  253. center: Offset(
  254. (position == BubbleArrowDirection.left)
  255. ? radius + arrHeight
  256. : radius,
  257. (position == BubbleArrowDirection.bottom)
  258. ? height - radius - arrHeight
  259. : height - radius),
  260. radius: radius),
  261. pi * 0.5,
  262. pi * 0.5,
  263. false);
  264. if (position == BubbleArrowDirection.left) {
  265. path.lineTo(arrHeight, height - radius - length);
  266. path.lineTo(0.0,
  267. height - radius - length - arrHeight * tan(_angle(arrAngle * 0.5)));
  268. path.lineTo(
  269. arrHeight,
  270. height -
  271. radius -
  272. length -
  273. arrHeight * tan(_angle(arrAngle * 0.5)) * 2);
  274. }
  275. path.lineTo((position == BubbleArrowDirection.left) ? arrHeight : 0.0,
  276. (position == BubbleArrowDirection.top) ? radius + arrHeight : radius);
  277. path.close();
  278. canvas.drawPath(
  279. path,
  280. Paint()
  281. ..color = color
  282. ..style = style
  283. ..strokeCap = StrokeCap.round
  284. ..strokeWidth = strokeWidth);
  285. }
  286. @override
  287. bool shouldRepaint(CustomPainter oldDelegate) {
  288. return true;
  289. }
  290. }
  291. double _angle(angle) {
  292. return angle * pi / 180;
  293. }

测试代码

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),
        ),
      ),
    );
  }
}

其他例子
image.png

import 'package:flutter/material.dart';
class LogisticsInformationItemextends StatefulWidget {
   ColortopColor;
  ColorcenterColor;
  ColorbottomColor;
  ColortextColor;
  Stringtext;
  LogisticsInformationItem({
this.topColor,
    this.centerColor,
    this.bottomColor,
    this.textColor,
    this.text,
  });
  @override
  _LogisticsInformationItemStatecreateState() =>_LogisticsInformationItemState();
}
class _LogisticsInformationItemStateextends State {
  double item_height =0.0;
  GlobalKey textKey =new GlobalKey();
  @override
  void initState() {
// TODO: implement initState
    super.initState();
    ///  监听是否渲染完
    WidgetsBinding widgetsBinding = WidgetsBinding.instance;
    widgetsBinding.addPostFrameCallback((callback){
///  获取相应控件的size
      RenderObject renderObject =textKey.currentContext.findRenderObject();
      setState(() {
item_height = renderObject.semanticBounds.size.height;
      });
    });
  }
@override
  Widget build(BuildContext context) {
return Container(
color: Colors.white,
      padding:EdgeInsets.only(left:20, right:10),
      child:Row(
children: [
///  左侧的线
          Container(
margin:EdgeInsets.only(left:20),
            width:10,
            height:item_height,
            child:Column(
mainAxisAlignment: MainAxisAlignment.center,
              children: [
Expanded(
child:Container(
width:0.9,
                      color:widget.topColor,
                    )
),
                Container(
height:10,
                  width:10,
                  decoration:BoxDecoration(
color:widget.centerColor,
                    borderRadius:BorderRadius.all(Radius.circular(5)),
                  ),
                ),
                Expanded(
child:Container(
width:0.9,
                      color:widget.bottomColor,
                    )
),
              ],
            ),
          ),
          ///  右侧的文案
          Expanded(
child:Padding(
key:textKey,
              padding:const EdgeInsets.only(left:20, top:10, bottom:10),
              child:Text(
widget.text,
                style:TextStyle(fontSize:15, color:widget.textColor),
              ),
            ),
          ),
        ],
      ),
    );
  }
}