最近使用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();
}
}