TreeviewCopyright © aleen42 all right reserved, powered by aleen42

真的是万万没想到,Flutter 中真的是“万物皆 Widget”,就连手势处理也是一个 Widget,也就是说,我们常用的单击、双击、长按甚至左滑右划也都是一个 Widget,是不是有些神奇?

先来看最基础的 —— GestureDetector。

在之前讲解 ListView 的时候,我们在 ListTile 中看到了关于点击的相关参数:onTaponLongPress,但假如是我们自定义的 Item 布局呢? ListView 并没有提供相应的类似于 Android 那样的 setOnItemClickListener 这样的方法,我们在讲之前的基础控件的时候,也并没有发现和 Android setOnClickListener 那样的方法,那 Flutter 中是如何设置点击事件的呢?

事实上,在 Flutter 中,只要要将 Widget 作为 GestureDetector 的子控件,那这个 Widget 就可以相应我们的手势了,神器不神奇?

先看来一下 GestureDetector 的构造方法:

  GestureDetector({
    Key key,
    this.child,    //监听手势的控件
    this.onTapDown,    //手指开始触摸屏幕触发
    this.onTapUp,    //手指从屏幕抬起触发
    this.onTap,    //一次完成的单击事件结束后触发,onTapUp之后触发
    this.onTapCancel,    //触摸屏幕后,未完成触摸动作,即未抬起手指可触发,如长按方可触发改事件
    this.onSecondaryTapDown,    //下面三个有些疑惑,看了 API,查了一些资料,应该是和轨迹球、手写笔或者屏幕上的指针标识产生的 onTapDown、onTtapUp、TapCancel
    this.onSecondaryTapUp,    //同上
    this.onSecondaryTapCancel,    //同上
    this.onDoubleTap,    //短时间内触发屏幕两次触发
    this.onLongPress,    //长按触发
    this.onLongPressStart,    //长按开始时触发
    this.onLongPressMoveUpdate,    //长按时手指移动触发
    this.onLongPressUp,    //长按结束时触发
    this.onLongPressEnd,    //长按结束时触发,在onLongPressUp之前触发
    this.onVerticalDragDown,    //手指按下并在垂直方向上移动时触发
    this.onVerticalDragStart,    //当触摸点开始在垂直方向上移动时触发,在onVerticalDragDown之后触发
    this.onVerticalDragUpdate,    //触摸点在垂直方向上位置每发生改变就会触发
    this.onVerticalDragEnd,    //手指离开屏幕后方可触发
    this.onVerticalDragCancel,    //用户突然停止拖拽时触发
    this.onHorizontalDragDown,    //同onVerticalDragDown,水平方向
    this.onHorizontalDragStart,    //同onVerticalDragStart,水平方向
    this.onHorizontalDragUpdate,    //同onVerticalDragUpdate,水平方向
    this.onHorizontalDragEnd,    //同onVerticalDragEnd,水平方向
    this.onHorizontalDragCancel,    //同onVerticalDragCancel
    this.onForcePressStart,    //手指按压屏幕开始时触发(有压力检测的屏幕才能触发)
    this.onForcePressPeak,    //手指与屏幕接触并且压力达到最大时触发
    this.onForcePressUpdate,    //手指有足够的压力按压屏幕并在屏幕上移动时触发,
    this.onForcePressEnd,    //按压屏幕的手指松开时触发
    this.onPanDown,    //同onVerticalDragDown,任意方向
    this.onPanStart,    //同onVerticalDragStart,任意方向
    this.onPanUpdate,    //同onVerticalDragUpdate,任意方向
    this.onPanEnd,    //同onVerticalDragEnd,任意方向
    this.onPanCancel,    //同onVerticalDragCancel
    this.onScaleStart,    //双指开始捏合时触发
    this.onScaleUpdate,    //双指捏合缩放触发
    this.onScaleEnd,    //    双指结束结合后触发
    this.behavior,    //此手势检测器在命中测试期间应如何表现。如果child不为null,则默认为HitTestBehavior.deferToChild;如果child为null,则默认为HitTestBehavior.translucent。
    this.excludeFromSemantics = false,    //是否从语义树中排除这些手势。
    this.dragStartBehavior = DragStartBehavior.start,    //确定处理拖拽开始行为的方式
  })

可以看到,除了 child,剩下的几乎都是跟手势相关的参数,我们需要做的,就是把我们需要监听手势的 Widget 作为 child 参数的值传入即可。

单击、双击和长按

简单的例子:

      body: Stack(
        children: <Widget>[
          GestureDetector(
            onTap: () {
              Scaffold.of(context).removeCurrentSnackBar();
              Scaffold.of(context).showSnackBar(SnackBar(
                content: Text("点击了橘色区域"),
              ));
            },
            child: Container(
              color: Colors.deepOrangeAccent,
              width: 400,
              height: 400,
            ),
          ),
          GestureDetector(
            onTap: () {
              Scaffold.of(context).removeCurrentSnackBar();
              Scaffold.of(context).showSnackBar(SnackBar(
                content: Text("点击了淡蓝色区域"),
              ));
            },
            child: Container(
              color: Colors.tealAccent,
              width: 300,
              height: 300,
            ),
          ),
          Container(
            color: Colors.deepPurple,
            width: 100,
            height: 100,
            alignment: Alignment.center,
            child: GestureDetector(
              onTap: () {
                Scaffold.of(context).removeCurrentSnackBar();
                Scaffold.of(context).showSnackBar(SnackBar(
                  content: Text("点击了文字"),
                ));
              },
              child: Text("Click me!"),
            ),
          ),
        ],
      ),

效果如下:

可以看到,点击、双击、长按不同的区域会触发相应的 tap,十分简单。

滑动拖动

除了单击双击,我们还经常会用到拖动和滑动的情况,这就需要获取手指在屏幕上的起始坐标以及移动坐标了。

onPanDownonPanUpdateonPanEnd 三个参数来说,他们分别是:GestureDragDownCallbackGestureDragUpdateCallbackGestureDragEndCallback 类型的,我们详细看一下:

typedef GestureDragDownCallback = void Function(DragDownDetails details);
typedef GestureDragUpdateCallback = void Function(DragUpdateDetails details);
typedef GestureDragEndCallback = void Function(DragEndDetails details);

  DragDownDetails({
    this.globalPosition = Offset.zero,    //手指在屏幕中按下的位置(原点为屏幕左上角)
    Offset localPosition,    //手指在控件中按下的位置(原点为控件左上角)
  }) 
  DragUpdateDetails({
    this.sourceTimeStamp,    //发动拖动事件的时间戳(但做测试的时候发现,好像并不是真正意义上的发生时间,有点儿像(可以计算)每个拖动事件的时间差的值)
    this.delta = Offset.zero,    //偏移量
    this.primaryDelta,    //主轴偏移量
    @required this.globalPosition,//手指在屏幕中按下的位置(原点为屏幕左上角)
    Offset localPosition,//手指在控件中按下的位置(原点为控件左上角)
  }) 
  DragEndDetails({
    this.velocity = Velocity.zero,    //代表用户抬起手指时的滑动速度(包含x、y两个轴的)
    this.primaryVelocity,    //用户抬起手指时沿主轴的移动速度
  })

我们既然知道了手指在屏幕中的偏移量,那么自然也就可以动态的设置控件在屏幕中的位置了:

我们使用 Stack 作为父布局,这样就可以搭配 Positioned 的属性来设置子控件的位置了:

class _SwitchAndCheckBoxTestRouteState
    extends State<SwitchAndCheckBoxTestRoute> {
  double top = 100; //距顶部的偏移
  double lfet = 100.0; //距左边的偏移
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("手势监听"),
      ),
      body: Stack(
        children: <Widget>[
          Positioned(
            top: top,
            left: lfet,
            child: GestureDetector(
              child: Icon(Icons.ac_unit,size: 100,),
              //手指按下时会触发此回调
              onPanDown: (DragDownDetails e) {
                //打印手指按下的位置(相对于屏幕)
                print("用户手指按下:${e.globalPosition}");
                print("用户手指按下:${e.localPosition}");
              },
              //手指滑动时会触发此回调
              onPanUpdate: (DragUpdateDetails e) {
                print("${e.delta.dx} * ${e.delta.dy}");
                //用户手指滑动时,更新偏移,重新构建
                setState(() {
                  lfet += e.delta.dx;
                  top += e.delta.dy;
                });
              },
              onPanEnd: (DragEndDetails e) {
                //打印滑动结束时在x、y轴上的速度
                print(e.velocity.pixelsPerSecond.dx);
              },
            ),
          )
        ],
      ),
    );
  }
}

效果如下

在上面的例子中,我们可以横移纵移控件,在最一开始的参数中可以看到,我们也可以只设置某一方向的移动,或者是缩放,只需要在相应的回调中处理就可以了,这里不赘述。

事件拦截

先看一个示例:

body: Stack(
    children: <Widget>[
        GestureDetector(
            onTap: () {
                print("红色被点击");
            },
            child: Container(
                width: 400,
                height: 400,
                color: Colors.deepOrange,
            ),
        ),
        GestureDetector(
            child: Container(
                width: 300,
                height: 300,
                color: Colors.tealAccent,
            ),
        ),
    ],
),

效果是这样的:

点击红色区域,会有得到相应,但是点击蓝色区域,因为被蓝色区域覆盖掉了,所以并没有触发红色区域的 onTap,但是如果我们想要点击蓝色区域也会触发红色区域的 tap 方法呢(就是类似于 Android 中的不让蓝色区域拦截事件)?

Flutter 又提供了一种 Widget,你说说这个事儿闹的,又是一个 Widget,真的是万物皆是 Widget。

Flutter 提供了两个 Widget:

  • IgnorePointer
  • AbsorbPointer

二者都能组织其子控件接收事件,区别在于,AbsorbPointer 本身是可以接收指针事件的(但其子树不行),而IgnorePointer 本身就不可以接收指针事件,所以上面的例子这样修改一下就可以了:

      body: Stack(
        children: <Widget>[
          GestureDetector(
              onTap: () {
                print("红色被点击 ${DateTime.now().millisecondsSinceEpoch}");
              },
              child: Container(
                width: 400,
                height: 400,
                color: Colors.deepOrange,
              )),
          GestureDetector(
            onTap: () {
              print("蓝色被点击 ${DateTime.now().millisecondsSinceEpoch}");
            },
            child: IgnorePointer(
              child: Container(
                width: 300,
                height: 300,
                color: Colors.tealAccent,
              ),
            ),
          ),
        ],
      ),

这样点击蓝色区域,就不会触发蓝色区域的 onTap 事件了,而是触发红色区域的。

那么疑问来了,上面说了 AbsorbPointer 本身是可以接收事件的,但是你看它的 API,却并没有类似于 onTap 之类的,那是怎么回事儿呢?

事实上,Flutter 还有一个 Listener,接下来看一下这个:

Listener

没错儿,它又是一个 Widget,哈哈哈。

我大概看了一下,这个玩意儿应该是一个简化版的 GestureDetector,废话不多说,看源码:

  const Listener({
    Key key,
    this.onPointerDown,    //手指按下
    this.onPointerMove,    //手指一动
    this.onPointerEnter,    //进入Widget区域时触发
    this.onPointerExit,    //离开Widget区域时触发
    this.onPointerHover,    //
    this.onPointerUp,    //手指抬起
    this.onPointerCancel,    //取消回掉
    this.onPointerSignal,    //这个没明白是干啥的
    this.behavior = HitTestBehavior.deferToChild,    //事件传递
    Widget child,    //子控件
  })

一个简单的例子:

body: Stack(
        children: <Widget>[
          Listener(
              onPointerDown: (detail) {
                print("手指按下");
              },
              onPointerMove: (detail) {
                print("手指移动 ${detail.localPosition.dx}");
              },
              onPointerUp: (detail) {
                print("手指抬起");
              },
              child: AbsorbPointer(
                child: Listener(
                  onPointerDown: (detail) {
                    print("child手指按下");
                  },
                  onPointerMove: (detail) {
                    print("child手指移动 ${detail.localPosition.dx}");
                  },
                  onPointerUp: (detail) {
                    print("child手指抬起");
                  },
                  child: Container(
                    width: 300,
                    height: 300,
                    color: Colors.deepOrange,
                  ),
                ),
              )),
        ],
      ),

效果如下:

可以发现,child 中的 Listener 一系列事件并没有触发,而父 Listener 自身的一系列额事件却触发 了,但是如果将 AbsorbPointer 改为 IgnorePointer,就都不会触发了,这里就不上效果图了。

results matching ""

    No results matching ""