likes
comments
collection
share

深度解读Flutter手势系统

作者站长头像
站长
· 阅读数 39

前言

对于做为客户端开发,永远绕不开的两座大山:手势系统渲染系统。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,
          ),
        ),
      ),
    );
  }
}

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

深度解读Flutter手势系统

深度解读Flutter手势系统

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

  • 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中的定义是什么样的

深度解读Flutter手势系统

看到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 {
}
@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手势系统

转载自:https://juejin.cn/post/7234337275344355389
评论
请登录