以PageView为例
pv基于scrollable进行定制,四个完成功能的主要组件:ScrollNotification、RawGestureDetector、ScrollController和ScrollPosition、ViewPort
ScrollNotification:封装Notificaiton获得该类通知,根据通知信息内的偏移判断页面是否切换,然后回调onPageChanged
RawGestureDetector:手势收集类,Scrollable的setCanDrag方法绑定了VerticalDragGestureRecognizer或者HorizontalDragGestureRecognizer用来收集两个方向的滑动信息
ScrollController和ScrollPosition:ScrollPosition是Scrollable中实际控制滑动的对象,在SrcollController的attach方法中,ScrollPosition会将ScrollController作为其观察者添加到Listeners中,常通过ScrollController.addListener方法添加滚动监听
ViewPort:接受来自ScrollPosition的偏移量,绘制区域来完成滑动
原生经过c++引擎转发到flutter
这里挑取部分方法进行分析
将data中的数据,映射到为逻辑像素,再转变为设备像素
//未处理的事件队列
final Queue<PointerEvent> _pendingPointerEvents = Queue<PointerEvent>();
//这里的packet是一个点的信息
void _handlePointerDataPacket(ui.PointerDataPacket packet) {
// 将data中的数据,映射到为逻辑像素,再转变为设备像素
_pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, window.devicePixelRatio));
if (!locked)
_flushPointerEventQueue();
}
将列表中的每个点击事件调用_handlePointerEvent进行处理
void _flushPointerEventQueue() {
assert(!locked);
while (_pendingPointerEvents.isNotEmpty)
//处理每个点的点击事件
_handlePointerEvent(_pendingPointerEvents.removeFirst());
}
对down、up、move三种事件进行处理,包含:加入集合、取出、移除、分发
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);
}
}
///renderview:负责绘制的root节点
RenderView get renderView => _pipelineOwner.rootNode;
///绘制树的owner,负责绘制,布局,合成
PipelineOwner get pipelineOwner => _pipelineOwner;
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));
}
}
bool hitTest(HitTestResult result, { Offset position }) {
if (child != null)
(RenderBox)child.hitTest(BoxHitTestResult.wrap(result), position: position);
result.add(HitTestEntry(this));
return true;
}
给出指定position的所有绘制控件
返回true,当这个控件或者他的子控件位于给定的position的时候,添加这个绘制的对象到给定的hitResult中 这样标志当前的控件已经吸收了这个点击事件,其他控件不响应
返回false,表示这个事件交给在当前对象之后的控件处理
例如一个row里面,多个区域可以响应点击,只要第一块能响应点击的话,那后续就不用判断是否能响应了
全局的坐标转换为RenderBox关联的坐标,RenderBox负责判断这个坐标是否包含在当前的范围里
此方法依赖于最新的layout而不是paint,因为判断区域只要布局即可
对于每一个child调用自己的hitTest,所以布局最深的子wiget放在最开始
该方法先检查自己是否在范围内,是的话调用hitTestChildren,递归调用子Widget的hitTest,越深的widget越先被加入HitTestResult中。
执行完后,HitTestResult得到了点击事件坐标上所有能响应的控件集合,最终GestureBinding中最后把自己添加Result的结尾
bool hitTest(BoxHitTestResult result, { Offset position }) {
if (_size.contains(position)) {
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
}
return false;
}
当hitTestResult不为空时,进行事件分发,循环调用集合中每个对象的handleEvent,但不是所有的控件都会处理handlerEvent,大部分时候只有RenderPointerListener会处理。
handleEvent会根据不同的事件类型,回调到RawGestureDetector的相关手势处理中。
down事件出现,hittest根据点击的position获得一个可以响应事件的object集合,该集合末尾为GestureBinding、
通过dispatchEvent进行分发事件,但不是所有空间的RenderObject子类都会处理handleEvent,大部分时候由嵌入RawGestureDetector中的RenderPointerListener处理、
handleEvent根据不同事件类型,回调到RawGestureDetector的相关手势处理
因为通常点击后会返回一组可响应事件的组件集合,需要交付给哪个组件进行处理?
当一个手势试图在竞技场开放时获胜 isOpen = true,它将成为一个带有“渴望获胜”属性的对象,
当竞技场关闭时,竞技场会试图寻找一个渴望获胜的对象成为新的参与者
导航事件去触发GestureRecognizer的handleEvent,一般 PointerDownEvent 在 route 执行中不怎么处理。
gestureArena 就是 GestureArenaManager
down事件驱动竞技场关闭
// from HitTestTarget
void handleEvent(PointerEvent event, HitTestEntry entry) {
pointerRouter.route(event);
if (event is PointerDownEvent) {
// 关闭竞技场
gestureArena.close(event.pointer);
}
else if (event is PointerUpEvent) {
// 清理竞技场选出一个胜利者
gestureArena.sweep(event.pointer);
}
else if (event is PointerSignalEvent) {
pointerSignalResolver.resolve(event);
}
}
在完成事件分发后调用,阻止新成员进入竞技
void close(int pointer) {
//拿到上面 addPointer 时添加的成员封装
final _GestureArena state = _arenas[pointer];
//关闭竞技场
state.isOpen = false;
//决出胜者
_tryToResolveArena(pointer, state);
}
void _tryToResolveArena(int pointer, _GestureArena state) {
if (state.members.length == 1) {
//只有一个竞技成员的话,直接获胜,触发对应空间的acceptGesture
scheduleMicrotask(() => _resolveByDefault(pointer, state));
}
else if (state.members.isEmpty) {
//无竞技成员
_arenas.remove(pointer);
}
else if (state.eagerWinner != null) {
//多个竞技成员
_resolveInFavorOf(pointer, state, state.eagerWinner);
}
}
迫使竞技场得出一个决胜者
sweep通常是在up事件发生之后。它确保了竞争不会造成卡顿,从而阻止用户与应用程序交互。
void sweep(int pointer) {
///获取竞争的对象
final _GestureArena state = _arenas[pointer];
if (state.isHeld) {
state.hasPendingSweep = true;
return;
}
_arenas.remove(pointer);
if (state.members.isNotEmpty) {
//第一个竞争者获取胜利,就是Widget树中最深的组件
state.members.first.acceptGesture(pointer);
for (int i = 1; i < state.members.length; i++)
///让其他的竞争者拒绝接收手势
state.members[i].rejectGesture(pointer);
}
}
标志手势竞争胜利,调用_checkDown(),若已经处理过则不再此处理,未处理过则调用handleTapDown
void acceptGesture(int pointer) {
//标志已经获得了手势的竞争
super.acceptGesture(pointer);
if (pointer == primaryPointer) {
_checkDown();
_wonArenaForPrimaryPointer = true;
_checkUp();
}
}
void _checkDown() {
//如果已经处理过了,就不会再次处理!!
if (_sentTapDown) {
return;
}
//交给子控件处理down事件
handleTapDown(down: _down);
_sentTapDown = true;
}
若非胜利者或事件为空则返回
否则处理up事件,并重置
void _checkUp() {
///_up为空或者不是手势竞争的胜利者,则直接返回
if (!_wonArenaForPrimaryPointer || _up == null) {
return;
}
handleTapUp(down: _down, up: _up);
_reset();
}
先执行onTapUp再到onTap,完成一次点击事件的识别
void handleTapUp({PointerDownEvent down, PointerUpEvent up}) {
final TapUpDetails details = TapUpDetails(
globalPosition: up.position,
localPosition: up.localPosition,
);
switch (down.buttons) {
case kPrimaryButton:
if (onTapUp != null)
invokeCallback<void>('onTapUp', () => onTapUp(details));
if (onTap != null)
invokeCallback<void>('onTap', onTap);
break;
case kSecondaryButton:
if (onSecondaryTapUp != null)
invokeCallback<void>('onSecondaryTapUp',
() => onSecondaryTapUp(details));
break;
default:
}
}