做者:
Android小安
,本文连接:https://blog.csdn.net/Coo123_/article/details/107513839android
一直以为高德地图的首页Drawer滑动起来很漂亮,还有一些科技感,以前用android实现了一遍,趁着最近不忙再用Flutter实现一遍。git
效果图
为了方便区分布局结构,我使用了不一样的颜色程序员

Drawer高度状态
能够看到drawer 高度有三种状况:github
最大高度
距离顶部有一小段空间,这里空间高度定位70,web
drawer的高度为:屏幕高度-70
编程

中等高度
这里咱们将drawer的显示高度定位300微信

最小高度
这里drawer的显示高度定位150app

Drawer的ui 结构

能够看到drawer内部的ui分为三块:编辑器
-
搜索区域 -
多功能区域 -
扩展区域
同时drawer在最大高度和中等高度之间滚动时,多功能区域须要缩进/展开 到 扩展区域ide
代码实现
基本布局
由于窗口最底层须要显示地图,同时drawer要显示不一样的高度,因此这里我采用stack做为跟布局:
size
由mediaQuery.of(context)
得到
@override
Widget build(BuildContext context) {
return Material(
color: Colors.white,
child: Container(
color: Colors.greenAccent,
width: size.width,height: size.height,
child: Stack(
children: <Widget>[
Positioned(
top: initPositionTop,
.......省去Drawer部分代码
)
],
),
),
);
咱们经过positioned
包裹drawer
,而后经过top来控制drawer上下移动的高度,为了捕获触摸事件,咱们须要用GestureDetector
对咱们的drawer进行包裹,代码:
Positioned(
top: initPositionTop,
child: GestureDetector(
onVerticalDragStart: verticalDragStart,
onVerticalDragUpdate: verticalDragUpdate,
onVerticalDragEnd: verticalDragEnd,
///Drawer
child: Container(
width: size.width,height: drawerHeight,
color: Colors.white,
///多功能区域须要实现缩进和站看,因此这里使用stack做为drawer的内部根布局
child: Stack(
children: <Widget>[
///搜索区域
Container(
alignment: Alignment.center,
color: Colors.pink,
width: size.width,height: searchHeight - minHeight,
child: Text('我是搜索'),
),
///多功能区域
Positioned(
top: searchHeight - minHeight,
child: Container(
alignment: Alignment.center,
color: Colors.white,
width: size.width,height: rowH * 3+20,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
normalRow(),
normalRow(),
Container(
color: Colors.grey[300],
width: size.width,height: rowH,
alignment: Alignment.topCenter,
child: Text('常去的地方',style: TextStyle(fontSize: 18,color: Colors.black),),
)
],
),
),
),
///扩展区域
Positioned(
top: expandPosTop + topArea,
child: Container(
color: Colors.lightGreen,
alignment: Alignment.topCenter,
width: size.width,height: drawerHeight - searchHeight -rowH,///这里须要在滚动时向下滑动
child: Text('我是扩展区域'),
),
),
],
),
),
),
)
至此整个UI布局就搞定了,接下来处理手势滑动。
手势处理
首先咱们只须要处理垂直滑动,所以在回调中,咱们实现这三个方法:
child: GestureDetector(
onVerticalDragStart: verticalDragStart, ///第一次触摸屏幕时触发
onVerticalDragUpdate: verticalDragUpdate,///滑动时会持续调用此方法
onVerticalDragEnd: verticalDragEnd,///手指离屏时会调用此方法
dragStart
当手指触摸屏幕时,咱们须要记录下点击位置:
Offset lastPos;
void verticalDragStart(DragStartDetails details){
lastPos = details.globalPosition;
}
dragUpdate
以后在用户滑动时,咱们刷新drawer的position
的top值(即initPositionTop
),以此来达到drawer的滑动效果。
若是只是简单的滑动,咱们能够直接将initPositionTop
加上滑动差值便可,可是根据经验判断,后面确定会须要滑动方向,因此我在这里顺便把滑动的方向也记录下来,这个能够根据滑动差值的正负来判断:
enum SlideDirection{
Up,
Down
}
void verticalDragUpdate(DragUpdateDetails details){
double dis = details.globalPosition.dy - lastPos.dy;
if(dis<0){
direction = SlideDirection.Up;
}else{
direction = SlideDirection.Down;
}
if(direction == SlideDirection.Up){
if(initPositionTop <= top1+cacheDy) return;
}else if(direction == SlideDirection.Down){
if(initPositionTop >= top3-cacheDy) return;
}
initPositionTop += dis;
///处理完一次后,记下当前的位置
lastPos = details.globalPosition;
///这里个方法暂时不用管
refreshExpandWidgetTop();
setState(() {
});
}
dragEnd
这里咱们什么都不须要作,代码以下:
void verticalDragEnd(DragEndDetails details){
}
这时咱们运行发现,drawer能够跟着手指的滑动表现收起/展开
的效果,可是咱们的手指离屏后,drawer也就停在那了(原始版抽屉)。
参见高德,能够看到抽屉始终会停留在三级状态中的一级,若是手指滑动超出界限/未到界限,抽屉会自动滚动/滚回到最近的等级高度,如今咱们要进行升级了。
升级
准备工做
首先咱们要记录一下三个高度对应的position
的top
值(drawer
的实时top值之后就叫initPositionTop
了):
///stack 中 根container 的position 的top 值的三种状况
double top1;// DrawerLvl lvl 1
double top2;// DrawerLvl lvl 2
double top3;// DrawerLvl lvl 3
double initPositionTop;
///初始化
top1 = size.height - drawerHeight;
top2 = size.height - searchHeight;
top3 = size.height - minHeight;
///页面最初显示的是 top2等级
initPositionTop = top2;
而后咱们须要记录一下drawer的状态:
enum DrawerLvl{
LVL1,
LVL2,
LVL3
}
///抽屉层级
DrawerLvl drawerLvl = DrawerLvl.LVL2;
///滑动方向
SlideDirection direction;
分别对应top1
,top2
,top3
当咱们滑动时,若是从top1
滑向top2
,可是未到top2
的高度,就松手了,这时咱们须要完成剩下的操做,这就用到了
AnimationController
Animation
animationController = AnimationController(vsync: this,duration: Duration(milliseconds: 300));
具体应该滑回top1
,仍是滑向top2
呢?这里咱们须要定两个阈值:
///层级之间的阈值
double threshold1To2;
double threshold2To3;
///构造函数
DrawerDemoState(this.size){
drawerHeight = size.height-paddingTop;
threshold1To2 = size.height/3;
threshold2To3 = size.height - 250;
}
升级 dragStart
如今咱们开始对原有的方法升级
void verticalDragStart(DragStartDetails details){
///肯定drawer 初始状态
markDrawerLvl();
///将原有的动画置空
animation = null;
///将控制器中止和复位
if(animationController.isAnimating){
animationController.stop();
}
animationController.reset();
lastPos = details.globalPosition;
log('start', '$initPositionTop');
}
当用户触摸时,咱们先要肯定drawer的初始状态:
markDrawerLvl(){
double l1 = (top1-initPositionTop).abs();
double l2 = (top2-initPositionTop).abs();
double l3 = (top3-initPositionTop).abs();
if(l1 == (math.min(l1, math.min(l2, l3)))){
drawerLvl = DrawerLvl.LVL1;
}else if(l2 == (math.min(l1, math.min(l2, l3)))){
drawerLvl = DrawerLvl.LVL2;
}else {
drawerLvl = DrawerLvl.LVL3;
}
}
升级 dragUpdate
void verticalDragUpdate(DragUpdateDetails details){
double dis = details.globalPosition.dy - lastPos.dy;
if(dis<0){
direction = SlideDirection.Up;
}else{
direction = SlideDirection.Down;
}
///cacheDy 避免滑动过快溢出范围致使的判断失效
if(direction == SlideDirection.Up){
///避免drawer滑出屏幕
if(initPositionTop <= top1+cacheDy) return;
}else if(direction == SlideDirection.Down){
if(initPositionTop >= top3-cacheDy) return;
}
initPositionTop += dis;
lastPos = details.globalPosition;
///暂时不用管
refreshExpandWidgetTop();
setState(() {
});
}
升级dragEnd
在用户手指离开屏幕时,咱们就要进行处理了,即:drawer
是继续滚动,仍是复位。
void verticalDragEnd(DragEndDetails details){
adjustPositionTop(details);
}
这个方法较长,我将说明写在注释里
void adjustPositionTop(DragEndDetails details){
switch(direction){
case SlideDirection.Up:
if(details.velocity.pixelsPerSecond.dy.abs() > thresholdV){
///用户fling速度超过阈值后,直接断定为滑向下一级别
switch(drawerLvl){
case DrawerLvl.LVL1:
///处于顶部上滑时,不须要作处理
// TODO: Handle this case.
break;
case DrawerLvl.LVL2:
slideTo(begin: initPositionTop,end: top1);
break;
case DrawerLvl.LVL3:
slideTo(begin: initPositionTop,end: top2);
break;
}
}else{
///未超过阈值的话,咱们则进行复位或者继续滑动
if(initPositionTop >= top1 && initPositionTop <= top2){
///在一、2级之间
这里根据手指离屏位置,进行复位或者滑向下一等级高度的处理
if(initPositionTop <= threshold1To2){
///小于二分之一屏幕高度 滚向top1
slideTo(begin:initPositionTop, end:top1);
}else{
///滑向top2
slideTo(begin: initPositionTop,end: top2);
}
}else if(initPositionTop >= top2 && initPositionTop <= top3){
///2-3之间
if(initPositionTop <= threshold2To3){
///滑向2
slideTo(begin: initPositionTop,end: top2);
}else{
///滑向3
slideTo(begin: initPositionTop,end: top3);
}
}
}
break;
case SlideDirection.Down:
///原理同上
if(details.velocity.pixelsPerSecond.dy.abs() > thresholdV){
switch(drawerLvl){
case DrawerLvl.LVL1:
slideTo(begin: initPositionTop,end: top2);
break;
case DrawerLvl.LVL2:
slideTo(begin: initPositionTop,end: top3);
break;
case DrawerLvl.LVL3:
//todo nothing
break;
}
}else{
if(initPositionTop >= top1 && initPositionTop <= top2){
///在一、2级之间
if(initPositionTop <= threshold1To2){
///小于二分之一屏幕高度 滚向top1
slideTo(begin: initPositionTop,end:top1);
}else{
///滑向top2
slideTo(begin: initPositionTop,end: top2);
}
}else if(initPositionTop >= top2 && initPositionTop <= top3){
///2-3之间
if(initPositionTop <= threshold2To3){
///滑向2
slideTo(begin: initPositionTop,end: top2);
}else{
///滑向3
slideTo(begin: initPositionTop,end: top3);
}
}
}
break;
}
}
在动画的listener
中,咱们刷新initPositionTop
的值:
animationController.addListener(() {
if(animation == null) return;
///暂时不用管
refreshExpandWidgetTop();
setState(() {
initPositionTop = animation.value;
});
});
至此咱们就相对完善的完成了drawer的滑动功能。
多功能widget 显隐效果
继续观察drawer内部的widget,咱们能够看到在top1和top2之间滚动时,内部的多功能区域也会进行相应的缩进和伸出,接下来咱们实现这个。
UI布局
由于咱们只须要移动扩展区域,就能够实现多功能区的滑出/收起 效果,因此咱们能够用stack来完成基本的布局:
Stack(
children: <Widget>[
///搜索
Container(
alignment: Alignment.center,
color: Colors.pink,
width: size.width,height: searchHeight - minHeight,
child: Text('我是搜索'),
),
///多功能区
Positioned(
top: searchHeight - minHeight,
child: Container(
alignment: Alignment.center,
color: Colors.white,
width: size.width,height: rowH * 3+20,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
normalRow(),
normalRow(),
Container(
color: Colors.grey[300],
width: size.width,height: rowH,
alignment: Alignment.topCenter,
child: Text('常去的地方',style: TextStyle(fontSize: 18,color: Colors.black),),
)
],
),
),
),
///扩展区
Positioned(
top: expandPosTop + topArea,
child: Container(
color: Colors.lightGreen,
alignment: Alignment.topCenter,
width: size.width,height: drawerHeight - searchHeight -rowH,///这里须要在滚动时向下滑动
child: Text('我是扩展区域'),
),
),
],
),
搜索区和多功能区,只须要调整top,使他们顺序排列便可。
而扩展区,咱们须要在页面初始是遮住一部分多功能区(只漏出一行圆)。
方便起见,将多功能的高度定位
rowH * 3
;
那么扩展区的top初始值就是多功能的top + rowH,这里咱们给扩展区的top值定义一个变量:
expandPosTop = 多功能区的top + rowH
进而,咱们能够肯定,expandPosTop
的变化范围是:
咱们给这个变化值定义一个变量:topArea
topArea = [0 - rowH * 2];
最终扩展区的代码以下:
///扩展区域
Positioned(
top: expandPosTop + topArea,
child: Container(
color: Colors.lightGreen,
alignment: Alignment.topCenter,
width: size.width,height: drawerHeight - searchHeight -rowH,///这里须要在滚动时向下滑动
child: Text('我是扩展区域'),
),
),
总体UI布局就完成了,咱们接着实现滚动功能。
扩展区滑动
咱们在dragUpdate
和动画的listener
中见到过这个方法:
refreshExpandWidgetTop();//这里就是实现对应功能的
这里我把说明写在注释里,方便阅读
///刷新 扩展区域的 position top值
///这里的差值是 rowH * 2
refreshExpandWidgetTop(){
///首先,咱们根据initPositionTop,和top2 - top1 之间的差值,来计算滑动进度
double progress = (initPositionTop-top2).abs() /(top2 - top1).abs();
///判断是从top1滑向top2 仍是反着
if(drawerLvl == DrawerLvl.LVL2){
///lvl2 滑向 lvl3时 不作处理
if(initPositionTop > top2) return;
///以后咱们根据进度,来刷新topArea的值
///这个值老是会在 0 到 rowh*2 这个范围内变化,具体由滑动方向来定
topArea = (progress * (rowH*2).clamp(0, rowH*2));
}else if(drawerLvl == DrawerLvl.LVL1){
///lvl2 滑向 lvl3时 不作处理
if(initPositionTop > top2) return;
topArea = (progress) * (rowH*2).clamp(0, rowH*2);
}
}
当咱们在调用上述方法外面刷新时,就会看到多功能区域的收起/伸出
的效果了(给加点阴影会更好看),至此咱们整个功能就实现了,若是对你有帮助点个赞或和star吧。 :)
https://github.com/bladeofgod/flutter-mock-amap-drawer
---END---
更文不易,点个“在看”支持一下👇
本文分享自微信公众号 - 技术最TOP(Tech-Android)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。