在 Flutter 中,自带手势监听的目前为止好像只有按钮部件和一些 chip 部件,例如 Text 等部件需要实现手势监听,就需要借助带有监听事件的部件来实现了,这节我们会讲下 InkWell 和 GestureDetector 来实现手势的监听。
InkWell
在前面的一些例子中,小伙伴应该看到了好几次 InkWell 这个部件,通过它我们可以实现对一些手势的监听,并实现 MD 的水波纹效果,举个简单的一个例子
1  | InkWell(  | 
那么当点击 Text 的时候就会响应点击事件,控制台输出日志
我们还是老套路,分析下源码。Ctrl 点击 InkWell 来查看源码(Android Studio 的操作,别的我不懂喔…),然后,「嗯…除了构造函数怎么什么都没有???」那只能看它的父类 InkResponse 了,在那之前,我们看下 InkWell 的说明
 1
2 > /// A rectangular area of a [Material] that responds to touch.
>
InkWell 是在 MaterialDesign 风格下的一个用来响应触摸的矩形区域(注意加粗的文字,1.如果不是 MD 风格的部件下,你是不能用这个来做点击响应的;2.InkWell 是一块矩形区域,如果你要的是圆形区域,8 好意思,不行!)
 1
2
3
4 > /// The [InkWell] widget must have a [Material] widget as an ancestor. The
> /// [Material] widget is where the ink reactions are actually painted. This
> /// matches the material design premise wherein the [Material] is what is
> /// actually reacting to touches by spreading ink.
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
`InkWell` 必须要有一个 `Material` 风格的部件作为锚点,巴拉巴拉巴拉....再次强调必须要在 `MD` 风格下使用。
接下来看下 `InkResponse` 吧
#### InkResponse
```dart
const InkResponse({
Key key,
this.child, // 需要监听的子部件
// 一个 `GestureTapCallback` 类型参数,看下 `GestureTapCallback` 的定义,
// `typedef GestureTapCallback = void Function();` 就是简单的无参无返回类型参数
// 监听手指点击事件
this.onTap,
// 一个 `GestureTapDownCallback` 类型参数,需要 `TapDownDetails` 类型参数,
// `TapDownDetails` 里面有个 `Offset` 参数用于记录点击的位置,监听手指点击屏幕的事件
this.onTapDown,
// 同 `onTap` 表示点击事件取消监听
this.onTapCancel,
// 同 `onTap` 表示双击事件监听
this.onDoubleTap,
// 一个 `GestureLongPressCallback` 类型参数,也是无参无返回值,表示长按的监听
this.onLongPress,
// 监听高亮的变化,返回 `true` 表示往高亮变化,`false` 相反
this.onHighlightChanged,
// 是否需要裁剪区域,`InkWell` 该值为 `true`,会根据 `highlightShape` 裁剪
this.containedInkWell = false,
// 高亮的外形,`InkWell` 该值设置成 `BoxShape.rectangle`,所以是个矩形区域
this.highlightShape = BoxShape.circle,
this.radius, // 手指点下去的时候,出现水波纹的半径
this.borderRadius, // 点击时候外圈阴影的圆角半径
this.customBorder,
this.highlightColor, // 高亮颜色
this.splashColor, // 手指点下生成的水波颜色
this.splashFactory, // 两个值 `InkRipple.splashFactory` 和 `InkSplash.splashFactory`
this.enableFeedback = true, // 检测到手势是否有反馈
this.excludeFromSemantics = false,
})
所以一些简单的触摸事件直接通过 InkWell 或者 InkResponse 就能够实现,但是面临一些比较复杂的手势,就有点不太够用了,我们需要通过 GestureDector 来进行处理
GestureDector
GestureDetector 也是一个部件,主要实现对各种手势动作的监听,其监听事件查看下面的表格
| 回调方法 | 回调描述 | 
|---|---|
onTapDown | 
点击屏幕的手势触碰到屏幕时候触发 | 
onTapUp | 
点击屏幕抬手后触发,点击结束 | 
onTap | 
点击事件已经完成的时候触发,和 onTapUp 几乎同时 | 
onTapCancel | 
点击未完成,被其它手势取代的时候触发 | 
onDoubleTap | 
双击屏幕的时候触发 | 
onLongPress | 
长按屏幕的时候触发 | 
onLongPressUp | 
长按屏幕后抬手触发 | 
onVerticalDragDown | 
触碰到屏幕,可能发生垂直方向移动触发,onVerticalDrag 系列事件不会同 onHorizontalDrag 系列事件同时发生 ,如果发生了 onVerticalDrag 则接下来如何变化移动,都不会触发 onHorizontalDrag 事件,除非取消后重新触发。判断两者的关键是准备滑动的意图,先发生横向滑动则触发 onHorizontalDrag 事件,否则 onVerticalDrag 事件。 | 
onVerticalDragStart | 
触碰到屏幕,并开始发生垂直方向的移动触发 | 
onVerticalDragUpdate | 
垂直方向移动的距离变化触发 | 
onVerticalDragEnd | 
抬手取消垂直方向移动的时候触发 | 
onVerticalDragCancel | 
触发 onVerticalDragDown 但是没有完成整个 onVerticalDrag 事件触发 | 
onHorizontalDrag 系列介绍省略同上… | 
|
onPanDown | 
触碰到屏幕,准备滑动的时候触发,onPan 系列回调不可和 onVerticalDrag 或者 onHorizontalDrag 系列回调同时设置 | 
onPanStart | 
触碰到屏幕,并开始滑动时候触发 | 
onPanUpdate | 
滑动位置发生改变的时候触发 | 
onPanEnd | 
滑动完成并抬手的时候触发 | 
onPanCancel | 
触发 onPanDown 但是没有完成整个 onPan 事件触发 | 
onScaleStart | 
两个手指之间建立联络点触发,初始缩放比例为 1.0 | 
onScaleUpdate | 
手指距离发生变化,缩放比例也跟随变化触发 | 
onScaleEnd | 
手指抬起,至间的联络断开时候触发 | 
还有 onForcePress 系列事件,这个是根据对屏幕的挤压力度进行触发,需要达到某些定值才能触发。GestureDetector 有个 behavior 属性用于设置手势监听过程中的表现形式
deferToChild默认值,触摸到child的范围才会触发手势,空白处不会触发opaque不透明模式,防止background widget接收到手势translucent半透明模式,刚好同opaque相反,允许background widget接收到手势
介绍完了手势,那就可以实际操练起来了,比如,实现一个跟随手指运动的小方块,先看下效果图

简单的分析下,通过 Positioned 来设置小方块的位置,根据 GestureDetector 的 onPanUpdate 修改 Positioned 的 left 和 top 值,当 onPanEnd 或者 onPanCancel 的时候设置为原点,那么就可以有如图的效果了
1  | class GestureDemoPage extends StatefulWidget {  | 
如果说要实现一个放大缩小的方块,就可以通过 onScaleUpdate 中获取到的 details.scale 来设置方块的宽高即可。这个比较简单就留给小伙伴们自己实现效果了。
该部分代码查看 gesture_main.dart 文件
Animation 动画
Flutter 的 Animation 是个抽象类,具体的实现需要看其子类 AnimationController,在这之前,先了解下 Animation 的一些方法和介绍。
addListener/removeListener添加的监听用于监听值的变化,remove用于停止监听addStatusListener/removeStatusListener添加动画状态变化的监听,remove停止监听,Animation的状态有 4 种:dismissed动画初始状态,反向运动结束状态,forward动画正向运动状态,reverse动画反向运动状态,completed动画正向运动结束状态。drive方法用于连接动画,例如官方举的例子,因为AnimationController是其子类,所以也拥有该方法1
2
3
4
5
6Animation<Alignment> _alignment1 = _controller.drive(
AlignmentTween(
begin: Alignment.topLeft,
end: Alignment.topRight,
),
);上面的例子将
AnimationController和AlignmentTween结合成一个Animation<Alignment>动画,当然drive可以结合多个动画,例如1
2
3
4
5
6Animation<Alignment> _alignment3 = _controller
.drive(CurveTween(curve: Curves.easeIn))
.drive(AlignmentTween(
begin: Alignment.topLeft,
end: Alignment.topRight,
));
因为 Animation 是抽象类,所以具体的还是需要通过 AnimationController 来实现。
AnimationController
1  | AnimationController({  | 
AnimationController 控制动画的方法有这么几个
forward启动动画,和上面提到的forward状态不一样reverse方向启动动画repeat重复使动画运行stop停止动画reset重置动画
大概了解了 AnimationController ,接下来通过一个实际的小例子来加深下印象,例如实现如下效果,点击开始动画,结束后再点击反向动画

1  | class _AnimationDemoPageState extends State<AnimationDemoPage> with TickerProviderStateMixin {  | 
那么如果要实现无限动画呢,那就可以通过 addStatusListener 监听动画的状态来执行,修改代码,在 initState 增加如下代码
1  | _animationController.addStatusListener((status) {  | 
把 Center 的 child 替换成一个 Icon,因为上面已经启动了动画,所以不需要再用点击去启动了,运行后就会无限放大缩小循环跑了。
在这个例子中,通过设置 AnimationController 的 lowerBound 和 upperBound 实现了动画的变化范围,接下来,将通过 Tween 来实现动画的变化范围。先看下 Tween 的一些介绍。
Tween
 1
2
3
4
5
6
7
8
9
10
11
12
13
14 > /// A linear interpolation between a beginning and ending value.
> ///
> /// [Tween] is useful if you want to interpolate across a range.
> ///
> /// To use a [Tween] object with an animation, call the [Tween] object's
> /// [animate] method and pass it the [Animation] object that you want to
> /// modify.
> ///
> /// You can chain [Tween] objects together using the [chain] method, so that a
> /// single [Animation] object is configured by multiple [Tween] objects called
> /// in succession. This is different than calling the [animate] method twice,
> /// which results in two separate [Animation] objects, each configured with a
> /// single [Tween].
>
Tween 是一个线性插值(如果要修改运动的插值,可以通过 CurveTween 来修改),所以在线性变化的时候很有用
通过调用 Tween 的 animate 方法生成一个 Animation(animate 一般传入 AnimationController)
还可以通过 chain 方法将多个 Tween 结合到一起,这样就不需要多次去调用 Tween 的 animate 方法来生成动画了,多次调用 animate 相当于使用了两个分开的动画来完成效果,但是 chain 结合到一起就是一个动画过程
那么对前面的动画进行一些修改,通过 Tween 来控制值的变化
1  | class _AnimationDemoPageState extends State<AnimationDemoPage> with TickerProviderStateMixin {  | 
再次运行,还是能过达到之前的效果,那么很多小伙伴肯定会问了,「**,加了那么多代码,效果还是和以前的一样,还不如不加…」好吧,我无法反驳,但是如果要实现多个动画呢,那么使用 Tween 就有优势了,比如我们让图标大小变化的同时,颜色和位置也发生变化,只通过 AnimationController 要怎么实现? 又比如说,运动的方式要先加速后减速,那只通过 AnimationController 要如何实现?这些问题通过 Tween 就会非常方便解决,直接上代码
1  | class _AnimationDemoPageState extends State<AnimationDemoPage> with TickerProviderStateMixin {  | 
那么最后的效果图

当然,Flutter 中已经实现的 Tween 还有很多,包括 BorderTween、TextStyleTween、ThemeDataTween ..等等,实现的方式都是类似的,小伙伴们可以自己慢慢看。
AnimationWidget
在上面的例子中,都是通过 addListener 监听动画值变化,然后通过 setState 方法来实现刷新效果。那么 Flutter 也提供了一个部件 AnimationWidget 来实现动画部件,就不需要一直监听了,还是实现上面的例子
1  | class RunningHeart extends AnimatedWidget {  | 
其实内部返回的部件和前面的是一样的
接着对 _AnimationDemoPageState 类进行修改,注释 initState 中的 _animationController.addListener 所有内容,然后将 body 属性替换成新建的 RunningHeart 部件,记得传入的动画列表的顺序
1  | body: RunningHeart(  | 
这样就实现了刚才一样的效果,并且没有一直调用 setState 来刷新。
该部分代码查看 animation_main.dart 文件
StaggeredAnimations
Flutter 还提供了交错动画,听名字就可以知道,是按照时间轴,进行不同的动画,并且由同个AnimationController 进行控制。因为没有找到好的例子,原谅我直接搬官方的例子来讲,官方交错动画 demo
在继续看之前,先了解下 Interval
 1
2
3
4
5 > /// An [Interval] can be used to delay an animation. For example, a six second
> /// animation that uses an [Interval] with its [begin] set to 0.5 and its [end]
> /// set to 1.0 will essentially become a three-second animation that starts
> /// three seconds later.
>
Interval 用来延迟动画,例如一个时长 6s 的动画,通过 Interval 设置其 begin 参数为 0.5,end 参数设置为 1.0,那么这个动画就会变成 3s 的动画,并且开始的时间延迟了 3s。
了解 Interval 功能后,就可以看下实例了,当然我们不和官方的 demo 一样,中间加个旋转动画
1  | class StaggeredAnim extends StatelessWidget {  | 
然后修改 body 的参数,设置成我们的动画,当点击的时候就会启动动画
1  | GestureDetector(  | 
看下最后的效果吧

该部分代码查看 staggered_animation_main.dart 文件
结束前,我们再讲一种比较简单的 Hreo 动画,用来过渡用。
Hero
通过指定 Hero 中的 tag,在切换的时候 Hero 会寻找相同的 tag,并实现动画,具体的实现逻辑,这里可以推荐一篇文章 谈一谈Flutter中的共享元素动画Hero,里面写的很详细,就不造车轮了。当然这边还是得提供个简单的 demo 的,替换前面的 body 参数
1  | body: Container(  | 
然后创建 HeroPage 界面,当然也可以是个 Dialog,只要通过路由实现即可
1  | class HeroPage extends StatelessWidget {  | 
看下最后的效果图:

该部分代码查看 animation_main.dart 文件
这一部分讲的比较多,小伙伴可以慢慢消化,下节我会尽量填下之前留下的状态管理的坑。
最后代码的地址还是要的:
- 文章中涉及的代码:demos
 - 基于郭神 
cool weather接口的一个项目,实现BLoC模式,实现状态管理:flutter_weather - 一个课程(当时买了想看下代码规范的,代码更新会比较慢,虽然是跟着课上的一些写代码,但是还是做了自己的修改,很多地方看着不舒服,然后就改成自己的实现方式了):flutter_shop