场景
有时候需要在指定位置进行 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),),],),),);}
}