最近使用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 }