Flutter最近比较热门,但是Flutter成体系的文章并不多,前期避免不了踩坑;我这篇文章主要介绍如何使用Flutter实现一个比较复杂的手势交互,顺便分享一下我在使用Flutter过程中遇到的一些小坑,减少大家入坑;
项目地址:https://github.com/HitenDev/FlutterDragCard
先睹为快
本项目支持ios
和android
平台,效果如下
对了,顺便分享一下生成gif
的小窍门,建议用手机自带录屏功能导出mp4
文件到电脑,然后电脑端用ffmpeg
命令行处理,控制gif
的质量和文件大小,我的建议是分辨率控制在270p,帧率在10左右;
交互分析
看文章的小伙伴最好能手持即刻App,亲自体验一下探索页的交互,是黄色Logo黄色主题色的即刻,人称‘黄即’;
即刻App原版功能有卡片旋转,卡片撤回和卡片自动移除,时间关系暂时没有实现,但核心的功能都在;
从一个Android开发者的习惯来看待,这个交互可拆分内外两层控件,外层我们需要一个整体下拉的控件,我称为下拉控件
;内层我们需要实现一个上、下、左、右四方向拖拽移动的控件,我们称为卡片控件
;下拉控件
和卡片控件
不仅要处理手势,还需要处理子Widget的布局;下面我再分析细节功能:
下拉控件:
- 子控件从上到下竖直摆放,顶部菜单默认隐藏在屏幕外
- 下拉手势所有子控件下移,菜单视觉差效果
- 支持点击自动展开、收起效果
卡片控件
- 卡片层叠布局,错落有致
- 最上层卡片支持手势拖拽
- 其他卡片响应拖拽小幅位移
- 松手移除卡片
码上入手
热身
套用App开发伎俩,实现上面的交互无非就是控件布局和手势识别。当然Flutter开发也是这些套路,只不过万物皆是Widget,在Flutter中常用的基本布局有Column
、Row
、Stack
等,手势识别有Listener
、GestureDetector
、RawGestureDetector
等,这是本文重点讲解的控件,不限于上面这几个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
,我想到两种方案:
- 采用外层
Transform
包裹整个布局,内层Transform
包裹header
,然后赋值内层dy = -headerHeight
,随着手势下拉动态,并不改变header
的Transform
,而是改变最外层Transform
的dy
; - 动态改变
header
高度,初始高度为0,随着手势下拉动态计算;
但是上面这两种都有坑,第一种方式会影响控件的点击事件,onTap
方法不会被回调;第二种由于高度在不断改变,会影响header
内部子Widget的布局,很难做视觉差的控制;
最终方案
最后采用Stack
来布局,通过Stack
配合Positioned
,实现header
布局在屏幕外,而且可以做到让content
布局填充父Widget;
PullDragWidget
Widget build(BuildContext context) {
return RawGestureDetector(
behavior: HitTestBehavior.translucent,
gestures: _contentGestures,
child: Stack(
children: <Widget>[
Positioned(//content布局
top: _offsetY,
bottom: -_offsetY,
left: 0,
right: 0,
child: IgnorePointer(
ignoring: _opened,
child: widget.child,
)),
Positioned(header布局
top: -widget.dragHeight + _offsetY,
bottom: null,
left: 0,
right: 0,
height: widget.dragHeight,
child: _headerWidget()),
],
));
}
首先解释一下Positioned
的基本用法,top
、bottom
、heig