1 Flutter 的滑动原理
1.1 PageView的核心结构
(一) PageView
PageView
就是基于Scrollable
进行了定制,通过封装Notification
获取到ScrollNotification
类的通知,根据通知信息里的偏移判断当前页面是否发生了切换,然后回调onPageChanged
。
(二)RawGestureDetector
手势收集类,在Scrollable
的setCanDrag
方法中,绑定了VerticalDragGestureRecognizer
或者HorizontalDragGestureRecognizer
用来收集垂直或水平方向的滑动信息。
(三) ScrollController与ScrollPosition
ScrollPosition
是Scrollable
中实际控制滑动的对象,在ScrollController
的attach
方法中。ScrollPosition
会将ScrollController
作为它的观察者添加到Listeners中,我们往往使用ScrollController.addListener
方法添加滚动监听,实际上的通知顺序是:ScrollPosition->ScrollController
->添加的回调
(四) Viewport
接受来自ScrollPosition
的偏移量,绘制不同的区域完成"滑动"。
1.2基本流程分析
当手指在屏幕上滑动的时候,首先RawGestureDetector
收集了手势信息(里面处理了手势竞争的流程,下期展开叙述),之后将手势信息回调到Scrollable
中。Scrollable
接受到信息后,通过ScrollPosition
进行滑动控制包含(1)修改偏移量,通知Viewport
绘制不同的区域 (2)通知ScrollController
,进行观察者的通知。
2 从一次点击探寻Flutter的事件分发原理
2.1 触摸事件传递
首先我们知道,用户的任何交互行为,一定是在原生设备上进行。所以我们事件分发肯定是从Native侧传递到Flutter,下面一张图描述了这个过程
一次点击响应可以分解为两个事件,一个手指按下的Down事件,一个手指抬起的Up事件。以安卓为例,首先这两个事件依次从Java层传递到了C++,最终传递至Dart。在Dart部分,我们注意到经过zone.runUnaryGuarded
方法之后会调用到window.onPointDataPacket
方法处(window是Flutter中一个非常核心的概念,是作为一个与Native交互的源对象,后面单独介绍),查看GestureBinding初始化的过程得知这个方法会执行_handlePointerDataPacket
mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, HitTestTarget {
@override
void initInstances() {
super.initInstances();
_instance = this;
//将_handlePointerDataPacket设置为 window.onPointerDataPacket回调
window.onPointerDataPacket = _handlePointerDataPacket;
}
}
注:mixin是面向对象程序设计语言中的类,提供了方法的实现。其他类可以访问mixin类的方法、变量而不必成为其子类。该方法中表示GestureBinding继承BindingBase(抽象类)。
GestureBinding# _handlePointerDataPacket(ui.PointerDataPacket packet)
//未处理的事件队列
final Queue<PointerEvent> _pendingPointerEvents = Queue<PointerEvent>();
//这里的packet是一个点的信息
void _handlePointerDataPacket(ui.PointerDataPacket packet) {
// We convert pointer data to logical pixels so that e.g. the touch slop can be
// defined in a device-independent manner.
// 将data中的数据,映射到为逻辑像素
// window.devicePixelRatio 一个逻辑像素对应的设备像素,比如nex6:3.5
_pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, window.devicePixelRatio));
if (!locked)
_flushPointerEventQueue();
}
这个方法首先会根据设备的属性将传递来数据映射到为逻辑像素后添加至队列,下一步调用_flushPointerEventQueue()
GestureBinding# _flushPointerEventQueue()
void _flushPointerEventQueue() {
assert(!locked);
while (_pendingPointerEvents.isNotEmpty)
//直接调用_handlePointerEvent
_handlePointerEvent(_pendingPointerEvents.removeFirst());
}
GestureBinding# _handlePointerEvent(PointerEvent event)
/// 此处的key是event.pointer,pointer是不会重复的,每个down事件的时候会去+1
final Map<int, HitTestResult> _hitTests = <int, HitTestResult>{
};
void _handlePointerEvent(PointerEvent event) {
HitTestResult hitTestResult;
if (event is PointerDownEvent || event is PointerSignalEvent) {
//down事件进行hitTest
hitTestResult = HitTestResult();
hitTest(hitTestResult, event.position);
if (event is PointerDownEvent) {
// dowmn事件的话对这个hitTest集合赋值
_hitTests[event.pointer] = hitTestResult;
}
} else if (event is PointerUpEvent || event is PointerCancelEvent) {
// up事件标识这次操作已经结束,所以移除
hitTestResult = _hitTests.remove(event.pointer);
} else if (event.down) {
// move事件也应该被分发在down事件初始点击的区域 比如点击了列表中的A item这个时候开始滑动,那处理这个事件的始终只是列表和A item, 只是如果滑动的话事件是由列表进行处理
hitTestResult = _hitTests[event.pointer];
}
if (hitTestResult != null ||
event is PointerHoverEvent ||
event is PointerAddedEvent ||
event is PointerRemovedEvent) {
dispatchEvent(event, hitTestResult);
}
}
调用最终走到_handlePointerEvent(PointerEvent event)
,因为点击事件肯定是从Down事件开始,在PointerDownEvent的流程中先声明了一个HitTestResult()
对象,之后调用 hitTest(hitTestResult, event.position)
。
2.2 HitTest收集响应控件与分发
///renderview:负责绘制的root节点
RenderView get renderView => _pipelineOwner.rootNode;
///绘制树的owner,负责绘制,布局,合成
PipelineOwner get pipelineOwner => _pipelineOwner;
@override
void hitTest(HitTestResult result, Offset position) {
assert(renderView != null);
renderView.hitTest(result, position: position);
super.hitTest(result, position);
=>
GestureBinding#hitTest(HitTestResult result, Offset position) {
result.add(HitTestEntry(this));
}
}
这个hitTest方法被RendererBinding重写,里面调用了renderView的hitTest(result, position: position)
。renderView是绘制树的根节点,是所有Widget的祖先。
RenderView#hitTest(BoxHitTestResult result, { @required Offset position })
bool hitTest(HitTestResult result, {
Offset position }) {
if (child != null)
(RenderBox)child.hitTest(BoxHitTestResult.wrap(result), position: position);
result.add(HitTestEntry(this));
return true;
}
RenderBox#hitTest(BoxHitTestResult result, { @required Offset position })
///作用:给出指定position的所有绘制控件
///返回true,当这个控件或者他的子控件位于给定的position的时候,添加这个绘制的对象到给定的hitResult中 这样标志当前的控件已经吸收了这个点击事件,其他控件不响应
///返回false,表示这个事件交给在当前对象之后的控件处理,
///例如一个row里面,多个区域可以响应点击,只要如果第一块能响应点击的话,那后续就不用判断是否能响应了
///调用方需要将全局的坐标转换为RenderBox关联的坐标,Renderbox负责判断这个坐标是否包含在当前的范围里
///这个方法依赖于最新的layout而不是paint,因为判断区域只要布局即可
bool hitTest(BoxHitTestResult result, {
@required Offset position }) {
if (_size.contains(position)) {
// 对于每一个child调用自己的 hitTest,所以布局最深的子wiget放在最开始
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
}
return false;
}
而RenderView的hitTest(BoxHitTestResult result, { @required Offset position })
最终调用RenderBox的hitTest(BoxHitTestResult result, { @required Offset position })
,hitTestChildren
和hitTestSelf
是两个抽象方法(因为Widget可能有一个child或者多个child),查看具体实现发现其实逻辑和这儿差不多,也是先判断自己是否在这次点击的Postion范围内,然后递归调用子Widget的hitTest。观察这个方法结构,我们知道,如果一个Widget越深,则越先被添加进HitTestResult中。这样的流程执行下来,HitTestResult就得到了这次点击事件坐标上所有能响应的控件集合。需要注意,GestureBinding中最后把自己添加到Result的结尾。
//GestureBinding中最后把自己添加到result中
@override // from HitTestable
void hitTest(HitTestResult result, Offset position) {
result.add(HitTestEntry(this));
}
GestureBinding#void dispatchEvent(PointerEvent event, HitTestResult hitTestResult)
_handlePointerEvent()
if (hitTestResult != null ||
event is PointerHoverEvent ||
event is PointerAddedEvent ||
event is PointerRemovedEvent) {
dispatchEvent(event,