Flutter 上下抽屜效果(一行代碼)

最近使用flutter實(shí)現(xiàn)了一個上下的抽屜效果,使用起來方便,一行代碼笤休,話不多說,直接上效果及代碼:


?視頻效果:



使用代碼:



核心代碼:



核心代碼下載鏈接(答應(yīng)我蛤育,不白嫖,給顆星):

https://github.com/huangzhiwu1023/flutter_Drawer

demo地址(先給星葫松,再下載):

https://github.com/huangzhiwu1023/flutter_Drawer


?代碼文字貼(本文參考很多瓦糕,只為為方便其他開發(fā)人員):

import 'package:flutter/cupertino.dart';

import 'package:flutter/gestures.dart';

import 'package:flutter/material.dart';

//上下抽屜效果

void showDrawer(

BuildContext context,

Widget dragWidget,

double minHight,

double maxHight

) {

showModalBottomSheet(

context: context,

isScrollControlled: true,

isDismissible: false,

enableDrag: false,

builder: (BuildContext context) {

return Stack(

children: [

GestureDetector(

onTap: () {

Navigator.of(context).pop();

},

child: Container(

color: Color(0x03000000),

width: MediaQuery.of(context).size.width,

height: MediaQuery.of(context).size.height * 0.7,

),

),

Align(

alignment: Alignment.bottomCenter,

child: DrawerContainer(

minHight: minHight,

maxHight: maxHight,

dragWidget: dragWidget,

///抽屜標(biāo)題點(diǎn)擊事件回調(diào)

),

),

],

);

});

}

///抽屜內(nèi)容Widget

class DrawerContainer extends StatefulWidget {

///抽屜主體內(nèi)容

final Widget dragWidget;

///默認(rèn)顯示的高度與屏幕的比率

final double minHight;

///可顯示的最大高度 與屏幕的比率

final double maxHight;

///抽屜滑動狀態(tài)回調(diào)

final Function(bool isOpen) dragCallBack;

///是否顯示標(biāo)題

final bool isShowHeader;

///是否直接顯示最大

final bool isShowMax;

///背景圓角

final double cornerRadius;

///滑動結(jié)束時 自動滑動到底部或者頂部的時間

final Duration duration;

///背景顏色

final Color backGroundColor;

///滑動位置超過這個位置,會滾到頂部腋么;

///小于咕娄,會滾動底部。

///向上或者向下滑動的臨界值

final double maxOffsetDistance;

///抽屜控制器

final DragController controller = DragController();

///抽屜中滑動視圖的控制器

///配置為true時

///當(dāng)抽屜為打開時珊擂,列表滑動到了頂部圣勒,再向下滑動時,抽屜會關(guān)閉

///當(dāng)抽屜為關(guān)閉時摧扇,列表向上滑動圣贸,抽屜會自動打開

final bool useAtEdge;

DrawerContainer(

{Key key,

@required this.dragWidget,

this.minHight = 460,

this.maxHight = 260,

this.cornerRadius = 12,

this.backGroundColor = Colors.white,

this.isShowHeader = true,

this.isShowMax = false,

this.useAtEdge = false,

this.duration = const Duration(milliseconds: 250),

this.maxOffsetDistance = 2.5,

this.dragCallBack});

@override

_DrawerContainerState createState() => _DrawerContainerState();

}

class _DrawerContainerState extends State<DrawerContainer>

with TickerProviderStateMixin {

///動畫控制器

AnimationController animalController;

///可顯示的最大高度 具體的像素

double maxChildSize;

///默認(rèn)顯示的高度 具體的像素

double initialChildSize;

double maxOffsetDistance;

///抽屜的偏移量

double offsetDistance;

///動畫

Animation<double> animation;

///快速輕掃標(biāo)識

///就是指手指在抽屜上快速的輕掃一下

bool isFiling = false;

///為true時為打開狀態(tài)

///初始化顯示時為閉合狀態(tài)

bool isOpen = false;

///開始時的位置

double startOffset = 0;

///開始滑動時會更新此標(biāo)識

///是否在頂部或底部

bool atEdge = false;

@override

void initState() {

super.initState();

///創(chuàng)建動畫控制器

/// widget.duration 配置的是抽屜的自動打開與關(guān)閉滑動所用的時間

animalController =

AnimationController(vsync: this, duration: widget.duration);

///添加控制器監(jiān)聽

if (widget.controller != null) {

widget.controller.setOpenDragListener((value) {

if (value == 1) {

///向上

offsetDistanceOpen(isCallBack: false);

print("向上");

} else {

///向下

offsetDistanceClose(isCallBack: false);

print("向下");

}

});

}

}

///初始化時,在initState()之后立刻調(diào)用

@override

void didChangeDependencies() {

super.didChangeDependencies();

///State 有一個屬性是mounted 用來標(biāo)識State當(dāng)前是否正確綁定在View樹中扛稽。

///當(dāng)創(chuàng)建 State 對象吁峻,并在調(diào)用 State.initState 之前,

///framework 會根據(jù) BuildContext 來標(biāo)記 mounted在张,

///然后在 State的生命周期里面用含,這個 mounted 屬性不會改變,

///直至 framework 調(diào)用 State.dispose

if (mounted) {

if (maxChildSize == null) {

///計(jì)算抽屜可展開的最大值

maxChildSize = widget.maxHight;

///計(jì)算抽屜關(guān)閉時的高度

initialChildSize = widget.minHight;

}

///計(jì)算臨界值

if (widget.maxOffsetDistance == null) {

///計(jì)算滑動結(jié)束向上或者向下滑動的臨界值

maxOffsetDistance = (maxChildSize - initialChildSize) / 3 * 2;

} else {

maxOffsetDistance =

(maxChildSize - initialChildSize) / widget.maxOffsetDistance;

}

///初始化偏移量 為抽屜的關(guān)閉狀態(tài)

offsetDistance = initialChildSize;

}

}

@override

void dispose() {

animalController.dispose();

super.dispose();

}

此處隱藏很多代碼帮匾,白嫖專用

Widget buildChild() {

return Container(

decoration: BoxDecoration(

///背景顏色設(shè)置

color: widget.backGroundColor,

///只上部分的圓角

borderRadius: BorderRadius.only(

///左上角

topLeft: Radius.circular(widget.cornerRadius),

///右上角

topRight: Radius.circular(widget.cornerRadius),

),

),

///可滑動的Widget 這里構(gòu)建的是一個

child: Column(

children: [

///默認(rèn)顯示的標(biāo)題橫線

buildHeader(),

///Column中使用滑動視圖需要結(jié)合

///Expanded填充頁面視圖

Expanded(

///通知(Notification)是Flutter中一個重要的機(jī)制啄骇,在widget樹中,

///每一個節(jié)點(diǎn)都可以分發(fā)通知瘟斜,通知會沿著當(dāng)前節(jié)點(diǎn)向上傳遞缸夹,

///所有父節(jié)點(diǎn)都可以通過NotificationListener來監(jiān)聽通知

child: NotificationListener(

///子Widget中的滾動組件滑動時就會分發(fā)滾動通知

child: GestureDetector(

behavior: HitTestBehavior.translucent,

onTap: () {

if (isOpen) {

offsetDistanceClose();

} else {

offsetDistanceOpen();

}

setState(() {});

},

child: Container(

child: widget.dragWidget,

padding: EdgeInsets.only(top: 0),

),

),

///每當(dāng)有滑動通知時就會回調(diào)此方法

onNotification: (Notification notification) {

///滾動處理 用來處理抽屜中的子列表項(xiàng)中的滑動

///與抽屜的聯(lián)動效果

scrollNotificationFunction(notification);

return true;

},

),

)

],

),

);

}

///滾動處理 用來處理抽屜中的子列表項(xiàng)中的滑動

void scrollNotificationFunction(Notification notification) {

///通知類型

switch (notification.runtimeType) {

case ScrollStartNotification:

print("開始滾動");

ScrollStartNotification scrollNotification = notification;

ScrollMetrics metrics = scrollNotification.metrics;

///當(dāng)前位置

startOffset = metrics.pixels;

///是否在頂部或底部

atEdge = metrics.atEdge;

break;

case ScrollUpdateNotification:

print("正在滾動");

ScrollUpdateNotification scrollNotification = notification;

///獲取滑動位置信息

ScrollMetrics metrics = scrollNotification.metrics;

///當(dāng)前位置

double pixels = metrics.pixels;

///當(dāng)前滑動的位置 - 開始滑動的位置

/// 值大于0表示向上滑動

/// 向上滑動時當(dāng)抽屜沒有打開時

/// 根據(jù)配置 widget.useAtEdge 來決定是否

/// 自動向上滑動打開抽屜

double flag = pixels - startOffset;

if (flag > 0 && !isOpen && widget.useAtEdge) {

///打開抽屜

offsetDistanceOpen();

}

break;

case ScrollEndNotification:

print("滾動停止");

break;

case OverscrollNotification:

print("滾動到邊界");

///startOffset記錄的是開始滾動時的位置信息

///atEdge 為true時為邊界

///widget.useAtEdge 是在使用組件時的配置是否啟用

///當(dāng) startOffset==0.0 & atEdge 為true 證明是在頂部向下滑動

///在頂部向下滑動時 抽屜打開時就關(guān)閉

if (startOffset == 0.0 && atEdge && isOpen && widget.useAtEdge) {

offsetDistanceClose();

}

break;

}

}

///開啟抽屜

void offsetDistanceOpen({bool isCallBack = true}) {

///性能優(yōu)化 當(dāng)抽屜為關(guān)閉狀態(tài)時再開啟

if (!isOpen) {

///不設(shè)置抽屜的偏移

double end = 0;

///從當(dāng)前的位置開始

double start = offsetDistance;

///執(zhí)行動畫 從當(dāng)前抽屜的偏移位置 過渡到0

///偏移量為0時,抽屜完全顯示出來螺句,呈打開狀態(tài)

offsetDistanceFunction(start, end, isCallBack);

}

}

///關(guān)閉抽屜

void offsetDistanceClose({bool isCallBack = true}) {

///性能優(yōu)化 當(dāng)抽屜為打開狀態(tài)時再關(guān)閉

if (isOpen) {

///將抽屜移動到底部

double end = maxChildSize - initialChildSize;

///從當(dāng)前的位置開始

double start = offsetDistance;

///執(zhí)行動畫過渡操作

offsetDistanceFunction(start, end, isCallBack);

}

}

///動畫滾動操作

///[start]開始滾動的位置

///[end]滾動結(jié)束的位置

///[isCallBack]是否觸發(fā)狀態(tài)回調(diào)

void offsetDistanceFunction(double start, double end, bool isCallBack) {

///判斷抽屜是否打開

if (end == 0.0) {

///當(dāng)無偏移量時 抽屜是打開狀態(tài)

isOpen = true;

} else {

///當(dāng)有偏移量時 抽屜是關(guān)閉狀態(tài)

isOpen = false;

}

///抽屜狀態(tài)回調(diào)

///當(dāng)調(diào)用 dragController 的open與close方法

///來觸發(fā)時不使用回調(diào)

if (widget.dragCallBack != null && isCallBack) {

widget.dragCallBack(isOpen);

}

// print(" start $start end $end");

///動畫插值器

///easeOut 先快后慢

CurvedAnimation curve =

new CurvedAnimation(parent: animalController, curve: Curves.easeOut);

///動畫變化滿園

animation = Tween(begin: start, end: end).animate(curve)

..addListener(() {

offsetDistance = animation.value;

setState(() {});

});

///開啟動畫

animalController.reset();

animalController.forward();

}

///構(gòu)建小標(biāo)題橫線

Widget buildHeader() {

///根據(jù)配置來決定是否構(gòu)建標(biāo)題

if (widget.isShowHeader) {

return Row(

///居中

mainAxisAlignment: MainAxisAlignment.center,

children: [

InkWell(

onTap: () {

if (isOpen) {

offsetDistanceClose();

} else {

offsetDistanceOpen();

}

setState(() {});

},

child: Container(

height: 10,

width: 320,

child: Align(

alignment: Alignment(0.0, 1.0),

child: Container(

height: 6,

width: 60,

decoration: BoxDecoration(

color: (isOpen || widget.isShowMax)

? Colors.blue

: Colors.grey,

borderRadius: BorderRadius.all(Radius.circular(6)),

border: Border.all(color: Colors.grey[600], width: 1.0)),

),

),

),

)

],

);

} else {

return SizedBox();

}

}

///手勢識別

GestureRecognizerFactoryWithHandlers<CustomVerticalDragGestureRecognizer>

getRecognizer() {

///手勢識別器工廠

return GestureRecognizerFactoryWithHandlers<

CustomVerticalDragGestureRecognizer>(

///參數(shù)一 自定義手勢識別

buildCustomGecognizer,

///參數(shù)二 手勢識別回調(diào)

buildCustomGecognizer2);

}

///創(chuàng)建自定義手勢識別

CustomVerticalDragGestureRecognizer buildCustomGecognizer() {

return CustomVerticalDragGestureRecognizer(filingListener: (bool isFiling) {

///滑動結(jié)束的回調(diào)

///為true 表示是輕掃手勢

this.isFiling = isFiling;

print("isFling $isFiling");

});

}

///手勢識別回調(diào)

buildCustomGecognizer2(

CustomVerticalDragGestureRecognizer gestureRecognizer) {

///手勢回調(diào)監(jiān)聽

gestureRecognizer

///開始拖動回調(diào)

..onStart = _handleDragStart

///拖動中的回調(diào)

..onUpdate = _handleDragUpdate

///拖動結(jié)束的回調(diào)

..onEnd = _handleDragEnd;

}

///手指開始拖動時

void _handleDragStart(DragStartDetails details) {

///更新標(biāo)識為普通滑動

isFiling = false;

}

///手勢拖動抽屜時移動抽屜的位置

void _handleDragUpdate(DragUpdateDetails details) {

///偏移量累加

offsetDistance = offsetDistance + details.delta.dy;

setState(() {});

}

///當(dāng)拖拽結(jié)束時調(diào)用

void _handleDragEnd(DragEndDetails details) {

///當(dāng)快速滑動時[isFiling]為true

if (isFiling) {

///當(dāng)前抽屜是關(guān)閉狀態(tài)時打開

if (!isOpen) {

///向上

offsetDistanceOpen();

} else {

///當(dāng)前抽屜是打開狀態(tài)時關(guān)閉

///向下

offsetDistanceClose();

}

} else {

///可滾動范圍中再開啟動畫

if (offsetDistance > 0) {

///這個判斷通過明未,說明已經(jīng)child位置超過警戒線了,需要滾動到頂部了

if (offsetDistance < widget.maxOffsetDistance) {

///向上

offsetDistanceOpen();

} else {

///向下

offsetDistanceClose();

}

//print(

// "${MediaQuery.of(context).size.height} widget.maxOffsetDistance ${widget.maxOffsetDistance} widget.maxChildSize $maxChildSize widget.initialChildSize $initialChildSize");

}

}

}

}

///抽屜狀態(tài)監(jiān)聽

typedef OpenDragListener = void Function(int value);

///抽屜控制器

class DragController {

OpenDragListener _openDragListener;

///控制器中添加監(jiān)聽

setOpenDragListener(OpenDragListener listener) {

_openDragListener = listener;

}

///打開抽屜

void open() {

if (_openDragListener != null) {

_openDragListener(1);

}

}

///關(guān)閉抽屜

void close() {

if (_openDragListener != null) {

_openDragListener(2);

}

}

}

typedef FilingListener = void Function(bool isFiling);

class CustomVerticalDragGestureRecognizer

extends VerticalDragGestureRecognizer {

///輕掃監(jiān)聽

final FilingListener filingListener;

///保存手勢點(diǎn)的集合

final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{};

CustomVerticalDragGestureRecognizer({Object debugOwner, this.filingListener})

: super(debugOwner: debugOwner);

@override

void addPointer(PointerEvent event) {

super.addPointer(event);

///添加一個VelocityTracker

_velocityTrackers[event.pointer] = VelocityTracker();

}

@override

void handleEvent(PointerEvent event) {

super.handleEvent(event);

if (!event.synthesized &&

(event is PointerDownEvent || event is PointerMoveEvent)) {

///主要用跟蹤觸摸屏事件(flinging事件和其他gestures手勢事件)的速率

final VelocityTracker tracker = _velocityTrackers[event.pointer];

assert(tracker != null);

///將指定時間的位置添加到跟蹤器

tracker.addPosition(event.timeStamp, event.position);

}

}

@override

void didStopTrackingLastPointer(int pointer) {

final double minVelocity = minFlingVelocity ?? kMinFlingVelocity;

final double minDistance = minFlingDistance ?? kTouchSlop;

final VelocityTracker tracker = _velocityTrackers[pointer];

///VelocityEstimate 計(jì)算二維速度的

final VelocityEstimate estimate = tracker.getVelocityEstimate();

bool isFling = false;

if (estimate != null && estimate.pixelsPerSecond != null) {

isFling = estimate.pixelsPerSecond.dy.abs() > minVelocity &&

estimate.offset.dy.abs() > minDistance;

}

_velocityTrackers.clear();

if (filingListener != null) {

filingListener(isFling);

}

///super.didStopTrackingLastPointer(pointer) 會調(diào)用[_handleDragEnd]

///所以將[lingListener(isFling);]放在前一步調(diào)用

super.didStopTrackingLastPointer(pointer);

}

@override

void dispose() {

_velocityTrackers.clear();

super.dispose();

}

}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末壹蔓,一起剝皮案震驚了整個濱河市趟妥,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌佣蓉,老刑警劉巖披摄,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件亲雪,死亡現(xiàn)場離奇詭異,居然都是意外死亡疚膊,警方通過查閱死者的電腦和手機(jī)义辕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來寓盗,“玉大人灌砖,你說我怎么就攤上這事】觯” “怎么了基显?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長善炫。 經(jīng)常有香客問我撩幽,道長,這世上最難降的妖魔是什么箩艺? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任窜醉,我火速辦了婚禮,結(jié)果婚禮上艺谆,老公的妹妹穿的比我還像新娘榨惰。我一直安慰自己,他們只是感情好静汤,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布读串。 她就那樣靜靜地躺著,像睡著了一般撒妈。 火紅的嫁衣襯著肌膚如雪恢暖。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天狰右,我揣著相機(jī)與錄音杰捂,去河邊找鬼。 笑死棋蚌,一個胖子當(dāng)著我的面吹牛嫁佳,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播谷暮,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蒿往,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了湿弦?” 一聲冷哼從身側(cè)響起瓤漏,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蔬充,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蝶俱,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年饥漫,在試婚紗的時候發(fā)現(xiàn)自己被綠了榨呆。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡庸队,死狀恐怖积蜻,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情彻消,我是刑警寧澤竿拆,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站证膨,受9級特大地震影響如输,放射性物質(zhì)發(fā)生泄漏鼓黔。R本人自食惡果不足惜央勒,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望澳化。 院中可真熱鬧崔步,春花似錦、人聲如沸缎谷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽列林。三九已至瑞你,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間希痴,已是汗流浹背者甲。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留砌创,地道東北人虏缸。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像嫩实,于是被迫代替她去往敵國和親刽辙。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評論 2 355

推薦閱讀更多精彩內(nèi)容