diff --git a/packages/flutter/lib/src/gestures/team.dart b/packages/flutter/lib/src/gestures/team.dart index b23f3e743e..7d3ac4d57c 100644 --- a/packages/flutter/lib/src/gestures/team.dart +++ b/packages/flutter/lib/src/gestures/team.dart @@ -33,7 +33,7 @@ class _CombiningGestureArenaMember extends GestureArenaMember { assert(_pointer == pointer); assert(_winner != null || _members.isNotEmpty); _close(); - _winner ??= _members[0]; + _winner ??= _owner.captain ?? _members[0]; for (GestureArenaMember member in _members) { if (member != _winner) member.rejectGesture(pointer); @@ -74,7 +74,7 @@ class _CombiningGestureArenaMember extends GestureArenaMember { _entry.resolve(disposition); } else { assert(disposition == GestureDisposition.accepted); - _winner ??= member; + _winner ??= _owner.captain ?? member; _entry.resolve(disposition); } } @@ -86,14 +86,19 @@ class _CombiningGestureArenaMember extends GestureArenaMember { /// Normally, a recognizer competes directly in the [GestureArenaManager] to /// recognize a sequence of pointer events as a gesture. With a /// [GestureArenaTeam], recognizers can compete in the arena in a group with -/// other recognizers. +/// other recognizers. Arena teams may have a captain which wins the arena on +/// behalf of its team. /// -/// When gesture recognizers are in a team together, then once there are no -/// other competing gestures in the arena, the first gesture to have been added -/// to the team automatically wins, instead of the gestures continuing to -/// compete against each other. +/// When gesture recognizers are in a team together without a captain, then once +/// there are no other competing gestures in the arena, the first gesture to +/// have been added to the team automatically wins, instead of the gestures +/// continuing to compete against each other. /// -/// For example, [Slider] uses this to support both a +/// When gesture recognizers are in a team with a captain, then once one of the +/// team members claims victory or there are no other competing gestures in the +/// arena, the captain wins the arena, and all other team members lose. +/// +/// For example, [Slider] uses a team without a captain to support both a /// [HorizontalDragGestureRecognizer] and a [TapGestureRecognizer], but without /// the drag recognizer having to wait until the user has dragged outside the /// slop region of the tap gesture before triggering. Since they compete as a @@ -105,11 +110,27 @@ class _CombiningGestureArenaMember extends GestureArenaMember { /// the horizontal nor vertical drag recognizers can claim victory) the tap /// recognizer still actually wins, despite being in the team. /// +/// [AndroidView] uses a team with a captain to decide which gestures are +/// forwarded to the native view. For example if we want to forward taps and +/// vertical scrolls to a native Android view, [TapGestureRecognizers] and +/// [VerticalDragGestureRecognizer] are added to a team with a captain(the captain is set to be a +/// gesture recognizer that never explicitly claims the gesture). +/// The captain allows [AndroidView] to know when any gestures in the team has been +/// recognized (or all other arena members are out), once the captain wins the +/// gesture is forwarded to the Android view. +/// /// To assign a gesture recognizer to a team, set /// [OneSequenceGestureRecognizer.team] to an instance of [GestureArenaTeam]. class GestureArenaTeam { final Map _combiners = {}; + /// A member that wins on behalf of the entire team. + /// + /// If not null, when any one of the [GestureArenaTeam] members claims victory + /// the captain accepts the gesture. + /// If null, the member that claims a victory accepts the gesture. + GestureArenaMember captain; + /// Adds a new member to the arena on behalf of this team. /// /// Used by [GestureRecognizer] subclasses that wish to compete in the arena diff --git a/packages/flutter/test/gestures/team_test.dart b/packages/flutter/test/gestures/team_test.dart index 54407d0f32..9eadbab921 100644 --- a/packages/flutter/test/gestures/team_test.dart +++ b/packages/flutter/test/gestures/team_test.dart @@ -11,7 +11,6 @@ void main() { setUp(ensureGestureBinding); testGesture('GestureArenaTeam rejection test', (GestureTester tester) { - final GestureArenaTeam team = new GestureArenaTeam(); final HorizontalDragGestureRecognizer horizontalDrag = new HorizontalDragGestureRecognizer()..team = team; final VerticalDragGestureRecognizer verticalDrag = new VerticalDragGestureRecognizer()..team = team; @@ -55,4 +54,87 @@ void main() { verticalDrag.dispose(); tap.dispose(); }); + + testGesture('GestureArenaTeam captain', (GestureTester tester) { + final GestureArenaTeam team = new GestureArenaTeam(); + final PassiveGestureRecognizer captain = new PassiveGestureRecognizer()..team = team; + final HorizontalDragGestureRecognizer horizontalDrag = new HorizontalDragGestureRecognizer()..team = team; + final VerticalDragGestureRecognizer verticalDrag = new VerticalDragGestureRecognizer()..team = team; + final TapGestureRecognizer tap = new TapGestureRecognizer(); + + team.captain = captain; + + final List log = []; + + captain.onGestureAccepted = () { log.add('captain accepted gesture'); }; + horizontalDrag.onStart = (DragStartDetails details) { log.add('horizontal-drag-start'); }; + verticalDrag.onStart = (DragStartDetails details) { log.add('vertical-drag-start'); }; + tap.onTap = () { log.add('tap'); }; + + void test(Offset delta) { + const Offset origin = Offset(10.0, 10.0); + final TestPointer pointer = new TestPointer(5); + final PointerDownEvent down = pointer.down(origin); + captain.addPointer(down); + horizontalDrag.addPointer(down); + verticalDrag.addPointer(down); + tap.addPointer(down); + expect(log, isEmpty); + tester.closeArena(5); + expect(log, isEmpty); + tester.route(down); + expect(log, isEmpty); + tester.route(pointer.move(origin + delta)); + tester.route(pointer.up()); + } + + test(Offset.zero); + expect(log, ['tap']); + log.clear(); + + test(const Offset(0.0, 30.0)); + expect(log, ['captain accepted gesture']); + log.clear(); + + horizontalDrag.dispose(); + verticalDrag.dispose(); + tap.dispose(); + captain.dispose(); + }); +} + +typedef void GestureAcceptedCallback(); + +class PassiveGestureRecognizer extends OneSequenceGestureRecognizer { + GestureAcceptedCallback onGestureAccepted; + + @override + void addPointer(PointerDownEvent event) { + startTrackingPointer(event.pointer); + } + + @override + String get debugDescription => 'passive'; + + @override + void didStopTrackingLastPointer(int pointer) { + resolve(GestureDisposition.rejected); + } + + @override + void handleEvent(PointerEvent event) { + if (event is PointerUpEvent || event is PointerCancelEvent) { + stopTrackingPointer(event.pointer); + } + } + + @override + void acceptGesture(int pointer) { + if (onGestureAccepted != null) { + onGestureAccepted(); + } + } + + @override + void rejectGesture(int pointer) { } }