Flutter浮层的实现 2023-08-10 周四

2023-08-11  本文已影响0人  勇往直前888

简介

企业微信截图_706b7623-2225-4cf5-af4e-38389435973b.png

这样的浮层该怎么实现?

方案1

bruno插件
这是一整套的UI库,其中的组件BrnPopupWindow可以大致实现。

企业微信截图_a304d55e-a2f0-4f44-bb55-11fa0147db9c.png
class BrnPopupRoute extends PopupRoute {
  final Duration _duration = Duration(milliseconds: 200);
  Widget child;

  BrnPopupRoute({required this.child});

  @override
  Color? get barrierColor => null;

  @override
  bool get barrierDismissible => true;

  @override
  String? get barrierLabel => null;

  @override
  Widget buildPage(BuildContext context, Animation<double> animation,
      Animation<double> secondaryAnimation) {
    return child;
  }

  @override
  Duration get transitionDuration => _duration;
}
  // 获取targetView的位置
  Rect _getWidgetGlobalRect(GlobalKey key) {
    try {
      BuildContext? ctx = key.currentContext;
      RenderObject? renderObject = ctx?.findRenderObject();
      RenderBox renderBox = renderObject as RenderBox;
      var offset = renderBox.localToGlobal(Offset.zero);
      return Rect.fromLTWH(
          offset.dx, offset.dy, renderBox.size.width, renderBox.size.height);
    } catch (e) {
      debugPrint('获取尺寸信息异常');
      return Rect.zero;
    }
  }
// 绘制箭头
class _TrianglePainter extends CustomPainter {
  bool isDownArrow;
  Color color;
  Color borderColor;

  _TrianglePainter({
    required this.isDownArrow,
    required this.color,
    required this.borderColor,
  });

  @override
  void paint(Canvas canvas, Size size) {
    Path path = Path();
    Paint paint = Paint();
    paint.strokeWidth = 2.0;
    paint.color = color;
    paint.style = PaintingStyle.fill;

    if (isDownArrow) {
      path.moveTo(0.0, -1.5);
      path.lineTo(size.width / 2.0, size.height);
      path.lineTo(size.width, -1.5);
    } else {
      path.moveTo(0.0, size.height + 1.5);
      path.lineTo(size.width / 2.0, 0.0);
      path.lineTo(size.width, size.height + 1.5);
    }

    canvas.drawPath(path, paint);
    Paint paintBorder = Paint();
    Path pathBorder = Path();
    paintBorder.strokeWidth = 0.5;
    paintBorder.color = borderColor;
    paintBorder.style = PaintingStyle.stroke;

    if (isDownArrow) {
      pathBorder.moveTo(0.0, -0.5);
      pathBorder.lineTo(size.width / 2.0, size.height);
      pathBorder.lineTo(size.width, -0.5);
    } else {
      pathBorder.moveTo(0.5, size.height + 0.5);
      pathBorder.lineTo(size.width / 2.0, 0);
      pathBorder.lineTo(size.width - 0.5, size.height + 0.5);
    }

    canvas.drawPath(pathBorder, paintBorder);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
}

方案2:

  /// 获取targetView的位置
  Rect _getWidgetGlobalRect(GlobalKey key) {
    try {
      BuildContext? ctx = key.currentContext;
      RenderObject? renderObject = ctx?.findRenderObject();
      RenderBox renderBox = renderObject as RenderBox;
      var offset = renderBox.localToGlobal(Offset.zero);
      return Rect.fromLTWH(
          offset.dx, offset.dy, renderBox.size.width, renderBox.size.height);
    } catch (e) {
      debugPrint('获取尺寸信息异常');
      return Rect.zero;
    }
  }

三角形画法

  Path getTrianglePath(double x, double y) {
    return Path()
      ..moveTo(0, y)
      ..lineTo(x / 2, 0)
      ..lineTo(x, y)
      ..lineTo(0, y);
  }
/// 三角形顶点的朝向
enum TriangleDirection {
  up,
  down,
  left,
  right,
}
class TrianglePainter extends CustomPainter {
  final Color fillColor;
  final TriangleDirection direction;

  TrianglePainter({
    required this.fillColor,
    this.direction = TriangleDirection.up,
  });

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
      ..color = fillColor
      ..style = PaintingStyle.fill;

    canvas.drawPath(getTrianglePath(size.width, size.height), paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;
  }

  Path getTrianglePath(double x, double y) {
    /// 朝下
    if (direction == TriangleDirection.down) {
      return Path()
        ..moveTo(0, 0)
        ..lineTo(x / 2, y)
        ..lineTo(x, 0)
        ..lineTo(0, 0);
    }

    /// 朝左
    if (direction == TriangleDirection.left) {
      return Path()
        ..moveTo(0, 0)
        ..lineTo(x, y / 2)
        ..lineTo(0, y)
        ..lineTo(0, 0);
    }

    /// 朝右
    if (direction == TriangleDirection.right) {
      return Path()
        ..moveTo(x, 0)
        ..lineTo(0, y / 2)
        ..lineTo(x, y)
        ..lineTo(x, 0);
    }

    /// 默认返回朝上的
    return Path()
      ..moveTo(0, y)
      ..lineTo(x / 2, 0)
      ..lineTo(x, y)
      ..lineTo(0, y);
  }
}

三角形定位

    /// 获取目标矩形的定位
    Rect targetRect = _getWidgetGlobalRect(key);

    /// 定位三角形
    const double triangleWith = 8.5;
    const double triangleHeight = 5;

    /// 三角形的顶点是目标矩形的中点
    var trianglePoint = targetRect.topCenter;

    /// 计算三角形左上角位置
    var triangleLeft = trianglePoint.dx - (triangleWith / 2);
    var triangleTop = trianglePoint.dy - triangleHeight;

测试界面

  /// 显示浮层,这里的context不能用Get.context;原因未知
  void showOverlay({
    required BuildContext context,
    required GlobalKey key,
  }) {
    /// 获取目标矩形的定位
    Rect targetRect = _getWidgetGlobalRect(key);

    /// 定位三角形
    const double triangleWith = 8.5;
    const double triangleHeight = 5;

    /// 三角形的顶点是目标矩形的中点
    var trianglePoint = targetRect.topCenter;

    /// 计算三角形左上角位置
    var triangleLeft = trianglePoint.dx - (triangleWith / 2);
    var triangleTop = trianglePoint.dy - triangleHeight;

    /// 定义浮层
    _overlayEntry = OverlayEntry(
      builder: (context) {
        return ExcludeSemantics(
          excluding: true,
          child: GestureDetector(
            behavior: HitTestBehavior.translucent,
            onTap: () {
              _overlayEntry.remove();
            },
            child: Material(
              color: Colors.transparent,
              child: Stack(
                children: <Widget>[
                  /// 目标矩形
                  Positioned(
                    left: targetRect.left,
                    top: targetRect.top,
                    width: targetRect.width,
                    height: targetRect.height,
                    child: Container(
                      color: Colors.red,
                    ),
                  ),

                  /// 顶点在目标矩形上边中点的向下的三角形
                  Positioned(
                    left: triangleLeft,
                    top: triangleTop,
                    child: CustomPaint(
                      size: const Size(triangleWith, triangleHeight),
                      painter: TrianglePainter(
                        fillColor: Colors.blue,
                        direction: TriangleDirection.down,
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        );
      },
    );

    Overlay.of(context).insert(_overlayEntry);
  }
image.png

文本

用一个Container就可以了,高度确定,底部与三角形的顶部重合,定位不难。

最终的代码

class PandaTip {
  /// 使用单例方式调用;用到成员变量的时候能够提供方便
  PandaTip._();
  static final PandaTip _instance = PandaTip._();
  static PandaTip get instance => _instance;

  /// 浮层
  late OverlayEntry _overlayEntry;

  /// 显示浮层,这里的context不能用Get.context;原因未知
  void showOverlay({
    required BuildContext context,
    required GlobalKey key,
  }) {
    /// 获取目标矩形的定位
    Rect targetRect = _getWidgetGlobalRect(key);

    /// 定位三角形
    const double triangleWith = 8.5;
    const double triangleHeight = 5;

    /// 三角形的顶点是目标矩形的中点
    var trianglePoint = targetRect.topCenter;

    /// 计算三角形左上角位置
    var triangleLeft = trianglePoint.dx - (triangleWith / 2);
    var triangleTop = trianglePoint.dy - triangleHeight;

    /// 定位消息框;底部和三角形矩形顶部重合
    const double messageHeight = 36;
    const double messageLeft = 5;
    double messageBottom = triangleTop;
    double messageTop = messageBottom - messageHeight;

    /// 定义浮层
    _overlayEntry = OverlayEntry(
      builder: (context) {
        return ExcludeSemantics(
          excluding: true,
          child: GestureDetector(
            behavior: HitTestBehavior.translucent,
            onTap: () {
              _overlayEntry.remove();
            },
            child: Material(
              color: Colors.transparent,
              child: Stack(
                children: <Widget>[
                  /// 目标矩形
                  Positioned(
                    left: targetRect.left,
                    top: targetRect.top,
                    width: targetRect.width,
                    height: targetRect.height,
                    child: Container(
                      color: Colors.red,
                    ),
                  ),

                  /// 顶点在目标矩形上边中点的向下的三角形
                  Positioned(
                    left: triangleLeft,
                    top: triangleTop,
                    child: CustomPaint(
                      size: const Size(triangleWith, triangleHeight),
                      painter: TrianglePainter(
                        fillColor: Colors.blue,
                        direction: TriangleDirection.down,
                      ),
                    ),
                  ),

                  /// 消息矩形
                  Positioned(
                    left: messageLeft,
                    top: messageTop,
                    child: Container(
                      height: messageHeight,
                      padding: const EdgeInsets.symmetric(
                          horizontal: 30, vertical: 10),
                      decoration: BoxDecoration(
                        color: Colors.yellow,
                        borderRadius: BorderRadius.circular(8),
                      ),
                      child: const Row(
                        children: [
                          Text('请阅读并同意后再提交'),
                        ],
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        );
      },
    );

    Overlay.of(context).insert(_overlayEntry);
  }

  /// 获取targetView的位置
  Rect _getWidgetGlobalRect(GlobalKey key) {
    try {
      BuildContext? ctx = key.currentContext;
      RenderObject? renderObject = ctx?.findRenderObject();
      RenderBox renderBox = renderObject as RenderBox;
      var offset = renderBox.localToGlobal(Offset.zero);
      return Rect.fromLTWH(
          offset.dx, offset.dy, renderBox.size.width, renderBox.size.height);
    } catch (e) {
      debugPrint('获取尺寸信息异常');
      return Rect.zero;
    }
  }
}
image.png

参考文章

具有自定义形状的 Flutter 按钮 - (三角形)

Flutter CustomPaint详解

上一篇 下一篇

猜你喜欢

热点阅读