Flutter sliding control

core structure

Taking PageView as an example
, pv is customized based on scrollable. There are four main components to complete the function: ScrollNotification, RawGestureDetector, ScrollController, ScrollPosition, and ViewPort

  • ScrollNotification: Encapsulate Notificaiton to obtain this type of notification, determine whether the page is switched based on the offset in the notification information, and then call back onPageChanged

  • RawGestureDetector: Gesture collection class, Scrollable's setCanDrag method is bound to VerticalDragGestureRecognizer or HorizontalDragGestureRecognizer to collect sliding information in both directions.

  • ScrollController and ScrollPosition: ScrollPosition is the object that actually controls sliding in Scrollable. In the attach method of SrcollController, ScrollPosition will add ScrollController as its observer to Listeners. Scroll listeners are often added through the ScrollController.addListener method.

  • ViewPort: accepts the offset from ScrollPosition and draws the area to complete the sliding

process analysis

  • RawGestureDetector collects gesture information when the finger slides
  • Call back gesture information into Scrollable
  • After Scrollable receives the information, it performs sliding control through ScrollPosition.
  • Modify the offset and draw different areas through the viewport
  • Notify scrollcontroller for observer notification

Click event delivery

The native data is forwarded to flutter through the C++ engine.

Here we select some methods for analysis

GestureBinding _handlePointerDataPacket(ui.PointerDataPacket packet)

Map the data in data to logical pixels and then convert them into device pixels

//未处理的事件队列
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()

Call _handlePointerEvent for each click event in the list to process

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

GestureBinding _handlePointerEvent(PointerEvent event)

Processing of down, up, and move events, including: adding to collection, taking out, removing, and distributing

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

heat test

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

Gives all drawing controls at the specified position

  • Returns true. When this control or its sub-controls are at the given position, add the drawn object to the given hitResult to mark that the current control has absorbed the click event and other controls will not respond.

  • Return false, indicating that this event is handed over to the control after the current object for processing

For example, in a row, multiple areas can respond to clicks. As long as the first block can respond to clicks, then there is no need to judge whether it can respond in the future.

The global coordinates are converted to the coordinates associated with the RenderBox. The RenderBox is responsible for determining whether the coordinates are included in the current range.

This method relies on the latest layout rather than paint, because the judgment area only needs to be laid out

For each child, its own hitTest is called, so the child wiget with the deepest layout is placed at the beginning.

This method first checks whether it is within the range. If so, it calls hitTestChildren and recursively calls the hitTest of the child Widget. The deeper the widget is, the earlier it is added to the HitTestResult.

After execution, HitTestResult obtains the collection of all responsive controls on the click event coordinates, and finally adds itself to the end of Result in GestureBinding.

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

When hitTestResult is not empty, event distribution is performed, and the handleEvent of each object in the collection is called in a loop. However, not all controls will handle handlerEvent. Most of the time, only RenderPointerListener will handle it.

handleEvent will call back to the relevant gesture processing of RawGestureDetector according to different event types.

Save money

The down event occurs, and hittest obtains a collection of objects that can respond to the event based on the clicked position. The end of the collection is GestureBinding, and events are distributed
through dispatchEvent . However, not all RenderObject subclasses in all spaces handle handleEvent. Most of the time, the handleEvent is processed by the object embedded in RawGestureDetector. RenderPointerListener processing,
handleEvent calls back to RawGestureDetector 's related gesture processing according to different event types.

gesture competition

Because usually after clicking, a set of components that can respond to events will be returned. Which component needs to be delivered to it for processing?

  • GestureRecoginizer : Gesture recognizer base class. Basically, the gesture events that need to be processed in RenderPointListener will be distributed to its corresponding GestureRecognizer, and then distributed after processing and competition. Common ones include: OneSequenceGestureRecognizer, MultiTapGestureRecognizer, VerticalDragGestureRecognizer, TapGestureRecognizer, etc.
  • GestureArenaManager : Gesture competition management, manages the competition process, the winning conditions are: the first member to win the competition or the last member not to be rejected .
  • GestureArenaEntry : An entity that provides gesture event competition information and encapsulates the members participating in the event competition.
  • GestureArenaMember : Abstract object of members participating in the competition. There are acceptGesture and rejectGesture methods internally. It represents the members of the gesture competition. The default GestureArenaRecognizer implements it. All members of the competition can be understood as competition between GestureRecognizers.
  • _GetureArena : The arena in GestureArenaManager, holding the list of members participating in the competition

When a gesture attempts to win when the arena is open with isOpen = true , it will become an object with the "eager to win" attribute.
When the arena is closed, the arena will try to find an eager to win object to become a new participant.

GestureBinding handleEvent(PointerEvent event, HitTestEntry entry)

Navigation events trigger the handleEvent of GestureRecognizer. Generally, PointerDownEvent is not processed in route execution.

gestureArena GestureArenaManager

down event drives arena closure

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

Called after event distribution is completed to prevent new members from entering the competition

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)

Forcing the arena to draw a winner
sweep usually occurs after the up event. It ensures that competition does not cause lags that prevent users from interacting with the application.

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)

To mark the gesture competition victory, call _checkDown(). If it has been processed, it will not be processed again. If it has not been processed, handleTapDown will be called.


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()

If it is not the winner or the event is empty, return
otherwise handle the up event and reset

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

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

First execute onTapUp and then onTap to complete the recognition of a click event.

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:
  }
}

Save money:

  • The event is passed from the Native layer to the Dart layer through C++, and is processed in GestureBinding after being mapped to logical pixels.
  • Gestures all start from down. In the down phase, HitTest starts from the root node responsible for drawing the tree, recursively adds controls that can respond to events to HitTestResult, and finally adds GesureBinidng to the end to distribute events to each object in the result.
  • RawGestureDetector will process the event. In the down stage, competitors will be added to _GestureArena to compete, and finally return to GestureBinding to close the arena. If there is only one RawGestureDetector competing, it will accept Gesture directly, but onTap will still not be triggered. After the up event ends Trigger onTap after triggering onTapup
  • When multiple RawGestureDetectors compete, the first one in sweep is selected as the winner.
  • Sliding creates a winner in the move stage.

Guess you like

Origin blog.csdn.net/weixin_51109304/article/details/131994361