美文网首页
Flutter 弹窗菜单实现 2023-08-12 周六

Flutter 弹窗菜单实现 2023-08-12 周六

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

简介

弹窗菜单用到的场景还是蛮多的,比如这样的:

image.png

实现方案

  • 采用PopupMenuButton组件,配合PopupMenuItem,可以非常方便地实现这种弹出菜单效果。

  • 实践下来,基本的功能实现是足够用了,但是如果需要自定义视图,就显得力不从心;

  • 弹窗菜单基本上是一种OverlayEntry,所以如果需要自定义,应该用这个;

  • OverlayEntry弹窗菜单与目标组件是紧挨着的,如何定位是个问题。

  • CompositedTransformFollower 与 CompositedTransformTarget的组合可以解决目标组件与弹出OverlayEntry的定位问题。

封装

  • 模仿PopupMenuButton,定义接口参数。为了增加自定义的自由度,弹出部分也只是个Widget,而不是一个数组。
/// 模仿PopupMenuButton写的弹窗菜单
class PandaPopupMenu extends StatelessWidget {
  const PandaPopupMenu({
    Key? key,
    required this.targetWiget,
    required this.menuWiget,
  }) : super(key: key);

  final Widget targetWiget;
  final Widget menuWiget;

  @override
  Widget build(BuildContext context) {
    return const Center(
      child: Text(
        'PandaPopupMenu is working',
        style: TextStyle(fontSize: 20),
      ),
    );
  }
}
  • 内部变量:组件对所需要的连线;弹窗用一个OverlayEntry
  /// 内部变量
  final LayerLink _layerLink = LayerLink();
  OverlayEntry? _overlayEntry;

Target布局

这个就是PopupMenuButton一直显示的部分,外面套一个Container,可以带来很大的方便。

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      behavior: HitTestBehavior.opaque,
      onTap: () {
        _showOverlay(context);
      },
      child: Container(
        margin: margin,
        padding: padding,
        color: color,
        width: width,
        height: height,
        decoration: decoration,
        child: CompositedTransformTarget(
          link: _layerLink,
          child: targetWidget,
        ),
      ),
    );
  }

菜单代码

  /// 显示浮层
  void _showOverlay(BuildContext context) {
    /// 防止重复创建,不然失去句柄的OverlayEntry将无法消除
    if (_overlayEntry == null) {
      _overlayEntry = _createOverlayEntry();
      if (_overlayEntry != null) {
        Overlay.of(context).insert(_overlayEntry!);
      }
    }
  }

  /// 隐藏浮层
  void _hideOverlay() {
    /// 防止null调用异常
    if (_overlayEntry != null) {
      _overlayEntry?.remove();
      _overlayEntry = null;
    }
  }

  /// 创建浮层
  OverlayEntry _createOverlayEntry() {
    return OverlayEntry(
      builder: (BuildContext context) {
        return GestureDetector(
          onTap: () {
            _hideOverlay();
          },
          child: UnconstrainedBox(
            child: CompositedTransformFollower(
              link: _layerLink,
              targetAnchor: Alignment.bottomCenter,
              followerAnchor: Alignment.topCenter,
              offset: const Offset(0, 10),
              child: Material(
                child: menuWigdet,
              ),
            ),
          ),
        );
      },
    );
  }
  • OverlayEntry默认是全屏充满的,PopupMenuButton就是这样的情况。不过,在很多时候我们不希望这样。比如,我们现在的设想是点击Target部分,显示菜单;点击菜单部分,隐藏菜单。要去掉全屏,只要在外面套一个UnconstrainedBox就可以了。

  • OverlayEntry不能反复创建,不然的话,丢失句柄的OverlayEntry会无法消除;所以创建和消除方法需要做好判空处理。

  • 默认锚点都在左上角,这样导致弹出的菜单盖住了Target,失去“跟随”的意义。这个只要改一下targetAnchor,followerAnchor取值就可以了。

  • 偏移量:Target和菜单是紧挨着的,如果需要间隔,那么只要修改offset参数就可以了。

调用代码

PandaPopupMenu(
  targetWigdet: Container(
    color: Colors.yellow,
    width: 30,
    height: 30,
  ),
  menuWigdet: Container(
    color: Colors.blue,
    width: 200,
    height: 300,
  ),
),

代码很简单,就是两个不同颜色的矩形

效果

  • 默认状态:就是一个黄色矩形
image.png
  • 展开状态:点击黄色矩形,添加一个浮层,目前是一个蓝色矩形;跟随状态,并且有间隔。点击弹出的蓝色矩形,可以隐藏。
image.png
  • 由于去掉了OverlayEntry的全屏属性,其他地方都能正常点击,响应也正常,页面跳转也不受影响。

  • 问题:由于这里页面的底部是Tab,切换Tab的时候,失去了“跟随”目标,所以弹出的蓝色就只能居中显示。

![image.png](https://img.haomeiwen.com/i1186939/35473caf95611854.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

变换

作为一个整体,变换之后,也能很好地跟随。

  • 旋转
Transform(
  transform: Matrix4.rotationZ(-15 / 180 * 3.14),
  alignment: Alignment.center,
  child: PandaPopupMenu(
    targetWigdet: Container(
      color: Colors.yellow,
      width: 30,
      height: 30,
    ),
    menuWigdet: Container(
      color: Colors.blue,
      width: 200,
      height: 300,
    ),
  ),
),
image.png
  • 缩放
Transform(
  transform: Matrix4.diagonal3Values(0.5, 0.5, 1),
  alignment: Alignment.center,
  child: PandaPopupMenu(
    targetWigdet: Container(
      color: Colors.yellow,
      width: 30,
      height: 30,
    ),
    menuWigdet: Container(
      color: Colors.blue,
      width: 200,
      height: 300,
    ),
  ),
),
image.png
  • 斜切
Transform(
  transform: Matrix4.skewX(15 / 180 * 3.14),
  alignment: Alignment.center,
  child: PandaPopupMenu(
    targetWigdet: Container(
      color: Colors.yellow,
      width: 30,
      height: 30,
    ),
    menuWigdet: Container(
      color: Colors.blue,
      width: 200,
      height: 300,
    ),
  ),
),
image.png
  • 平移
Transform(
  transform: Matrix4.translationValues(30, 10, 0),
  alignment: Alignment.center,
  child: PandaPopupMenu(
    targetWigdet: Container(
      color: Colors.yellow,
      width: 30,
      height: 30,
    ),
    menuWigdet: Container(
      color: Colors.blue,
      width: 200,
      height: 300,
    ),
  ),
),
image.png

代码最后的样子

// ignore_for_file: must_be_immutable

import 'package:flutter/material.dart';

/// 模仿PopupMenuButton写的弹窗菜单
class PandaPopupMenu extends StatelessWidget {
  PandaPopupMenu({
    Key? key,
    required this.targetWigdet,
    required this.menuWigdet,
    this.margin,
    this.padding,
    this.color,
    this.width,
    this.height,
    this.decoration,
    this.offset = const Offset(0, 10),
    this.targetAnchor = Alignment.bottomCenter,
    this.followerAnchor = Alignment.topCenter,
  }) : super(key: key);

  final Widget targetWigdet;
  final Widget menuWigdet;
  final EdgeInsetsGeometry? margin;
  final EdgeInsetsGeometry? padding;
  final Color? color;
  final double? width;
  final double? height;
  final Decoration? decoration;
  final Offset offset;
  final Alignment targetAnchor;
  final Alignment followerAnchor;

  /// 内部变量
  final LayerLink _layerLink = LayerLink();
  OverlayEntry? _overlayEntry;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      behavior: HitTestBehavior.opaque,
      onTap: () {
        _showOverlay(context);
      },
      child: Container(
        margin: margin,
        padding: padding,
        color: color,
        width: width,
        height: height,
        decoration: decoration,
        child: CompositedTransformTarget(
          link: _layerLink,
          child: targetWigdet,
        ),
      ),
    );
  }

  /// 显示浮层
  void _showOverlay(BuildContext context) {
    /// 防止重复创建,不然失去句柄的OverlayEntry将无法消除
    if (_overlayEntry == null) {
      _overlayEntry = _createOverlayEntry();
      if (_overlayEntry != null) {
        Overlay.of(context).insert(_overlayEntry!);
      }
    }
  }

  /// 隐藏浮层
  void _hideOverlay() {
    /// 防止null调用异常
    if (_overlayEntry != null) {
      _overlayEntry?.remove();
      _overlayEntry = null;
    }
  }

  /// 创建浮层
  OverlayEntry _createOverlayEntry() {
    return OverlayEntry(
      builder: (BuildContext context) {
        return GestureDetector(
          onTap: () {
            _hideOverlay();
          },
          child: UnconstrainedBox(
            child: CompositedTransformFollower(
              link: _layerLink,
              targetAnchor: Alignment.bottomCenter,
              followerAnchor: Alignment.topCenter,
              offset: const Offset(0, 10),
              child: Material(
                child: menuWigdet,
              ),
            ),
          ),
        );
      },
    );
  }
}

参考文章

Flutter 组件 | 手牵手,一起走 CompositedTransformFollower 与 CompositedTransformTarget

Flutter 带指示器的悬浮窗口

CompositedTransformTarget + OverlayEntry实现悬浮窗

相关文章

网友评论

      本文标题:Flutter 弹窗菜单实现 2023-08-12 周六

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