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

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

简介

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

image.png

实现方案

封装

/// 模仿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),
      ),
    );
  }
}
  /// 内部变量
  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,
              ),
            ),
          ),
        );
      },
    );
  }

调用代码

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

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

效果

image.png image.png
![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实现悬浮窗

上一篇 下一篇

猜你喜欢

热点阅读