做者:
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源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。