Browse Source

feat:键盘插件,增加气泡Widget

hezihao 8 months ago
parent
commit
ade145af2e

+ 2 - 2
plugins/keyboard_android/android/src/main/res/layout/floating_button_layout.xml

@@ -9,7 +9,7 @@
         android:layout_height="40dp"
         android:padding="10dp"
         android:scaleType="centerInside"
-        android:scaleX="2"
-        android:scaleY="2"
+        android:scaleX="1.8"
+        android:scaleY="1.8"
         android:src="@mipmap/ic_keyboard_icon" />
 </FrameLayout>

+ 35 - 13
plugins/keyboard_android/example/lib/page/chat_page.dart

@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
 import '../model/msg.dart';
 import '../util/ToastUtil.dart';
 import '../util/clipboard_util.dart';
+import '../widget/bubble/bubble_widget.dart';
 
 /// 聊天页面
 class ChatPage extends StatefulWidget {
@@ -27,6 +28,17 @@ class ChatPageState extends State<ChatPage> {
   /// 消息列表
   final List<Msg> _msgList = [];
 
+  @override
+  void initState() {
+    super.initState();
+    _inputFocusNode.addListener(() {
+      // 输入框获取焦点,滚动列表到底部
+      _scrollToBottom();
+    });
+    // 进入页面,就获取输入框焦点
+    _inputFocusNode.requestFocus();
+  }
+
   /// 添加消息到消息列表中
   void _addMsg2List(String msg, bool isMe) {
     setState(() {
@@ -34,8 +46,13 @@ class ChatPageState extends State<ChatPage> {
         Msg(isMe: isMe, msg: msg, createTime: DateTime.now().millisecond),
       );
     });
+    _scrollToBottom();
+  }
+
+  /// 滚动到列表底部
+  void _scrollToBottom() {
+    //延迟300毫秒,再滚动到列表底部
     Future.delayed(const Duration(milliseconds: 300), () {
-      //延迟300毫秒,再滚动到列表底部
       _scrollController.jumpTo(_scrollController.position.maxScrollExtent);
     });
   }
@@ -68,17 +85,22 @@ class ChatPageState extends State<ChatPage> {
 
   /// 构建聊天气泡
   Widget _buildMsgBubble(Msg msg) {
-    return Container(
-      padding: const EdgeInsets.all(15.0),
-      decoration: BoxDecoration(
-        color:
-            msg.isMe
-                ? const Color.fromARGB(255, 164, 208, 238)
-                : const Color.fromARGB(255, 153, 231, 169),
-        borderRadius: BorderRadius.circular(12),
-      ),
-      // 消息文本
-      child: Text(msg.msg),
+    return BubbleWidget(
+      // 箭头方向
+      arrowDirection: msg.isMe ? AxisDirection.right : AxisDirection.left,
+      arrowOffset: 22,
+      arrowLength: 8,
+      arrowRadius: 4,
+      arrowWidth: 14,
+      padding: const EdgeInsets.all(12),
+      borderRadius: BorderRadius.circular(8),
+      backgroundColor:
+          msg.isMe
+              ? const Color.fromARGB(255, 164, 208, 238)
+              : const Color.fromARGB(255, 153, 231, 169),
+      contentBuilder: (context) {
+        return Text(msg.msg);
+      },
     );
   }
 
@@ -197,7 +219,7 @@ class ChatPageState extends State<ChatPage> {
                         ),
                       ),
                       Container(
-                        margin: const EdgeInsets.only(left: 5.0),
+                        margin: const EdgeInsets.only(left: 15.0),
                         child: ElevatedButton(
                           child: const Text("发送"),
                           onPressed: () {

+ 3 - 1
plugins/keyboard_android/example/lib/page/home_page.dart

@@ -24,12 +24,14 @@ class _HomePageState extends State<HomePage> {
   final _keyboardAndroidPlugin = KeyboardAndroid();
 
   /// 是否启用悬浮窗
-  bool _enableFloatingWindow = false;
+  bool _enableFloatingWindow = true;
 
   @override
   void initState() {
     super.initState();
     _initPlatformState();
+    // 初始化悬浮窗
+    _keyboardAndroidPlugin.enableFloatingWindow(_enableFloatingWindow);
   }
 
   /// 获取平台版本

+ 505 - 0
plugins/keyboard_android/example/lib/widget/bubble/bubble_border_arrow_properties.dart

@@ -0,0 +1,505 @@
+import 'dart:math';
+import 'package:flutter/material.dart';
+
+class _BubbleBorderArrowProperties {
+  /// 箭头宽度的一半
+  final double halfWidth;
+
+  /// 箭头斜边的长度
+  final double hypotenuse;
+
+  /// 该斜边在主轴上的投影(水平时为X轴)
+  final double projectionOnMain;
+
+  /// 该斜边在纵轴上的投影(水平时为Y轴)
+  final double projectionOnCross;
+
+  /// 计算箭头半径在主轴上的投影(水平时为X轴)
+  final double arrowProjectionOnMain;
+
+  /// 计算箭头半径尖尖的长度
+  final double topLen;
+
+  _BubbleBorderArrowProperties({
+    required this.halfWidth,
+    required this.hypotenuse,
+    required this.projectionOnMain,
+    required this.projectionOnCross,
+    required this.arrowProjectionOnMain,
+    required this.topLen,
+  });
+}
+
+class BubbleShapeBorder extends OutlinedBorder {
+  final BorderRadius borderRadius;
+  final AxisDirection arrowDirection;
+  final double arrowLength;
+  final double arrowWidth;
+  final double arrowRadius;
+  final double? arrowOffset;
+  final Color? fillColor;
+
+  const BubbleShapeBorder({
+    super.side,
+    required this.arrowDirection,
+    this.borderRadius = BorderRadius.zero,
+    this.arrowLength = 12,
+    this.arrowWidth = 18,
+    this.arrowRadius = 3,
+    this.arrowOffset,
+    this.fillColor,
+  });
+
+  @override
+  OutlinedBorder copyWith({
+    AxisDirection? arrowDirection,
+    BorderSide? side,
+    BorderRadius? borderRadius,
+    double? arrowLength,
+    double? arrowWidth,
+    double? arrowRadius,
+    double? arrowOffset,
+    Color? fillColor,
+  }) {
+    return BubbleShapeBorder(
+      arrowDirection: arrowDirection ?? this.arrowDirection,
+      side: side ?? this.side,
+      borderRadius: borderRadius ?? this.borderRadius,
+      arrowLength: arrowLength ?? this.arrowLength,
+      arrowWidth: arrowWidth ?? this.arrowWidth,
+      arrowRadius: arrowRadius ?? this.arrowRadius,
+      arrowOffset: arrowOffset ?? this.arrowOffset,
+      fillColor: fillColor ?? this.fillColor,
+    );
+  }
+
+  @override
+  EdgeInsetsGeometry get dimensions => EdgeInsets.zero;
+
+  @override
+  Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
+    return _buildPath(rect);
+  }
+
+  @override
+  Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
+    return _buildPath(rect);
+  }
+
+  _BubbleBorderArrowProperties _calculateArrowProperties() {
+    final arrowHalfWidth = arrowWidth / 2;
+    final double hypotenuse = sqrt(
+      arrowLength * arrowLength + arrowHalfWidth * arrowHalfWidth,
+    );
+    final double projectionOnMain = arrowHalfWidth * arrowRadius / hypotenuse;
+    final double projectionOnCross =
+        projectionOnMain * arrowLength / arrowHalfWidth;
+    final double arrowProjectionOnMain = arrowLength * arrowRadius / hypotenuse;
+    final double pointArrowTopLen =
+        arrowProjectionOnMain * arrowLength / arrowHalfWidth;
+    return _BubbleBorderArrowProperties(
+      halfWidth: arrowHalfWidth,
+      hypotenuse: hypotenuse,
+      projectionOnMain: projectionOnMain,
+      projectionOnCross: projectionOnCross,
+      arrowProjectionOnMain: arrowProjectionOnMain,
+      topLen: pointArrowTopLen,
+    );
+  }
+
+  /// 核心逻辑:构建路径
+  /// 计算方向为:上、右、下、左
+  ///
+  /// 爱今天灵眸(ijtkj.cn),一款可根据天气改变系统显示模式的软件,期待各位有钱的码友支持
+  Path _buildPath(Rect rect) {
+    final path = Path();
+    EdgeInsets padding = EdgeInsets.zero;
+    if (arrowDirection == AxisDirection.up) {
+      padding = EdgeInsets.only(top: arrowLength);
+    } else if (arrowDirection == AxisDirection.right) {
+      padding = EdgeInsets.only(right: arrowLength);
+    } else if (arrowDirection == AxisDirection.down) {
+      padding = EdgeInsets.only(bottom: arrowLength);
+    } else if (arrowDirection == AxisDirection.left) {
+      padding = EdgeInsets.only(left: arrowLength);
+    }
+    final nRect = Rect.fromLTRB(
+      rect.left + padding.left,
+      rect.top + padding.top,
+      rect.right - padding.right,
+      rect.bottom - padding.bottom,
+    );
+
+    final arrowProp = _calculateArrowProperties();
+
+    final startPoint = Offset(nRect.left + borderRadius.topLeft.x, nRect.top);
+
+    path.moveTo(startPoint.dx, startPoint.dy);
+    // 箭头在上边
+    if (arrowDirection == AxisDirection.up) {
+      Offset pointCenter = Offset(
+        nRect.left + (arrowOffset ?? nRect.width / 2),
+        nRect.top,
+      );
+      Offset pointStart = Offset(
+        pointCenter.dx - arrowProp.halfWidth,
+        nRect.top,
+      );
+      Offset pointArrow = Offset(pointCenter.dx, rect.top);
+      Offset pointEnd = Offset(pointCenter.dx + arrowProp.halfWidth, nRect.top);
+
+      // 下面计算开始的圆弧
+      {
+        Offset pointStartArcBegin = Offset(
+          pointStart.dx - arrowRadius,
+          pointStart.dy,
+        );
+        Offset pointStartArcEnd = Offset(
+          pointStart.dx + arrowProp.projectionOnMain,
+          pointStart.dy - arrowProp.projectionOnCross,
+        );
+        path.lineTo(pointStartArcBegin.dx, pointStartArcBegin.dy);
+        path.quadraticBezierTo(
+          pointStart.dx,
+          pointStart.dy,
+          pointStartArcEnd.dx,
+          pointStartArcEnd.dy,
+        );
+      }
+      // 计算中间箭头的圆弧
+      {
+        Offset pointArrowArcBegin = Offset(
+          pointArrow.dx - arrowProp.arrowProjectionOnMain,
+          pointArrow.dy + arrowProp.topLen,
+        );
+        Offset pointArrowArcEnd = Offset(
+          pointArrow.dx + arrowProp.arrowProjectionOnMain,
+          pointArrow.dy + arrowProp.topLen,
+        );
+        path.lineTo(pointArrowArcBegin.dx, pointArrowArcBegin.dy);
+        path.quadraticBezierTo(
+          pointArrow.dx,
+          pointArrow.dy,
+          pointArrowArcEnd.dx,
+          pointArrowArcEnd.dy,
+        );
+      }
+      // 下面计算结束的圆弧
+      {
+        Offset pointEndArcBegin = Offset(
+          pointEnd.dx - arrowProp.projectionOnMain,
+          pointEnd.dy - arrowProp.projectionOnCross,
+        );
+        Offset pointEndArcEnd = Offset(pointEnd.dx + arrowRadius, pointEnd.dy);
+        path.lineTo(pointEndArcBegin.dx, pointEndArcBegin.dy);
+        path.quadraticBezierTo(
+          pointEnd.dx,
+          pointEnd.dy,
+          pointEndArcEnd.dx,
+          pointEndArcEnd.dy,
+        );
+      }
+    }
+
+    path.lineTo(nRect.right - borderRadius.topRight.x, nRect.top);
+    // topRight radius
+    path.arcToPoint(
+      Offset(nRect.right, nRect.top + borderRadius.topRight.y),
+      radius: borderRadius.topRight,
+      rotation: 90,
+    );
+
+    // 箭头在右边
+    if (arrowDirection == AxisDirection.right) {
+      Offset pointCenter = Offset(
+        nRect.right,
+        nRect.top + (arrowOffset ?? nRect.height / 2),
+      );
+      Offset pointStart = Offset(
+        nRect.right,
+        pointCenter.dy - arrowProp.halfWidth,
+      );
+      Offset pointArrow = Offset(rect.right, pointCenter.dy);
+      Offset pointEnd = Offset(
+        nRect.right,
+        pointCenter.dy + arrowProp.halfWidth,
+      );
+
+      // 下面计算开始的圆弧
+      {
+        Offset pointStartArcBegin = Offset(
+          pointStart.dx,
+          pointStart.dy - arrowRadius,
+        );
+        Offset pointStartArcEnd = Offset(
+          pointStart.dx + arrowProp.projectionOnCross,
+          pointStart.dy + arrowProp.projectionOnMain,
+        );
+        path.lineTo(pointStartArcBegin.dx, pointStartArcBegin.dy);
+        path.quadraticBezierTo(
+          pointStart.dx,
+          pointStart.dy,
+          pointStartArcEnd.dx,
+          pointStartArcEnd.dy,
+        );
+      }
+      // 计算中间箭头的圆弧
+      {
+        Offset pointArrowArcBegin = Offset(
+          pointArrow.dx - arrowProp.topLen,
+          pointArrow.dy - arrowProp.arrowProjectionOnMain,
+        );
+        Offset pointArrowArcEnd = Offset(
+          pointArrow.dx - arrowProp.topLen,
+          pointArrow.dy + arrowProp.arrowProjectionOnMain,
+        );
+        path.lineTo(pointArrowArcBegin.dx, pointArrowArcBegin.dy);
+        path.quadraticBezierTo(
+          pointArrow.dx,
+          pointArrow.dy,
+          pointArrowArcEnd.dx,
+          pointArrowArcEnd.dy,
+        );
+      }
+      // 下面计算结束的圆弧
+      {
+        Offset pointEndArcBegin = Offset(
+          pointEnd.dx + arrowProp.projectionOnCross,
+          pointEnd.dy - arrowProp.projectionOnMain,
+        );
+        Offset pointEndArcEnd = Offset(pointEnd.dx, pointEnd.dy + arrowRadius);
+        path.lineTo(pointEndArcBegin.dx, pointEndArcBegin.dy);
+        path.quadraticBezierTo(
+          pointEnd.dx,
+          pointEnd.dy,
+          pointEndArcEnd.dx,
+          pointEndArcEnd.dy,
+        );
+      }
+    }
+
+    path.lineTo(nRect.right, nRect.bottom - borderRadius.bottomRight.y);
+    // bottomRight radius
+    path.arcToPoint(
+      Offset(nRect.right - borderRadius.bottomRight.x, nRect.bottom),
+      radius: borderRadius.bottomRight,
+      rotation: 90,
+    );
+
+    // 箭头在下边
+    if (arrowDirection == AxisDirection.down) {
+      Offset pointCenter = Offset(
+        nRect.left + (arrowOffset ?? nRect.width / 2),
+        nRect.bottom,
+      );
+      Offset pointStart = Offset(
+        pointCenter.dx + arrowProp.halfWidth,
+        nRect.bottom,
+      );
+      Offset pointArrow = Offset(pointCenter.dx, rect.bottom);
+      Offset pointEnd = Offset(
+        pointCenter.dx - arrowProp.halfWidth,
+        nRect.bottom,
+      );
+
+      // 下面计算开始的圆弧
+      {
+        Offset pointStartArcBegin = Offset(
+          pointStart.dx + arrowRadius,
+          pointStart.dy,
+        );
+        Offset pointStartArcEnd = Offset(
+          pointStart.dx - arrowProp.projectionOnMain,
+          pointStart.dy + arrowProp.projectionOnCross,
+        );
+        path.lineTo(pointStartArcBegin.dx, pointStartArcBegin.dy);
+        path.quadraticBezierTo(
+          pointStart.dx,
+          pointStart.dy,
+          pointStartArcEnd.dx,
+          pointStartArcEnd.dy,
+        );
+      }
+      // 计算中间箭头的圆弧
+      {
+        Offset pointArrowArcBegin = Offset(
+          pointArrow.dx + arrowProp.arrowProjectionOnMain,
+          pointArrow.dy - arrowProp.topLen,
+        );
+        Offset pointArrowArcEnd = Offset(
+          pointArrow.dx - arrowProp.arrowProjectionOnMain,
+          pointArrow.dy - arrowProp.topLen,
+        );
+        path.lineTo(pointArrowArcBegin.dx, pointArrowArcBegin.dy);
+        path.quadraticBezierTo(
+          pointArrow.dx,
+          pointArrow.dy,
+          pointArrowArcEnd.dx,
+          pointArrowArcEnd.dy,
+        );
+      }
+      // 下面计算结束的圆弧
+      {
+        Offset pointEndArcBegin = Offset(
+          pointEnd.dx + arrowProp.projectionOnMain,
+          pointEnd.dy + arrowProp.projectionOnCross,
+        );
+        Offset pointEndArcEnd = Offset(pointEnd.dx - arrowRadius, pointEnd.dy);
+        path.lineTo(pointEndArcBegin.dx, pointEndArcBegin.dy);
+        path.quadraticBezierTo(
+          pointEnd.dx,
+          pointEnd.dy,
+          pointEndArcEnd.dx,
+          pointEndArcEnd.dy,
+        );
+      }
+    }
+
+    path.lineTo(nRect.left + borderRadius.bottomLeft.x, nRect.bottom);
+    // bottomLeft radius
+    path.arcToPoint(
+      Offset(nRect.left, nRect.bottom - borderRadius.bottomRight.y),
+      radius: borderRadius.bottomLeft,
+      rotation: 90,
+    );
+
+    // 箭头在左边
+    if (arrowDirection == AxisDirection.left) {
+      Offset pointCenter = Offset(
+        nRect.left,
+        nRect.top + (arrowOffset ?? nRect.height / 2),
+      );
+      Offset pointStart = Offset(
+        nRect.left,
+        pointCenter.dy + arrowProp.halfWidth,
+      );
+      Offset pointArrow = Offset(rect.left, pointCenter.dy);
+      Offset pointEnd = Offset(
+        nRect.left,
+        pointCenter.dy - arrowProp.halfWidth,
+      );
+
+      // 下面计算开始的圆弧
+      {
+        Offset pointStartArcBegin = Offset(
+          pointStart.dx,
+          pointStart.dy + arrowRadius,
+        );
+        Offset pointStartArcEnd = Offset(
+          pointStart.dx - arrowProp.projectionOnCross,
+          pointStart.dy - arrowProp.projectionOnMain,
+        );
+        path.lineTo(pointStartArcBegin.dx, pointStartArcBegin.dy);
+        path.quadraticBezierTo(
+          pointStart.dx,
+          pointStart.dy,
+          pointStartArcEnd.dx,
+          pointStartArcEnd.dy,
+        );
+      }
+      // 计算中间箭头的圆弧
+      {
+        Offset pointArrowArcBegin = Offset(
+          pointArrow.dx + arrowProp.topLen,
+          pointArrow.dy + arrowProp.arrowProjectionOnMain,
+        );
+        Offset pointArrowArcEnd = Offset(
+          pointArrow.dx + arrowProp.topLen,
+          pointArrow.dy - arrowProp.arrowProjectionOnMain,
+        );
+        path.lineTo(pointArrowArcBegin.dx, pointArrowArcBegin.dy);
+        path.quadraticBezierTo(
+          pointArrow.dx,
+          pointArrow.dy,
+          pointArrowArcEnd.dx,
+          pointArrowArcEnd.dy,
+        );
+      }
+      // 下面计算结束的圆弧
+      {
+        Offset pointEndArcBegin = Offset(
+          pointEnd.dx - arrowProp.projectionOnCross,
+          pointEnd.dy + arrowProp.projectionOnMain,
+        );
+        Offset pointEndArcEnd = Offset(pointEnd.dx, pointEnd.dy - arrowRadius);
+        path.lineTo(pointEndArcBegin.dx, pointEndArcBegin.dy);
+        path.quadraticBezierTo(
+          pointEnd.dx,
+          pointEnd.dy,
+          pointEndArcEnd.dx,
+          pointEndArcEnd.dy,
+        );
+      }
+    }
+
+    path.lineTo(nRect.left, nRect.top + borderRadius.topLeft.y);
+    path.arcToPoint(startPoint, radius: borderRadius.topLeft, rotation: 90);
+
+    return path;
+  }
+
+  @override
+  void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {
+    if (fillColor == null && side == BorderSide.none) {
+      return;
+    }
+
+    final path = _buildPath(rect);
+    final Paint paint =
+        Paint()
+          ..color = side.color
+          ..style = PaintingStyle.stroke;
+    if (fillColor != null) {
+      paint.color = fillColor!;
+      paint.style = PaintingStyle.fill;
+      canvas.drawPath(path, paint);
+    }
+    if (side != BorderSide.none) {
+      paint.color = side.color;
+      paint.strokeWidth = side.width;
+      paint.style = PaintingStyle.stroke;
+      canvas.drawPath(path, paint);
+    }
+  }
+
+  @override
+  ShapeBorder scale(double t) {
+    return BubbleShapeBorder(
+      arrowDirection: arrowDirection,
+      side: side.scale(t),
+      borderRadius: borderRadius * t,
+      arrowLength: arrowLength * t,
+      arrowWidth: arrowWidth * t,
+      arrowRadius: arrowRadius * t,
+      arrowOffset: (arrowOffset ?? 0) * t,
+    );
+  }
+
+  @override
+  bool operator ==(Object other) {
+    if (other.runtimeType != runtimeType) {
+      return false;
+    }
+    return other is BubbleShapeBorder &&
+        other.side == side &&
+        other.borderRadius == borderRadius &&
+        other.arrowLength == arrowLength &&
+        other.arrowWidth == arrowWidth &&
+        other.arrowRadius == arrowRadius &&
+        other.arrowDirection == arrowDirection &&
+        other.arrowOffset == arrowOffset &&
+        other.fillColor == fillColor;
+  }
+
+  @override
+  int get hashCode => Object.hash(
+    side,
+    borderRadius,
+    arrowLength,
+    arrowWidth,
+    arrowRadius,
+    arrowDirection,
+    arrowOffset,
+    fillColor,
+  );
+}

+ 68 - 0
plugins/keyboard_android/example/lib/widget/bubble/bubble_widget.dart

@@ -0,0 +1,68 @@
+import 'package:flutter/material.dart';
+import 'bubble_border_arrow_properties.dart';
+
+/// 气泡组件
+class BubbleWidget extends StatelessWidget {
+  final BorderSide border;
+  final AxisDirection arrowDirection;
+  final BorderRadius? borderRadius;
+  final double arrowLength;
+  final double arrowWidth;
+  final double? arrowOffset;
+  final double arrowRadius;
+  final Color? backgroundColor;
+  final EdgeInsets? padding;
+  final WidgetBuilder contentBuilder;
+  final List<BoxShadow>? shadows;
+  final EdgeInsetsGeometry? margin;
+
+  const BubbleWidget({
+    super.key,
+    required this.arrowDirection,
+    this.arrowOffset,
+    required this.contentBuilder,
+    this.border = BorderSide.none,
+    this.borderRadius,
+    this.arrowLength = 10,
+    this.arrowWidth = 17,
+    this.arrowRadius = 3,
+    this.backgroundColor,
+    this.shadows,
+    this.padding,
+    this.margin,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    EdgeInsets bubblePadding = EdgeInsets.zero;
+    if (arrowDirection == AxisDirection.up) {
+      bubblePadding = EdgeInsets.only(top: arrowLength);
+    } else if (arrowDirection == AxisDirection.down) {
+      bubblePadding = EdgeInsets.only(bottom: arrowLength);
+    } else if (arrowDirection == AxisDirection.left) {
+      bubblePadding = EdgeInsets.only(left: arrowLength);
+    } else if (arrowDirection == AxisDirection.right) {
+      bubblePadding = EdgeInsets.only(right: arrowLength);
+    }
+    return Container(
+      margin: margin,
+      decoration: ShapeDecoration(
+        shape: BubbleShapeBorder(
+          side: border,
+          arrowDirection: arrowDirection,
+          borderRadius: borderRadius ?? BorderRadius.circular(4),
+          arrowLength: arrowLength,
+          arrowWidth: arrowWidth,
+          arrowRadius: arrowRadius,
+          arrowOffset: arrowOffset,
+          fillColor: backgroundColor ?? const Color.fromARGB(255, 65, 65, 65),
+        ),
+        shadows: shadows,
+      ),
+      child: Padding(
+        padding: bubblePadding.add(padding ?? EdgeInsets.zero),
+        child: contentBuilder(context),
+      ),
+    );
+  }
+}