前言

Flutter最近比较热门,但是Flutter成体系的文章并不多,前期避免不了踩坑;我这篇文章主要介绍如何使用Flutter实现一个比较复杂的手势交互,顺便分享一下我在使用Flutter过程中遇到的一些小坑,减少大家入坑;

作者:HitenDev
链接:https://www.jianshu.com/p/4d1e81ab3f54

image

先睹为快

本项目支持ios&android运行,效果如下

image

image

image

image

对了,顺便分享一下生成gif的小窍门,建议用手机自带录屏功能导出mp4文件到电脑,然后电脑端用ffmpeg命令行处理,控制gif的质量和文件大小,我的建议是分辨率控制在270p,帧率在10左右;

交互分析

看文章的小伙伴最好能手持即刻App,亲自体验一下探索页的交互,是黄色Logo黄色主题色的即刻;有人称‘黄即’;

image

即刻App原版功能有卡片旋转,卡片撤回和卡片自动移除,时间关系暂时没有实现,但核心的功能都在;

从一个Android开发者的习惯来看待,这个交互可拆分内外两层控件,外层我们需要一个整体下拉的控件,我称为下拉控件;内层我们需要实现一个上、下、左、右四方向拖拽移动的控件,我们称为卡片控件;下拉控件和卡片控件不仅要处理手势,还需要处理子Widget的布局;下面我再分析细节功能:

下拉控件:

  • 子控件从上到下竖直摆放,顶部菜单默认隐藏在屏幕外
  • 下拉手势所有子控件下移,菜单视觉差效果
  • 支持点击自动展开、收起效果

卡片控件

  • 卡片层叠布局,错落有致
  • 最上层卡片支持手势拖拽
  • 其他卡片相应拖拽小幅位移
  • 松手移除卡片

码上入手

热身

套用App开发伎俩,实现上面的交互无非就是控件布局和手势识别。当然Flutter开发也是这些套路,只不过万物皆是Widget,在Flutter中常用的基本布局有Column、Row、Stack等,手势识别有Listener、GestureDetector、RawGestureDetector等,这是本文重点讲解的控件,不限于上面这几个Widget,因为Flutter提供的Widget太多了,重点的控件需要牢记外,其他时候真是现用现查;

所以下面我们从布局和手势这两个大的技术点,来一一击破功能点;

布局摆放

这里所谓的布局,包括Widget的尺寸大小和位置的控制,一般都是父Widget掌管子Widget的命运,Flutter就是一层一层Widget嵌套,不要担心,下面从外到内具体案例讲解;

下拉控件

首先我们要实现最外层布局,效果是:子Widget竖直摆放,且最上面的Widget默认需要摆放在屏幕外;

image

如上图所示,红色区域是屏幕范围,header是头部隐藏的菜单布局,content是卡片布局的主体;

先说入的坑

竖直布局我最先想到的是Column,我想要的效果是content高度和父Widget的高度一致,我首先想到是让Expanded包裹content,结果是content的高度永远等于Column高度减header高度,造成现象就是content高度不填充,或者是挤压现象,如果继续使用Colunm可能就得放弃Expanded,手动给content赋值高度,没准是个办法,但我不愿意手动赋值content的高度,太不优雅了,最后果断弃用Column;

另一个问题是如何隐藏header,我想到两种方案:

  1. 采用外层Transform包裹整个布局,内层Transform包裹header,然后赋值内层dy = -headerHeight,随着手势下拉动态,并不改变header的Transform,而是改变最外层Transform的dy;
  2. 动态改变header高度,初始高度为0,随着手势下拉动态计算;

但是上面这两种都有坑,第一种方式会影响控件的点击事件,onTap方法不会被回调;第二种由于高度在不断改变,会影响header内部子Widget的布局,很难做视觉差的控制;

最终方案

最后采用Stack来布局,通过Stack配合Positioned,实现header布局在屏幕外,而且可以做到让content布局填充父Widget;

PullDragWidget

image.png

首先解释一下Positioned的基本用法,top、bottom、height控制高度和位置,而且两两配合使用,top和bottom可以理解成marginTop和marginBottom,height顾名思义是直接Widget的高度,如果top配置bottom,意味着高度等于parentHeight-top-bottom,如果top/bottom配合height使用,高度一般是固定的,当然top和bottom是接受负数的;

再分析代码,首先_offsetY是下拉距离,是一个改变的量初始值为0,content需要设置top = _offsetY和bottom = -_offsetY,改变的是上下位置,高度不会改变;同理,header是采用top和height控制,高度固定,只需要动态改变top即可;

用Flutter写布局真的很简单,我极力推崇使用Stack布局,因为它比较灵活,没有太多的限制,用好Stack主要还得用好Positioned,学好它没错;

卡片控件

卡片实现的效果就是依次层叠,错落有致,这个很容易想到Stack来实现,当然有了上面踩坑,用Stack算是很轻松了;

image

重叠的效果使用Stack很简单,错落有致的效果实在起来可能性就比较多了,比如可以使用Positioned,也可以包裹Container改变margin或者padding,但是考虑到角度的旋转,我选择使用Transform,因为Transform不仅可以玩转位移,还有角度和缩放等,其内部实际上是操作一个矩阵变换;Transform挺好用,但是在Transform多层嵌套的某些特殊情况下,会存在不响应onTap事件的情况,我想这应该是Transform的bug,拖拽事件暂时没有发现问题,这个是不是bug有待确认,暂时不影响使用;

CardStackWidget

 

_CardWidget

 

简单总结一下卡片布局代码,CardStackWidget是管理卡片Stack的父控件,负责对每个卡片进行布局,_CardWidget是对单独卡片内部进行布局,总体来说没有什么难点,细节控制逻辑是在对上层_CardWidget和底层_CardWidget偏移量的计算;

布局的内容就讲这么多,整体来说还是比较简单,所谓的有些坑也不一定算是坑,只是不适应某些应用场景罢了;

手势识别

Flutter手势识别最常用的是Listener和GestureDetector这两个Widget,其中Listener主要针对原始触摸点进行处理,GestureDetector已经对原始触摸点加工成了不同的手势;这两个类的方法介绍如下;

Listener

image.png

GestureDetector手势回调:

 

image.png

Listener和GestureDetector如何抉择,首先GestureDetector是基于Listener封装,它解决了大部分手势冲突,我们使用GestureDetector就够用了,但是GestureDetector不是万能的,必要时候需要自定义RawGestureDetector;

另外一个很重要的概念,Flutter手势事件是一个从内Widget向外Widget的冒泡机制,假设内外Widget同时监听竖直方向的拖拽事件onVerticalDragUpdate,往往都是内层控件获得事件,外层事件被动取消;这样的概念和Android父布局拦截机制就完全不同了;

虽然Flutter没有外层拦截机制,但是似乎还有一线希望,那就是IgnorePointer和AbsorbPointerWidget,这俩哥们可以忽略或者阻止子Widget树不响应Event;

手势分析

基本原理介绍完了,接下来分析案例交互,上面说了我把整体布局拆分成了下拉控件和卡片控件,分析即刻App的拖拽的行为:当下拉控件没有展开下拉菜单时,卡片控件是可以相应上、左、右三个方向的手势,下拉控件只相应一个向下方向的手势;当下拉菜单展开时,卡片不能相应任何手势,下拉控件可以相应竖直方向的所有事件;

image

上图更加形象解释两种状态下的手势响应,下拉控件是父Widget,卡片控件是子Widget,由于子Widget能优先响手势,所以在初始阶段,我们不能让子Widget响应向下的手势;

由于GestureDetector只封装水平和竖直方向的手势,且两种手势不能同时使用,我们从GestureDetector源码来看,能不能封装一个监听不同四个方向的手势,;

GestureDetector

GestureDetector最终返回的是RawGestureDetector,其中gestures是一个map,竖直方向的手势在VerticalDragGestureRecognizer这个类;

VerticalDragGestureRecognizer

 

VerticalDragGestureRecognizer继承DragGestureRecognizer,大部分逻辑都在DragGestureRecognizer中,我们只关注重写的方法:

  • _hasSufficientPendingDragDeltaToAccept方法是关键逻辑,控制是否接受该拖拽手势
  • _getDeltaForDetails返回拖拽进度的dx、dy偏移量
  • _getPrimaryValueFromOffset返回单方向手势value,不同方向(同时拥有水平和竖直)的可以传null
  • _isFlingGesture是否该手势的Fling行为
自定义DragGestureRecognizer

想实现接受三个方向的手势,自定义DragGestureRecognizer是一个好的思路;我希望接受上、下、左、右四个方向的参数,根据参数不同监听不同的手势行为,照葫芦画瓢自定义一个接受方向的GestureRecognizer:

DirectionGestureRecognizer

可参考原Demo

由于DragGestureRecognizer的很多方法是私有的,想重新只能copy一份代码出来,然后重写主要的方法,根据不同入参处理不同的手势逻辑;

注意事项

敲黑板了,在自定义DragGestureRecognizer时:_getDeltaForDetails返回值表示dx和dy的偏移量,在只存在水平或者只存在竖直方向的情况下,需要将另一个方向的dx或dy置0;

当前Widget树有且只存在一个手势时,手势判断的逻辑_hasSufficientPendingDragDeltaToAccept可能不会被调用,这时候一定要重写_getDeltaForDetails控制返回dx和dy;

如何使用

自定义的DirectionGestureRecognizer可以配置left、right、up、down四个方向的手势,而且支持不同方向的组合;

比如我们只想监听竖直向下方向,就创建DirectionGestureRecognizer(DirectionGestureRecognizer.down)的手势识别;

想监听上、左、右的手势,创建DirectionGestureRecognizer(DirectionGestureRecognizer.left | DirectionGestureRecognizer.right | DirectionGestureRecognizer.up)的手势识别;

DirectionGestureRecognizer就像一把磨刀石,刀已经磨锋利,砍材就很轻松了,下面进行控件的手势实现;

下拉控件手势

PullDragWidget

image.png

 

PullDragWidget是下拉拖拽控件,根Widget是一个RawGestureDetector用来监听手势,其中gestures支持向下拖拽和点击两个手势;当下拉控件处于_opened状态说header已经拉下来,此时配合IgnorePointer,禁用子Widget所有的事件监听,自然内部的卡片就相应不了任何事件;

卡片控件手势

同下拉控件一样,卡片控件只需要监听其余三个方向的手势,即可完成任务:

CardStackWidget

 
手势答疑
  • 为什么不用 onPanDown onPanUpdate onPanEnd 来拖动?

这是掘金评论提的问题,我解答一下:在GestureDetector中有Pan手势和Drag手势,这两个手势都能用处拖拽的场景,但不同的是Drag手势仅限于水平和竖直方向的监听,Pan手势不约束方向任意方向都能监听,除此之外触发条件也不一致,Pan手势的触发条件是滑动动屏幕的距离distance大于kTouchSlop2Drag手势的触发条件是dx或者dy大于kTouchSlopdxdydistance形成勾股定理的三个边长;假设同样在监听竖直滑动这种场景,VerticalDrag总是比Pan先触发;如果下拉控件用VerticalDrag卡片控件用Pan,下拉控件会优先获取向上的拖拽,卡片控件就会失去向上拖拽的机会,这就实现不了交互了,退一步即使Pan的触发条件跟VerticalDrag*一样,由于Flutter的事件传递是从内到外的,这会导致外层下拉控件完全失去响应机会。以上我的个人理解,如有误导还请大佬评论指正。

手势小结

分析Flutter手势冒泡的特性,父Widget既没有响应事件的优先权,也没有监听单独方向(left、right 、up 、down)的手势,只能自己想办法自定义GestureRecognizer,把原本Vertical和Horizontal两个方向的手势识别扩展成left、right、up 、down四个方向,区分开会产生冲突的手势;

当然也可能有其他的方案来实现该交互的手势识别,条条大路通罗马,我只是抛砖引玉,大家有好的方案可以积极留言提出宝贵意见;

总结

知识点

由于篇幅有限并没有介绍完该交互的所有内容,深表遗憾,总结归纳一下代码中用到的知识点:

  • Column、Row、Expanded、Stack、Positioned、Transform等Widget的使用;
  • GestureDetector、RawGestureDetector、IgnorePointer等Widget的使用;
  • 自定义GestureRecognizer实现自定义手势识别;
  • AnimationController、Tween等动画的使用;
  • EventBus的使用;

最后

上面章节主要介绍在当前场景下用Flutter布局和手势的实战技巧,其中更深层次手势竞技和分发的源码级分析,有机会再做深入学习和分享;

另外本篇并不是循序渐进的零基础入门,对刚接触的同学可能感觉有点懵,但是没有关系,建议你clone一份代码跑起来效果,没准就能提起自己学习的兴趣;

最最后,本篇所有代码都是开源的,你的点赞是对我最大的鼓励。

项目地址:
https://github.com/HitenDev/FlutterDragCard

阅读更多

一波Flutter酷炫特效来袭

金三银四,2019最新面试实战总结

从来不纠结算法,冒泡排序这样优化?

动画:一招学会TCP的三次握手和四次挥手

关于Gradle, 搞定Groovy闭包这一篇就够了

Flutter交互实战-即刻App探索页下拉&拖拽效果的更多相关文章

  1. 学习笔记---Javascript事件Event、IE浏览器下的拖拽效果

    学习笔记---Javascript事件Event.IE浏览器下的拖拽效果     1. 关于event常用属性有returnValue(是否允许事件处理继续进行, false为停止继续操作).srcE ...

  2. Swift2.0下UICollectionViews拖拽效果的实现

    文/过客又见过客(简书作者)原文链接:http://www.jianshu.com/p/569c65b12c8b著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”. 原文UICollecti ...

  3. 带你实现开发者头条APP(五)--RecyclerView下拉刷新上拉加载

    title: 带你实现开发者头条APP(五)--RecyclerView下拉刷新上拉加载 tags: -RecyclerView,下拉刷新,上拉加载更多 grammar_cjkRuby: true - ...

  4. 美团、点评、猫眼App下拉加载效果的源码分享

    今天我准备拿大众点评.美团.猫眼电影三款App的实例来分享一下APICloud下拉加载这个模块的效果. 美团App下拉加载效果   以美团中的下拉酷似动画的萌萌着小人儿效果作为参考,来实现的一个加载模 ...

  5. javascript+html5+css3下拉刷新 数据效果

    文章摘自:suchso.com/projecteactual/javascript-html5-css3-taobao-xiala-data.html segmentfault.com/a/11900 ...

  6. android 滚动栏下拉反弹的效果(相似微信朋友圈)

    微信朋友圈上面的图片封面,QQ空间说说上面的图片封面都有下拉反弹的效果,这些都是使用滚动栏实现的.下拉,当松开时候.反弹至原来的位置.下拉时候能看到背景图片.那么这里简介一下这样的效果的实现. 本文源 ...

  7. Jenkins配置下拉菜单联动效果

    在使用Jenkins集成时,经常需要配置一些环境信息,由于测试.线上.预发布需要切换环境和域名,需要在Jenkins中配置下拉菜单联动效果. 首先选择参数化构建过程,然后首先配置环境,环境分为:测试环 ...

  8. UI中经常出现的下拉框下拉自动筛选效果的实现

    小需求是当你在第一个下拉框选择了国家时,会自动更新第二个省份的下拉框,效果如下 两个下拉选择Html如下: <select id="country_select"> & ...

  9. MVC 下拉框联动效果(单选)

    下拉框联动效果,我们以部门--职位为例,选择部门时,关联到该部门的职位.下拉框的写法就不多说了,详细请参照前文. 视图: 其中,dept是部门的属性,deptlist是部门下拉框的属性,job是职位的 ...

随机推荐

  1. Redis实践系列丨Codis数据迁移原理与优化

    Codis介绍 Codis 是一种Redis集群的实现方案,与Redis社区的Redis cluster类似,基于slot的分片机制构建一个更大的Redis节点集群,对于连接到codis的Redis客 ...

  2. 利用shuf对数据记录进行随机采样

    最近在用SVM为分类器做实验,但是发现数据量太大(2000k条记录)但是训练时间过长...让我足足等了1天的啊!有人指导说可以先进行一下随机采样,再训练,这样对训练结果不会有太大影响(这个待考证).所 ...

  3. 李洪强iOS开发之性能优化技巧

    李洪强iOS开发之性能优化技巧 通过静态 Analyze 工具,以及运行时 Profile 工具分析性能瓶颈,并进行性能优化.结合本人在开发中遇到的问题,可以从以下几个方面进行性能优化. 一.view ...

  4. Django's CSRF mechanism

    Forbidden (403) CSRF verification failed. Request aborted. You are seeing this message because this ...

  5. HTML5你必须知道的28个新特性

    1. 新的Doctype 尽管使用<!DOCTYPE html>,即使浏览器不懂这句话也会按照标准模式去渲染 2. Figure元素 用<figure>和<figcapt ...

  6. redis与spring整合·

    单机版: 配置spring配置文件applicationContext.xml <?xml version="1.0" encoding="UTF-8"? ...

  7. 基于TCP的字符串传输程序

    ---恢复内容开始--- LINUX中的网络编程是通过SOCKET接口来进行的. Socket(套接字) Socket相当于进行网络通信两端的插座,只要对方的Socket和自己的Socket有通信联接 ...

  8. Linux设备模型 (4)

    <Linux设备模型 (2)>和<Linux设备模型 (3)>主要通过一些简单的实作介绍了kobject.kset.kobj_type.attribute等数据结构的用法,但这 ...

  9. 阿里DNS 223.5.5.5 223.6.6.6 (转载)

    转自:http://it.oyksoft.com/post/6780/ 阿里DNS:   223.5.5.5       223.6.6.6 为何用它? 一.选择阿里DNS让你购物更爽,如果是淘宝狂人 ...

  10. layui 添加第三方插件

    关于 layui 添加第三方 JS 库 在写公司项目时,需要将第三方 JS 库整合到 layui 中,具体操作如下: 示例:https://www.jianshu.com/p/7a182e8bff10 ...