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 }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· 因为Apifox不支持离线,我果断选择了Apipost!