场景
有时候需要在指定位置进行 tip-widget 的弹出与展示,常见的方式是通过给指定位置上的指定 widget 添加 GlobalKey 来实现;
但是,使用这种方式的话,【一】大多数时候都需要进行全局定位转换(localToGlobal)及计算,【二】使用系统 Popup 控件,当 Popup 展示时,列表可能不可滑动,【三】如果需要跟随列表滑动,就需要使用实时监听滑动回调的方式去实时更新 tip-widget 的实时跟随滑动;
以上提到的问题,不仅实现上“费时费力”,还会使相关功能代码分散、不相关功能代码耦合,造成代码维护及迭代上的难度;
所以,在此提出一种“使用 Stack 替代 GlobalKey 的定位 tip-widget 实现”,通过一种“讨巧”的方式,尝试较好地解决以上提到的问题;
当然,解决方法还有很多,骚操作是程序员的乐趣,但是更好地解决问题才是程序员的追求 ~!
效果
说明
这种方案,是通过使用 Stack 将指定 widget 与 tip-widget 进行绑定,主要有两种场景:
1、如果指定 widget 本身的父 widget 就是 Stack 的话,直接把 tip-widget 加入 Stack 进行定位维护就行
2、如果指定 widget 本身的父 widget 不是 Stack 的话,就使用一个 Stack 包裹住指定 widget,然后保证 Stack 的宽高为指定 widget 的宽高,同时设置 Stack 的 clipBehavior 为 Clip.none,最后把 tip-widget 加入 Stack 进行定位维护
如果指定 widget 本身的父 widget 不是 Stack 的话,需要注意以下限定条件:
并不是所有场景都适合使用这种方案进行适配,需要根据指定 widget 的父 widget 的布局特点进行选用
比如 Column 控件,Column 控件对于子 widget 的绘制顺序是由上向下(逻辑在 RenderFlex 的 performLayout() 和 paint(PaintingContext context, Offset offset) 中);
正常情况下,Column 子 widget 间是根据其本身在 Column 中的偏移量紧密衔接的;
但是,如果使用 Stack 的 Clip.none 特性后,Stack 包裹的内容可超范围绘制显示,这时,体现在 Column 的绘制顺序的话,顺序靠后的子 widget 就会覆盖顺序靠前的子 widget 的超范围绘制的内容;
所以,在 Column 中如果 tip-widget 是向上展示的,一般能正常展示,但是,如果 tip-widget 是向下展示的,就会有覆盖的问题。
范例
class _TestPageState extends State<TestPage> {
bool show = false;
void initState() {
super.initState();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Stack妙用')),
body: SingleChildScrollView(
child: Column(
children: [
Container(
width: double.maxFinite,
height: 150,
color: Colors.red.withOpacity(0.5),
),
Container(
width: double.maxFinite,
height: 150,
color: Colors.orange.withOpacity(0.5),
),
Container(
alignment: Alignment.center,
width: double.maxFinite,
height: 150,
color: Colors.yellow.withOpacity(0.5),
child: Container(
color: show ? Colors.red : null,
child: Stack(
clipBehavior: Clip.none,
children: [
GestureDetector(
child: const Icon(Icons.add_circle, size: 50),
onTap: () {
setState(() {
show = !show;
});
},
),
if (show)
Positioned(
top: -90,
left: 30,
child: Container(
alignment: Alignment.center,
color: Colors.red.withOpacity(0.8),
width: 50,
height: 80,
child: const Text('菜单'),
),
),
if (show)
Positioned(
top: 60,
left: 30,
child: Container(
alignment: Alignment.center,
color: Colors.red.withOpacity(0.8),
width: 50,
height: 80,
child: const Text('菜单'),
),
),
],
),
),
),
Container(
width: double.maxFinite,
height: 150,
color: Colors.green.withOpacity(0.5),
),
Container(
width: double.maxFinite,
height: 150,
color: Colors.blue.withOpacity(0.5),
),
Container(
width: double.maxFinite,
height: 150,
color: Colors.indigo.withOpacity(0.5),
),
Container(
width: double.maxFinite,
height: 150,
color: Colors.purple.withOpacity(0.5),
),
],
),
),
);
}
}