首页 > 其他分享 >Flutter 陈航 19-手势识别 PointerEvent GestureDetector GestureRecognizer

Flutter 陈航 19-手势识别 PointerEvent GestureDetector GestureRecognizer

时间:2023-01-07 18:00:54浏览次数:64  
标签:GestureDetector PointerEvent GestureRecognizer flog 视图 事件 识别 手势

本文地址


目录

目录

19 | 用户交互事件该如何响应?

手势操作在 Flutter 中分为两类:

  • 第一类是原始的指针事件(Pointer Event),即原生开发中常见的触摸事件,表示屏幕上触摸行为触发的位移行为
  • 第二类则是手势识别(Gesture Detector),表示多个原始指针事件的组合操作,如点击、双击、长按等,是指针事件的语义化封装

指针事件 Listener

指针事件表示用户交互的原始触摸数据,如手指接触屏幕 PointerDownEvent、手指在屏幕上移动 PointerMoveEvent、手指抬起 PointerUpEvent,以及触摸取消 PointerCancelEvent,这与原生系统的底层触摸事件抽象是一致的。

在手指接触屏幕,触摸事件发起时,Flutter 会确定手指与屏幕发生接触的位置上究竟有哪些组件,并将触摸事件交给最内层的组件去响应。与浏览器中的事件冒泡机制类似,事件会从这个最内层的组件开始,沿着组件树向根节点向上冒泡分发。

不过 Flutter 无法像浏览器冒泡那样取消或者停止事件进一步分发,我们只能通过 hitTestBehavior 去调整组件在命中测试期内应该如何表现,比如把触摸事件交给子组件,或者交给其视图层级之下的组件去响应。

Flutter 提供了 Listener Widget,可以监听其子 Widget 的原始指针事件。

Listener

Listener(
  child: Container(color: Colors.blue, width: 200, height: 200),
  onPointerDown: (event) => flog("按下 $event"),
  onPointerMove: (event) => flog("移动 $event"),
  onPointerUp: (event) => flog("抬起 $event"),
)

完整代码

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'flog.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) => const MaterialApp(home: HomePage(title: '白乾涛'));
}

class HomePage extends StatefulWidget {
  final String title;

  const HomePage({super.key, required this.title});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text(widget.title)),
        body: Listener(...),
        floatingActionButton: FloatingActionButton(
          onPressed: () => flog("onPressed"),
          child: const Text("按钮"),
        ));
  }
}
import 'dart:developer';
import 'package:flutter/widgets.dart';

void flog(String text) {
  log(text);
  debugPrint(text);
}

手势识别

如果我们想从组件层监听手势,则需要使用 GestureDetector。GestureDetector 是一个处理各种高级用户触摸行为的 Widget,与 Listener 一样,也是一个功能性组件。

GestureDetector

Stack(
  children: <Widget>[
    Positioned(
      top: _top,
      left: _left,
      child: GestureDetector(
        child: Container(color: Colors.red, width: 200, height: 200),
        onTap: () => flog("点击"),
        onDoubleTap: () => flog("双击"),
        onLongPress: () => flog("长按"),
        onPanUpdate: (e) => flog("拖拽 ${e.delta.toString()}"),
      ),
    )
  ],
),

拖拽和缩放

拖拽 onPanUpdate 和 缩放 onScaleUpdate 不可以同时使用

onPanUpdate: (e) => setState(() {
  flog("拖拽 ${e.delta.info(1)}");
  _left += e.delta.dx;
  _top += e.delta.dy;
}),
onScaleUpdate: (details) => setState(() {
  flog("缩放 scale= ${details.scale} ${details.horizontalScale} ${details.verticalScale}");
  _width = 200 * details.horizontalScale;
  _height = 200 * details.verticalScale;
})
extension on Offset {
  String info([int? fractionDigits]) {
    var dxValue = dx.toStringAsExponential(fractionDigits);
    var dyValue = dy.toStringAsExponential(fractionDigits);
    return "dx=$dxValue dy=$dyValue";
  }
}

手势竞技场

尽管我们可以对一个 Widget 同时监听多个手势事件,但最终只会有一个手势能够得到本次事件的处理权。对于多个手势的识别,Flutter 引入了手势竞技场(Arena),用来识别究竟哪个手势可以响应用户事件。

手势竞技场会考虑用户触摸屏幕的时长、位移以及拖动方向,来确定最终手势。

实际上,GestureDetector 内部对每一个手势都建立了一个工厂类。而工厂类的内部会使用手势识别 GestureRecognizer 来确定当前处理的手势。

而所有手势的工厂类都会被交给 RawGestureDetector 类,以完成监测手势的大量工作:使用 Listener 监听原始指针事件,并在状态改变时把信息同步给所有的手势识别器,然后这些手势会在竞技场决定最后由谁来响应用户事件。

竞技场默认行为

有些时候我们可能会在应用中给多个视图注册同类型的手势监听器,像这样的手势识别发生在多个存在父子关系的视图时,手势竞技场会一并检查父视图和子视图的手势,并且通常最终会确认由子视图来响应事件。

GestureDetector(
  onTap: () => flog('父视图的点击回调'),
  child: Container(
    color: Colors.green,
    child: Center(
      child: GestureDetector(
        onTap: () => flog('子视图的点击回调'),
        child: Container(color: Colors.blue, width: 200, height: 200),
      ),
    ),
  ),
)

改变竞技场行为

为了让父容器也能接收到手势,我们需要同时使用 RawGestureDetector 和 GestureFactory,来改变竞技场决定由谁来响应用户事件的结果。

GestureRecognizer

GestureDetector 内部对每一个手势都建立了一个工厂类,工厂类的内部会使用手势识别类 GestureRecognizer 来确定当前要处理的手势。

TapGestureRecognizer 继承自 GestureRecognizer

/// 自定义点击手势识别器,让其在竞技场被 PK 失败时,把自己重新添加回来,以便接下来能继续响应手势事件
class MyTapGestureRecognizer extends TapGestureRecognizer {
  @override
  void rejectGesture(int pointer) {
    flog("rejectGesture(拒绝响应手势) pointer=$pointer");
    acceptGesture(pointer);
  }
}

GestureRecognizerFactory

GestureRecognizerFactory 用于工厂类的初始化,其提供了手势识别对象创建(_constructor),以及对应的初始化入口(_initializer)。

Factory for creating gesture recognizers that delegates to callbacks.

GestureRecognizerFactoryWithHandlers 继承自 GestureRecognizerFactory

/// 定义手势识别工厂的初始化逻辑
var _gestureRecognizerFactory = GestureRecognizerFactoryWithHandlers<MyTapGestureRecognizer>(
  _constructor,
  _initializer,
);

/// 提供手势识别对象创建的方法
MyTapGestureRecognizer Function() _constructor = () {
  flog("_constructor");
  return MyTapGestureRecognizer();
};

/// 提供初始化方法
_initializer(MyTapGestureRecognizer instance) {
  flog('_initializer');
  instance.onTap = () => flog('instance.onTap');
}

建立映射关系

建立多手势识别器 GestureRecognizer 与手势识别工厂 GestureRecognizerFactory 的映射关系

/// 用于建立多 手势识别器 GestureRecognizer 与手势识别工厂 GestureRecognizerFactory 的映射关系
Map<Type, GestureRecognizerFactory<MyTapGestureRecognizer>> _gestures = {
  MyTapGestureRecognizer: _gestureRecognizerFactory,
};

RawGestureDetector

将手势识别器和其工厂类 通过 属性 gestures 传递给 RawGestureDetector,以便用户产生手势交互事件时能够立刻找到对应的识别方法。事实上,RawGestureDetector 的初始化函数所做的配置工作,就是定义不同手势识别器和其工厂类的映射关系。

由于我们只需要在父容器监听子容器的点击事件,所以只需要将父容器用 RawGestureDetector 包装起来就可以了,而子容器保持不变。

RawGestureDetector(
  gestures: _gestures, // 构造父 Widget 的手势识别映射关系
  child: Container(
    color: Colors.green,
    child: Center(
      child: GestureDetector(
        onTap: () => flog('子视图的点击回调'), // 子视图可以继续使用 GestureDetector
        child: Container(color: Colors.blue, width: 200, height: 200),
      ),
    ),
  ),
),

效果

// 初始化
[log] _constructor
[log] _initializer
[log] _initializer

// 点击子视图区域
[log] 子视图的点击回调
[log] rejectGesture(拒绝响应手势) pointer=7
[log] instance.onTap

// 点击父视图区域
[log] instance.onTap

总结

在 Flutter 中,尽管我们可以对一个 Widget 监听多个手势,或是对多个 Widget 监听同一个手势,但 Flutter 会使用手势竞技场来进行各个手势的 PK,以保证最终只会有一个手势能够响应用户行为。如果我们希望同时能有多个手势去响应用户行为,需要去自定义手势,利用 RawGestureDetector 和手势工厂类,在竞技场 PK 失败时,手动把它复活。

在处理多个手势识别场景,很容易出现手势冲突的问题。比如,当需要对图片进行点击、长按、旋转、缩放、拖动等操作的时候,如何识别用户当前是点击还是长按,是旋转还是缩放。如果想要精确地处理复杂交互手势,我们势必需要介入手势识别过程解决异常。

不过需要注意的是,冲突的只是手势的语义化识别过程,原始指针事件是不会冲突的。所以,在遇到复杂的冲突场景通过手势很难搞定时,我们也可以通过 Listener 直接识别原始指针事件,从而解决手势识别的冲突。

2023-1-7

标签:GestureDetector,PointerEvent,GestureRecognizer,flog,视图,事件,识别,手势
From: https://www.cnblogs.com/baiqiantao/p/17033128.html

相关文章

  • Android GestureDetector手势识别类
    为了加强鼠标响应事件,Android提供了GestureDetector手势识别类。通过​​GestureDetector.OnGestureListener​​来获取当前被触发的操作手势(SingleTapUp、ShowPress、Lo......
  • Android GestureDetector
    之前一直不知道这个类,在Android就以为只有鼠标的down和up事件,原来android为了增加用户体验,新增了GestureDetector类,也就是手势识别类,感觉就是将手指触摸屏幕的touch事件更加......
  • 使用iOS手势UIGestureRecognizer(转)
    UIKit中包含了UIGestureRecognizer类,用于检测发生在设备中的手势。UIGestureRecognizer是一个抽象类,定义了所有手势的基本行为,它有下面一些子类用于处理具体的手势:1、......