美文网首页
Flutter浮层的实现 2023-08-10 周四

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

作者: 勇往直前888 | 来源:发表于2023-08-11 11:16 被阅读0次

简介

企业微信截图_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;
}
  • 目标位置通过GlobalKey得到
  // 获取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:

  • 可以通过OverlayEntry来展示浮层;

  • 展示浮层中的context不能用Get.context替代

  • 定位可以通过GlobalKey来完成,拿到目标的位置(Rect

  /// 获取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;
    }
  }

三角形画法

  • 没有现成的Widget可以用,需要用CustomPaint画出来

  • 暂时不考虑空心的三角形,只考虑实心的三角形,所以style固定为PaintingStyle.fill就可以,默认也是这个,不设置也行。strokeWidth对于实心三角形没有意义,不需要设置

  • 对外的参数,只需要一个填充颜色就可以了

  • 三角形只要三个点,通过一笔画就可以了。可以想象为在一个矩形中一笔画一个三角形,x和y分别为矩形的宽和高。

  Path getTrianglePath(double x, double y) {
    return Path()
      ..moveTo(0, y)
      ..lineTo(x / 2, 0)
      ..lineTo(x, y)
      ..lineTo(0, y);
  }
  • 上面的是顶点朝上的三角形。顶点可以有4个方向,所以用一个枚举类型表示
/// 三角形顶点的朝向
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);
  }
}

三角形定位

  • 三角形的顶点向下,所以方向direction参数给TriangleDirection.down

  • 三角形的大小可以固定,这个参数由CustomPaintsize参数给出;

  • 三角形的顶点应该是目标矩形上边的中点。结合三角形矩形的大小,可以算出三角形矩形左上角顶点,然后给Positioned的lefttop参数,就能定位三角形。

    /// 获取目标矩形的定位
    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详解

相关文章

网友评论

      本文标题:Flutter浮层的实现 2023-08-10 周四

      本文链接:https://www.haomeiwen.com/subject/jpzopdtx.html