Refactor: Base tap gesture recognizer (#41329)

* Extracts the logic of TapGestureRecognizer into an abstract class BaseTapGestureRecognizer
* Fixes ModalBarrier unable to dismiss when competing
diff --git a/packages/flutter/lib/src/gestures/arena.dart b/packages/flutter/lib/src/gestures/arena.dart
index 9b4a23e..1afa901 100644
--- a/packages/flutter/lib/src/gestures/arena.dart
+++ b/packages/flutter/lib/src/gestures/arena.dart
@@ -59,7 +59,7 @@
   bool isHeld = false;
   bool hasPendingSweep = false;
 
-  /// If a gesture attempts to win while the arena is still open, it becomes the
+  /// If a member attempts to win while the arena is still open, it becomes the
   /// "eager winner". We look for an eager winner when closing the arena to new
   /// participants, and if there is one, we resolve the arena in its favor at
   /// that time.
diff --git a/packages/flutter/lib/src/gestures/monodrag.dart b/packages/flutter/lib/src/gestures/monodrag.dart
index 95ca2a3..48203ed 100644
--- a/packages/flutter/lib/src/gestures/monodrag.dart
+++ b/packages/flutter/lib/src/gestures/monodrag.dart
@@ -68,10 +68,10 @@
 
   /// Configure the behavior of offsets sent to [onStart].
   ///
-  /// If set to [DragStartBehavior.start], the [onStart] callback will be called at the time and
-  /// position when the gesture detector wins the arena. If [DragStartBehavior.down],
-  /// [onStart] will be called at the time and position when a down event was
-  /// first detected.
+  /// If set to [DragStartBehavior.start], the [onStart] callback will be called
+  /// at the time and position when this gesture recognizer wins the arena. If
+  /// [DragStartBehavior.down], [onStart] will be called at the time and
+  /// position when a down event was first detected.
   ///
   /// For more information about the gesture arena:
   /// https://flutter.dev/docs/development/ui/advanced/gestures#gesture-disambiguation
@@ -80,9 +80,9 @@
   ///
   /// ## Example:
   ///
-  /// A finger presses down on the screen with offset (500.0, 500.0),
-  /// and then moves to position (510.0, 500.0) before winning the arena.
-  /// With [dragStartBehavior] set to [DragStartBehavior.down], the [onStart]
+  /// A finger presses down on the screen with offset (500.0, 500.0), and then
+  /// moves to position (510.0, 500.0) before winning the arena. With
+  /// [dragStartBehavior] set to [DragStartBehavior.down], the [onStart]
   /// callback will be called at the time corresponding to the touch's position
   /// at (500.0, 500.0). If it is instead set to [DragStartBehavior.start],
   /// [onStart] will be called at the time corresponding to the touch's position
diff --git a/packages/flutter/lib/src/gestures/tap.dart b/packages/flutter/lib/src/gestures/tap.dart
index 38766dd..21221f9 100644
--- a/packages/flutter/lib/src/gestures/tap.dart
+++ b/packages/flutter/lib/src/gestures/tap.dart
@@ -98,6 +98,198 @@
 ///  * [TapGestureRecognizer], which uses this signature in one of its callbacks.
 typedef GestureTapCancelCallback = void Function();
 
+/// A base class for gesture recognizers that recognize taps.
+///
+/// Gesture recognizers take part in gesture arenas to enable potential gestures
+/// to be disambiguated from each other. This process is managed by a
+/// [GestureArenaManager].
+///
+/// A tap is defined as a sequence of events that starts with a down, followed
+/// by optional moves, then ends with an up. All move events must contain the
+/// same `buttons` as the down event, and must not be too far from the initial
+/// position. The gesture is rejected on any violation, a cancel event, or
+/// if any other recognizers wins the arena. It is accepted only when it is the
+/// last member of the arena.
+///
+/// The [BaseTapGestureRecognizer] considers all the pointers involved in the
+/// pointer event sequence as contributing to one gesture. For this reason,
+/// extra pointer interactions during a tap sequence are not recognized as
+/// additional taps. For example, down-1, down-2, up-1, up-2 produces only one
+/// tap on up-1.
+///
+/// The [BaseTapGestureRecognizer] can not be directly used, since it does not
+/// define which buttons to accept, or what to do when a tap happens. If you
+/// want to build a custom tap recognizer, extend this class by overriding
+/// [isPointerAllowed] and the handler methods.
+///
+/// See also:
+///
+///  * [TapGestureRecognizer], a ready-to-use tap recognizer that recognizes
+///    taps of the primary button and taps of the secondary button.
+///  * [ModalBarrier], a widget that uses a custom tap recognizer that accepts
+///    any buttons.
+abstract class BaseTapGestureRecognizer extends PrimaryPointerGestureRecognizer {
+  /// Creates a tap gesture recognizer.
+  BaseTapGestureRecognizer({ Object debugOwner })
+    : super(deadline: kPressTimeout , debugOwner: debugOwner);
+
+  bool _sentTapDown = false;
+  bool _wonArenaForPrimaryPointer = false;
+
+  PointerDownEvent _down;
+  PointerUpEvent _up;
+
+  /// A pointer has contacted the screen, which might be the start of a tap.
+  ///
+  /// This triggers after the down event, once a short timeout ([deadline]) has
+  /// elapsed, or once the gesture has won the arena, whichever comes first.
+  ///
+  /// The parameter `down` is the down event of the primary pointer that started
+  /// the tap sequence.
+  ///
+  /// If this recognizer doesn't win the arena, [handleTapCancel] is called next.
+  /// Otherwise, [handleTapUp] is called next.
+  @protected
+  void handleTapDown({ PointerDownEvent down });
+
+  /// A pointer has stopped contacting the screen, which is recognized as a tap.
+  ///
+  /// This triggers on the up event, if the recognizer wins the arena with it
+  /// or has previously won.
+  ///
+  /// The parameter `down` is the down event of the primary pointer that started
+  /// the tap sequence, and `up` is the up event that ended the tap sequence.
+  ///
+  /// If this recognizer doesn't win the arena, [handleTapCancel] is called
+  /// instead.
+  @protected
+  void handleTapUp({ PointerDownEvent down, PointerUpEvent up });
+
+  /// A pointer that previously triggered [handleTapDown] will not end up
+  /// causing a tap.
+  ///
+  /// This triggers once the gesture loses the arena, if [handleTapDown] has
+  /// been previously triggered.
+  ///
+  /// The parameter `down` is the down event of the primary pointer that started
+  /// the tap sequence; `cancel` is the cancel event, which might be null;
+  /// `reason` is a short description of the cause if `cancel` is null, which
+  /// can be "forced" if other gestures won the arena, or "spontaneous"
+  /// otherwise.
+  ///
+  /// If this recognizer wins the arena, [handleTapUp] is called instead.
+  @protected
+  void handleTapCancel({ PointerDownEvent down, PointerCancelEvent cancel, String reason });
+
+  @override
+  void addAllowedPointer(PointerDownEvent event) {
+    if (state == GestureRecognizerState.ready) {
+      // `_down` must be assigned in this method instead of `handlePrimaryPointer`,
+      // because `acceptGesture` might be called before `handlePrimaryPointer`,
+      // which relies on `_down` to call `handleTapDown`.
+      _down = event;
+    }
+    super.addAllowedPointer(event);
+  }
+
+  @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);
+    }
+  }
+
+  @override
+  void resolve(GestureDisposition disposition) {
+    if (_wonArenaForPrimaryPointer && disposition == GestureDisposition.rejected) {
+      // This can happen if the gesture has been canceled. For example, when
+      // the pointer has exceeded the touch slop, the buttons have been changed,
+      // or if the recognizer is disposed.
+      assert(_sentTapDown);
+      _checkCancel(null, 'spontaneous');
+      _reset();
+    }
+    super.resolve(disposition);
+  }
+
+  @override
+  void didExceedDeadline() {
+    _checkDown();
+  }
+
+  @override
+  void acceptGesture(int pointer) {
+    super.acceptGesture(pointer);
+    if (pointer == primaryPointer) {
+      _checkDown();
+      _wonArenaForPrimaryPointer = true;
+      _checkUp();
+    }
+  }
+
+  @override
+  void rejectGesture(int pointer) {
+    super.rejectGesture(pointer);
+    if (pointer == primaryPointer) {
+      // Another gesture won the arena.
+      assert(state != GestureRecognizerState.possible);
+      if (_sentTapDown)
+        _checkCancel(null, 'forced');
+      _reset();
+    }
+  }
+
+  void _checkDown() {
+    if (_sentTapDown) {
+      return;
+    }
+    handleTapDown(down: _down);
+    _sentTapDown = true;
+  }
+
+  void _checkUp() {
+    if (!_wonArenaForPrimaryPointer || _up == null) {
+      return;
+    }
+    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;
+  }
+
+  @override
+  String get debugDescription => 'base tap';
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(FlagProperty('wonArenaForPrimaryPointer', value: _wonArenaForPrimaryPointer, ifTrue: 'won arena'));
+    properties.add(DiagnosticsProperty<Offset>('finalPosition', _up?.position, defaultValue: null));
+    properties.add(DiagnosticsProperty<Offset>('finalLocalPosition', _up?.localPosition, defaultValue: _up?.position));
+    properties.add(DiagnosticsProperty<int>('button', _down?.buttons, defaultValue: null));
+    properties.add(FlagProperty('sentTapDown', value: _sentTapDown, ifTrue: 'sent tap down'));
+  }
+}
+
 /// Recognizes taps.
 ///
 /// Gesture recognizers take part in gesture arenas to enable potential gestures
@@ -118,17 +310,17 @@
 ///
 ///  * [GestureDetector.onTap], which uses this recognizer.
 ///  * [MultiTapGestureRecognizer]
-class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
+class TapGestureRecognizer extends BaseTapGestureRecognizer {
   /// Creates a tap gesture recognizer.
-  TapGestureRecognizer({ Object debugOwner }) : super(deadline: kPressTimeout, debugOwner: debugOwner);
+  TapGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
 
-  /// A pointer that might cause a tap of a primary button has contacted the
-  /// screen at a particular location.
+  /// A pointer has contacted the screen at a particular location with a primary
+  /// button, which might be the start of a tap.
   ///
-  /// This triggers once a short timeout ([deadline]) has elapsed, or once
-  /// the gestures has won the arena, whichever comes first.
+  /// This triggers after the down event, once a short timeout ([deadline]) has
+  /// elapsed, or once the gestures has won the arena, whichever comes first.
   ///
-  /// If the gesture doesn't win the arena, [onTapCancel] is called next.
+  /// If this recognizer doesn't win the arena, [onTapCancel] is called next.
   /// Otherwise, [onTapUp] is called next.
   ///
   /// See also:
@@ -139,13 +331,13 @@
   ///  * [GestureDetector.onTapDown], which exposes this callback.
   GestureTapDownCallback onTapDown;
 
-  /// A pointer that will trigger a tap of a primary button has stopped
-  /// contacting the screen at a particular location.
+  /// A pointer has stopped contacting the screen at a particular location,
+  /// which is recognized as a tap of a primary button.
   ///
-  /// This triggers once the gesture has won the arena, immediately before
-  /// [onTap].
+  /// This triggers on the up event, if the recognizer wins the arena with it
+  /// or has previously won, immediately followed by [onTap].
   ///
-  /// If the gesture doesn't win the arena, [onTapCancel] is called instead.
+  /// If this recognizer doesn't win the arena, [onTapCancel] is called instead.
   ///
   /// See also:
   ///
@@ -155,12 +347,13 @@
   ///  * [GestureDetector.onTapUp], which exposes this callback.
   GestureTapUpCallback onTapUp;
 
-  /// A tap of a primary button has occurred.
+  /// A pointer has stopped contacting the screen, which is recognized as a tap
+  /// of a primary button.
   ///
-  /// This triggers once the gesture has won the arena, immediately after
-  /// [onTapUp].
+  /// This triggers on the up event, if the recognizer wins the arena with it
+  /// or has previously won, immediately following [onTap].
   ///
-  /// If the gesture doesn't win the arena, [onTapCancel] is called instead.
+  /// If this recognizer doesn't win the arena, [onTapCancel] is called instead.
   ///
   /// See also:
   ///
@@ -169,12 +362,14 @@
   ///  * [GestureDetector.onTap], which exposes this callback.
   GestureTapCallback onTap;
 
-  /// The pointer that previously triggered [onTapDown] will not end up causing
+  /// A pointer that previously triggered [onTapDown] will not end up causing
   /// a tap.
   ///
-  /// This triggers if the gesture loses the arena.
+  /// This triggers once the gesture loses the arena, if [onTapDown] has
+  /// previously been triggered.
   ///
-  /// If the gesture wins the arena, [onTapUp] and [onTap] are called instead.
+  /// If this recognizer wins the arena, [onTapUp] and [onTap] are called
+  /// instead.
   ///
   /// See also:
   ///
@@ -183,14 +378,14 @@
   ///  * [GestureDetector.onTapCancel], which exposes this callback.
   GestureTapCancelCallback onTapCancel;
 
-  /// A pointer that might cause a tap of a secondary button has contacted the
-  /// screen at a particular location.
+  /// A pointer has contacted the screen at a particular location with a
+  /// secondary button, which might be the start of a secondary tap.
   ///
-  /// This triggers once a short timeout ([deadline]) has elapsed, or once
-  /// the gestures has won the arena, whichever comes first.
+  /// This triggers after the down event, once a short timeout ([deadline]) has
+  /// elapsed, or once the gestures has won the arena, whichever comes first.
   ///
-  /// If the gesture doesn't win the arena, [onSecondaryTapCancel] is called next.
-  /// Otherwise, [onSecondaryTapUp] is called next.
+  /// If this recognizer doesn't win the arena, [onSecondaryTapCancel] is called
+  /// next. Otherwise, [onSecondaryTapUp] is called next.
   ///
   /// See also:
   ///
@@ -200,12 +395,13 @@
   ///  * [GestureDetector.onSecondaryTapDown], which exposes this callback.
   GestureTapDownCallback onSecondaryTapDown;
 
-  /// A pointer that will trigger a tap of a secondary button has stopped
-  /// contacting the screen at a particular location.
+  /// A pointer has stopped contacting the screen at a particular location,
+  /// which is recognized as a tap of a secondary button.
   ///
-  /// This triggers once the gesture has won the arena.
+  /// This triggers on the up event, if the recognizer wins the arena with it
+  /// or has previously won.
   ///
-  /// If the gesture doesn't win the arena, [onSecondaryTapCancel] is called
+  /// If this recognizer doesn't win the arena, [onSecondaryTapCancel] is called
   /// instead.
   ///
   /// See also:
@@ -216,12 +412,13 @@
   ///  * [GestureDetector.onSecondaryTapUp], which exposes this callback.
   GestureTapUpCallback onSecondaryTapUp;
 
-  /// The pointer that previously triggered [onSecondaryTapDown] will not end up
+  /// A pointer that previously triggered [onSecondaryTapDown] will not end up
   /// causing a tap.
   ///
-  /// This triggers if the gesture loses the arena.
+  /// This triggers once the gesture loses the arena, if [onSecondaryTapDown]
+  /// has previously been triggered.
   ///
-  /// If the gesture wins the arena, [onSecondaryTapUp] is called instead.
+  /// If this recognizer wins the arena, [onSecondaryTapUp] is called instead.
   ///
   /// See also:
   ///
@@ -230,13 +427,6 @@
   ///  * [GestureDetector.onTapCancel], which exposes this callback.
   GestureTapCancelCallback onSecondaryTapCancel;
 
-  bool _sentTapDown = false;
-  bool _wonArenaForPrimaryPointer = false;
-  OffsetPair _finalPosition;
-  // The buttons sent by `PointerDownEvent`. If a `PointerMoveEvent` comes with a
-  // different set of buttons, the gesture is canceled.
-  int _initialButtons;
-
   @override
   bool isPointerAllowed(PointerDownEvent event) {
     switch (event.buttons) {
@@ -259,82 +449,15 @@
     return super.isPointerAllowed(event);
   }
 
+  @protected
   @override
-  void addAllowedPointer(PointerDownEvent event) {
-    super.addAllowedPointer(event);
-    // `_initialButtons` must be assigned here instead of `handlePrimaryPointer`,
-    // because `acceptGesture` might be called before `handlePrimaryPointer`,
-    // which relies on `_initialButtons` to create `TapDownDetails`.
-    _initialButtons = event.buttons;
-  }
-
-  @override
-  void handlePrimaryPointer(PointerEvent event) {
-    if (event is PointerUpEvent) {
-      _finalPosition = OffsetPair(global: event.position, local: event.localPosition);
-      _checkUp();
-    } else if (event is PointerCancelEvent) {
-      resolve(GestureDisposition.rejected);
-      if (_sentTapDown) {
-        _checkCancel('');
-      }
-      _reset();
-    } else if (event.buttons != _initialButtons) {
-      resolve(GestureDisposition.rejected);
-      stopTrackingPointer(primaryPointer);
-    }
-  }
-
-  @override
-  void resolve(GestureDisposition disposition) {
-    if (_wonArenaForPrimaryPointer && disposition == GestureDisposition.rejected) {
-      // This can happen if the gesture has been canceled. For example, when
-      // the pointer has exceeded the touch slop, the buttons have been changed,
-      // or if the recognizer is disposed.
-      assert(_sentTapDown);
-      _checkCancel('spontaneous ');
-      _reset();
-    }
-    super.resolve(disposition);
-  }
-
-  @override
-  void didExceedDeadlineWithEvent(PointerDownEvent event) {
-    _checkDown(event.pointer);
-  }
-
-  @override
-  void acceptGesture(int pointer) {
-    super.acceptGesture(pointer);
-    if (pointer == primaryPointer) {
-      _checkDown(pointer);
-      _wonArenaForPrimaryPointer = true;
-      _checkUp();
-    }
-  }
-
-  @override
-  void rejectGesture(int pointer) {
-    super.rejectGesture(pointer);
-    if (pointer == primaryPointer) {
-      // Another gesture won the arena.
-      assert(state != GestureRecognizerState.possible);
-      if (_sentTapDown)
-        _checkCancel('forced ');
-      _reset();
-    }
-  }
-
-  void _checkDown(int pointer) {
-    if (_sentTapDown) {
-      return;
-    }
+  void handleTapDown({PointerDownEvent down}) {
     final TapDownDetails details = TapDownDetails(
-      globalPosition: initialPosition.global,
-      localPosition: initialPosition.local,
-      kind: getKindForPointer(pointer),
+      globalPosition: down.position,
+      localPosition: down.localPosition,
+      kind: getKindForPointer(down.pointer),
     );
-    switch (_initialButtons) {
+    switch (down.buttons) {
       case kPrimaryButton:
         if (onTapDown != null)
           invokeCallback<void>('onTapDown', () => onTapDown(details));
@@ -346,18 +469,16 @@
         break;
       default:
     }
-    _sentTapDown = true;
   }
 
-  void _checkUp() {
-    if (!_wonArenaForPrimaryPointer || _finalPosition == null) {
-      return;
-    }
+  @protected
+  @override
+  void handleTapUp({PointerDownEvent down, PointerUpEvent up}) {
     final TapUpDetails details = TapUpDetails(
-      globalPosition: _finalPosition.global,
-      localPosition: _finalPosition.local,
+      globalPosition: up.position,
+      localPosition: up.localPosition,
     );
-    switch (_initialButtons) {
+    switch (down.buttons) {
       case kPrimaryButton:
         if (onTapUp != null)
           invokeCallback<void>('onTapUp', () => onTapUp(details));
@@ -371,11 +492,13 @@
         break;
       default:
     }
-    _reset();
   }
 
-  void _checkCancel(String note) {
-    switch (_initialButtons) {
+  @protected
+  @override
+  void handleTapCancel({PointerDownEvent down, PointerCancelEvent cancel, String reason}) {
+    final String note = reason == '' ? reason : ' $reason';
+    switch (down.buttons) {
       case kPrimaryButton:
         if (onTapCancel != null)
           invokeCallback<void>('${note}onTapCancel', onTapCancel);
@@ -389,23 +512,6 @@
     }
   }
 
-  void _reset() {
-    _sentTapDown = false;
-    _wonArenaForPrimaryPointer = false;
-    _finalPosition = null;
-    _initialButtons = null;
-  }
-
   @override
   String get debugDescription => 'tap';
-
-  @override
-  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
-    super.debugFillProperties(properties);
-    properties.add(FlagProperty('wonArenaForPrimaryPointer', value: _wonArenaForPrimaryPointer, ifTrue: 'won arena'));
-    properties.add(DiagnosticsProperty<Offset>('finalPosition', _finalPosition?.global, defaultValue: null));
-    properties.add(DiagnosticsProperty<Offset>('finalLocalPosition', _finalPosition?.local, defaultValue: _finalPosition?.global));
-    properties.add(FlagProperty('sentTapDown', value: _sentTapDown, ifTrue: 'sent tap down'));
-    // TODO(tongmu): Add property _initialButtons and update related tests
-  }
 }
diff --git a/packages/flutter/lib/src/widgets/modal_barrier.dart b/packages/flutter/lib/src/widgets/modal_barrier.dart
index d6936b0..cc449ef 100644
--- a/packages/flutter/lib/src/widgets/modal_barrier.dart
+++ b/packages/flutter/lib/src/widgets/modal_barrier.dart
@@ -191,99 +191,41 @@
 //
 // It is similar to [TapGestureRecognizer.onTapDown], but accepts any single
 // button, which means the gesture also takes parts in gesture arenas.
-class _AnyTapGestureRecognizer extends PrimaryPointerGestureRecognizer {
-  _AnyTapGestureRecognizer({
-    Object debugOwner,
-  }) : super(debugOwner: debugOwner);
+class _AnyTapGestureRecognizer extends BaseTapGestureRecognizer {
+  _AnyTapGestureRecognizer({ Object debugOwner })
+    : super(debugOwner: debugOwner);
 
   VoidCallback onAnyTapDown;
 
-  bool _sentTapDown = false;
-  bool _wonArenaForPrimaryPointer = false;
-  // The buttons sent by `PointerDownEvent`. If a `PointerMoveEvent` comes with a
-  // different set of buttons, the gesture is canceled.
-  int _initialButtons;
-
+  @protected
   @override
-  void addAllowedPointer(PointerDownEvent event) {
-    super.addAllowedPointer(event);
-    // `_initialButtons` must be assigned here instead of `handlePrimaryPointer`,
-    // because `acceptGesture` might be called before `handlePrimaryPointer`,
-    // which relies on `_initialButtons` to create `TapDownDetails`.
-    _initialButtons = event.buttons;
+  bool isPointerAllowed(PointerDownEvent event) {
+    if (onAnyTapDown == null)
+      return false;
+    return super.isPointerAllowed(event);
   }
 
+  @protected
   @override
-  void handlePrimaryPointer(PointerEvent event) {
-    if (event is PointerUpEvent || event is PointerCancelEvent) {
-      resolve(GestureDisposition.rejected);
-      _reset();
-    } else if (event.buttons != _initialButtons) {
-      resolve(GestureDisposition.rejected);
-      stopTrackingPointer(primaryPointer);
-    }
-  }
-
-  @override
-  void resolve(GestureDisposition disposition) {
-    if (_wonArenaForPrimaryPointer && disposition == GestureDisposition.rejected) {
-      // This can happen if the gesture has been canceled. For example, when
-      // the pointer has exceeded the touch slop, the buttons have been changed,
-      // or if the recognizer is disposed.
-      assert(_sentTapDown);
-      _reset();
-    }
-    super.resolve(disposition);
-  }
-
-  @override
-  void didExceedDeadlineWithEvent(PointerDownEvent event) {
-    _checkDown(event.pointer);
-  }
-
-  @override
-  void acceptGesture(int pointer) {
-    super.acceptGesture(pointer);
-    if (pointer == primaryPointer) {
-      _checkDown(pointer);
-      _wonArenaForPrimaryPointer = true;
-      _reset();
-    }
-  }
-
-  @override
-  void rejectGesture(int pointer) {
-    super.rejectGesture(pointer);
-    if (pointer == primaryPointer) {
-      // Another gesture won the arena.
-      assert(state != GestureRecognizerState.possible);
-      _reset();
-    }
-  }
-
-  void _checkDown(int pointer) {
-    if (_sentTapDown)
-      return;
+  void handleTapDown({PointerDownEvent down}) {
     if (onAnyTapDown != null)
       onAnyTapDown();
-    _sentTapDown = true;
   }
 
-  void _reset() {
-    _sentTapDown = false;
-    _wonArenaForPrimaryPointer = false;
-    _initialButtons = null;
+  @protected
+  @override
+  void handleTapUp({PointerDownEvent down, PointerUpEvent up}) {
+    // Do nothing.
+  }
+
+  @protected
+  @override
+  void handleTapCancel({PointerDownEvent down, PointerCancelEvent cancel, String reason}) {
+    // Do nothing.
   }
 
   @override
   String get debugDescription => 'any tap';
-
-  @override
-  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
-    super.debugFillProperties(properties);
-    properties.add(FlagProperty('wonArenaForPrimaryPointer', value: _wonArenaForPrimaryPointer, ifTrue: 'won arena'));
-    properties.add(FlagProperty('sentTapDown', value: _sentTapDown, ifTrue: 'sent tap down'));
-  }
 }
 
 class _ModalBarrierSemanticsDelegate extends SemanticsGestureDelegate {
diff --git a/packages/flutter/test/gestures/debug_test.dart b/packages/flutter/test/gestures/debug_test.dart
index 1abad2a..25a2a28 100644
--- a/packages/flutter/test/gestures/debug_test.dart
+++ b/packages/flutter/test/gestures/debug_test.dart
@@ -25,7 +25,7 @@
     tap.addPointer(event);
     expect(log, hasLength(2));
     expect(log[0], equalsIgnoringHashCodes('Gesture arena 1    ❙ ★ Opening new gesture arena.'));
-    expect(log[1], equalsIgnoringHashCodes('Gesture arena 1    ❙ Adding: TapGestureRecognizer#00000(state: ready)'));
+    expect(log[1], equalsIgnoringHashCodes('Gesture arena 1    ❙ Adding: TapGestureRecognizer#00000(state: ready, button: 1)'));
     log.clear();
 
     GestureBinding.instance.gestureArena.close(1);
@@ -43,7 +43,7 @@
     GestureBinding.instance.gestureArena.sweep(1);
     expect(log, hasLength(2));
     expect(log[0], equalsIgnoringHashCodes('Gesture arena 1    ❙ Sweeping with 1 member.'));
-    expect(log[1], equalsIgnoringHashCodes('Gesture arena 1    ❙ Winner: TapGestureRecognizer#00000(state: ready, finalPosition: Offset(12.0, 8.0))'));
+    expect(log[1], equalsIgnoringHashCodes('Gesture arena 1    ❙ Winner: TapGestureRecognizer#00000(state: ready, finalPosition: Offset(12.0, 8.0), button: 1)'));
     log.clear();
 
     tap.dispose();
@@ -83,9 +83,9 @@
 
     GestureBinding.instance.gestureArena.sweep(1);
     expect(log, hasLength(3));
-    expect(log[0], equalsIgnoringHashCodes('TapGestureRecognizer#00000(state: ready, finalPosition: Offset(12.0, 8.0)) calling onTapDown callback.'));
-    expect(log[1], equalsIgnoringHashCodes('TapGestureRecognizer#00000(state: ready, won arena, finalPosition: Offset(12.0, 8.0), sent tap down) calling onTapUp callback.'));
-    expect(log[2], equalsIgnoringHashCodes('TapGestureRecognizer#00000(state: ready, won arena, finalPosition: Offset(12.0, 8.0), sent tap down) calling onTap callback.'));
+    expect(log[0], equalsIgnoringHashCodes('TapGestureRecognizer#00000(state: ready, finalPosition: Offset(12.0, 8.0), button: 1) calling onTapDown callback.'));
+    expect(log[1], equalsIgnoringHashCodes('TapGestureRecognizer#00000(state: ready, won arena, finalPosition: Offset(12.0, 8.0), button: 1, sent tap down) calling onTapUp callback.'));
+    expect(log[2], equalsIgnoringHashCodes('TapGestureRecognizer#00000(state: ready, won arena, finalPosition: Offset(12.0, 8.0), button: 1, sent tap down) calling onTap callback.'));
     log.clear();
 
     tap.dispose();
@@ -114,7 +114,7 @@
     tap.addPointer(event);
     expect(log, hasLength(2));
     expect(log[0], equalsIgnoringHashCodes('Gesture arena 1    ❙ ★ Opening new gesture arena.'));
-    expect(log[1], equalsIgnoringHashCodes('Gesture arena 1    ❙ Adding: TapGestureRecognizer#00000(state: ready)'));
+    expect(log[1], equalsIgnoringHashCodes('Gesture arena 1    ❙ Adding: TapGestureRecognizer#00000(state: ready, button: 1)'));
     log.clear();
 
     GestureBinding.instance.gestureArena.close(1);
@@ -132,10 +132,10 @@
     GestureBinding.instance.gestureArena.sweep(1);
     expect(log, hasLength(5));
     expect(log[0], equalsIgnoringHashCodes('Gesture arena 1    ❙ Sweeping with 1 member.'));
-    expect(log[1], equalsIgnoringHashCodes('Gesture arena 1    ❙ Winner: TapGestureRecognizer#00000(state: ready, finalPosition: Offset(12.0, 8.0))'));
-    expect(log[2], equalsIgnoringHashCodes('                   ❙ TapGestureRecognizer#00000(state: ready, finalPosition: Offset(12.0, 8.0)) calling onTapDown callback.'));
-    expect(log[3], equalsIgnoringHashCodes('                   ❙ TapGestureRecognizer#00000(state: ready, won arena, finalPosition: Offset(12.0, 8.0), sent tap down) calling onTapUp callback.'));
-    expect(log[4], equalsIgnoringHashCodes('                   ❙ TapGestureRecognizer#00000(state: ready, won arena, finalPosition: Offset(12.0, 8.0), sent tap down) calling onTap callback.'));
+    expect(log[1], equalsIgnoringHashCodes('Gesture arena 1    ❙ Winner: TapGestureRecognizer#00000(state: ready, finalPosition: Offset(12.0, 8.0), button: 1)'));
+    expect(log[2], equalsIgnoringHashCodes('                   ❙ TapGestureRecognizer#00000(state: ready, finalPosition: Offset(12.0, 8.0), button: 1) calling onTapDown callback.'));
+    expect(log[3], equalsIgnoringHashCodes('                   ❙ TapGestureRecognizer#00000(state: ready, won arena, finalPosition: Offset(12.0, 8.0), button: 1, sent tap down) calling onTapUp callback.'));
+    expect(log[4], equalsIgnoringHashCodes('                   ❙ TapGestureRecognizer#00000(state: ready, won arena, finalPosition: Offset(12.0, 8.0), button: 1, sent tap down) calling onTap callback.'));
     log.clear();
 
     tap.dispose();
@@ -152,7 +152,7 @@
     expect(tap.toString(), equalsIgnoringHashCodes('TapGestureRecognizer#00000(state: ready)'));
     const PointerEvent event = PointerDownEvent(pointer: 1, position: Offset(10.0, 10.0));
     tap.addPointer(event);
-    tap.didExceedDeadlineWithEvent(event);
-    expect(tap.toString(), equalsIgnoringHashCodes('TapGestureRecognizer#00000(state: possible, sent tap down)'));
+    tap.didExceedDeadline();
+    expect(tap.toString(), equalsIgnoringHashCodes('TapGestureRecognizer#00000(state: possible, button: 1, sent tap down)'));
   });
 }
diff --git a/packages/flutter/test/gestures/tap_test.dart b/packages/flutter/test/gestures/tap_test.dart
index 2d377d6..a2af628 100644
--- a/packages/flutter/test/gestures/tap_test.dart
+++ b/packages/flutter/test/gestures/tap_test.dart
@@ -140,7 +140,7 @@
     tap.dispose();
   });
 
-  testGesture('Should not recognize two overlapping taps', (GestureTester tester) {
+  testGesture('Should not recognize two overlapping taps (FIFO)', (GestureTester tester) {
     final TapGestureRecognizer tap = TapGestureRecognizer();
 
     int tapsRecognized = 0;
@@ -174,6 +174,40 @@
     tap.dispose();
   });
 
+  testGesture('Should not recognize two overlapping taps (FILO)', (GestureTester tester) {
+    final TapGestureRecognizer tap = TapGestureRecognizer();
+
+    int tapsRecognized = 0;
+    tap.onTap = () {
+      tapsRecognized++;
+    };
+
+    tap.addPointer(down1);
+    tester.closeArena(1);
+    expect(tapsRecognized, 0);
+    tester.route(down1);
+    expect(tapsRecognized, 0);
+
+    tap.addPointer(down2);
+    tester.closeArena(2);
+    expect(tapsRecognized, 0);
+    tester.route(down1);
+    expect(tapsRecognized, 0);
+
+
+    tester.route(up2);
+    expect(tapsRecognized, 0);
+    GestureBinding.instance.gestureArena.sweep(2);
+    expect(tapsRecognized, 0);
+
+    tester.route(up1);
+    expect(tapsRecognized, 1);
+    GestureBinding.instance.gestureArena.sweep(1);
+    expect(tapsRecognized, 1);
+
+    tap.dispose();
+  });
+
   testGesture('Distance cancels tap', (GestureTester tester) {
     final TapGestureRecognizer tap = TapGestureRecognizer();
 
diff --git a/packages/flutter/test/widgets/modal_barrier_test.dart b/packages/flutter/test/widgets/modal_barrier_test.dart
index 9bf6c2e..8a447df 100644
--- a/packages/flutter/test/widgets/modal_barrier_test.dart
+++ b/packages/flutter/test/widgets/modal_barrier_test.dart
@@ -130,7 +130,7 @@
     await tester.pump(); // begin transition
     await tester.pump(const Duration(seconds: 1)); // end transition
 
-    // Tap on the barrier to dismiss it
+    // Press the barrier to dismiss it
     await tester.press(find.byKey(const ValueKey<String>('barrier')));
     await tester.pump(); // begin transition
     await tester.pump(const Duration(seconds: 1)); // end transition
@@ -155,7 +155,7 @@
     await tester.pump(); // begin transition
     await tester.pump(const Duration(seconds: 1)); // end transition
 
-    // Tap on the barrier to dismiss it
+    // Press the barrier to dismiss it
     await tester.press(find.byKey(const ValueKey<String>('barrier')), buttons: kSecondaryButton);
     await tester.pump(); // begin transition
     await tester.pump(const Duration(seconds: 1)); // end transition
@@ -164,6 +164,31 @@
       reason: 'The route should have been dismissed by tapping the barrier.');
   });
 
+  testWidgets('ModalBarrier may pop the Navigator when competing with other gestures', (WidgetTester tester) async {
+    final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
+      '/': (BuildContext context) => FirstWidget(),
+      '/modal': (BuildContext context) => SecondWidgetWithCompetence (),
+    };
+
+    await tester.pumpWidget(MaterialApp(routes: routes));
+
+    // Initially the barrier is not visible
+    expect(find.byKey(const ValueKey<String>('barrier')), findsNothing);
+
+    // Tapping on X routes to the barrier
+    await tester.tap(find.text('X'));
+    await tester.pump(); // begin transition
+    await tester.pump(const Duration(seconds: 1)); // end transition
+
+    // Tap on the barrier to dismiss it
+    await tester.tap(find.byKey(const ValueKey<String>('barrier')));
+    await tester.pump(); // begin transition
+    await tester.pump(const Duration(seconds: 1)); // end transition
+
+    expect(find.byKey(const ValueKey<String>('barrier')), findsNothing,
+      reason: 'The route should have been dismissed by tapping the barrier.');
+  });
+
   testWidgets('ModalBarrier does not pop the Navigator with a WillPopScope that returns false', (WidgetTester tester) async {
     bool willPopCalled = false;
     final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
@@ -316,3 +341,22 @@
     );
   }
 }
+
+class SecondWidgetWithCompetence extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    return Stack(
+      children: <Widget>[
+        const ModalBarrier(
+          key: ValueKey<String>('barrier'),
+          dismissible: true,
+        ),
+        GestureDetector(
+          onVerticalDragStart: (_) {},
+          behavior: HitTestBehavior.translucent,
+          child: Container(),
+        )
+      ],
+    );
+  }
+}