Flutter 滑动控制

核心结构

以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的偏移量,绘制区域来完成滑动

流程分析

  • 当手指滑动时,RawGestureDetector收集手势信息
  • 将手势信息回调到Scrollable中
  • Scrollable接收到信息后,通过ScrollPosition进行滑动控制
  • 修改偏移量,通过viewport绘制不同区域
  • 通知scrollcontroller,进行观察者通知

点击事件传递

原生经过c++引擎转发到flutter

这里挑取部分方法进行分析

GestureBinding _handlePointerDataPacket(ui.PointerDataPacket packet)

将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();
}

GestureBinding _flushPointerEventQueue()

将列表中的每个点击事件调用_handlePointerEvent进行处理

void _flushPointerEventQueue() {
    
    
  assert(!locked);
  while (_pendingPointerEvents.isNotEmpty)
     //处理每个点的点击事件
    _handlePointerEvent(_pendingPointerEvents.removeFirst());
}

GestureBinding _handlePointerEvent(PointerEvent event)

对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);
  }
}

hittest

///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));
  }
}

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,因为判断区域只要布局即可

对于每一个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的相关手势处理

手势竞争

因为通常点击后会返回一组可响应事件的组件集合,需要交付给哪个组件进行处理?

  • GestureRecoginizer:手势识别器基类,基本上RenderPointListener中需要处理的手势事件,都会分发到它对应的GestureRecognizer,通过处理和竞技后再分发,常见有:OneSequenceGestureRecognizer 、 MultiTapGestureRecognizer 、VerticalDragGestureRecognizer 、TapGestureRecognizer 等等。
  • GestureArenaManager:手势竞技管理,管理竞技过程,胜出条件为:第一个竞技获胜的成员最后一个不被拒绝的成员
  • GestureArenaEntry:提供手势事件竞技信息的实体,内封装参与事件竞技的成员
  • GestureArenaMember:参与竞技的成员抽象对象,内部有acceptGesture和rejectGesture方法,它代表手势竞技的成员,默认GestureArenaRecognizer都实现了它,所有竞技的成员可以理解为GestureRecognizer间的竞争
  • _GetureArena:GestureArenaManager内的竞技场,持有参与竞技的members列表

当一个手势试图在竞技场开放时获胜 isOpen = true,它将成为一个带有“渴望获胜”属性的对象,
当竞技场关闭时,竞技场会试图寻找一个渴望获胜的对象成为新的参与者

GestureBinding handleEvent(PointerEvent event, HitTestEntry entry)

导航事件去触发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);
  }
}

GestureArenaManager void close(int pointer)

在完成事件分发后调用,阻止新成员进入竞技

void close(int pointer) {
    
    
   //拿到上面 addPointer 时添加的成员封装
  final _GestureArena state = _arenas[pointer];
  //关闭竞技场
  state.isOpen = false;
  //决出胜者
  _tryToResolveArena(pointer, state);
}

GestureArenaManager _tryToResolveArena(int pointer, _GestureArena 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);
  }
}

GestureArenaManager sweep(int pointer)

迫使竞技场得出一个决胜者
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);
  }
}

BaseTapGestureRecognizer acceptGesture(int 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;
}

BaseTapGestureRecognizer _checkUp()

若非胜利者或事件为空则返回
否则处理up事件,并重置

void _checkUp() {
    
    
  ///_up为空或者不是手势竞争的胜利者,则直接返回
  if (!_wonArenaForPrimaryPointer || _up == null) {
    
    
    return;
  }
  handleTapUp(down: _down, up: _up);
  _reset();
}

TapGestureRecognizer#handleTapUp({PointerDownEvent down, PointerUpEvent up})

先执行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:
  }
}

省流:

  • 事件从Native层通过C++传递到Dart层,通过映射为逻辑像素后在GestureBinding中进行处理
  • 手势都从down开始,down阶段,HitTest从负责绘制树的根结点开始,递归将可响应事件的控件添加到HitTestResult中,最后添加GesureBinidng到末尾,对result中每个对象进行事件分发
  • RawGestureDetector会进行处理事件,down阶段,会将竞争者添加到_GestureArena进行竞争,最后回到GestureBinding关闭竞技场,若只有一个RawGestureDetector进行竞争,则直接acceptGesture,但依然不会触发onTap,up事件结束后触发onTapup后再触发onTap
  • 当多个RawGestureDetector竞争,在sweep中选取第一个为胜利者
  • 滑动则是move阶段就产生胜利者

猜你喜欢

转载自blog.csdn.net/weixin_51109304/article/details/131994361
今日推荐