簡(jiǎn)介
彈窗菜單用到的場(chǎng)景還是蠻多的,比如這樣的:

實(shí)現(xiàn)方案
采用
PopupMenuButton組件,配合PopupMenuItem,可以非常方便地實(shí)現(xiàn)這種彈出菜單效果。實(shí)踐下來(lái),基本的功能實(shí)現(xiàn)是足夠用了,但是如果需要自定義視圖,就顯得力不從心;
彈窗菜單基本上是一種
OverlayEntry,所以如果需要自定義,應(yīng)該用這個(gè);OverlayEntry彈窗菜單與目標(biāo)組件是緊挨著的,如何定位是個(gè)問(wèn)題。CompositedTransformFollower 與 CompositedTransformTarget的組合可以解決目標(biāo)組件與彈出
OverlayEntry的定位問(wèn)題。
封裝
- 模仿
PopupMenuButton,定義接口參數(shù)。為了增加自定義的自由度,彈出部分也只是個(gè)Widget,而不是一個(gè)數(shù)組。
/// 模仿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),
),
);
}
}
- 內(nèi)部變量:組件對(duì)所需要的連線;彈窗用一個(gè)
OverlayEntry
/// 內(nèi)部變量
final LayerLink _layerLink = LayerLink();
OverlayEntry? _overlayEntry;
Target布局
這個(gè)就是PopupMenuButton一直顯示的部分,外面套一個(gè)Container,可以帶來(lái)很大的方便。
@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) {
/// 防止重復(fù)創(chuàng)建,不然失去句柄的OverlayEntry將無(wú)法消除
if (_overlayEntry == null) {
_overlayEntry = _createOverlayEntry();
if (_overlayEntry != null) {
Overlay.of(context).insert(_overlayEntry!);
}
}
}
/// 隱藏浮層
void _hideOverlay() {
/// 防止null調(diào)用異常
if (_overlayEntry != null) {
_overlayEntry?.remove();
_overlayEntry = null;
}
}
/// 創(chuàng)建浮層
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默認(rèn)是全屏充滿的,PopupMenuButton就是這樣的情況。不過(guò),在很多時(shí)候我們不希望這樣。比如,我們現(xiàn)在的設(shè)想是點(diǎn)擊Target部分,顯示菜單;點(diǎn)擊菜單部分,隱藏菜單。要去掉全屏,只要在外面套一個(gè)UnconstrainedBox就可以了。OverlayEntry不能反復(fù)創(chuàng)建,不然的話,丟失句柄的OverlayEntry會(huì)無(wú)法消除;所以創(chuàng)建和消除方法需要做好判空處理。默認(rèn)錨點(diǎn)都在左上角,這樣導(dǎo)致彈出的菜單蓋住了
Target,失去“跟隨”的意義。這個(gè)只要改一下targetAnchor,followerAnchor取值就可以了。偏移量:
Target和菜單是緊挨著的,如果需要間隔,那么只要修改offset參數(shù)就可以了。
調(diào)用代碼
PandaPopupMenu(
targetWigdet: Container(
color: Colors.yellow,
width: 30,
height: 30,
),
menuWigdet: Container(
color: Colors.blue,
width: 200,
height: 300,
),
),
代碼很簡(jiǎn)單,就是兩個(gè)不同顏色的矩形
效果
- 默認(rèn)狀態(tài):就是一個(gè)黃色矩形

- 展開狀態(tài):點(diǎn)擊黃色矩形,添加一個(gè)浮層,目前是一個(gè)藍(lán)色矩形;跟隨狀態(tài),并且有間隔。點(diǎn)擊彈出的藍(lán)色矩形,可以隱藏。

由于去掉了
OverlayEntry的全屏屬性,其他地方都能正常點(diǎn)擊,響應(yīng)也正常,頁(yè)面跳轉(zhuǎn)也不受影響。問(wèn)題:由于這里頁(yè)面的底部是
Tab,切換Tab的時(shí)候,失去了“跟隨”目標(biāo),所以彈出的藍(lán)色就只能居中顯示。

變換
作為一個(gè)整體,變換之后,也能很好地跟隨。
- 旋轉(zhuǎn)
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,
),
),
),

- 縮放
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,
),
),
),

- 斜切
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,
),
),
),

- 平移
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,
),
),
),

代碼最后的樣子
// 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;
/// 內(nèi)部變量
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) {
/// 防止重復(fù)創(chuàng)建,不然失去句柄的OverlayEntry將無(wú)法消除
if (_overlayEntry == null) {
_overlayEntry = _createOverlayEntry();
if (_overlayEntry != null) {
Overlay.of(context).insert(_overlayEntry!);
}
}
}
/// 隱藏浮層
void _hideOverlay() {
/// 防止null調(diào)用異常
if (_overlayEntry != null) {
_overlayEntry?.remove();
_overlayEntry = null;
}
}
/// 創(chuàng)建浮層
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