Flutter|初探事件处理

前言

作为一名前两年只专注于Android最近才开始接触Flutter(Dart)的跨平台小白来说,很多东西都是在解决业务需求过程中边做边学习的,甚至还会带着Android惯性思维去看待问题。在上手了一个简单UI需求之后就遇到了一个和“事件处理”有关的需求,通过这个需求算是对Flutter事件处理有了一个初步的认知,所以就有了这一篇学习笔记的诞生。
划重点:通篇是比较基础的知识,适合有原生开发经验的Flutter初学者阅读,已入门的大神们可以直接跳过啦┭┮

需求背景

现有一个动态详情页如图,可以在屏幕上通过手动上下滑或者点击最右侧上下两个按钮进行动态内容的切换。

很显然这是个 PageView(对应Android中的ViewPager),它有个属性physics可以定义如何响应用户操作,比如滑动到边界如何展示,默认情况下 iOS 会有弹性伸缩效果,Android 上会有弧形微光效果。

现在为了增强滑动到边界的体验,当用户处于第一条并继续向上滑或者处于最后一条并继续向下滑,需要弹出toast提示用户已经到顶/底了。

触摸事件

刚接到需求第一反应是分两个场景实现:按钮切换、手动切换。可以知道决定是否要弹出提示以及提示内容的两大因素是:滑动趋势(向上or向下)和当前页面index(判断第一页or最后一页)。

当前页面index是已知的,按钮的位置就是趋势,这个很好做;而手动切换,我立刻想起了Android中的MotionEvent,它包括了ACTION_DOWNACTION_MOVEACTION_UP等触摸事件,所以只要在DOWN&UP事件中通过Y轴差值就能得到滑动趋势。

PointerEvent

Android中的MotionEvent在Flutter对应的是PointerEvent,相应触摸事件有PointerDownEventPointerMoveEventPointerUpEvent这些子类。

Listener

Flutter中提供Listener 来监听以上PointerEvent,知道这些信息后,很快就写了以下代码(主要是手动切换的逻辑,按钮切换很简单不贴了):

// 1、在PageView上新增一个Listener节点
Listener(
  onPointerDown: (event) {
     // 2、在手指按下时记录此时的位置和页面
    _downY = event.position.dy;
    _downPageIndex = _currentPageIndex;
  },
  onPointerUp: (event) {、
    // 3、在手指抬起时计算Y轴差值
    final delta = event.position.dy - _downY;
    if (delta > 0 && _downPageIndex == 0) {
      Toasts.show(Strings.already_reached_the_top);
    }
    if (delta < 0 && _downPageIndex == value.length - 1) {
      Toasts.show(Strings.already_reached_the_bottom);
    }
  },
  child: PageView.builder(
    ...
  ),
),

我以为这就结束了,结果自测的时候才注意到PageView中item右侧有个评论区ListView,上下滑动这个ListView也会产生上述触摸事件,同样会通知到Listener,导致错误地弹出toast。所以关键就是如何只获取到PageView本身的事件,这时我又产生了新的疑问,Flutter又是怎么分发触摸事件的,会和Android是类似的吗,或许会有什么突破口?

事件分发机制

其实Flutter和Android事件分发机制的核心步骤一样,整体包括事件分发、拦截和响应(dispatch-intercept-onTouch)这三块。此外还有一个重要概念是HitTestResult,它维护了一组可以分发事件的渲染对象,比如手指按下后触发了一个PointerDownEvent事件,它会从顶层开始按照深度优先遍历整棵渲染树,如果事件的触发位置在该渲染对象范围内则会被放入HitTestResult,由于是深度遍历方式,子节点肯定会比父节点优先分发事件。

由于直接使用的是UI库中PageView,很显然已经处理过和ListView的滑动冲突问题了,那么它是否有提供自己滑动信息的回调呢?通过查看API文档,发现确实可以监听PageView的滚动距离(offset)和滚动页面(page),如下图表现的是从第4页通过按钮不断切换到第一页。

_pageController.addListener(() {
  print("minmin pageView offset = ${_pageController.offset},page = ${_pageController.page}");
});

但一旦到达边界(比如已经到第一页继续向上)就不会继续监听了,看来还要想想其他办法。

手势和手势冲突

GestureDetector

在学习PointerEvent时,我注意到Flutter也有手势的概念,描述一个或多个PointerEvent组成的语义动作,如GestureDetector就可以识别如双击、长按、拖动、缩放等手势,实际上它内部封装了 Listener,不同的是它还有手势竞争这一概念,即一个手势只会被一个GestureDetector消耗,一个GestureDetector监听多个手势也会产生冲突。

为了理解这一点, 现在把PageView上个节点换成GestureDetector并监听水平和垂直拖动事件看会发生什么。

// 1、在PageView上新增一个GestureDetector节点
GestureDetector(
  // 2、监听水平和垂直拖动事件
  onVerticalDragStart: (details) => print('minmin onVerticalDragStart'),
  onVerticalDragEnd: (details) => print('minmin onVerticalDragEnd'),
  onHorizontalDragStart: (details) => print('minmin onHorizontalDragStart'),
  onHorizontalDragEnd: (details) => print('minmin onHorizontalDragEnd'),
  child: PageView.builder(
    ...
  ),
),

由于PageView 本身已经处理了垂直拖动的手势,此时GestureDetector只能监听到水平拖动事件了。可以看到不管是Listener还是GestureDetector都不能只获取PageView 本身的事件,换个思路,那是否可以通过拦截ListView上的事件曲线救国呢?

通知机制

实际上Flutter有这样一套通知机制,每个节点都可以向上发送通知,而且通知可以在任意节点中止,中止后通知不会再向上传递。

ScrollNotification

比如PageViewListView都会发送ScrollNotification滚动通知,包括不同类型的通知子类有ScrollStartNotificationScrollEndNotificationOverscrollNotification等,下面总结了PageView在不同操作下的滚动通知情况。

// 无论手动切换还是按钮切换,都是从1个 ScrollStartNotification 开始到1个 ScrollEndNotification 结束
// 其中,可以通过中间是 ScrollUpdateNotification 和 OverscrollNotification 区分是否滑动到边界
// 另外,如果滚动到边界后手动操作,中间会有n个 OverscrollNotification
// 即 PageView 在不同操作下 ScrollNotification 的回调情况:
// 按钮切换(非边界):1次 ScrollStartNotification -> n次 ScrollUpdateNotification -> 1次 ScrollEndNotification
// 手动切换(非边界):1次 ScrollStartNotification  -> n次 ScrollUpdateNotification -> 1次 ScrollEndNotification
// 按钮切换(边界):1次 ScrollStartNotification -> 1次 OverscrollNotification-> 1次 ScrollEndNotification
// 手动切换(边界):1次 ScrollStartNotification -> n次 OverscrollNotification -> 1次
NotificationListener

NotificationListener用来监听子节点的通知,通过设置onNotification 通知处理回调的返回值来控制是否继续向上发送通知,返回 true表示父节点将不再收到通知。知道这些信息后就有了以下代码:

// 1、在PageView上新增一个NotificationListener节点,只接收ScrollNotification
NotificationListener<ScrollNotification>(
  onNotification: (notification) {
 ScrollEndNotification
    // 2、根据PageView的滚动表现,在 ScrollEndNotification 中判断是否要弹出toast
    switch (notification.runtimeType) {
      case ScrollUpdateNotification:
        _isOverscroll = false;
        break;
      case ScrollEndNotification:
        if (_isOverscroll) {
          if (_currentPageIndex == 0) {
            Toasts.show(Strings.already_reached_the_top);
          }
          if (_currentPageIndex == value.length - 1) {
            Toasts.show(Strings.already_reached_the_bottom);
          }
        }
        break;
      case OverscrollNotification:
        _isOverscroll = true;
        break;
      default:
        break;
    }
    return false;
  },
  child: PageView.builder(
    ...
       // 3、设置为ClampingScrollPhysics保证一定调用到OverscrollNotification
        physics: ClampingScrollPhysics(),
    itemBuilder: (BuildContext context, int index) {
      ...
      return NotificationListener(
          onNotification: (_) {
            // 4、在item外新增NotificationListener节点,并中止滚动通知
            return true;
          },
          child: ...;
    },
  ),
),

如此完成了需求,也不需要单独处理按钮切换了。


总结

以上就是我通过一个小需求对Flutter事件处理相关知识的学习记录,其实都是很常用的API,一开始没有直接想到解决办法还是自己不熟悉Flutter。因水平有限,如有错漏还望指出~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容