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

最近使用flutter實現了一個上下的抽屜效果,使用起來方便,一行代碼,話不多說,直接上效果及代碼:


?視頻效果:



使用代碼:



核心代碼:



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

https://github.com/huangzhiwu1023/flutter_Drawer

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

https://github.com/huangzhiwu1023/flutter_Drawer


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

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,

///抽屜標題點擊事件回調

),

),

],

);

});

}

///抽屜內容Widget

class DrawerContainer extends StatefulWidget {

///抽屜主體內容

final Widget dragWidget;

///默認顯示的高度與屏幕的比率

final double minHight;

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

final double maxHight;

///抽屜滑動狀態回調

final Function(bool isOpen) dragCallBack;

///是否顯示標題

final bool isShowHeader;

///是否直接顯示最大

final bool isShowMax;

///背景圓角

final double cornerRadius;

///滑動結束時 自動滑動到底部或者頂部的時間

final Duration duration;

///背景顏色

final Color backGroundColor;

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

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

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

final double maxOffsetDistance;

///抽屜控制器

final DragController controller = DragController();

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

///配置為true時

///當抽屜為打開時,列表滑動到了頂部,再向下滑動時,抽屜會關閉

///當抽屜為關閉時,列表向上滑動,抽屜會自動打開

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;

///默認顯示的高度 具體的像素

double initialChildSize;

double maxOffsetDistance;

///抽屜的偏移量

double offsetDistance;

///動畫

Animation<double> animation;

///快速輕掃標識

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

bool isFiling = false;

///為true時為打開狀態

///初始化顯示時為閉合狀態

bool isOpen = false;

///開始時的位置

double startOffset = 0;

///開始滑動時會更新此標識

///是否在頂部或底部

bool atEdge = false;

@override

void initState() {

super.initState();

///創建動畫控制器

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

animalController =

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

///添加控制器監聽

if (widget.controller != null) {

widget.controller.setOpenDragListener((value) {

if (value == 1) {

///向上

offsetDistanceOpen(isCallBack: false);

print("向上");

} else {

///向下

offsetDistanceClose(isCallBack: false);

print("向下");

}

});

}

}

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

@override

void didChangeDependencies() {

super.didChangeDependencies();

///State 有一個屬性是mounted 用來標識State當前是否正確綁定在View樹中。

///當創建 State 對象,并在調用 State.initState 之前,

///framework 會根據 BuildContext 來標記 mounted,

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

///直至 framework 調用 State.dispose

if (mounted) {

if (maxChildSize == null) {

///計算抽屜可展開的最大值

maxChildSize = widget.maxHight;

///計算抽屜關閉時的高度

initialChildSize = widget.minHight;

}

///計算臨界值

if (widget.maxOffsetDistance == null) {

///計算滑動結束向上或者向下滑動的臨界值

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

} else {

maxOffsetDistance =

(maxChildSize - initialChildSize) / widget.maxOffsetDistance;

}

///初始化偏移量 為抽屜的關閉狀態

offsetDistance = initialChildSize;

}

}

@override

void dispose() {

animalController.dispose();

super.dispose();

}

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

Widget buildChild() {

return Container(

decoration: BoxDecoration(

///背景顏色設置

color: widget.backGroundColor,

///只上部分的圓角

borderRadius: BorderRadius.only(

///左上角

topLeft: Radius.circular(widget.cornerRadius),

///右上角

topRight: Radius.circular(widget.cornerRadius),

),

),

///可滑動的Widget 這里構建的是一個

child: Column(

children: [

///默認顯示的標題橫線

buildHeader(),

///Column中使用滑動視圖需要結合

///Expanded填充頁面視圖

Expanded(

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

///每一個節點都可以分發通知,通知會沿著當前節點向上傳遞,

///所有父節點都可以通過NotificationListener來監聽通知

child: NotificationListener(

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

child: GestureDetector(

behavior: HitTestBehavior.translucent,

onTap: () {

if (isOpen) {

offsetDistanceClose();

} else {

offsetDistanceOpen();

}

setState(() {});

},

child: Container(

child: widget.dragWidget,

padding: EdgeInsets.only(top: 0),

),

),

///每當有滑動通知時就會回調此方法

onNotification: (Notification notification) {

///滾動處理 用來處理抽屜中的子列表項中的滑動

///與抽屜的聯動效果

scrollNotificationFunction(notification);

return true;

},

),

)

],

),

);

}

///滾動處理 用來處理抽屜中的子列表項中的滑動

void scrollNotificationFunction(Notification notification) {

///通知類型

switch (notification.runtimeType) {

case ScrollStartNotification:

print("開始滾動");

ScrollStartNotification scrollNotification = notification;

ScrollMetrics metrics = scrollNotification.metrics;

///當前位置

startOffset = metrics.pixels;

///是否在頂部或底部

atEdge = metrics.atEdge;

break;

case ScrollUpdateNotification:

print("正在滾動");

ScrollUpdateNotification scrollNotification = notification;

///獲取滑動位置信息

ScrollMetrics metrics = scrollNotification.metrics;

///當前位置

double pixels = metrics.pixels;

///當前滑動的位置 - 開始滑動的位置

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

/// 向上滑動時當抽屜沒有打開時

/// 根據配置 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 是在使用組件時的配置是否啟用

///當 startOffset==0.0 & atEdge 為true 證明是在頂部向下滑動

///在頂部向下滑動時 抽屜打開時就關閉

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

offsetDistanceClose();

}

break;

}

}

///開啟抽屜

void offsetDistanceOpen({bool isCallBack = true}) {

///性能優化 當抽屜為關閉狀態時再開啟

if (!isOpen) {

///不設置抽屜的偏移

double end = 0;

///從當前的位置開始

double start = offsetDistance;

///執行動畫 從當前抽屜的偏移位置 過渡到0

///偏移量為0時,抽屜完全顯示出來,呈打開狀態

offsetDistanceFunction(start, end, isCallBack);

}

}

///關閉抽屜

void offsetDistanceClose({bool isCallBack = true}) {

///性能優化 當抽屜為打開狀態時再關閉

if (isOpen) {

///將抽屜移動到底部

double end = maxChildSize - initialChildSize;

///從當前的位置開始

double start = offsetDistance;

///執行動畫過渡操作

offsetDistanceFunction(start, end, isCallBack);

}

}

///動畫滾動操作

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

///[end]滾動結束的位置

///[isCallBack]是否觸發狀態回調

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

///判斷抽屜是否打開

if (end == 0.0) {

///當無偏移量時 抽屜是打開狀態

isOpen = true;

} else {

///當有偏移量時 抽屜是關閉狀態

isOpen = false;

}

///抽屜狀態回調

///當調用 dragController 的open與close方法

///來觸發時不使用回調

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();

}

///構建小標題橫線

Widget buildHeader() {

///根據配置來決定是否構建標題

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>(

///參數一 自定義手勢識別

buildCustomGecognizer,

///參數二 手勢識別回調

buildCustomGecognizer2);

}

///創建自定義手勢識別

CustomVerticalDragGestureRecognizer buildCustomGecognizer() {

return CustomVerticalDragGestureRecognizer(filingListener: (bool isFiling) {

///滑動結束的回調

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

this.isFiling = isFiling;

print("isFling $isFiling");

});

}

///手勢識別回調

buildCustomGecognizer2(

CustomVerticalDragGestureRecognizer gestureRecognizer) {

///手勢回調監聽

gestureRecognizer

///開始拖動回調

..onStart = _handleDragStart

///拖動中的回調

..onUpdate = _handleDragUpdate

///拖動結束的回調

..onEnd = _handleDragEnd;

}

///手指開始拖動時

void _handleDragStart(DragStartDetails details) {

///更新標識為普通滑動

isFiling = false;

}

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

void _handleDragUpdate(DragUpdateDetails details) {

///偏移量累加

offsetDistance = offsetDistance + details.delta.dy;

setState(() {});

}

///當拖拽結束時調用

void _handleDragEnd(DragEndDetails details) {

///當快速滑動時[isFiling]為true

if (isFiling) {

///當前抽屜是關閉狀態時打開

if (!isOpen) {

///向上

offsetDistanceOpen();

} else {

///當前抽屜是打開狀態時關閉

///向下

offsetDistanceClose();

}

} else {

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

if (offsetDistance > 0) {

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

if (offsetDistance < widget.maxOffsetDistance) {

///向上

offsetDistanceOpen();

} else {

///向下

offsetDistanceClose();

}

//print(

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

}

}

}

}

///抽屜狀態監聽

typedef OpenDragListener = void Function(int value);

///抽屜控制器

class DragController {

OpenDragListener _openDragListener;

///控制器中添加監聽

setOpenDragListener(OpenDragListener listener) {

_openDragListener = listener;

}

///打開抽屜

void open() {

if (_openDragListener != null) {

_openDragListener(1);

}

}

///關閉抽屜

void close() {

if (_openDragListener != null) {

_openDragListener(2);

}

}

}

typedef FilingListener = void Function(bool isFiling);

class CustomVerticalDragGestureRecognizer

extends VerticalDragGestureRecognizer {

///輕掃監聽

final FilingListener filingListener;

///保存手勢點的集合

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 計算二維速度的

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) 會調用[_handleDragEnd]

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

super.didStopTrackingLastPointer(pointer);

}

@override

void dispose() {

_velocityTrackers.clear();

super.dispose();

}

}

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容