一、先来看下实现的效果
- 实现上面的效果需要解决俩个问题
- 当列表进行向下滑动到顶部的时候,继续滑动可以让弹窗向下收起来
- 弹出上下拖动的时候,视图内容跟着上下移动、缩放大小
二、实现弹窗上下滑动的时候,动态改变内容区的位置和大小
- 通过
showModalBottomSheet
显示底部对话框
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.white,
transitionAnimationController: _controller,
builder: (_) {
///省略部分代码...
},
);
1、那问题来了,怎么去监听对话框当前显示的高度呢?
可以发现
showModalBottomSheet
有一个transitionAnimationController
参数,这个就是对话框显示的动画控制器了值为[0,1],当全部显示是为1。
那么当将弹窗设为固定高度时,就可以通过这个值进行计算了
- 假设我们顶部留的最小空间为:
宽度 = 屏幕宽度,高度 = 屏幕宽度 / (16 / 9)
,那么对话框的高度就等与屏幕高度 - 顶部高度
///屏幕宽度
double get screenWidth => MediaQuery.of(context).size.width;
///屏幕高度
double get screenHeight => MediaQuery.of(context).size.height;
///顶部留的高度
double get topSpaceHeight => screenWidth / (16 / 9);
///对话框高度
double get bottomSheetHeight => screenHeight - topSpaceHeight;
2、监听对话框高度改变
void initState() {
super.initState();
_controller = BottomSheet.createAnimationController(this);
_controller.addListener(() {
final value = _controller.value * bottomSheetHeight;
///更新UI
_bottomSheetController.sink.add(value);
});
}
Widget build(BuildContext context) {
final bottom = MediaQuery.of(context).padding.bottom;
return ColoredBox(
color: Colors.black,
child: Stack(
children: [
StreamBuilder<double>(
stream: _bottomSheetController.stream,
initialData: 0,
builder: (_, snapshot) {
return Container(
height: screenHeight - snapshot.data!,
alignment: Alignment.center,
child: Image.network(
'https://5b0988e595225.cdn.sohucs.com/images/20200112/75b4a498fdaa48c7813419c2d4bac477.jpeg',
),
);
},
),
],
),
);
}
通过上面这样处理,内容区的上移和缩小就已经实现了
三、弹窗内容向下滑动,当滑动到顶继续向下滑动时,可以让对话框继续向下滑动(不打断此次触摸事件)
- 这里借鉴了这位博主的解决方案可以先看一下,https://www.jianshu.com/p/4f2d10750f5c
1、在向下滑动到顶,继续向下的时候,动态改变弹窗内部的高度来达到弹窗下拉的效果,这里本来是想通过改变transitionAnimationController.value的值来改变弹窗的高度,但是实际中发现或的效果不理想,不知道为什么
Widget build(BuildContext context) {
return StreamBuilder<double>(
stream: _dragController.stream,
initialData: widget.height,
builder: (context, snapshot) {
return AnimatedContainer(
height: snapshot.data ?? widget.height,
duration: const Duration(milliseconds: 50),
child: Column(
children: [
widget.pinedHeader ?? const SizedBox.shrink(),
Expanded(
child: Listener(
onPointerMove: (event) {
///没有滚动到顶部不处理
if (_scrollController.offset != 0) {
return;
}
///获取滑动到顶部开始下拉的位置
_startY ??= event.position.dy;
final distance = event.position.dy - _startY!;
///弹窗滑动后剩余高度
if ((widget.height - distance) > widget.height) {
return;
}
_dragController.sink.add(widget.height - distance);
///剩余弹出高度所占百分比
final percent = 1 - distance / widget.height;
///为了处理图片大小缩放需要使用
widget.transitionAnimationController.value = percent;
},
/// 触摸事件结束 恢复可滚动
onPointerUp: (event) {
_startY = null;
if (snapshot.data! <= widget.height * 0.5) {
///下拉到了一半直接关闭
widget.transitionAnimationController.animateTo(0,
duration: const Duration(microseconds: 250));
} else {
///未到一半 恢复展示
_dragController.sink.add(widget.height);
widget.transitionAnimationController.animateTo(1,
duration: const Duration(microseconds: 250));
}
},
child: SingleChildScrollView(
controller: _scrollController,
physics: snapshot.data == widget.height
? const ClampingScrollPhysics()
: const NeverScrollableScrollPhysics(),
child: widget.child,
),
),
),
],
),
);
},
);
}
2、解决原理:
- 使用
Listener
包裹底部可滚动组件,然后监听用户的滑动,当滑动到了最顶部且继续向下滑动就将SingleChildScrollView
的physics
设置为不可滚动 - 同时改变内容的高度,同时也要改变
transitionAnimationController.value
的值这样内容区才会跟着移动,缩放 - 最后在触摸结束的时候进行判断是需要收起弹窗还是关闭弹窗
本文含有隐藏内容,请 开通VIP 后查看