主頁 > 移動端開發 > Flutter 手勢GestureDetector決議

Flutter 手勢GestureDetector決議

2021-11-09 08:26:14 移動端開發

對于移動端的開發者來說,手勢是一個非常重要的模塊,基本上做任何App都會遇到各種各樣的手勢問題,而手勢也是移動的一個不算小的模塊吧,要徹底搞得還是得費一些時間的,如果之前對Android或者IOS的手勢或者說點擊事件的原理有所了解的,那么了解其它語言的手勢原理相對來說幫助還是挺大的,

好了,切入正題,在Flutter中,對于Flutter有一定了解的人都知道,可以通過GestureDetector來給不具有點擊事件或者手勢回呼的Widget添加手勢回呼,然后為了點擊水波紋的點擊效果,大多數開發者可能會使用InkWell widget來包裝一個需要添加點擊事件的控制元件,

前戲部分: InkWell 和 GestureDetector的區別

對Flutter有一一些深入了解的人可能知道,InkWell就是對GestureDetector的一個封裝,看圖:
在這里插入圖片描述

  1. InkWell是繼承于InkResponse,
  2. InkResponse是集成于StatelessWidget類,
  3. 在onBuild中回傳了_InkResponseStateWidget

由于以上這部分代碼沒有什么邏輯,為了減少篇幅我就不貼原始碼了,
_InkResponseStateWidget中的核心代碼如下:

return _ParentInkResponseProvider(
      state: this,
      child: Actions(
        actions: _actionMap,
        child: Focus(
          focusNode: widget.focusNode,
          canRequestFocus: _canRequestFocus,
          onFocusChange: _handleFocusUpdate,
          autofocus: widget.autofocus,
          child: MouseRegion(
            cursor: effectiveMouseCursor,
            onEnter: _handleMouseEnter,
            onExit: _handleMouseExit,
            child: Semantics(
              onTap: widget.excludeFromSemantics || widget.onTap == null ? null : _simulateTap,
              onLongPress: widget.excludeFromSemantics || widget.onLongPress == null ? null : _simulateLongPress,
              child: GestureDetector(//InkWell手勢的來源
                onTapDown: enabled ? _handleTapDown : null,
                onTap: enabled ? _handleTap : null,
                onTapCancel: enabled ? _handleTapCancel : null,
                onDoubleTap: widget.onDoubleTap != null ? _handleDoubleTap : null,
                onLongPress: widget.onLongPress != null ? _handleLongPress : null,
                behavior: HitTestBehavior.opaque,
                excludeFromSemantics: true,
                child: widget.child,
              ),
            ),
          ),
        ),
      ),
    );

通過上述代碼可以看出,GestureDetector是Flutter中手勢的一個最基本類,我們可以直接用,也可以給予GestureDetector來做一些列的自定義封裝

切入正題

一、 GestureDetector簡介

class GestureDetector extends StatelessWidget {
// 省略代碼
}

/// A widget that detects gestures.
///
/// Attempts to recognize gestures that correspond to its non-null callbacks.
///
/// If this widget has a child, it defers to that child for its sizing behavior.
/// If it does not have a child, it grows to fit the parent instead.
///
/// By default a GestureDetector with an invisible child ignores touches;
/// this behavior can be controlled with [behavior].

這個是官方簡介,我理解的大概意思就是說GestureDetector是一個小控制元件,事件的點擊區域會以子控制元件為準,如果子控制元件為不可見或者沒有子控制元件,則會去適應父控制元件,而這個行為可以通過behavior屬性來控制,這塊內容不是今天的重點,我們先看重點吧,

二、GestureDetector功能決議

既然是Widiget,那么核心代碼肯定在onBuild中,我們直接先看看一下原始碼,

 @override
  Widget build(BuildContext context) {
    final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};

    if (onTapDown != null ||
        onTapUp != null ||
        onTap != null ||
        onTapCancel != null ||
        onSecondaryTap != null ||
        onSecondaryTapDown != null ||
        onSecondaryTapUp != null ||
        onSecondaryTapCancel != null||
        onTertiaryTapDown != null ||
        onTertiaryTapUp != null ||
        onTertiaryTapCancel != null
    ) {
      gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
        () => TapGestureRecognizer(debugOwner: this),
        (TapGestureRecognizer instance) {
          instance
            ..onTapDown = onTapDown
            ..onTapUp = onTapUp
            ..onTap = onTap
            ..onTapCancel = onTapCancel
            ..onSecondaryTap = onSecondaryTap
            ..onSecondaryTapDown = onSecondaryTapDown
            ..onSecondaryTapUp = onSecondaryTapUp
            ..onSecondaryTapCancel = onSecondaryTapCancel
            ..onTertiaryTapDown = onTertiaryTapDown
            ..onTertiaryTapUp = onTertiaryTapUp
            ..onTertiaryTapCancel = onTertiaryTapCancel;
        },
      );
    }

    if (onDoubleTap != null) {
      gestures[DoubleTapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
        () => DoubleTapGestureRecognizer(debugOwner: this),
        (DoubleTapGestureRecognizer instance) {
          instance
            ..onDoubleTapDown = onDoubleTapDown
            ..onDoubleTap = onDoubleTap
            ..onDoubleTapCancel = onDoubleTapCancel;
        },
      );
    }

    if (onLongPress != null ||
        onLongPressUp != null ||
        onLongPressStart != null ||
        onLongPressMoveUpdate != null ||
        onLongPressEnd != null ||
        onSecondaryLongPress != null ||
        onSecondaryLongPressUp != null ||
        onSecondaryLongPressStart != null ||
        onSecondaryLongPressMoveUpdate != null ||
        onSecondaryLongPressEnd != null) {
      gestures[LongPressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
        () => LongPressGestureRecognizer(debugOwner: this),
        (LongPressGestureRecognizer instance) {
          instance
            ..onLongPress = onLongPress
            ..onLongPressStart = onLongPressStart
            ..onLongPressMoveUpdate = onLongPressMoveUpdate
            ..onLongPressEnd = onLongPressEnd
            ..onLongPressUp = onLongPressUp
            ..onSecondaryLongPress = onSecondaryLongPress
            ..onSecondaryLongPressStart = onSecondaryLongPressStart
            ..onSecondaryLongPressMoveUpdate = onSecondaryLongPressMoveUpdate
            ..onSecondaryLongPressEnd = onSecondaryLongPressEnd
            ..onSecondaryLongPressUp = onSecondaryLongPressUp;
        },
      );
    }

    if (onVerticalDragDown != null ||
        onVerticalDragStart != null ||
        onVerticalDragUpdate != null ||
        onVerticalDragEnd != null ||
        onVerticalDragCancel != null) {
      gestures[VerticalDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers<VerticalDragGestureRecognizer>(
        () => VerticalDragGestureRecognizer(debugOwner: this),
        (VerticalDragGestureRecognizer instance) {
          instance
            ..onDown = onVerticalDragDown
            ..onStart = onVerticalDragStart
            ..onUpdate = onVerticalDragUpdate
            ..onEnd = onVerticalDragEnd
            ..onCancel = onVerticalDragCancel
            ..dragStartBehavior = dragStartBehavior;
        },
      );
    }

    if (onHorizontalDragDown != null ||
        onHorizontalDragStart != null ||
        onHorizontalDragUpdate != null ||
        onHorizontalDragEnd != null ||
        onHorizontalDragCancel != null) {
      gestures[HorizontalDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>(
        () => HorizontalDragGestureRecognizer(debugOwner: this),
        (HorizontalDragGestureRecognizer instance) {
          instance
            ..onDown = onHorizontalDragDown
            ..onStart = onHorizontalDragStart
            ..onUpdate = onHorizontalDragUpdate
            ..onEnd = onHorizontalDragEnd
            ..onCancel = onHorizontalDragCancel
            ..dragStartBehavior = dragStartBehavior;
        },
      );
    }

    if (onPanDown != null ||
        onPanStart != null ||
        onPanUpdate != null ||
        onPanEnd != null ||
        onPanCancel != null) {
      gestures[PanGestureRecognizer] = GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
        () => PanGestureRecognizer(debugOwner: this),
        (PanGestureRecognizer instance) {
          instance
            ..onDown = onPanDown
            ..onStart = onPanStart
            ..onUpdate = onPanUpdate
            ..onEnd = onPanEnd
            ..onCancel = onPanCancel
            ..dragStartBehavior = dragStartBehavior;
        },
      );
    }

    if (onScaleStart != null || onScaleUpdate != null || onScaleEnd != null) {
      gestures[ScaleGestureRecognizer] = GestureRecognizerFactoryWithHandlers<ScaleGestureRecognizer>(
        () => ScaleGestureRecognizer(debugOwner: this),
        (ScaleGestureRecognizer instance) {
          instance
            ..onStart = onScaleStart
            ..onUpdate = onScaleUpdate
            ..onEnd = onScaleEnd
            ..dragStartBehavior = dragStartBehavior;
        },
      );
    }

    if (onForcePressStart != null ||
        onForcePressPeak != null ||
        onForcePressUpdate != null ||
        onForcePressEnd != null) {
      gestures[ForcePressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<ForcePressGestureRecognizer>(
        () => ForcePressGestureRecognizer(debugOwner: this),
        (ForcePressGestureRecognizer instance) {
          instance
            ..onStart = onForcePressStart
            ..onPeak = onForcePressPeak
            ..onUpdate = onForcePressUpdate
            ..onEnd = onForcePressEnd;
        },
      );
    }

    return RawGestureDetector(
      gestures: gestures,
      behavior: behavior,
      excludeFromSemantics: excludeFromSemantics,
      child: child,
    );
  }

代碼相當的長,看著很復雜,其實邏輯非常的簡單,就是注冊手勢的識別器,

   final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
將邏輯細分一下:
  1. 創建一個Map容器gestures
  2. 添加單擊識別器
  3. 添加雙擊識別器
  4. 添加縱向(y軸方向)滑動識別器
  5. 添加橫向(x軸方向)滑動識別器
  6. 添加雙向(y軸和x軸)同時滑動識別器
  7. 添加縮放手勢識別器
  8. 添加帶有力傳感器的識別器
  9. 最后根據這些引數創建一個RawGestureDetector控制元件,

當然,添加這些手勢識別器的前提條件就是有回呼需求,也就是if中的那些判斷,因此通過上述代碼可以總結出我們通過使用GestureDetector的功能可以理解為再有需要的情況下注冊手勢識別器的監聽,那么既然有監聽,肯定就有地方將事件發送出來,

三、手勢事件分發跟蹤

因為flutter中的很多方法都是回呼的方式,而且很多原始碼都是介面的形式去呼叫的,直接扒原始碼比較難,因此我們通過打斷點觀察方法呼叫堆疊的形式來追蹤手勢的傳遞程序,
在這里插入圖片描述
在這里插入圖片描述
上面兩個截圖,一個是通過InkWell來注冊一個手勢回呼,第二個截圖則是方法呼叫堆疊,從這個斷點可以看到,點擊手勢的最終來源于Honks.dart檔案中的_dispatchPointerDataPacket方法

@pragma('vm:entry-point')
// ignore: unused_element
void _dispatchPointerDataPacket(ByteData packet) {
  PlatformDispatcher.instance._dispatchPointerDataPacket(packet);
}

原始碼比較簡單,就是從引擎VM中獲取到點擊螢屏的一個ByteData資料包,拿到之后就丟給PlatformDispatcher中的方法去處理,
再看PlatformDispathcer._dispatchPointerDataPacket方法

  // Called from the engine, via hooks.dart
  void _dispatchPointerDataPacket(ByteData packet) {
    if (onPointerDataPacket != null) {
      _invoke1<PointerDataPacket>(
        onPointerDataPacket,
        _onPointerDataPacketZone,
        _unpackPointerDataPacket(packet),
      );
    }
  }

上面這部分不是核心代碼,只是一個方法的呼叫,核心代碼_unpackPointerDataPacket方法中的邏輯,如下:

static PointerDataPacket _unpackPointerDataPacket(ByteData packet) {
    const int kStride = Int64List.bytesPerElement;
    const int kBytesPerPointerData = _kPointerDataFieldCount * kStride;
    final int length = packet.lengthInBytes ~/ kBytesPerPointerData;
    assert(length * kBytesPerPointerData == packet.lengthInBytes);
    final List<PointerData> data = <PointerData>[];
    for (int i = 0; i < length; ++i) {
      int offset = i * _kPointerDataFieldCount;
      data.add(PointerData(
        embedderId: packet.getInt64(kStride * offset++, _kFakeHostEndian),
        timeStamp: Duration(microseconds: packet.getInt64(kStride * offset++, _kFakeHostEndian)),
        change: PointerChange.values[packet.getInt64(kStride * offset++, _kFakeHostEndian)],
        kind: PointerDeviceKind.values[packet.getInt64(kStride * offset++, _kFakeHostEndian)],
        signalKind: PointerSignalKind.values[packet.getInt64(kStride * offset++, _kFakeHostEndian)],
        device: packet.getInt64(kStride * offset++, _kFakeHostEndian),
        pointerIdentifier: packet.getInt64(kStride * offset++, _kFakeHostEndian),
        physicalX: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
        physicalY: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
        physicalDeltaX: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
        physicalDeltaY: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
        buttons: packet.getInt64(kStride * offset++, _kFakeHostEndian),
        obscured: packet.getInt64(kStride * offset++, _kFakeHostEndian) != 0,
        synthesized: packet.getInt64(kStride * offset++, _kFakeHostEndian) != 0,
        pressure: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
        pressureMin: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
        pressureMax: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
        distance: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
        distanceMax: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
        size: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
        radiusMajor: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
        radiusMinor: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
        radiusMin: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
        radiusMax: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
        orientation: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
        tilt: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
        platformData: packet.getInt64(kStride * offset++, _kFakeHostEndian),
        scrollDeltaX: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
        scrollDeltaY: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
      ));
      assert(offset == (i + 1) * _kPointerDataFieldCount);
    }
    return PointerDataPacket(data: data);
  }

這塊代碼的計算邏輯比較復雜,如果不細看,只是了解大概邏輯的話,還是比較好理解的,就是從引擎獲取到的Bytedata中決議出PointerData,這個PointerData中包含了螢屏的物理觸摸位置相關的資料

根據最開始的那張方法呼叫堆疊可以看到,接下來呼叫的是

GestureBinding._handlePointerDataPacket (binding.dart:279)
_rootRunUnary (zone.dart:1370)
_CustomZone.runUnary (zone.dart:1265)
_CustomZone.runUnaryGuarded (zone.dart:1170)
_invoke1 (hooks.dart:182)

這5個方法,由于前4個方法(從下往上)基本上沒啥業務邏輯,都是callback回呼,這就不細講了,重點關注一下GestureBinding._handlePointerDataPacket 這個方法,從方法名可以猜到,就是處理PointerData的方法,

  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.
    _pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, window.devicePixelRatio));
    if (!locked)
      _flushPointerEventQueue();
  }

從這個方法的內容可以看出,這個方法主要做的事情

  1. PointerData資料決議成PointerEvent
  2. 將PointerEvent添加到_pendingPointerEvents佇列中
  3. 處理完之后再從佇列里取出這些資料,

看一下轉化程序的源代碼,大概了解一下邏輯就好了,

static Iterable<PointerEvent> expand(Iterable<ui.PointerData> data, double devicePixelRatio) sync* {
    for (final ui.PointerData datum in data) {
      //根據解析度計算邏輯位置
      final Offset position = Offset(datum.physicalX, datum.physicalY) / devicePixelRatio;
      assert(position != null);
      final Offset delta = Offset(datum.physicalDeltaX, datum.physicalDeltaY) / devicePixelRatio;
      final double radiusMinor = _toLogicalPixels(datum.radiusMinor, devicePixelRatio);
      final double radiusMajor = _toLogicalPixels(datum.radiusMajor, devicePixelRatio);
      final double radiusMin = _toLogicalPixels(datum.radiusMin, devicePixelRatio);
      final double radiusMax = _toLogicalPixels(datum.radiusMax, devicePixelRatio);
      final Duration timeStamp = datum.timeStamp;
      final PointerDeviceKind kind = datum.kind;
      assert(datum.change != null);
      if (datum.signalKind == null || datum.signalKind == ui.PointerSignalKind.none) {
        switch (datum.change) {
          case ui.PointerChange.add:
            yield PointerAddedEvent(
              //省略引數
            );
            break;
          case ui.PointerChange.hover:
            yield PointerHoverEvent(
               //省略引數
            );
            break;
          case ui.PointerChange.down:
            yield PointerDownEvent(
              //省略引數
            );
            break;
          case ui.PointerChange.move:
            yield PointerMoveEvent(
               //省略引數
            );
            break;
          case ui.PointerChange.up:
            yield PointerUpEvent(
               //省略引數
            );
            break;
          case ui.PointerChange.cancel:
            yield PointerCancelEvent(
               //省略引數
            );
            break;
          case ui.PointerChange.remove:
            yield PointerRemovedEvent(
              //省略引數
            );
            break;
        }
      } else {
        switch (datum.signalKind!) {
          case ui.PointerSignalKind.scroll:
            final Offset scrollDelta =
                Offset(datum.scrollDeltaX, datum.scrollDeltaY) / devicePixelRatio;
            yield PointerScrollEvent(
               //省略引數
            );
            break;
          case ui.PointerSignalKind.none:
            assert(false); // This branch should already have 'none' filtered out.
            break;
          case ui.PointerSignalKind.unknown:
            // Ignore unknown signals.
            break;
        }
      }
    }
  }

這段代碼比較長,但是邏輯其實也不復雜

  1. 將PointerData中的資料根據手機顯示幕的解析度去計算出相對應的邏輯位置
  2. 根據PointerChange的不同型別,把相關資料封成相對應的Event,

這里可能有兩個關鍵點不太好理解,一個是邏輯位置,一個是yield關鍵字
關于物理指標
所為的物理指標資訊就是我們手機的觸摸屏相關的資料,可以理解為從觸摸屏硬體獲取到的資料,然后這個資料其實是和手機的螢屏有關系的,
關于邏輯位置
那就是和作業系統有關系了,作業系統會根據手機的解析度生成一個包含非常多像素的一個矩陣,矩陣中的每個點通過某種映射邏輯可以對應到螢屏物理的某個位置點,
物理是客觀存在的,邏輯是主觀定義的,

這里有個關鍵字yield,不理解的可以看去查一下,和return有點類似,但是不會中斷代碼的執行,

 void _flushPointerEventQueue() {
    assert(!locked);

    while (_pendingPointerEvents.isNotEmpty)
      handlePointerEvent(_pendingPointerEvents.removeFirst());
  }

這個代碼應該都可以看懂,就是佇列的出隊操作,取出資料之后將資料移除,先進先出的原則(removeFirst),

void _handlePointerEventImmediately(PointerEvent event) {
    HitTestResult? hitTestResult;
    if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent) {
      assert(!_hitTests.containsKey(event.pointer));
      hitTestResult = HitTestResult();
      hitTest(hitTestResult, event.position);
      if (event is PointerDownEvent) {
        _hitTests[event.pointer] = hitTestResult;
      }
      assert(() {
        if (debugPrintHitTestResults)
          debugPrint('$event: $hitTestResult');
        return true;
      }());
    } else if (event is PointerUpEvent || event is PointerCancelEvent) {
      hitTestResult = _hitTests.remove(event.pointer);
    } else if (event.down) {
      // Because events that occur with the pointer down (like
      // [PointerMoveEvent]s) should be dispatched to the same place that their
      // initial PointerDownEvent was, we want to re-use the path we found when
      // the pointer went down, rather than do hit detection each time we get
      // such an event.
      hitTestResult = _hitTests[event.pointer];
    }
    assert(() {
      if (debugPrintMouseHoverEvents && event is PointerHoverEvent)
        debugPrint('$event');
      return true;
    }());
    if (hitTestResult != null ||
        event is PointerAddedEvent ||
        event is PointerRemovedEvent) {
      assert(event.position != null);
      dispatchEvent(event, hitTestResult);
    }
  }

這個方法邏輯看著很復雜,耐心看,我們來過濾

  1. 創建一個hitTestResult物件
  2. 根據PointerEvent的不同分為三種情況
    1. down,signal,hover,這三種情況,因為這三種情況都是點擊事件的開始,所以他們需要進行命中測驗,
    2. up和cancel,如果這兩種情況,表示事件結束或者是取消了,
    3. 最后這種情況,其實注釋也寫的挺清楚地,就是說對于中情況,這種情況就是出去最開始和結束之間的情況,只需要重用之前已經找到的路徑,不需要再次進行命中測驗了,
  3. 將命中的點添加到HitTestResult
  4. 最后就是大家熟悉的事件分發

命中測驗這個地方,需要重點看一下這個邏輯,

  bool hitTest(BoxHitTestResult result, { required Offset position }) {
    // 省略assert
    if (_size!.contains(position)) {
      if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
        result.add(BoxHitTestEntry(this, position));
        return true;
      }
    }
    return false;
  }

從代碼可以看出,命中測驗的時候,子控制元件是優先的,

  @protected
  bool hitTestChildren(BoxHitTestResult result, { required Offset position }) => false;

這個是默認方法回傳false,也就是說默認不命中,
再看看本身命中測驗的方法

  @override
  bool hitTestSelf(Offset position) => behavior == HitTestBehavior.opaque;

回傳值是根據這個behavior的值來確定的,
點擊事件的攔截是不是就可以通過這個方法來處理了?

好了,事件的命中測驗就大概過了一遍,接下來我們看事件的分發了,

 @override // from HitTestDispatcher
  void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
    assert(!locked);
    // No hit test information implies that this is a [PointerHoverEvent],
    // [PointerAddedEvent], or [PointerRemovedEvent]. These events are specially
    // routed here; other events will be routed through the `handleEvent` below.
    if (hitTestResult == null) {
      assert(event is PointerAddedEvent || event is PointerRemovedEvent);
      try {
        pointerRouter.route(event);
      } catch (exception, stack) {
        FlutterError.reportError(FlutterErrorDetailsForPointerEventDispatcher(
          exception: exception,
          stack: stack,
          library: 'gesture library',
          context: ErrorDescription('while dispatching a non-hit-tested pointer event'),
          event: event,
          hitTestEntry: null,
          informationCollector: () sync* {
            yield DiagnosticsProperty<PointerEvent>('Event', event, style: DiagnosticsTreeStyle.errorProperty);
          },
        ));
      }
      return;
    }
    for (final HitTestEntry entry in hitTestResult.path) {
      try {
        entry.target.handleEvent(event.transformed(entry.transform), entry);
      } catch (exception, stack) {
        FlutterError.reportError(FlutterErrorDetailsForPointerEventDispatcher(
          exception: exception,
          stack: stack,
          library: 'gesture library',
          context: ErrorDescription('while dispatching a pointer event'),
          event: event,
          hitTestEntry: entry,
          informationCollector: () sync* {
            yield DiagnosticsProperty<PointerEvent>('Event', event, style: DiagnosticsTreeStyle.errorProperty);
            yield DiagnosticsProperty<HitTestTarget>('Target', entry.target, style: DiagnosticsTreeStyle.errorProperty);
          },
        ));
      }
    }
  }

這塊邏輯代碼分兩部分,兩種情況的路由是不一樣的,當沒有

  1. 沒有命中的時候,直接將原Event發送出去
  2. 有命中的時候,會將坐標轉換之后,把命中之后事件發送出去,
  @override // 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);
    }
  }

不得不說這個注釋寫的非常好,源代碼里的,
前面entry.target.handleEvent這個方法把事件發送出來之后,就被這個方法接到了,梳理一下這個方法做的事情

  1. 把event存盤在pointerRouter這個路由里,
  2. 關閉競技場
  3. 消除競技場,把勝利者給到第一個,
  4. 如果是信號事件,則處理信號事件

這里有一點要特別說明一下,if中的這幾個方法,都是在我們處理完所有事件之后才會呼叫,
這里還會涉及到一個競技場的問題,由于篇幅問題,這個競技場就不詳細講了,他主要邏輯就是解決手勢搶奪問題,最終有一個widget獲得這個手勢,

接下來到了BaseTapGestureRecognizer.handlePrimaryPointer方法了,到這個地方基本上就是處理最后的事件分發的邏輯了,

 @override
  void acceptGesture(int pointer) {
    super.acceptGesture(pointer);
    if (pointer == primaryPointer) {
      _checkDown();
      _wonArenaForPrimaryPointer = true;
      _checkUp();
    }
  }
  @override
  void handlePrimaryPointer(PointerEvent event) {
    if (event is PointerUpEvent) {
      _up = event;
      _checkUp();
    } else if (event is PointerCancelEvent) {
      resolve(GestureDisposition.rejected);
      if (_sentTapDown) {
        _checkCancel(event, '');
      }
      _reset();
    } else if (event.buttons != _down!.buttons) {
      resolve(GestureDisposition.rejected);
      stopTrackingPointer(primaryPointer!);
    }
  }

這里解釋一下,acceptGesture這個方法是在close關閉競技場中呼叫的,handlePrimaryPointer這個方法是在route中呼叫的,

  void _checkDown() {
    if (_sentTapDown) {
      return;
    }
    handleTapDown(down: _down!);
    _sentTapDown = true;
  }
  void _checkUp() {
    if (!_wonArenaForPrimaryPointer || _up == null) {
      return;
    }
    assert(_up!.pointer == _down!.pointer);
    handleTapUp(down: _down!, up: _up!);
    _reset();
  }

  void _checkCancel(PointerCancelEvent? event, String note) {
    handleTapCancel(down: _down!, cancel: event, reason: note);
  }

  void _reset() {
    _sentTapDown = false;
    _wonArenaForPrimaryPointer = false;
    _up = null;
    _down = null;
  }

結合這兩部分代碼可以知道,呼叫onTab之前,肯定是得先在競技場中競技成功,然后然后通過呼叫acceptGesture這個方法就可以獲取到點擊事件的down方法了,于此同時,在接收到分發的PointerUpEvent事件的時候,才會把up的回呼賦值過去、
總結下來點擊事件onTab的呼叫有2個前提條件:

  1. 必須競技成功
  2. 接收到PointerUpEvent事件,

最后執行到TapGestureRecognizer.中的handleTapDown和handleTapUp方法了

  @protected
  @override
  void handleTapDown({required PointerDownEvent down}) {
    final TapDownDetails details = TapDownDetails(
      globalPosition: down.position,
      localPosition: down.localPosition,
      kind: getKindForPointer(down.pointer),
    );
    switch (down.buttons) {
      case kPrimaryButton:
        if (onTapDown != null)
          invokeCallback<void>('onTapDown', () => onTapDown!(details));
        break;
      case kSecondaryButton:
        if (onSecondaryTapDown != null)
          invokeCallback<void>('onSecondaryTapDown', () => onSecondaryTapDown!(details));
        break;
      case kTertiaryButton:
        if (onTertiaryTapDown != null)
          invokeCallback<void>('onTertiaryTapDown', () => onTertiaryTapDown!(details));
        break;
      default:
    }
  }
  @protected
  @override
  void handleTapUp({ required PointerDownEvent down, required PointerUpEvent up}) {
    final TapUpDetails details = TapUpDetails(
      kind: up.kind,
      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));
        if (onSecondaryTap != null)
          invokeCallback<void>('onSecondaryTap', () => onSecondaryTap!());
        break;
      case kTertiaryButton:
        if (onTertiaryTapUp != null)
          invokeCallback<void>('onTertiaryTapUp', () => onTertiaryTapUp!(details));
        break;
      default:
    }
  }

TapGestureRecognizer這個類不知道是否還記得,這個是在最開始介紹代碼的時候介紹的,在GestureDetector中注冊的手勢識別器,
看到這個方法的代碼,也就算是結束了,同樣,這里可以看到onTapUp和onTap 兩個方法的執行順序,微觀的角度上講,onTapUp方法執行的順序還是稍微靠前一點的,從宏觀的角度上來講,這2個方法幾乎是同時呼叫的,

最后總結一下flutter中手勢的分發程序,

  1. 當手或者觸摸筆或者是滑鼠按下的時候,引擎會手機這些手勢的資料包ByteData
  2. PlatformDispatcher_dispatchPointerDataPacket()方法去做資料的決議,最后得到一個PointerDataPacket資料包
  3. GestureBinding.handlePointerDataPacket 把PointerDataPacket資料包中的PointerData資料轉化成對應的PointerEvent手勢事件, 并且將它們加入到_pendingPointerEvents待分發的手勢事佇列中,而PointerEvent只是一個基類,它有很多實作類,如:
    1. PointerAddedEvent
    2. PointerHoverEvent
    3. PointerDownEvent,
    4. PointerMoveEvent
    5. PointerUpEvent
    6. PointerCancelEvent
    7. PointerRemovedEvent
  4. 手勢資料全部添加到待分發的事件佇列之后,從佇列中逐個取出分發下去,先進先出的原則,
  5. 在事件分發程序中,有兩個邏輯
    1. 根據命中測驗確定這個邏輯是否在點擊區域
    2. 根據競技結果確認把事件分發給哪個物件
  6. 最后只想相關回呼方法(onTapDown,onTapUp,onTap等等),

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/353445.html

標籤:其他

上一篇:Kotlin學習筆記——(六)介面、抽象類、泛型、擴展、集合運算子、與Java互操作性、單例

下一篇:從用戶輸入創建物件

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more