效果展示

在写Flutter时发现并没有图中这种效果,所以自己写了一个:
需要实现的功能如下
- 支持列表展开动画
- 支持背景fade动画
- 切换时禁止收起
- 菜单切换
使用OverlayEntry自定义一个浮窗
因为需要OverlayEntry
跟随点击的按钮,所以需要使用CompositedTransformTarget
与CompositedTransformFollower
关联。
- 自定义Model存放OverlayEntry
dart
class FilterDropDownModel {
final LayerLink layerLink;
final String name;
FilterDropDownModel({required this.name, required this.layerLink});
}
// ...
// 初始化菜单
final List<FilterDropDownModel> _filterList = [
FilterDropDownModel(name: '类别', layerLink: LayerLink()),
FilterDropDownModel(name: '排序', layerLink: LayerLink()),
FilterDropDownModel(name: '区域', layerLink: LayerLink()),
];
LayerLink
用来关联CompositedTransformTarget
与CompositedTransformFollower
dart
CompositedTransformTarget(
link: _filterList[index].layerLink,
child: GestureDetector(
onTap: () {
RenderBox? renderBox;
if (_buttonRowKey.currentContext != null) {
renderBox = _buttonRowKey.currentContext?.findRenderObject() as RenderBox;
}
Overlay.of(context).insert(
_overlayEntry = OverlayEntry(
builder: (context) {
return CompositedTransformFollower(
link: _filterList[index].layerLink,
offset: Offset(0, renderBox!.size.height),
child: Container(
color: Colors.red,
child: ListView(
shrinkWrap: true,
children: [
Text('dataasdasdasd2======$index'),
const Text('dataasdasdasd3======'),
const Text('dataasdasdasd3======'),
const Text('dataasdasdasd=qe====='),
const Text('dataasdasdasd======'),
const Text('dataasdasdasd======'),
],
),
),
);
},
),
);
},
child: Container(
color: Colors.amber,
height: 30,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
_filterList[index].name,
style: const TextStyle(fontSize: 14, color: AppColors.black85),
),
const Icon(
Icons.keyboard_arrow_down_rounded,
size: 20,
color: Colors.black26,
),
],
),
),
),
),
- 由于
CompositedTransformFollower
跟随的坐标为CompositedTransformTarget
的左下角位置

这里计算了Offse的位置,设置CompositedTransformFollower
值为offset:Offset(left, renderBox!.size.height)
即可解决坐标的问题
dart
double left = -(renderBox!.size.width / _filterList.length) * index;
多个tab切换
使用OverlayEntry向页面中添加时,需要先清除之前的OverlayEntry,否则会叠加,当切换tab时调用_overlayEntry
的remove
方法进行清空。
dart
_overlayEntry!.remove();
_overlayEntry = null;
菜单高度自适应时的动画
图中红色菜单菜单使用了Container
进行包裹并没有设置高度,但如果我们需要使用高度动画,就需要利用Tween动画begin/end的值。
此时可以使用AnimatedBuilder
配合SizeTransition
进行动画,SizeTransition
可以实现高度自适应时的收起展开动画效果:
存储一个_isExpanded
变量,用来记录收起展开的状态,该状态用于控制AnimatedBuilder
的动画触发时机:
dart
_isExpanded = !_isExpanded;
// ...
if (_isExpanded) {
_animationController.forward();
} else {
_animationController.reverse();
}
收起展开的效果已经实现,但是收起展开的方向是由中间开始,不是很符合预期,所以这里我们需要稍微改造一下,在SizeTransition
内嵌套一个SlideTransition
组件并设置position
,具体代码如下:
dart
AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return SizeTransition(
sizeFactor: _animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, -1), // 重点
end: Offset.zero,
).animate(_animation),
child: Container(
color: Colors.red,
child: ListView(
shrinkWrap: true,
children: [
Text('dataasdasdasd2======$index'),
const Text('dataasdasdasd3======'),
const Text('dataasdasdasd3======'),
const Text('dataasdasdasd=qe====='),
const Text('dataasdasdasd======'),
const Text('dataasdasdasd======'),
],
),
),
),
);
},
),
最终的收起展开动画如下图:
添加透明遮照层
到这里其实就比较常规,用Stack
组件将CompositedTransformFollower
的child
进行包裹,并创建一个AnimatedBuilder
作为透明遮照层,
dart
Stack(
children: [
AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return FadeTransition(
opacity: _animation,
child: Container(color: _maskColor),
);
},
),
// ...
],
),
总结
菜单容器使用OverlayPortal
CompositedTransformTarget
CompositedTransformFollower
三者结合;
收起展开动画使用AnimatedBuilder
SizeTransition
SlideTransition
三者结合;
渐显的动画直接使用FadeTransition
;