我们在APP中经常可以看到各种抽屉,比如:某音的评论以及经典的豆瓣评论。这种抽屉效果,都是十分好看经典的设计。

但是在flutter中,只有侧边抽屉,没看到有上拉的抽屉。项目中UI需要下面的效果:

Flutter抽屉

本文更多是传递flutter学习与开发自定义Widget的一个思想。能够更好的理解Flutter的GestureRecognizer、Transform、AnimationController等等

分析

遇到一个问题或者需求,我更建议大家把需求细化,细分。然后逐个分析,个个击破。

抽屉里存放列表数据。上拉小于一定值 ,自动回弹到底部

当抽屉未到达顶部时,上拉列表,抽屉上移。

当抽屉到到达顶部时,上拉列表,抽屉不动,列表数据移动。

抽屉的列表数据,下拉时,出现最后一条数据时,整个抽屉随之下拉

抽屉上拉时,有一个向上的加速度时,手指离开屏幕,抽屉会自动滚到顶部

解决方案

GestureRecognizer

母庸质疑,这里涉及到更多的是监听手势。监听手指按下、移动、抬起以及加速度移动等。这些,通过flutter强大的GestureRecognizer就可以搞定。

Flutter Gestures 中简单来说就是可以监听用户的以下手势:

Tap

onTabDown 按下

onTapUp 抬起

onTap 点击

onTapCancel

Double tap 双击

Vertical drag 垂直拖动屏幕

onVerticalDragStart

onVerticalDragUpdate

onVerticalDragEnd

Horizontal drag 水平拖动屏幕

onHorizontalDragStart

onHorizontalDragUpdate

onHorizontalDragEnd

Pan

onPanStart 可能开始水平或垂直移动。如果设置了onHorizontalDragStart或onVerticalDragStart回调,则会导致崩溃 。

onPanUpdate 触摸到屏幕并在垂直或水平方移动。如果设置了onHorizontalDragUpdate或onVerticalDragUpdate回调,则会导致崩溃 。

onPanEnd 在停止接触屏幕时以特定速度移动。如果设置了onHorizontalDragEnd或onVerticalDragEnd回调,则会导致崩溃 。

每个行为,均有着对应的Recognizer去处理。

分别对应着下面:

GestureRecognizer

在这里我们用到的就是VerticalDragGestureRecognizer,用来监听控件垂直方向接收的行为。

import 'package:flutter/gestures.dart';

import 'package:flutter/material.dart';

class BottomDragWidget extends StatefulWidget {

@override

_BottomDragWidgetState createState() => _BottomDragWidgetState();

}

class _BottomDragWidgetState extends State {

@override

Widget build(BuildContext context) {

return Stack(children: [

Align(

alignment: Alignment.bottomCenter,

child: DragContainer(),

)

],);

}

}

class DragContainer extends StatefulWidget {

@override

_DragContainerState createState() => _DragContainerState();

}

class _DragContainerState extends State {

double offsetDistance = 0.0;

@override

Widget build(BuildContext context) {

///使用Transform.translate 移动drag的位置

return Transform.translate(

offset: Offset(0.0, offsetDistance),

child: RawGestureDetector(

gestures: {MyVerticalDragGestureRecognizer: getRecognizer()},

child: Container(

width: 100.0,

height: 100.0,

color: Colors.brown,

),

),

);

}

GestureRecognizerFactoryWithHandlers

getRecognizer() {

return GestureRecognizerFactoryWithHandlers(

() => MyVerticalDragGestureRecognizer(), this._initializer);

}

void _initializer(MyVerticalDragGestureRecognizer instance) {

instance

..onStart = _onStart

..onUpdate = _onUpdate

..onEnd = _onEnd;

}

///接受触摸事件

void _onStart(DragStartDetails details) {

print('触摸屏幕${details.globalPosition}');

}

///垂直移动

void _onUpdate(DragUpdateDetails details) {

print('垂直移动${details.delta}');

offsetDistance = offsetDistance + details.delta.dy;

setState(() {});

}

///手指离开屏幕

void _onEnd(DragEndDetails details) {

print('离开屏幕');

}

}

class MyVerticalDragGestureRecognizer extends VerticalDragGestureRecognizer {

MyVerticalDragGestureRecognizer({Object debugOwner})

: super(debugOwner: debugOwner);

}

3.gif

很简单的,我们就完成了widget跟随手指上下移动。

使用动画

之前我们有说道,当我们松开手时,控件会自动跑到最下面,或者跑到最顶端。这里呢,我们就需要使用到AnimationController了

animalController = AnimationController(

vsync: this, duration: const Duration(milliseconds: 250));

///easeOut 先快后慢

final CurvedAnimation curve =

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

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

..addListener(() {

offsetDistance = animation.value;

setState(() {});

});

///自己滚动

animalController.forward();

33.gif

在手指离开屏幕的回调方法中,在void _onEnd(DragEndDetails details)使用animalController,也就是当手指离开屏幕,将上层的DragContainer归到原位。

到这里,已经解决了。滚动,自动归位。下一步,就是解决比较困难的情况。

解决嵌套列表数据

在抽屉中,我们经常存放的是列表数据。所以,会有下面的情况:

列表数据

也就是说,在下拉列表时,只有第一条显示后,整个DragContainer才会随之下移。但是在Flutter中,并没有可以判断显示第一条数据的回调监听。但是官方,有NotificationListener,用来进行滑动监听的。

ScrollNotification

ScrollStartNotification 部件开始滑动

ScrollUpdateNotification 部件位置发生改变

OverscrollNotification 表示窗口小部件未更改它的滚动位置,因为更改会导致滚动位置超出其滚动范围

ScrollEndNotification 部件停止滚动

可以有童鞋有疑问,为什么使用监听垂直方向的手势去移动位置,而不用 ScrollUpdateNotification去更新DragContainer的位置。这是因为:ScrollNotification这个东西是一个滑动通知,他的通知是有延迟!

的。官方有说:Any attempt to adjust the build or layout based on a scroll notification would result in a layout that lagged one frame behind, which is a poor user experience.

也就是说,我们可以将DragContainer放在NotificationListener中,当触发了ScrollEndNotification的时候,也就是说整个列表数据需要向下移动了。

///在ios中,默认返回BouncingScrollPhysics,对于[BouncingScrollPhysics]而言,

///由于 double applyBoundaryConditions(ScrollMetrics position, double value) => 0.0;

///会导致:当listview的第一条目显示时,继续下拉时,不会调用上面提到的Overscroll监听。

///故这里,设定为[ClampingScrollPhysics]

class OverscrollNotificationWidget extends StatefulWidget {

const OverscrollNotificationWidget({

Key key,

@required this.child,

// this.scrollListener,

}) : assert(child != null),

super(key: key);

final Widget child;

// final ScrollListener scrollListener;

@override

OverscrollNotificationWidgetState createState() =>

OverscrollNotificationWidgetState();

}

/// Contains the state for a [OverscrollNotificationWidget]. This class can be used to

/// programmatically show the refresh indicator, see the [show] method.

class OverscrollNotificationWidgetState

extends State

with TickerProviderStateMixin {

final GlobalKey _key = GlobalKey();

///[ScrollStartNotification] 部件开始滑动

///[ScrollUpdateNotification] 部件位置发生改变

///[OverscrollNotification] 表示窗口小部件未更改它的滚动位置,因为更改会导致滚动位置超出其滚动范围

///[ScrollEndNotification] 部件停止滚动

///之所以不能使用这个来build或者layout,是因为这个通知的回调是会有延迟的。

///Any attempt to adjust the build or layout based on a scroll notification would

///result in a layout that lagged one frame behind, which is a poor user experience.

@override

Widget build(BuildContext context) {

print('NotificationListener build');

final Widget child = NotificationListener(

key: _key,

child: NotificationListener(

child: NotificationListener(

child: NotificationListener(

child: widget.child,

onNotification: (ScrollEndNotification notification) {

_controller.updateDragDistance(

0.0, ScrollNotificationListener.end);

return false;

},

),

onNotification: (OverscrollNotification notification) {

if (notification.dragDetails != null &&

notification.dragDetails.delta != null) {

_controller.updateDragDistance(notification.dragDetails.delta.dy,

ScrollNotificationListener.edge);

}

return false;

},

),

onNotification: (ScrollUpdateNotification notification) {

return false;

},

),

onNotification: (ScrollStartNotification scrollUpdateNotification) {

_controller.updateDragDistance(0.0, ScrollNotificationListener.start);

return false;

},

);

return child;

}

}

enum ScrollNotificationListener {

///滑动开始

start,

///滑动结束

end,

///滑动时,控件在边缘(最上面显示或者最下面显示)位置

edge

}

通过这个方案,我们就解决了列表数据的问题。最后一个问题,当手指快速向上滑动的时候然后松开手的时候,让列表数据自动滚动顶端。这个快速上滑,如何解决。

当dragContainer中使用的是ScrollView,一定要将physics的值设定为ClampingScrollPhysics,否则不能监听到ScrollEndNotification。这是平台不一致性导致的。在scroll_configuration.dart中,有这么一段:

scroll_configuration

判断Fling

对于这个,是我在由项目需求,魔改源码的时候,无意中看到的。所以需要翻源码了。在DragGestureRecognizer中,官方有一个也是判断Filing的地方,

_isFlingGesture

不过这个方法是私有的,我们无法调用。(虽然dart可以反射,但是不建议。),我们就按照官方的思路一样的写就好了。

///MyVerticalDragGestureRecognizer 负责任务

///1.监听child的位置更新

///2.判断child在手松的那一刻是否是出于fling状态

class MyVerticalDragGestureRecognizer extends VerticalDragGestureRecognizer {

final FlingListener flingListener;

/// Create a gesture recognizer for interactions in the vertical axis.

MyVerticalDragGestureRecognizer({Object debugOwner, this.flingListener})

: super(debugOwner: debugOwner);

final Map _velocityTrackers = {};

@override

void handleEvent(PointerEvent event) {

super.handleEvent(event);

if (!event.synthesized &&

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

final VelocityTracker tracker = _velocityTrackers[event.pointer];

assert(tracker != null);

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

}

}

@override

void addPointer(PointerEvent event) {

super.addPointer(event);

_velocityTrackers[event.pointer] = VelocityTracker();

}

///来检测是否是fling

@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 (flingListener != null) {

flingListener(isFling);

}

///super.didStopTrackingLastPointer(pointer) 会调用[_handleDragEnd]

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

super.didStopTrackingLastPointer(pointer);

}

@override

void dispose() {

_velocityTrackers.clear();

super.dispose();

}

}

好的,这就解决了Filing的判断。

最后效果

part1.gif

part2.gif

模拟器有点卡~

博客地址

android 上拉抽屉,Flutter上拉抽屉实现相关推荐

  1. flutter上拉抽屉效果 flutter拖动抽屉效果

    题记 -- 执剑天涯,从你的点滴积累开始,所及之处,必精益求精,即是折腾每一天. 重要消息 网易云[玩转大前端]配套课程 EDU配套 教程 Flutter开发的点滴积累系列文章 示例一 示例二 1. ...

  2. Flutter 21: 图解 ListView 下拉刷新与上拉加载 (三)【RefreshIndicator】

    小菜前段时间整理了两种 ListView 的异步加载数据时,下拉刷新与上滑加载更多的方式,每种方式都有自己的优势,网上也有很多大神讲解过 ListView 数据流的种种处理方式,小菜根据实际遇到的情况 ...

  3. Flutter ListView封装,下拉刷新、上拉加载更多

    Flutter ListView封装,下拉刷新.上拉加载更多 ​ 封装了Flutter的ListView,只要传递请求数据的方法和绘制item的方法进去就可以绘制ListView,同时支持下拉刷新.上 ...

  4. Flutter ListView 下拉刷新与上拉加载更多

    ListView 下拉刷新与上拉加载更多 import 'dart:async'; import 'package:flutter/material.dart';/*** 有状态StatefulWid ...

  5. Android RecyclerView封装下拉刷新与上拉加载更多

    1 scanlistlibrary 基础组件说明(基于 RecyclerView的封装) 基本数据列表(支持下拉刷新与上拉加载更多) 九宫格数据显示封装(支持下拉刷新与上拉加载更多) 瀑布流数据显示封 ...

  6. Android RecyclerView(八)设置自定义 下拉刷新 与 上拉加载数据

    Android RecyclerView(八)设置下拉刷新 与 上拉加载数据 GitHub 项目源码 CSDN 博客说明 智慧安卓App 文章分析 下拉刷新效果 上拉加载数据效果 1 xml布局文件中 ...

  7. Android PullToRefreshListView上拉刷新和下拉刷新

    PullToRefreshListView实现上拉和下拉刷新有两个步骤: 1.设置刷新方式 pullToRefreshView.setMode(PullToRefreshBase.Mode.BOTH) ...

  8. android中上拉下滑布局,3年以上勿进!最简单的Android自定义ListView下拉刷新与上拉加载,代码直接拿去用~...

    本文主要针对开发新手,手写实现一个最简单Android自定义listview下拉刷新和上拉加载demo. 不喜可喷,欢迎大佬留言指点. 效果图 一:编写需要在ListView中增加头加载的布局文件,与 ...

  9. 【Android归纳】基于XListView的下拉刷新、上拉加载更多的控件分析

    目录 前言 功能介绍 总体设计 组成 类关系图 详细设计 XlistViewHeader原理分析 XListViewFooter原理分析 XListView原理分析 代码带注释下载 目录 前言 如果你 ...

  10. 微信小程序/网页/app/android等各种bar图标导航图标文章图标标题图标下拉/检索收藏上传客服等图标整理

    在做微信小程序的时候,需要给底部放置图标.但是找了好久都没有找到自己想要的,适合的.后来我就把所有的小程序的图标都看了下.后来想想还是整理出来,方便自己以后用,也方便大家一下.我敢保证有你想要的,适合 ...

最新文章

  1. 从算法+数据结构到MVC
  2. VIM编辑器使用技巧
  3. 杨凌农业自贸区谋定功能-万祥军:对话农民丰收节交易会
  4. mysql通过函数完成10的阶乘_请使用函数的递归调用编写求阶乘的函数,并计算1!+2!+3!+4!+5!...
  5. Java EE 8 MVC:使用路径参数
  6. jQuery介绍 DOM对象和jQuery对象的转换与区别
  7. 测试工程师需要具备的技能
  8. 如何修改Git仓库的URL(地址)
  9. 创建ros的程序包--3
  10. python 搭建的http 动态服务器_Python3搭建http服务器的实现代码
  11. 2010年3月份第三周51aspx发布WinForm源码
  12. 深度学习笔记_评分函数/损失函数
  13. stm32中如何避免等待_地坪漆施工中如何避免常见的小问题
  14. 单元测试和sit测试和uat测试
  15. 工业机械臂直线插补相关记录
  16. php7语法 mysql_php7语法
  17. toFixed() is not a function toFixed方法数字类型才能使用
  18. 普华永道java面试_新鲜的普华永道面试题来了,四大求职必看
  19. Python字体成灰色有波浪号
  20. java 基本数据类型所占字节数

热门文章

  1. Data URL和图片
  2. 简单多人聊天室——java网络编程
  3. 罗永浩当年求职新东方一万多字的求职信
  4. 韩媒:开城韩商访朝申请或最晚25日出结果
  5. 缓冲区溢出实例(一)--Windows
  6. springboot基于微信小程序的在线考试系统
  7. 计算机英语总结800,高三英语教师工作总结800字(通用5篇)
  8. kvm坐席系统通过IP网关实现kvm设备系统去中心化管理应用
  9. 有没有不用加班的程序员?如何衡量程序员的工作量?
  10. 人生感悟经典哲理句子,句句都是人生哲理!