深度解读Flutter手势系统

前言

对于做为客户端开发,永远绕不开的两座大山:手势系统渲染系统。Flutter做为当前比较火的跨平台开发框架,学习它的手势系统也是很有必要的。当然网上也有一些讲解,你可能会看到手势竞争,竞争胜出的会消费事件,但却很少能把如何竞争,以及为什么它能胜出或者失败能够讲清楚,当自己要处理手势问题时还是无从下手,不知道重点在哪里。文章涉及的源码很多,会拿一些widget来举例。内容可能会有一点枯燥。

适合哪些人看?

  • 业务开发中遇到手势相关问题

  • 对Flutter手势分发感兴趣,想要了解底层实现原理

  • 面试需要

先做一道题开开胃

下面的代码运行后会屏幕中会出现一个红色方块,蓝色方块上覆盖着一个红色方块,请问分别进行以下操作,控制台的打印会是什么?

  1. 点击蓝色方块时

  2. 长按蓝色方块时


import 'package:flutter/material.dart';

void main() {

  runApp(const MyApp());

}

class MyApp extends StatelessWidget {

  const MyApp({super.key});

  @override

  Widget build(BuildContext context) {

    return MaterialApp(

      theme: ThemeData(

        primarySwatch: Colors.blue,

      ),

      home: const MyHomePage(title: '手势示例'),

    );

  }

}

class MyHomePage extends StatefulWidget {

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

  final String title;

  @override

  State<MyHomePage> createState() => _MyHomePageState();

}

class _MyHomePageState extends State<MyHomePage> {

  @override

  Widget build(BuildContext context) {

    return Scaffold(

      appBar: AppBar(

        title: Text(widget.title),

      ),

      body: Container(

        alignment: Alignment.center,

        child: GestureDetector(

          onTapDown: (TapDownDetails details) {

            print("red onTapDown");

          },

          onTap: () {

            print("red onTap");

          },

          onLongPressDown: (LongPressDownDetails details){

            print("red onLongPressDown");

          },

          child: Container(

            color: Colors.red,

            height: 300,

            width: 300,

            alignment: Alignment.center,

            child: GestureDetector(

              onTapDown: (TapDownDetails details) {

                print("blue onTapDown");

              },

              onTap: () {

                print("blue onTap");

              },

              onLongPressDown: (LongPressDownDetails details){

                print("blue onLongPressDown");

              },

              child: Container(

                color: Colors.blue,

                height: 150,

                width: 150,

              ),

            ),

          ),

        ),

      ),

    );

  }

}

如果你发现打印的出乎你的意料,那你是否有兴趣进入Flutter的手势分发的世界!

手势分发

下面介绍Flutter手势分发的流程

示例一 onTapDown之无竞争手势

我们从一个简单的示例来入手,一个居中大小为300的红色方块


class _MyHomePageState extends State<MyHomePage> {

  @override

  Widget build(BuildContext context) {

    return Scaffold(

      appBar: AppBar(

        title: Text(widget.title),

      ),

      body: Container(

        alignment: Alignment.center,

        child: GestureDetector(

          onTapDown: (TapDownDetails details){

            print("red onTapDown");

          },

          child: Container(

            color: Colors.red,

            height: 300,

            width: 300,

          ),

        ),

      ),

    );

  }

}

我们打一个断点,点击蓝色的方块,看一下调用链:

[图片上传失败...(image-8b5b5b-1684980168825)]

[图片上传失败...(image-baa766-1684980168825)]

从调用链我们就能大概看到事件分发处理的流程

  • dispatchEvent

  • handleEvent

我们就从handlePointerEvent开始看,这个方法来自GestureBinding

如果看过flutter启动流程的同学应该知道,flutter定义了若干个Binding,如果处理手势的GestureBinding,渲染的RenderBinding,调度任务的SchedulerBinding。我们今天讲的手势系统,那理所应当就是在GestureBinding中


void handlePointerEvent(PointerEvent event) {

  //...

  _handlePointerEventImmediately(event);

}

我们继续进入到_handlePointerEventImmediately


void _handlePointerEventImmediately(PointerEvent event) {

  HitTestResult? hitTestResult;

  if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent || event is PointerPanZoomStartEvent) {

    hitTestResult = HitTestResult();

    hitTest(hitTestResult, event.position);//重点

    if (event is PointerDownEvent || event is PointerPanZoomStartEvent) {

      _hitTests[event.pointer] = hitTestResult;

    }

  } else if (event is PointerUpEvent || event is PointerCancelEvent || event is PointerPanZoomEndEvent) {

    hitTestResult = _hitTests.remove(event.pointer);

  } else if (event.down || event is PointerPanZoomUpdateEvent) {

    hitTestResult = _hitTests[event.pointer];

  }

  if (hitTestResult != null ||

      event is PointerAddedEvent ||

      event is PointerRemovedEvent) {

    dispatchEvent(event, hitTestResult); //重点

  }

}

手势事件的处理就在这个方法里了。我们先从第一个事件PointerDownEvent开始,整体就是2个步骤

  • 执行hitTest的到HitTestResult

  • dispatchEvent分发HitTestResult

下面先看hitTest

hitTest

GestureBinding的hitTest

我们先看它在GestureBinding中的定义是什么样的

[图片上传失败...(image-f0bd91-1684980168825)]

看到AS里的这两个箭头,应该能反应过来,它是一个覆写方法,同时还有子类覆写它。我们先看它继承自哪里,直接箭头点过去:

HitTestable


/// An object that can hit-test pointers.

abstract class HitTestable {

  // This class is intended to be used as an interface, and should not be

  // extended directly; this constructor prevents instantiation and extension.

  HitTestable._();

  /// Check whether the given position hits this object.

  ///

  /// If this given position hits this object, consider adding a [HitTestEntry]

  /// to the given hit test result.

  void hitTest(HitTestResult result, Offset position);

}

其实我觉得flutter的注释写的非常清楚了:一个可以测试pointers是否命中的对象

一个方法hitTest,用来检查给定位置是否命中该对象。如果这个给定的位置命中了这个对象,考虑添加一个[HitTestEntry],返回给定的HitTestResult。

RenderBinding的hitTest

前面我们看了继承类的hitTest,还是箭头直接点过去,我们发现实现是在RenderBinding中

如果有同学好奇为什么覆写跑到了RenderBinding中,可以了解下dart的mixin机制,WidgetsFlutterBinding的定义如下


class WidgetsFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding, ServicesBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {

}```


```dart

@override

void hitTest(HitTestResult result, Offset position) {

 renderView.hitTest(result, position: position);

 super.hitTest(result, position);

}

_handlePointerEventImmediately中hitTest的关键流程

通过前面的hitTest的继承实现分析,我们的结论是在_handlePointerEventImmediately执行的hitTest逻辑是

  • renderView.hitTest(result, position: position);

  • GestureBindingresult.add(HitTestEntry(this));

    GestreueBinding把自己包装成HitTestEntry添加到了result中,这一点很重要,后面会用到

更多请查看 深度解读Flutter手势系统

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

推荐阅读更多精彩内容