前言

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

作者:HitenDev 链接:www.jianshu.com/p/4d1e81ab3…

先睹为快

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

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

交互分析

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

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

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

下拉控件:

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

卡片控件

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

码上入手

热身

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

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

布局摆放

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

下拉控件

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

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

先说入的坑

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

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

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

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

最终方案

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

PullDragWidget

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

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

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

卡片控件

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

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

CardStackWidget

_CardWidget

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

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

手势识别

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

Listener

GestureDetector手势回调:

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

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

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

手势分析

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

上图更加形象解释两种状态下的手势响应,下拉控件是父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返回值表示dxdy的偏移量,在只存在水平或者只存在竖直方向的情况下,需要将另一个方向的dxdy置0;

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

如何使用

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

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

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

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

下拉控件手势

PullDragWidget

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

卡片控件手势

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

CardStackWidget

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

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

手势小结

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

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

总结

知识点

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

  • ColumnRowExpandedStackPositionedTransform等Widget的使用;
  • GestureDetectorRawGestureDetectorIgnorePointer等Widget的使用;
  • 自定义GestureRecognizer实现自定义手势识别;
  • AnimationControllerTween等动画的使用;
  • EventBus的使用;

最后

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

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

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

项目地址: github.com/HitenDev/Fl…

一波Flutter酷炫特效来袭

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

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

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

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

相信自己,没有做不到的,只有想不到的

在这里获得的不仅仅是技术!

最后如果对技术比较感兴趣,欢迎关注我的微信公众号:终端研发部,id:codeGooger,一起进阶技术

转载于:https://juejin.im/post/5ccff19ae51d453a59418b7c

5G到来,App的未来,是JavaScript,Flutter还是Native ?相关推荐

  1. “5G 消息”APP 短暂上线,它会干掉微信还是变成另一个飞信?

    作者 | 罗燕珊 "5G 消息"APP 终于还是要来了...... "5G 消息"App 短暂上线 近日,中国移动上线了备受关注的"5G 消息&quo ...

  2. 5G将如何推动未来十年智能城市的发展

    来源: IEEE电气电子工程师 过去十年的结束标志着部署5G的技术军备竞赛.移动运营商竞相安装网络基础设施,在澳大利亚.中国.韩国.瑞士和美国等数十个国家提供每秒超过1千兆的无线连接.但是,为什么这么 ...

  3. 5G的来龙去脉及未来

    前言 通信世界的演化很快,几乎10年就是一个时代,从上世纪90年代的2G,到2010年左右兴起的4G.而今,5G技术飞速发展,5G应用的场景技术需求也会变得很高,例如20 Gbit/s的峰值速率.0. ...

  4. 中国移动回应“5G消息APP”下架:并非面向客户商用发布的产品

    日前,中国移动悄然上线"5G消息"APP,用户在安卓市场或苹果应用商店可以免费下载.不过,不到一天,该应用已在苹果应用商店下架. 针对下架原因,中国移动表示,因当前一些终端尚未支持 ...

  5. 顺丰正式杀入外卖领域;中国移动推出 5G 消息 App;GCC 10.1 发布 | 极客头条

    整理 | 屠敏 头图 | CSDN 下载自东方 IC 快来收听极客头条音频版吧,智能播报由标贝科技提供技术支持. 「极客头条」-- 技术人员的新闻圈! CSDN 的读者朋友们早上好哇,「极客头条」来啦 ...

  6. 为全面到来的数字化未来准备就绪 戴尔科技峰会赋能企业数字化发展新动力

    "当今科技以平均每五年提高十倍的速度迅猛发展,企业迎来巨大发展机遇的同时也面临着生产力变革的挑战.而以全球经验和IT力量,为企业提供高效灵活的解决方案,提高生产效率,实现商业理想,正是戴尔商 ...

  7. 5g消息与服务器,5G消息app下架 中国移动回应会重新上架

    5月10日,中国移动5G消息App上架,但这款5G消息App上线仅一天下架,中移动回应下架5G消息APP的原因是这款app并非面向客户商用发布的产品,目前临时下线,稍后会重新上线. 中国移动回应5G消 ...

  8. 5G到来之前,智能手机还应该做什么?

    今年是5G网络覆盖的元年.头部厂商开始了新一轮的竞争,二线厂商也不愿在这个浪潮中失利.苹果与高通达成和解后暂时解决了自己的缺"芯"之痛,再次加入到与三星.华为.小米.OPPO等国内 ...

  9. 【Flutter】Flutter 混合开发 ( Flutter 与 Native 通信 | 完整代码示例 )

    文章目录 前言 一.Android 端完整代码示例 二.Flutter 端完整代码示例 三.相关资源 前言 前置博客 : [Flutter]Flutter 混合开发 ( Flutter 与 Nativ ...

  10. 【Flutter】Flutter 混合开发 ( Flutter 与 Native 通信 | 在 Flutter 端实现 EventChannel 通信 )

    文章目录 一.EventChannel 简介 二.EventChannel 在 Dart 端的实现 1.EventChannel 构造方法 2.创建广播流 Stream 3.设置监听回调函数 4.Ev ...

最新文章

  1. 在CentOS7.2上部署Postgres-XL分布式数据库
  2. echarts柱状图间距调整_Excel每天学个统计图(1)-折线柱状图
  3. makefile通用版本
  4. java 一级缓存,MyBatis一级缓存避坑完全指南
  5. [转载] python实现三角形面积计算
  6. wireshark解密本地https流量笔记
  7. 冒险岛PHP源码,岁月最新源码 岁月冒险岛端源码。。部分源码需要的拿走了、不谢!!! 联合开发网 - pudn.com...
  8. 一维热传导方程求数值解
  9. Ubuntu 16.04 安装无线网卡 Tenda U12
  10. 图像特效---LOMO Filter
  11. Rsync: @ERROR: Auth Failed On Module XXX错误原因及解决办法
  12. WORD2010 页眉横线和页面顶端距离相同显示不一样
  13. 服务器物理内存利用率,服务器提高物理内存利用率
  14. 如何使用 R 从 Internet 下载文件
  15. 大货跟踪程序精简版v1.20200731
  16. 文末有福利 | 停不下来!程序员在GitHub上开源了一个自制表情包项目
  17. Canvas+html+css+position定位
  18. HTML/CSS常见面试题
  19. Oracle:根据身份证号码查询年龄最大的人
  20. day34-数据分析与Excel

热门文章

  1. Android studio 升级指定dradle
  2. (转)jquery图片左右滚动
  3. dwz ajax session超时跳转登录页(struts2自定义阻碍器)
  4. IT兄弟连 JavaWeb教程 文件下载技术
  5. 危机十足站长的生命觉悟:拼命也得每天挤一滴墨水!
  6. 电脑休眠跟睡眠的区别
  7. Struts2 Problem Report: No result defined for action ... and result exception
  8. 与技术无关,犯错了,希望大家帮下
  9. 第 6 章 存储 - 039 - Data Volume 之 bind mount
  10. MapXtreme2004 连接oracle spatial的问题