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 }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM