Flutter筆記-深入分析GestureDetector

android中存在事件沖突茬缩,flutter其實(shí)也存在,但是官方友好的出了一個(gè)控件(GestureDetector)來解決這個(gè)問題
在事件分發(fā)那里吼旧,分析了基礎(chǔ)手勢控件Listener的事件傳遞(傳送門)凰锡,我們先看下面這個(gè)例子:

class TouchDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Listener(
      child: Container(
        color: Colors.blue,
        width: 300,
        height: 300,
        child: Stack(
          //將布局按比例分成坐標(biāo),原點(diǎn)(0,0)位于中間,(-1,-1)為左上
          alignment: Alignment(0, 0),
          children: <Widget>[
            Listener(
              child: Container(
                color: Colors.red,
                width: 100,
                height: 100,
              ),
              onPointerDown: (event){
                print("內(nèi)層");
              },
            ),
          ],
        )
      ),
      onPointerDown: (event){
        print("外層");
      },
    );
  }
}

這個(gè)界面簡單掂为,就是一個(gè)大的正方形里有一個(gè)紅色的小正方形裕膀。當(dāng)我們點(diǎn)擊紅色正方形時(shí):

I/flutter ( 4898): 內(nèi)層
I/flutter ( 4898): 外層

打印結(jié)果是先內(nèi)層,再外層勇哗。根據(jù)事件分發(fā)里L(fēng)istener的分析昼扛,可以知道result列表的添加順序是先內(nèi)層,再外層
Ok智绸,進(jìn)入正題野揪,GestureDetector如何解決事件沖突的?

GestureDetector

GestureDetector是一個(gè)StatelessWidget瞧栗,直接看build方法

class GestureDetector extends StatelessWidget {
  ...
  @override
  Widget build(BuildContext context) {
    //保存手勢識別工廠的map
    //GestureRecognizerFactory保存GestureRecognizer的構(gòu)造方法和init方法
    final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
    //單擊事件
    if (onTapDown != null || onTapUp != null || onTap != null || onTapCancel != null) {
      gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
        //1.構(gòu)造方法
        () => TapGestureRecognizer(debugOwner: this),
        //2.初始化方法
        (TapGestureRecognizer instance) {
          instance
            ..onTapDown = onTapDown
            ..onTapUp = onTapUp
            ..onTap = onTap
            ..onTapCancel = onTapCancel;
        },
      );
    }
    //雙擊事件
    if (onDoubleTap != null) {
      gestures[DoubleTapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
        () => DoubleTapGestureRecognizer(debugOwner: this),
        (DoubleTapGestureRecognizer instance) {
          instance
            ..onDoubleTap = onDoubleTap;
        },
      );
    }
    //長按事件
    if (onLongPress != null || onLongPressUp !=null) {
      gestures[LongPressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
        () => LongPressGestureRecognizer(debugOwner: this),
        (LongPressGestureRecognizer instance) {
          instance
            ..onLongPress = onLongPress
            ..onLongPressUp = onLongPressUp;
        },
      );
    }
    //垂直拖拽事件
    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;
        },
      );
    }
     //水平拖拽事件
    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;
        },
      );
    }
    //同時(shí)水平和垂直拖拽事件
    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;
        },
      );
    }
    //縮放手勢事件
    if (onScaleStart != null || onScaleUpdate != null || onScaleEnd != null) {
      gestures[ScaleGestureRecognizer] = GestureRecognizerFactoryWithHandlers<ScaleGestureRecognizer>(
        () => ScaleGestureRecognizer(debugOwner: this),
        (ScaleGestureRecognizer instance) {
          instance
            ..onStart = onScaleStart
            ..onUpdate = onScaleUpdate
            ..onEnd = onScaleEnd;
        },
      );
    }

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

在gestures中斯稳,已經(jīng)將回調(diào)事件方法分好了手勢識別類,繼續(xù)往下看

class RawGestureDetector extends StatefulWidget {
  ...
  @override
  RawGestureDetectorState createState() => RawGestureDetectorState();
}

查看state的build方法

class RawGestureDetectorState extends State<RawGestureDetector> {
  ...
  @override
  Widget build(BuildContext context) {
    Widget result = Listener(
      onPointerDown: _handlePointerDown,
      behavior: widget.behavior ?? _defaultBehavior,
      child: widget.child
    );
    //excludeFromSemantics默認(rèn)為false的
    if (!widget.excludeFromSemantics)
      result = _GestureSemantics(owner: this, child: result);
    return result;
  }

  void _handlePointerDown(PointerDownEvent event) {
    assert(_recognizers != null);
    //給每個(gè)手勢識別類添加手指按下事件
    for (GestureRecognizer recognizer in _recognizers.values)
      //關(guān)鍵
      recognizer.addPointer(event);
  }
}

GestureRecognizer的子類比較多迹恐,這里我們選水平滑動(dòng)HorizontalDragGestureRecognizer進(jìn)行分析挣惰, addPointer位于其父類DragGestureRecognizer中:

@override
  void addPointer(PointerEvent event) {
    //開始追蹤手勢
    startTrackingPointer(event.pointer);
    //慣性滑動(dòng)追蹤類
    _velocityTrackers[event.pointer] = VelocityTracker();
    if (_state == _DragState.ready) {
      _state = _DragState.possible;
      _initialPosition = event.position;
      _pendingDragOffset = Offset.zero;
      _lastPendingEventTimestamp = event.timeStamp;
      if (onDown != null)  //回調(diào)方法onDown
        invokeCallback<void>('onDown', () => onDown(DragDownDetails(globalPosition: _initialPosition)));
    } else if (_state == _DragState.accepted) {
      resolve(GestureDisposition.accepted);
    }
  }

_DragState共三個(gè)狀態(tài):ready(準(zhǔn)備,初始值)、possible(正在手勢競爭中)殴边、accepted(競爭勝利)

@protected
void startTrackingPointer(int pointer) {
  //handleEvent為一個(gè)函數(shù)
  GestureBinding.instance.pointerRouter
    .addRoute(pointer, handleEvent);
  _trackedPointers.add(pointer);
  assert(!_entries.containsValue(pointer));
  //每個(gè)手勢的入場券列表賦值
  _entries[pointer] = _addPointerToArena(pointer);
}

addRoute傳遞的handleEvent為一個(gè)函數(shù)憎茂,這個(gè)函數(shù)后面再分析

a. pointerRouter中的_routeMap賦值

_routeMap的值就是通過addRoute方法來添加的

typedef PointerRoute = void Function(PointerEvent event);
//參數(shù)二為handleEvent函數(shù)
void addRoute(int pointer, PointerRoute route) {
    final LinkedHashSet<PointerRoute> routes = _routeMap.putIfAbsent(pointer, () => LinkedHashSet<PointerRoute>());
    assert(!routes.contains(route));
    routes.add(route);
  }

b. gestureArena中的_arenas賦值

回到前面,繼續(xù)看_addPointerToArena方法

GestureArenaEntry _addPointerToArena(int pointer) {
    if (_team != null)
      return _team.add(pointer, this);
    return GestureBinding.instance.gestureArena.add(pointer, this);
  }

基本和上面的添加方法是一致的

GestureArenaEntry add(int pointer, GestureArenaMember member) {
    //put: 重復(fù)的key覆蓋
    //putIfAbsent: 重復(fù)的key不添加
    final _GestureArena state = _arenas.putIfAbsent(pointer, () {
      assert(_debugLogDiagnostic(pointer, '★ Opening new gesture arena.'));
      return _GestureArena();
    });
    state.add(member);
    assert(_debugLogDiagnostic(pointer, 'Adding: $member'));
    //返回了GestureArenaEntry對象锤岸,持有手勢競技場管理類對象竖幔、手勢的id、手勢識別類的對象
    return GestureArenaEntry._(this, pointer, member);
  }

_instance是GestureBinding中的一個(gè)靜態(tài)對象是偷,構(gòu)造函數(shù)中對其賦值拳氢,還記得事件分發(fā)那最終分析到handleEvent:

final PointerRouter pointerRouter = PointerRouter();
final GestureArenaManager gestureArena = GestureArenaManager();

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

之前到了這一步就沒有繼續(xù)追蹤下去,其實(shí)內(nèi)部還通過列表進(jìn)行了操作蛋铆,看下route方法做了什么

  void route(PointerEvent event) {
    //根據(jù)前面的分析馋评,_routeMap列表中已經(jīng)有數(shù)據(jù)了
    final LinkedHashSet<PointerRoute> routes = _routeMap[event.pointer];
    final List<PointerRoute> globalRoutes = List<PointerRoute>.from(_globalRoutes);
    if (routes != null) {
      //遍歷列表
      for (PointerRoute route in List<PointerRoute>.from(routes)) {
        if (routes.contains(route))
           //執(zhí)行
          _dispatch(event, route);
      }
    }
    for (PointerRoute route in globalRoutes) {
      if (_globalRoutes.contains(route))
        _dispatch(event, route);
    }
  }

PointerRoute是一個(gè)帶參數(shù)無返回類型的函數(shù)(注:PointerRouter是一個(gè)類)

void _dispatch(PointerEvent event, PointerRoute route){
  try {
    //執(zhí)行匿名函數(shù),即handleEvent函數(shù)
    route(event);
  } catch (exception, stack) {
    ...
  }
}

執(zhí)行HorizontalDragGestureRecognizer中的handleEvent

@override
  void handleEvent(PointerEvent event) {
    assert(_state != _DragState.ready);
    if (!event.synthesized
        && (event is PointerDownEvent || event is PointerMoveEvent)) {
      final VelocityTracker tracker = _velocityTrackers[event.pointer];
      assert(tracker != null);
      tracker.addPosition(event.timeStamp, event.position);
    }
    //手勢為PointerMoveEvent
    if (event is PointerMoveEvent) {
      final Offset delta = event.delta;
      if (_state == _DragState.accepted) {
        //如果手勢競爭勝利刺啦,則回調(diào)onUpdate方法
        if (onUpdate != null) {
          invokeCallback<void>('onUpdate', () => onUpdate(DragUpdateDetails(
            sourceTimeStamp: event.timeStamp,
            delta: _getDeltaForDetails(delta),
            primaryDelta: _getPrimaryValueFromOffset(delta),
            globalPosition: event.position,
          )));
        }
      } else {
        //手勢開始競爭判斷
        _pendingDragOffset += delta;
        _lastPendingEventTimestamp = event.timeStamp;
        //當(dāng)添加滿足留特,則決策判斷為accepted
        if (_hasSufficientPendingDragDeltaToAccept)
          resolve(GestureDisposition.accepted);
      }
    }
    //如果是up或canel事件禽炬,則刪除路由关面,即pointerRouter移除此handleEvent,即此次事件不再調(diào)用
    stopTrackingIfPointerNoLongerDown(event);
  }

_hasSufficientPendingDragDeltaToAccept如何為true呢横蜒?

const double kTouchSlop = 18.0; 
@override
bool get _hasSufficientPendingDragDeltaToAccept => _pendingDragOffset.dx.abs() > kTouchSlop;

只要移動(dòng)的x差值絕對值大于18符合條件捧韵,垂直滑動(dòng)則為dy市咆,至于PanGestureRecognizer:

const double kPanSlop = kTouchSlop * 2.0; 
@override
bool get _hasSufficientPendingDragDeltaToAccept {
  //斜邊的絕對值大于2倍的kTouchSlop
  return _pendingDragOffset.distance > kPanSlop;
}

由上面可以得出,當(dāng)down手勢傳遞過來再来,只記錄速度,而move手勢傳遞過來,列表中所有的DragGestureRecognizer類都會(huì)調(diào)用handleEvent

如果此時(shí)列表中同時(shí)HorizontalDragGestureRecognizer和VerticalDragGestureRecognizer芒篷,那么它們誰獲得勝利呢搜变?

主要看dx和dy的差值誰先大于kTouchSlop,但如果正好在斜45°移動(dòng)時(shí)针炉,dx和dy差值變化是一樣的挠他,這是主要看它們加入列表的先后順序決定,先添加的獲勝

_state 狀態(tài)修改為_DragState.accepted過程

假如此時(shí)已經(jīng)滿足條件篡帕,進(jìn)入resolve方法

void resolve(GestureDisposition disposition) {
    //_entries前面流程創(chuàng)建了殖侵,_entries.values鍵值對中取出全部的value列表
    final List<GestureArenaEntry> localEntries = List<GestureArenaEntry>.from(_entries.values);
    _entries.clear();
    for (GestureArenaEntry entry in localEntries)
      entry.resolve(disposition);
  }

會(huì)遍歷所有 GestureArenaEntry 的 resolve

class GestureArenaEntry {
  GestureArenaEntry._(this._arena, this._pointer, this._member);

  final GestureArenaManager _arena; //當(dāng)前競技場管理者
  final int _pointer;  //手勢的id
  final GestureArenaMember _member; //即HorizontalDragGestureRecognizer
  void resolve(GestureDisposition disposition) {
    _arena._resolve(_pointer, _member, disposition);
  }
}

管理者管理所有的事件

void _resolve(int pointer, GestureArenaMember member, GestureDisposition disposition) {
    final _GestureArena state = _arenas[pointer];
    if (state == null)
      return; 
    //省略斷言
    ...
    //state.members為當(dāng)前競技場里所有手勢識別類的列表
    //所有不滿足的類移出列表
    if (disposition == GestureDisposition.rejected) {
      state.members.remove(member);
      member.rejectGesture(pointer);
      if (!state.isOpen)
        _tryToResolveArena(pointer, state);
    } else {
      assert(disposition == GestureDisposition.accepted);
      //默認(rèn)為true,當(dāng)競技場close是修改為false镰烧,此時(shí)為false
      if (state.isOpen) {
        //記錄獲勝的手勢識別類
        state.eagerWinner ??= member;
      } else {
        assert(_debugLogDiagnostic(pointer, 'Self-declared winner: $member'));
        //調(diào)用
        _resolveInFavorOf(pointer, state, member);
      }
    }

state.isOpen這個(gè)值在PointerDownEvent手勢后會(huì)修改為false

void _resolveInFavorOf(int pointer, _GestureArena state, GestureArenaMember member) {
    //省略斷言
    ...
    _arenas.remove(pointer);
    for (GestureArenaMember rejectedMember in state.members) {
      if (rejectedMember != member)
        rejectedMember.rejectGesture(pointer);
    }
    member.acceptGesture(pointer);
  }

其他的手勢識別類調(diào)用rejectGesture拢军,而勝者調(diào)用acceptGesture

@override
  void acceptGesture(int pointer) {
    if (_state != _DragState.accepted) {
      //修改狀態(tài)
      _state = _DragState.accepted;
      final Offset delta = _pendingDragOffset;
      final Duration timestamp = _lastPendingEventTimestamp;
      _pendingDragOffset = Offset.zero;
      _lastPendingEventTimestamp = null;
      //回調(diào)onStart方法
      if (onStart != null) {
        invokeCallback<void>('onStart', () => onStart(DragStartDetails(
          sourceTimeStamp: timestamp,
          globalPosition: _initialPosition,
        )));
      }
      //回調(diào)onUpdate方法
      if (delta != Offset.zero && onUpdate != null) {
        final Offset deltaForDetails = _getDeltaForDetails(delta);
        invokeCallback<void>('onUpdate', () => onUpdate(DragUpdateDetails(
          sourceTimeStamp: timestamp,
          delta: deltaForDetails,
          primaryDelta: _getPrimaryValueFromOffset(delta),
          globalPosition: _initialPosition + deltaForDetails,
        )));
      }
    }
  }

勝利后,因?yàn)樾薷牧藸顟B(tài)怔鳖,所以之后的move手勢不會(huì)再走這一步茉唉,在前面的方法就直接回調(diào)onUpdate方法
再來看看競爭失敗后會(huì)怎么樣

@override
  void rejectGesture(int pointer) {
    stopTrackingPointer(pointer);
  }

@protected
  void stopTrackingPointer(int pointer) {
    if (_trackedPointers.contains(pointer)) {
      //刪除路由
      GestureBinding.instance.pointerRouter.removeRoute(pointer, handleEvent);
     //移出手勢id
      _trackedPointers.remove(pointer);
      if (_trackedPointers.isEmpty)
        didStopTrackingLastPointer(pointer);
    }
  }

isOpen值修改

回到最開始,gestureArena.close(event.pointer)中

void close(int pointer) {
    final _GestureArena state = _arenas[pointer];
    if (state == null)
      return; 
    //關(guān)閉競技場结执,等待勝者出來
    state.isOpen = false;
    assert(_debugLogDiagnostic(pointer, 'Closing', state));
    _tryToResolveArena(pointer, state);
  }

查看 _tryToResolveArena方法

void _tryToResolveArena(int pointer, _GestureArena state) {
    assert(_arenas[pointer] == state);
    assert(!state.isOpen);
    //只剩一個(gè)度陆,即無競爭者
    if (state.members.length == 1) {
      //直接調(diào)用
      scheduleMicrotask(() => _resolveByDefault(pointer, state));
    } else if (state.members.isEmpty) {
      //無識別類對象
      _arenas.remove(pointer);
      assert(_debugLogDiagnostic(pointer, 'Arena empty.'));
    } else if (state.eagerWinner != null) {
      //得出勝利者
      assert(_debugLogDiagnostic(pointer, 'Eager winner: ${state.eagerWinner}'));
      //移出勝利者之外的其他手勢識別類
      _resolveInFavorOf(pointer, state, state.eagerWinner);
    }
  }

而gestureArena.sweep(event.pointer)則是清除所有的路由和記錄(當(dāng)無選手hold)

hold和release

目前源碼里只DoubleTapGestureRecognizer使用

void hold(int pointer) {
    final _GestureArena state = _arenas[pointer];
    if (state == null)
      return; 
    state.isHeld = true;
    assert(_debugLogDiagnostic(pointer, 'Holding', state));
  }

  void release(int pointer) {
    final _GestureArena state = _arenas[pointer];
    if (state == null)
      return; 
    state.isHeld = false;
    assert(_debugLogDiagnostic(pointer, 'Releasing', state));
    if (state.hasPendingSweep)
      sweep(pointer);
  }

hold后,狀態(tài)isHeld修改為true献幔,在sweep中就不會(huì)清理

void sweep(int pointer) {
    final _GestureArena state = _arenas[pointer];
    if (state == null)
      return;
    assert(!state.isOpen);
    //hold懂傀,直接返回,不做清空處理
    if (state.isHeld) {
      //修改狀態(tài)蜡感,在release方法中用于重新調(diào)用sweep方法
      state.hasPendingSweep = true;
      assert(_debugLogDiagnostic(pointer, 'Delaying sweep', state));
      return; 
    }
    assert(_debugLogDiagnostic(pointer, 'Sweeping', state));
    _arenas.remove(pointer);
    if (state.members.isNotEmpty) {
      assert(_debugLogDiagnostic(pointer, 'Winner: ${state.members.first}'));
      //第一個(gè)勝者接收手勢
      state.members.first.acceptGesture(pointer);
      //所有選手退出競技場
      for (int i = 1; i < state.members.length; i++)
        state.members[i].rejectGesture(pointer);
    }
  }

來看一下DoubleTapGestureRecognizer中具體怎么使用

void _handleEvent(PointerEvent event) {
    final _TapTracker tracker = _trackers[event.pointer];
    assert(tracker != null);
    if (event is PointerUpEvent) {
      if (_firstTap == null)
        //這里會(huì)hold蹬蚁,讓競技場等待下一次分出勝負(fù)
        _registerFirstTap(tracker);
      else
        //調(diào)用release,釋放
        _registerSecondTap(tracker);
    } else if (event is PointerMoveEvent) {
      if (!tracker.isWithinTolerance(event, kDoubleTapTouchSlop))
        _reject(tracker);
    } else if (event is PointerCancelEvent) {
      _reject(tracker);
    }
  }

記錄第一次點(diǎn)擊

void _registerFirstTap(_TapTracker tracker) {
    //開啟計(jì)時(shí)器铸敏,超時(shí)時(shí)間為300ms
    _startDoubleTapTimer();
    //調(diào)用hold
    GestureBinding.instance.gestureArena.hold(tracker.pointer);
    _freezeTracker(tracker);
    _trackers.remove(tracker.pointer);
    _clearTrackers();
    //記錄
    _firstTap = tracker;
  }

記錄第二次點(diǎn)擊

void _registerSecondTap(_TapTracker tracker) {
    _firstTap.entry.resolve(GestureDisposition.accepted);
    tracker.entry.resolve(GestureDisposition.accepted);
    _freezeTracker(tracker);
    _trackers.remove(tracker.pointer);
    //雙擊回調(diào)方法是否存在
    if (onDoubleTap != null)
      invokeCallback<void>('onDoubleTap', onDoubleTap);
    _reset();
  }

在_reset() 清空記錄缚忧,狀態(tài)重置

void _reset() {
    //停止計(jì)時(shí)器
    _stopDoubleTapTimer();
    if (_firstTap != null) {
       //清空firstTap
       final _TapTracker tracker = _firstTap;
      _firstTap = null;
      _reject(tracker);
     //調(diào)用release
      GestureBinding.instance.gestureArena.release(tracker.pointer);
    }
    _clearTrackers();
  }

總結(jié)

  1. 路由在Listener的onPointerDown建立,在gestureArena的sweep清空(可能在這之前已經(jīng)清空)
  2. 一個(gè)完整的事件由down手勢開始杈笔、up手勢結(jié)束闪水。down手勢時(shí)列表中登記所有要競爭的手勢識別類(競技選手),競技場管理者進(jìn)行管理蒙具,后續(xù)的事件會(huì)通過路由依次傳遞到列表的手勢識別類中球榆,當(dāng)?shù)怯浭謩葑R別類不滿足時(shí)會(huì)退出競爭

GestureArenaManager:競技場管理者,根據(jù)Pointer的多少來構(gòu)建場地

  • close:競技場關(guān)閉禁筏,PointerDown手勢觸發(fā)
  • sweep:競技場清理持钉,PointerUp手勢觸發(fā)
  • hold:保持,sweep時(shí)不會(huì)清理
  • release:釋放篱昔,取消保持每强,會(huì)調(diào)用sweep
    _GestureArena:競技場場地
    GestureArenaEntry:競技場入場券
    GestureRecognizer:競技選手
  • acceptGesture: 勝者回調(diào)
  • rejectGesture: 敗者回調(diào)
  • addPointer: 添加手勢始腾,啟動(dòng)追蹤
  • invokeCallback: 回調(diào)方法
class GestureArenaManager {
    final Map<int, _GestureArena> _arenas = <int, _GestureArena>{};
}

class _GestureArena {
    final List<GestureArenaMember> members = <GestureArenaMember>[];
}

class OneSequenceGestureRecognizer extends GestureArenaMember {
    final Map<int, GestureArenaEntry> _entries = <int, GestureArenaEntry>{};
}
  1. 水平或垂直或pan 拖動(dòng),由滑動(dòng)的距離來判斷
    tap和doubleTap 由按的次數(shù)判斷(hold)
    tap和longPress 由按下的時(shí)間判斷
    父類和子類共用同一手勢空执,優(yōu)先子類
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末浪箭,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子辨绊,更是在濱河造成了極大的恐慌奶栖,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件门坷,死亡現(xiàn)場離奇詭異宣鄙,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)默蚌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進(jìn)店門冻晤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人敏簿,你說我怎么就攤上這事明也。” “怎么了惯裕?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵温数,是天一觀的道長。 經(jīng)常有香客問我蜻势,道長撑刺,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任握玛,我火速辦了婚禮够傍,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘挠铲。我一直安慰自己冕屯,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布拂苹。 她就那樣靜靜地躺著安聘,像睡著了一般。 火紅的嫁衣襯著肌膚如雪瓢棒。 梳的紋絲不亂的頭發(fā)上浴韭,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天,我揣著相機(jī)與錄音脯宿,去河邊找鬼念颈。 笑死,一個(gè)胖子當(dāng)著我的面吹牛连霉,可吹牛的內(nèi)容都是我干的榴芳。 我是一名探鬼主播嗡靡,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼翠语!你這毒婦竟也來了叽躯?” 一聲冷哼從身側(cè)響起财边,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤肌括,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后酣难,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谍夭,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年憨募,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了紧索。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,569評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡菜谣,死狀恐怖珠漂,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情尾膊,我是刑警寧澤媳危,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站冈敛,受9級特大地震影響待笑,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜抓谴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一暮蹂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧癌压,春花似錦仰泻、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至丐吓,卻和暖如春浅悉,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背券犁。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工术健, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人粘衬。 一個(gè)月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓荞估,卻偏偏與公主長得像咳促,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子勘伺,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評論 2 348

推薦閱讀更多精彩內(nèi)容