前言
对于做为客户端开发,永远绕不开的两座大山:手势系统,渲染系统。Flutter做为当前比较火的跨平台开发框架,学习它的手势系统也是很有必要的。当然网上也有一些讲解,你可能会看到手势竞争,竞争胜出的会消费事件,但却很少能把如何竞争,以及为什么它能胜出或者失败能够讲清楚,当自己要处理手势问题时还是无从下手,不知道重点在哪里。文章涉及的源码很多,会拿一些widget来举例。内容可能会有一点枯燥。
适合哪些人看?
业务开发中遇到手势相关问题
对Flutter手势分发感兴趣,想要了解底层实现原理
面试需要
先做一道题开开胃
下面的代码运行后会屏幕中会出现一个红色方块,蓝色方块上覆盖着一个红色方块,请问分别进行以下操作,控制台的打印会是什么?
点击蓝色方块时
长按蓝色方块时
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);
-
GestureBinding
中result.add(HitTestEntry(this));
GestreueBinding把自己包装成HitTestEntry添加到了result中,这一点很重要,后面会用到
更多请查看 深度解读Flutter手势系统