需求
- 支持底部弹出对话框。
- 支持手势滑动关闭。
- 支持在widget中嵌入引用。
- 支持底部弹出框弹出后不影响其他操作。
- 支持弹出框中内容固定头部和下面列表时,支持触摸头部并在列表不在头部的时候支持滑动关闭
简述
通过上面的需求可知,就是在界面中可以支持底部弹出一个弹出框,但是又不影响除了这个弹出框外其他操作,同时支持手势滑动关闭。想到这里我们其实会想到一个控件DraggableScrollableSheet,Flutter提供一种可以支持在widget中引入的底部弹出布局,同时不影响其他的操作,所以我就使用这个控件测试了下,发现一个问题,就是当底部弹出框内容为上面有个头部,底部是个列表时,每次都需要列表滑动到顶部的时候才可以手势滑动关闭,所以需要支持触摸顶部布局也支持手势滑动关闭,所以在此控件上做一个修改。
效果
代码如下
`import 'package:flutter/material.dart'; /// 底部弹出Widget /// 1、支持手势下拉关闭 /// 2、支持动画弹出收起 /// 3、支持弹出无法关闭 class DragBottomSheetWidget extends StatefulWidget { DragBottomSheetWidget({ required Key key, required this.builder, this.duration, this.childHeightRatio = 0.8, this.onStateChange, }) : super(key: key); Function(bool)? onStateChange; final double childHeightRatio; final Duration? duration; final ScrollableWidgetBuilder builder; @override State<DragBottomSheetWidget> createState() => DragBottomSheetWidgetState(); } class DragBottomSheetWidgetState extends State<DragBottomSheetWidget> with TickerProviderStateMixin { final controller = DraggableScrollableController(); //是否可以关闭 bool isCanClose = true; //是否展开 bool isExpand = false; double verticalDistance = 0; @override void initState() { super.initState(); } @override void dispose() { controller.dispose(); super.dispose(); } Future show({bool isCanClose = true}) { return controller .animateTo(1, duration: widget.duration ?? const Duration(milliseconds: 200), curve: Curves.linear) .then((value) { if (!isCanClose) { setState(() { this.isCanClose = isCanClose; }); } }); } void hide() { controller.animateTo( 0, duration: widget.duration ?? const Duration(milliseconds: 200), curve: Curves.linear, ); } void _dragJumpTo(double y) { var size = y / MediaQuery.of(context).size.height; controller.jumpTo(widget.childHeightRatio - size); } void _dragEndChange() { controller.size >= widget.childHeightRatio / 2 ? controller.animateTo( 1, duration: widget.duration ?? const Duration(milliseconds: 200), curve: Curves.linear, ) : hide(); } @override Widget build(BuildContext context) { return NotificationListener<DraggableScrollableNotification>( onNotification: (notification) { if (notification.extent == widget.childHeightRatio) { if (!isExpand) { isExpand = true; widget.onStateChange?.call(true); } } else if (notification.extent < 0.00001) { if (isExpand) { isExpand = false; widget.onStateChange?.call(false); } } return true; }, child: DraggableScrollableSheet( initialChildSize: isCanClose && !isExpand ? 0 : widget.childHeightRatio, minChildSize: isCanClose ? 0 : widget.childHeightRatio, maxChildSize: widget.childHeightRatio, expand: true, snap: true, controller: controller, builder: (BuildContext context, ScrollController scrollController) { return GestureDetector( behavior: HitTestBehavior.translucent, onVerticalDragDown: (details) { verticalDistance = 0; }, onVerticalDragUpdate: (details) { verticalDistance += details.delta.dy; _dragJumpTo(verticalDistance); }, onVerticalDragEnd: (details) { _dragEndChange(); }, child: widget.builder.call(context, scrollController), ); }, ), ); } }
使用
import 'package:flutter/material.dart'; import 'package:flutter_xy/widgets/xy_app_bar.dart'; import 'package:flutter_xy/xydemo/darg/drag_bottom_sheet_widget.dart'; class DragBottomSheetPage extends StatefulWidget { const DragBottomSheetPage({super.key}); @override State<DragBottomSheetPage> createState() => _DragBottomSheetPageState(); } class _DragBottomSheetPageState extends State<DragBottomSheetPage> { final GlobalKey<DragBottomSheetWidgetState> bottomSheetKey = GlobalKey<DragBottomSheetWidgetState>(); bool isExpand = false; @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.deepPurple.withAlpha(50), appBar: XYAppBar( title: "底部弹出布局", backgroundColor: Colors.transparent, titleColor: Colors.white, onBack: () { Navigator.pop(context); }, ), body: Stack( children: [ Positioned( top: 0, child: Column( mainAxisSize: MainAxisSize.max, children: [ InkWell( onTap: () { if (isExpand) { bottomSheetKey.currentState?.hide(); } else { bottomSheetKey.currentState?.show(); } }, child: Container( width: MediaQuery.of(context).size.width - 60, alignment: Alignment.center, height: 50, margin: const EdgeInsets.symmetric(horizontal: 30), decoration: BoxDecoration( borderRadius: BorderRadius.circular(32), color: Colors.deepPurple, ), child: Text( isExpand ? "收起" : "弹出", style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white, ), ), ), ), const SizedBox(height: 20), ], )), Positioned( child: DragBottomSheetWidget( key: bottomSheetKey, onStateChange: (isExpand) { setState(() { this.isExpand = isExpand; }); }, builder: (BuildContext context, ScrollController scrollController) { return Container( alignment: Alignment.topLeft, child: Stack( children: [ Container( height: 50, decoration: const BoxDecoration( borderRadius: BorderRadius.only( topLeft: Radius.circular(16), topRight: Radius.circular(16)), color: Colors.yellow, ), ), Expanded( child: Container( margin: const EdgeInsets.only(top: 50), child: ListView.builder( itemCount: 500, controller: scrollController, itemBuilder: (context, index) { return index % 2 == 0 ? Container( height: 100, width: MediaQuery.of(context).size.width, color: Colors.red, ) : Container( height: 100, width: MediaQuery.of(context).size.width, color: Colors.blue, ); }, ), ), ), ], ), ); }, ), ), ], ), ); } }