Flutter 上下抽屉效果(一行代码实现)

最近使用flutter实现了一个上下的抽屉效果,使用起来方便,一行代码,话不多说,直接上效果及代码:

效果:

 

 视频效果:

 

 

使用代码:

 

 

核心代码:

 

 

核心代码下载链接(答应我,不白嫖,给颗星):

https://github.com/huangzhiwu1023/flutter_Drawer

demo地址(先给星,再下载):

https://github.com/huangzhiwu1023/flutter_Drawer

 

 代码文字贴(本文参考很多,只为为方便其他开发人员):

  1 import 'package:flutter/cupertino.dart';
  2 import 'package:flutter/gestures.dart';
  3 import 'package:flutter/material.dart';
  4 
  5 //上下抽屉效果
  6 void showDrawer(
  7 BuildContext context,
  8 Widget dragWidget,
  9 double minHight,
 10 double maxHight
 11 ) {
 12 showModalBottomSheet(
 13 context: context,
 14 isScrollControlled: true,
 15 isDismissible: false,
 16 enableDrag: false,
 17 builder: (BuildContext context) {
 18 return Stack(
 19 children: [
 20 GestureDetector(
 21 onTap: () {
 22 Navigator.of(context).pop();
 23 },
 24 child: Container(
 25 color: Color(0x03000000),
 26 width: MediaQuery.of(context).size.width,
 27 height: MediaQuery.of(context).size.height * 0.7,
 28 ),
 29 ),
 30 Align(
 31 alignment: Alignment.bottomCenter,
 32 child: DrawerContainer(
 33 minHight: minHight,
 34 maxHight: maxHight,
 35 dragWidget: dragWidget,
 36 
 37 ///抽屉标题点击事件回调
 38 ),
 39 ),
 40 ],
 41 );
 42 });
 43 }
 44 
 45 ///抽屉内容Widget
 46 class DrawerContainer extends StatefulWidget {
 47 ///抽屉主体内容
 48 final Widget dragWidget;
 49 
 50 ///默认显示的高度与屏幕的比率
 51 final double minHight;
 52 
 53 ///可显示的最大高度 与屏幕的比率
 54 final double maxHight;
 55 
 56 ///抽屉滑动状态回调
 57 final Function(bool isOpen) dragCallBack;
 58 
 59 ///是否显示标题
 60 final bool isShowHeader;
 61 
 62 ///是否直接显示最大
 63 final bool isShowMax;
 64 
 65 ///背景圆角
 66 final double cornerRadius;
 67 
 68 ///滑动结束时 自动滑动到底部或者顶部的时间
 69 final Duration duration;
 70 
 71 ///背景颜色
 72 final Color backGroundColor;
 73 
 74 ///滑动位置超过这个位置,会滚到顶部;
 75 ///小于,会滚动底部。
 76 ///向上或者向下滑动的临界值
 77 final double maxOffsetDistance;
 78 
 79 ///抽屉控制器
 80 final DragController controller = DragController();
 81 
 82 ///抽屉中滑动视图的控制器
 83 
 84 ///配置为true时
 85 ///当抽屉为打开时,列表滑动到了顶部,再向下滑动时,抽屉会关闭
 86 ///当抽屉为关闭时,列表向上滑动,抽屉会自动打开
 87 final bool useAtEdge;
 88 
 89 DrawerContainer(
 90 {Key key,
 91 @required this.dragWidget,
 92 this.minHight = 460,
 93 this.maxHight = 260,
 94 this.cornerRadius = 12,
 95 this.backGroundColor = Colors.white,
 96 this.isShowHeader = true,
 97 this.isShowMax = false,
 98 this.useAtEdge = false,
 99 this.duration = const Duration(milliseconds: 250),
100 this.maxOffsetDistance = 2.5,
101 this.dragCallBack});
102 
103 @override
104 _DrawerContainerState createState() => _DrawerContainerState();
105 }
106 
107 class _DrawerContainerState extends State<DrawerContainer>
108 with TickerProviderStateMixin {
109 ///动画控制器
110 AnimationController animalController;
111 
112 ///可显示的最大高度 具体的像素
113 double maxChildSize;
114 
115 ///默认显示的高度 具体的像素
116 double initialChildSize;
117 double maxOffsetDistance;
118 
119 ///抽屉的偏移量
120 double offsetDistance;
121 
122 ///动画
123 Animation<double> animation;
124 
125 ///快速轻扫标识
126 ///就是指手指在抽屉上快速的轻扫一下
127 bool isFiling = false;
128 
129 ///为true时为打开状态
130 ///初始化显示时为闭合状态
131 bool isOpen = false;
132 
133 ///开始时的位置
134 double startOffset = 0;
135 
136 ///开始滑动时会更新此标识
137 ///是否在顶部或底部
138 bool atEdge = false;
139 
140 @override
141 void initState() {
142 super.initState();
143 
144 ///创建动画控制器
145 /// widget.duration 配置的是抽屉的自动打开与关闭滑动所用的时间
146 animalController =
147 AnimationController(vsync: this, duration: widget.duration);
148 
149 ///添加控制器监听
150 if (widget.controller != null) {
151 widget.controller.setOpenDragListener((value) {
152 if (value == 1) {
153 ///向上
154 offsetDistanceOpen(isCallBack: false);
155 print("向上");
156 } else {
157 ///向下
158 offsetDistanceClose(isCallBack: false);
159 print("向下");
160 }
161 });
162 }
163 }
164 
165 ///初始化时,在initState()之后立刻调用
166 @override
167 void didChangeDependencies() {
168 super.didChangeDependencies();
169 
170 ///State 有一个属性是mounted 用来标识State当前是否正确绑定在View树中。
171 ///当创建 State 对象,并在调用 State.initState 之前,
172 ///framework 会根据 BuildContext 来标记 mounted,
173 ///然后在 State的生命周期里面,这个 mounted 属性不会改变,
174 ///直至 framework 调用 State.dispose
175 if (mounted) {
176 if (maxChildSize == null) {
177 ///计算抽屉可展开的最大值
178 maxChildSize = widget.maxHight;
179 
180 ///计算抽屉关闭时的高度
181 initialChildSize = widget.minHight;
182 }
183 
184 ///计算临界值
185 if (widget.maxOffsetDistance == null) {
186 ///计算滑动结束向上或者向下滑动的临界值
187 maxOffsetDistance = (maxChildSize - initialChildSize) / 3 * 2;
188 } else {
189 maxOffsetDistance =
190 (maxChildSize - initialChildSize) / widget.maxOffsetDistance;
191 }
192 
193 ///初始化偏移量 为抽屉的关闭状态
194 offsetDistance = initialChildSize;
195 }
196 }
197 
198 @override
199 void dispose() {
200 animalController.dispose();
201 super.dispose();
202 }
203 
204 
205 此处隐藏很多代码,白嫖专用
206 
207 
208 Widget buildChild() {
209 return Container(
210 decoration: BoxDecoration(
211 ///背景颜色设置
212 color: widget.backGroundColor,
213 
214 ///只上部分的圆角
215 borderRadius: BorderRadius.only(
216 ///左上角
217 topLeft: Radius.circular(widget.cornerRadius),
218 
219 ///右上角
220 topRight: Radius.circular(widget.cornerRadius),
221 ),
222 ),
223 
224 ///可滑动的Widget 这里构建的是一个
225 child: Column(
226 children: [
227 ///默认显示的标题横线
228 buildHeader(),
229 
230 ///Column中使用滑动视图需要结合
231 ///Expanded填充页面视图
232 Expanded(
233 ///通知(Notification)是Flutter中一个重要的机制,在widget树中,
234 ///每一个节点都可以分发通知,通知会沿着当前节点向上传递,
235 ///所有父节点都可以通过NotificationListener来监听通知
236 child: NotificationListener(
237 ///子Widget中的滚动组件滑动时就会分发滚动通知
238 child: GestureDetector(
239 behavior: HitTestBehavior.translucent,
240 onTap: () {
241 if (isOpen) {
242 offsetDistanceClose();
243 } else {
244 offsetDistanceOpen();
245 }
246 setState(() {});
247 },
248 child: Container(
249 child: widget.dragWidget,
250 padding: EdgeInsets.only(top: 0),
251 ),
252 ),
253 
254 ///每当有滑动通知时就会回调此方法
255 onNotification: (Notification notification) {
256 ///滚动处理 用来处理抽屉中的子列表项中的滑动
257 ///与抽屉的联动效果
258 scrollNotificationFunction(notification);
259 return true;
260 },
261 ),
262 )
263 ],
264 ),
265 );
266 }
267 
268 ///滚动处理 用来处理抽屉中的子列表项中的滑动
269 void scrollNotificationFunction(Notification notification) {
270 ///通知类型
271 switch (notification.runtimeType) {
272 case ScrollStartNotification:
273 print("开始滚动");
274 ScrollStartNotification scrollNotification = notification;
275 ScrollMetrics metrics = scrollNotification.metrics;
276 
277 ///当前位置
278 startOffset = metrics.pixels;
279 
280 ///是否在顶部或底部
281 atEdge = metrics.atEdge;
282 break;
283 case ScrollUpdateNotification:
284 print("正在滚动");
285 ScrollUpdateNotification scrollNotification = notification;
286 
287 ///获取滑动位置信息
288 ScrollMetrics metrics = scrollNotification.metrics;
289 
290 ///当前位置
291 double pixels = metrics.pixels;
292 
293 ///当前滑动的位置 - 开始滑动的位置
294 /// 值大于0表示向上滑动
295 /// 向上滑动时当抽屉没有打开时
296 /// 根据配置 widget.useAtEdge 来决定是否
297 /// 自动向上滑动打开抽屉
298 double flag = pixels - startOffset;
299 if (flag > 0 && !isOpen && widget.useAtEdge) {
300 ///打开抽屉
301 offsetDistanceOpen();
302 }
303 break;
304 case ScrollEndNotification:
305 print("滚动停止");
306 break;
307 case OverscrollNotification:
308 print("滚动到边界");
309 
310 ///startOffset记录的是开始滚动时的位置信息
311 ///atEdge 为true时为边界
312 ///widget.useAtEdge 是在使用组件时的配置是否启用
313 ///当 startOffset==0.0 & atEdge 为true 证明是在顶部向下滑动
314 ///在顶部向下滑动时 抽屉打开时就关闭
315 if (startOffset == 0.0 && atEdge && isOpen && widget.useAtEdge) {
316 offsetDistanceClose();
317 }
318 break;
319 }
320 }
321 
322 ///开启抽屉
323 void offsetDistanceOpen({bool isCallBack = true}) {
324 ///性能优化 当抽屉为关闭状态时再开启
325 if (!isOpen) {
326 ///不设置抽屉的偏移
327 double end = 0;
328 
329 ///从当前的位置开始
330 double start = offsetDistance;
331 
332 ///执行动画 从当前抽屉的偏移位置 过渡到0
333 ///偏移量为0时,抽屉完全显示出来,呈打开状态
334 offsetDistanceFunction(start, end, isCallBack);
335 }
336 }
337 
338 ///关闭抽屉
339 void offsetDistanceClose({bool isCallBack = true}) {
340 ///性能优化 当抽屉为打开状态时再关闭
341 if (isOpen) {
342 ///将抽屉移动到底部
343 double end = maxChildSize - initialChildSize;
344 
345 ///从当前的位置开始
346 double start = offsetDistance;
347 
348 ///执行动画过渡操作
349 offsetDistanceFunction(start, end, isCallBack);
350 }
351 }
352 
353 ///动画滚动操作
354 ///[start]开始滚动的位置
355 ///[end]滚动结束的位置
356 ///[isCallBack]是否触发状态回调
357 void offsetDistanceFunction(double start, double end, bool isCallBack) {
358 ///判断抽屉是否打开
359 if (end == 0.0) {
360 ///当无偏移量时 抽屉是打开状态
361 isOpen = true;
362 } else {
363 ///当有偏移量时 抽屉是关闭状态
364 isOpen = false;
365 }
366 
367 ///抽屉状态回调
368 ///当调用 dragController 的open与close方法
369 ///来触发时不使用回调
370 if (widget.dragCallBack != null && isCallBack) {
371 widget.dragCallBack(isOpen);
372 }
373 // print(" start $start end $end");
374 
375 ///动画插值器
376 ///easeOut 先快后慢
377 CurvedAnimation curve =
378 new CurvedAnimation(parent: animalController, curve: Curves.easeOut);
379 
380 ///动画变化满园
381 animation = Tween(begin: start, end: end).animate(curve)
382 ..addListener(() {
383 offsetDistance = animation.value;
384 setState(() {});
385 });
386 
387 ///开启动画
388 animalController.reset();
389 animalController.forward();
390 }
391 
392 ///构建小标题横线
393 Widget buildHeader() {
394 ///根据配置来决定是否构建标题
395 if (widget.isShowHeader) {
396 return Row(
397 ///居中
398 mainAxisAlignment: MainAxisAlignment.center,
399 children: [
400 InkWell(
401 onTap: () {
402 if (isOpen) {
403 offsetDistanceClose();
404 } else {
405 offsetDistanceOpen();
406 }
407 setState(() {});
408 },
409 child: Container(
410 height: 10,
411 width: 320,
412 child: Align(
413 alignment: Alignment(0.0, 1.0),
414 child: Container(
415 height: 6,
416 width: 60,
417 decoration: BoxDecoration(
418 color: (isOpen || widget.isShowMax)
419 ? Colors.blue
420 : Colors.grey,
421 borderRadius: BorderRadius.all(Radius.circular(6)),
422 border: Border.all(color: Colors.grey[600], width: 1.0)),
423 ),
424 ),
425 ),
426 )
427 ],
428 );
429 } else {
430 return SizedBox();
431 }
432 }
433 
434 ///手势识别
435 GestureRecognizerFactoryWithHandlers<CustomVerticalDragGestureRecognizer>
436 getRecognizer() {
437 ///手势识别器工厂
438 return GestureRecognizerFactoryWithHandlers<
439 CustomVerticalDragGestureRecognizer>(
440 
441 ///参数一 自定义手势识别
442 buildCustomGecognizer,
443 
444 ///参数二 手势识别回调
445 buildCustomGecognizer2);
446 }
447 
448 ///创建自定义手势识别
449 CustomVerticalDragGestureRecognizer buildCustomGecognizer() {
450 return CustomVerticalDragGestureRecognizer(filingListener: (bool isFiling) {
451 ///滑动结束的回调
452 ///为true 表示是轻扫手势
453 this.isFiling = isFiling;
454 print("isFling $isFiling");
455 });
456 }
457 
458 ///手势识别回调
459 buildCustomGecognizer2(
460 CustomVerticalDragGestureRecognizer gestureRecognizer) {
461 ///手势回调监听
462 gestureRecognizer
463 
464 ///开始拖动回调
465 ..onStart = _handleDragStart
466 
467 ///拖动中的回调
468 ..onUpdate = _handleDragUpdate
469 
470 ///拖动结束的回调
471 ..onEnd = _handleDragEnd;
472 }
473 
474 ///手指开始拖动时
475 void _handleDragStart(DragStartDetails details) {
476 ///更新标识为普通滑动
477 isFiling = false;
478 }
479 
480 ///手势拖动抽屉时移动抽屉的位置
481 void _handleDragUpdate(DragUpdateDetails details) {
482 ///偏移量累加
483 offsetDistance = offsetDistance + details.delta.dy;
484 setState(() {});
485 }
486 
487 ///当拖拽结束时调用
488 void _handleDragEnd(DragEndDetails details) {
489 ///当快速滑动时[isFiling]为true
490 if (isFiling) {
491 ///当前抽屉是关闭状态时打开
492 if (!isOpen) {
493 ///向上
494 offsetDistanceOpen();
495 } else {
496 ///当前抽屉是打开状态时关闭
497 ///向下
498 offsetDistanceClose();
499 }
500 } else {
501 ///可滚动范围中再开启动画
502 if (offsetDistance > 0) {
503 ///这个判断通过,说明已经child位置超过警戒线了,需要滚动到顶部了
504 if (offsetDistance < widget.maxOffsetDistance) {
505 ///向上
506 offsetDistanceOpen();
507 } else {
508 ///向下
509 offsetDistanceClose();
510 }
511 //print(
512 // "${MediaQuery.of(context).size.height} widget.maxOffsetDistance ${widget.maxOffsetDistance} widget.maxChildSize $maxChildSize widget.initialChildSize $initialChildSize");
513 }
514 }
515 }
516 }
517 
518 ///抽屉状态监听
519 typedef OpenDragListener = void Function(int value);
520 
521 ///抽屉控制器
522 class DragController {
523 OpenDragListener _openDragListener;
524 
525 ///控制器中添加监听
526 setOpenDragListener(OpenDragListener listener) {
527 _openDragListener = listener;
528 }
529 
530 ///打开抽屉
531 void open() {
532 if (_openDragListener != null) {
533 _openDragListener(1);
534 }
535 }
536 
537 ///关闭抽屉
538 void close() {
539 if (_openDragListener != null) {
540 _openDragListener(2);
541 }
542 }
543 }
544 
545 typedef FilingListener = void Function(bool isFiling);
546 
547 class CustomVerticalDragGestureRecognizer
548 extends VerticalDragGestureRecognizer {
549 ///轻扫监听
550 final FilingListener filingListener;
551 
552 ///保存手势点的集合
553 final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{};
554 
555 CustomVerticalDragGestureRecognizer({Object debugOwner, this.filingListener})
556 : super(debugOwner: debugOwner);
557 
558 @override
559 void addPointer(PointerEvent event) {
560 super.addPointer(event);
561 
562 ///添加一个VelocityTracker
563 _velocityTrackers[event.pointer] = VelocityTracker();
564 }
565 
566 @override
567 void handleEvent(PointerEvent event) {
568 super.handleEvent(event);
569 if (!event.synthesized &&
570 (event is PointerDownEvent || event is PointerMoveEvent)) {
571 ///主要用跟踪触摸屏事件(flinging事件和其他gestures手势事件)的速率
572 final VelocityTracker tracker = _velocityTrackers[event.pointer];
573 assert(tracker != null);
574 
575 ///将指定时间的位置添加到跟踪器
576 tracker.addPosition(event.timeStamp, event.position);
577 }
578 }
579 
580 @override
581 void didStopTrackingLastPointer(int pointer) {
582 final double minVelocity = minFlingVelocity ?? kMinFlingVelocity;
583 final double minDistance = minFlingDistance ?? kTouchSlop;
584 final VelocityTracker tracker = _velocityTrackers[pointer];
585 
586 ///VelocityEstimate 计算二维速度的
587 final VelocityEstimate estimate = tracker.getVelocityEstimate();
588 bool isFling = false;
589 if (estimate != null && estimate.pixelsPerSecond != null) {
590 isFling = estimate.pixelsPerSecond.dy.abs() > minVelocity &&
591 estimate.offset.dy.abs() > minDistance;
592 }
593 _velocityTrackers.clear();
594 if (filingListener != null) {
595 filingListener(isFling);
596 }
597 
598 ///super.didStopTrackingLastPointer(pointer) 会调用[_handleDragEnd]
599 ///所以将[lingListener(isFling);]放在前一步调用
600 super.didStopTrackingLastPointer(pointer);
601 }
602 
603 @override
604 void dispose() {
605 _velocityTrackers.clear();
606 super.dispose();
607 }
608 }

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值