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

}

}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容