| // Copyright 2015 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'package:flutter/foundation.dart'; |
| |
| import 'arena.dart'; |
| import 'constants.dart'; |
| import 'events.dart'; |
| import 'recognizer.dart'; |
| |
| /// Details for [GestureTapDownCallback], such as position |
| /// |
| /// See also: |
| /// |
| /// * [GestureDetector.onTapDown], which receives this information. |
| /// * [TapGestureRecognizer], which passes this information to one of its callbacks. |
| class TapDownDetails { |
| /// Creates details for a [GestureTapDownCallback]. |
| /// |
| /// The [globalPosition] argument must not be null. |
| TapDownDetails({ |
| this.globalPosition = Offset.zero, |
| Offset localPosition, |
| this.kind, |
| }) : assert(globalPosition != null), |
| localPosition = localPosition ?? globalPosition; |
| |
| /// The global position at which the pointer contacted the screen. |
| final Offset globalPosition; |
| |
| /// The kind of the device that initiated the event. |
| final PointerDeviceKind kind; |
| |
| /// The local position at which the pointer contacted the screen. |
| final Offset localPosition; |
| } |
| |
| /// Signature for when a pointer that might cause a tap has contacted the |
| /// screen. |
| /// |
| /// The position at which the pointer contacted the screen is available in the |
| /// `details`. |
| /// |
| /// See also: |
| /// |
| /// * [GestureDetector.onTapDown], which matches this signature. |
| /// * [TapGestureRecognizer], which uses this signature in one of its callbacks. |
| typedef GestureTapDownCallback = void Function(TapDownDetails details); |
| |
| /// Details for [GestureTapUpCallback], such as position. |
| /// |
| /// See also: |
| /// |
| /// * [GestureDetector.onTapUp], which receives this information. |
| /// * [TapGestureRecognizer], which passes this information to one of its callbacks. |
| class TapUpDetails { |
| /// The [globalPosition] argument must not be null. |
| TapUpDetails({ |
| this.globalPosition = Offset.zero, |
| Offset localPosition, |
| }) : assert(globalPosition != null), |
| localPosition = localPosition ?? globalPosition; |
| |
| /// The global position at which the pointer contacted the screen. |
| final Offset globalPosition; |
| |
| /// The local position at which the pointer contacted the screen. |
| final Offset localPosition; |
| } |
| |
| /// Signature for when a pointer that will trigger a tap has stopped contacting |
| /// the screen. |
| /// |
| /// The position at which the pointer stopped contacting the screen is available |
| /// in the `details`. |
| /// |
| /// See also: |
| /// |
| /// * [GestureDetector.onTapUp], which matches this signature. |
| /// * [TapGestureRecognizer], which uses this signature in one of its callbacks. |
| typedef GestureTapUpCallback = void Function(TapUpDetails details); |
| |
| /// Signature for when a tap has occurred. |
| /// |
| /// See also: |
| /// |
| /// * [GestureDetector.onTap], which matches this signature. |
| /// * [TapGestureRecognizer], which uses this signature in one of its callbacks. |
| typedef GestureTapCallback = void Function(); |
| |
| /// Signature for when the pointer that previously triggered a |
| /// [GestureTapDownCallback] will not end up causing a tap. |
| /// |
| /// See also: |
| /// |
| /// * [GestureDetector.onTapCancel], which matches this signature. |
| /// * [TapGestureRecognizer], which uses this signature in one of its callbacks. |
| typedef GestureTapCancelCallback = void Function(); |
| |
| /// Recognizes 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]. |
| /// |
| /// [TapGestureRecognizer] 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. |
| /// |
| /// [TapGestureRecognizer] competes on pointer events of [kPrimaryButton] only |
| /// when it has at least one non-null `onTap*` callback, and events of |
| /// [kSecondaryButton] only when it has at least one non-null `onSecondaryTap*` |
| /// callback. If it has no callbacks, it is a no-op. |
| /// |
| /// See also: |
| /// |
| /// * [GestureDetector.onTap], which uses this recognizer. |
| /// * [MultiTapGestureRecognizer] |
| class TapGestureRecognizer extends PrimaryPointerGestureRecognizer { |
| /// Creates a tap gesture recognizer. |
| TapGestureRecognizer({ Object debugOwner }) : super(deadline: kPressTimeout, debugOwner: debugOwner); |
| |
| /// A pointer that might cause a tap of a primary button has contacted the |
| /// screen at a particular location. |
| /// |
| /// This triggers 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. |
| /// Otherwise, [onTapUp] is called next. |
| /// |
| /// See also: |
| /// |
| /// * [kPrimaryButton], the button this callback responds to. |
| /// * [onSecondaryTapDown], a similar callback but for a secondary button. |
| /// * [TapDownDetails], which is passed as an argument to this callback. |
| /// * [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. |
| /// |
| /// This triggers once the gesture has won the arena, immediately before |
| /// [onTap]. |
| /// |
| /// If the gesture doesn't win the arena, [onTapCancel] is called instead. |
| /// |
| /// See also: |
| /// |
| /// * [kPrimaryButton], the button this callback responds to. |
| /// * [onSecondaryTapUp], a similar callback but for a secondary button. |
| /// * [TapUpDetails], which is passed as an argument to this callback. |
| /// * [GestureDetector.onTapUp], which exposes this callback. |
| GestureTapUpCallback onTapUp; |
| |
| /// A tap of a primary button has occurred. |
| /// |
| /// This triggers once the gesture has won the arena, immediately after |
| /// [onTapUp]. |
| /// |
| /// If the gesture doesn't win the arena, [onTapCancel] is called instead. |
| /// |
| /// See also: |
| /// |
| /// * [kPrimaryButton], the button this callback responds to. |
| /// * [onTapUp], which has the same timing but with details. |
| /// * [GestureDetector.onTap], which exposes this callback. |
| GestureTapCallback onTap; |
| |
| /// The pointer that previously triggered [onTapDown] will not end up causing |
| /// a tap. |
| /// |
| /// This triggers if the gesture loses the arena. |
| /// |
| /// If the gesture wins the arena, [onTapUp] and [onTap] are called instead. |
| /// |
| /// See also: |
| /// |
| /// * [kPrimaryButton], the button this callback responds to. |
| /// * [onSecondaryTapCancel], a similar callback but for a secondary button. |
| /// * [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. |
| /// |
| /// This triggers 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. |
| /// |
| /// See also: |
| /// |
| /// * [kSecondaryButton], the button this callback responds to. |
| /// * [onPrimaryTapDown], a similar callback but for a primary button. |
| /// * [TapDownDetails], which is passed as an argument to this callback. |
| /// * [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. |
| /// |
| /// This triggers once the gesture has won the arena. |
| /// |
| /// If the gesture doesn't win the arena, [onSecondaryTapCancel] is called |
| /// instead. |
| /// |
| /// See also: |
| /// |
| /// * [kSecondaryButton], the button this callback responds to. |
| /// * [onPrimaryTapUp], a similar callback but for a primary button. |
| /// * [TapUpDetails], which is passed as an argument to this callback. |
| /// * [GestureDetector.onSecondaryTapUp], which exposes this callback. |
| GestureTapUpCallback onSecondaryTapUp; |
| |
| /// The pointer that previously triggered [onSecondaryTapDown] will not end up |
| /// causing a tap. |
| /// |
| /// This triggers if the gesture loses the arena. |
| /// |
| /// If the gesture wins the arena, [onSecondaryTapUp] is called instead. |
| /// |
| /// See also: |
| /// |
| /// * [kSecondaryButton], the button this callback responds to. |
| /// * [onPrimaryTapCancel], a similar callback but for a primary button. |
| /// * [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) { |
| case kPrimaryButton: |
| if (onTapDown == null && |
| onTap == null && |
| onTapUp == null && |
| onTapCancel == null) |
| return false; |
| break; |
| case kSecondaryButton: |
| if (onSecondaryTapDown == null && |
| onSecondaryTapUp == null && |
| onSecondaryTapCancel == null) |
| return false; |
| break; |
| default: |
| return false; |
| } |
| return super.isPointerAllowed(event); |
| } |
| |
| @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; |
| } |
| final TapDownDetails details = TapDownDetails( |
| globalPosition: initialPosition.global, |
| localPosition: initialPosition.local, |
| kind: getKindForPointer(pointer), |
| ); |
| switch (_initialButtons) { |
| case kPrimaryButton: |
| if (onTapDown != null) |
| invokeCallback<void>('onTapDown', () => onTapDown(details)); |
| break; |
| case kSecondaryButton: |
| if (onSecondaryTapDown != null) |
| invokeCallback<void>('onSecondaryTapDown', |
| () => onSecondaryTapDown(details)); |
| break; |
| default: |
| } |
| _sentTapDown = true; |
| } |
| |
| void _checkUp() { |
| if (!_wonArenaForPrimaryPointer || _finalPosition == null) { |
| return; |
| } |
| final TapUpDetails details = TapUpDetails( |
| globalPosition: _finalPosition.global, |
| localPosition: _finalPosition.local, |
| ); |
| switch (_initialButtons) { |
| case kPrimaryButton: |
| if (onTapUp != null) |
| invokeCallback<void>('onTapUp', () => onTapUp(details)); |
| if (onTap != null) |
| invokeCallback<void>('onTap', onTap); |
| break; |
| case kSecondaryButton: |
| if (onSecondaryTapUp != null) |
| invokeCallback<void>('onSecondaryTapUp', |
| () => onSecondaryTapUp(details)); |
| break; |
| default: |
| } |
| _reset(); |
| } |
| |
| void _checkCancel(String note) { |
| switch (_initialButtons) { |
| case kPrimaryButton: |
| if (onTapCancel != null) |
| invokeCallback<void>('${note}onTapCancel', onTapCancel); |
| break; |
| case kSecondaryButton: |
| if (onSecondaryTapCancel != null) |
| invokeCallback<void>('${note}onSecondaryTapCancel', |
| onSecondaryTapCancel); |
| break; |
| default: |
| } |
| } |
| |
| 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 |
| } |
| } |