tap.dart 13.9 KB
Newer Older
1 2 3 4
// 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.

5 6
import 'package:flutter/foundation.dart';

7
import 'arena.dart';
8
import 'constants.dart';
9
import 'events.dart';
10
import 'recognizer.dart';
11

12
/// Details for [GestureTapDownCallback], such as position
13 14 15 16 17
///
/// See also:
///
///  * [GestureDetector.onTapDown], which receives this information.
///  * [TapGestureRecognizer], which passes this information to one of its callbacks.
18 19 20 21
class TapDownDetails {
  /// Creates details for a [GestureTapDownCallback].
  ///
  /// The [globalPosition] argument must not be null.
22 23
  TapDownDetails({
    this.globalPosition = Offset.zero,
24
    Offset localPosition,
25
    this.kind,
26 27
  }) : assert(globalPosition != null),
       localPosition = localPosition ?? globalPosition;
28 29

  /// The global position at which the pointer contacted the screen.
30
  final Offset globalPosition;
31 32 33

  /// The kind of the device that initiated the event.
  final PointerDeviceKind kind;
34 35 36

  /// The local position at which the pointer contacted the screen.
  final Offset localPosition;
37 38 39 40 41 42 43
}

/// 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`.
44 45 46 47 48
///
/// See also:
///
///  * [GestureDetector.onTapDown], which matches this signature.
///  * [TapGestureRecognizer], which uses this signature in one of its callbacks.
49
typedef GestureTapDownCallback = void Function(TapDownDetails details);
50 51

/// Details for [GestureTapUpCallback], such as position.
52 53 54 55 56
///
/// See also:
///
///  * [GestureDetector.onTapUp], which receives this information.
///  * [TapGestureRecognizer], which passes this information to one of its callbacks.
57 58
class TapUpDetails {
  /// The [globalPosition] argument must not be null.
59 60 61 62 63
  TapUpDetails({
    this.globalPosition = Offset.zero,
    Offset localPosition,
  }) : assert(globalPosition != null),
       localPosition = localPosition ?? globalPosition;
64 65

  /// The global position at which the pointer contacted the screen.
66
  final Offset globalPosition;
67 68 69

  /// The local position at which the pointer contacted the screen.
  final Offset localPosition;
70
}
71 72

/// Signature for when a pointer that will trigger a tap has stopped contacting
73 74 75 76
/// the screen.
///
/// The position at which the pointer stopped contacting the screen is available
/// in the `details`.
77 78 79 80 81
///
/// See also:
///
///  * [GestureDetector.onTapUp], which matches this signature.
///  * [TapGestureRecognizer], which uses this signature in one of its callbacks.
82
typedef GestureTapUpCallback = void Function(TapUpDetails details);
83 84

/// Signature for when a tap has occurred.
85 86 87 88 89
///
/// See also:
///
///  * [GestureDetector.onTap], which matches this signature.
///  * [TapGestureRecognizer], which uses this signature in one of its callbacks.
90
typedef GestureTapCallback = void Function();
91 92 93

/// Signature for when the pointer that previously triggered a
/// [GestureTapDownCallback] will not end up causing a tap.
94 95 96 97 98
///
/// See also:
///
///  * [GestureDetector.onTapCancel], which matches this signature.
///  * [TapGestureRecognizer], which uses this signature in one of its callbacks.
99
typedef GestureTapCancelCallback = void Function();
100

101 102
/// Recognizes taps.
///
103 104
/// Gesture recognizers take part in gesture arenas to enable potential gestures
/// to be disambiguated from each other. This process is managed by a
105
/// [GestureArenaManager].
106
///
107 108 109
/// [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
110
/// taps. For example, down-1, down-2, up-1, up-2 produces only one tap on up-1.
111
///
112 113 114 115
/// [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.
116
///
117 118
/// See also:
///
119
///  * [GestureDetector.onTap], which uses this recognizer.
120
///  * [MultiTapGestureRecognizer]
121
class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
122
  /// Creates a tap gesture recognizer.
123
  TapGestureRecognizer({ Object debugOwner }) : super(deadline: kPressTimeout, debugOwner: debugOwner);
124

125 126
  /// A pointer that might cause a tap of a primary button has contacted the
  /// screen at a particular location.
127
  ///
128 129
  /// This triggers once a short timeout ([deadline]) has elapsed, or once
  /// the gestures has won the arena, whichever comes first.
130 131 132 133 134 135
  ///
  /// If the gesture doesn't win the arena, [onTapCancel] is called next.
  /// Otherwise, [onTapUp] is called next.
  ///
  /// See also:
  ///
136 137 138
  ///  * [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.
139
  ///  * [GestureDetector.onTapDown], which exposes this callback.
140
  GestureTapDownCallback onTapDown;
141

142 143
  /// A pointer that will trigger a tap of a primary button has stopped
  /// contacting the screen at a particular location.
144 145 146 147 148 149 150 151
  ///
  /// 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:
  ///
152 153
  ///  * [kPrimaryButton], the button this callback responds to.
  ///  * [onSecondaryTapUp], a similar callback but for a secondary button.
154
  ///  * [TapUpDetails], which is passed as an argument to this callback.
155
  ///  * [GestureDetector.onTapUp], which exposes this callback.
Hixie's avatar
Hixie committed
156
  GestureTapUpCallback onTapUp;
157

158
  /// A tap of a primary button has occurred.
159 160 161 162 163 164 165 166
  ///
  /// 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:
  ///
167 168
  ///  * [kPrimaryButton], the button this callback responds to.
  ///  * [onTapUp], which has the same timing but with details.
169
  ///  * [GestureDetector.onTap], which exposes this callback.
170
  GestureTapCallback onTap;
171 172 173

  /// The pointer that previously triggered [onTapDown] will not end up causing
  /// a tap.
174 175 176 177 178 179 180
  ///
  /// This triggers if the gesture loses the arena.
  ///
  /// If the gesture wins the arena, [onTapUp] and [onTap] are called instead.
  ///
  /// See also:
  ///
181 182
  ///  * [kPrimaryButton], the button this callback responds to.
  ///  * [onSecondaryTapCancel], a similar callback but for a secondary button.
183
  ///  * [GestureDetector.onTapCancel], which exposes this callback.
184
  GestureTapCancelCallback onTapCancel;
185

186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232
  /// 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;

Hixie's avatar
Hixie committed
233
  bool _sentTapDown = false;
234
  bool _wonArenaForPrimaryPointer = false;
235
  OffsetPair _finalPosition;
236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269
  // 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;
  }
270

271
  @override
Ian Hickson's avatar
Ian Hickson committed
272 273
  void handlePrimaryPointer(PointerEvent event) {
    if (event is PointerUpEvent) {
274
      _finalPosition = OffsetPair(global: event.position, local: event.localPosition);
275
      _checkUp();
276
    } else if (event is PointerCancelEvent) {
277 278 279
      resolve(GestureDisposition.rejected);
      if (_sentTapDown) {
        _checkCancel('');
280
      }
281
      _reset();
282 283 284
    } else if (event.buttons != _initialButtons) {
      resolve(GestureDisposition.rejected);
      stopTrackingPointer(primaryPointer);
285 286 287
    }
  }

288
  @override
Ian Hickson's avatar
Ian Hickson committed
289
  void resolve(GestureDisposition disposition) {
290
    if (_wonArenaForPrimaryPointer && disposition == GestureDisposition.rejected) {
291 292 293
      // 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.
294
      assert(_sentTapDown);
295
      _checkCancel('spontaneous ');
Ian Hickson's avatar
Ian Hickson committed
296
      _reset();
297
    }
Ian Hickson's avatar
Ian Hickson committed
298 299 300
    super.resolve(disposition);
  }

301
  @override
302 303
  void didExceedDeadlineWithEvent(PointerDownEvent event) {
    _checkDown(event.pointer);
Hixie's avatar
Hixie committed
304 305
  }

306
  @override
307 308 309
  void acceptGesture(int pointer) {
    super.acceptGesture(pointer);
    if (pointer == primaryPointer) {
310
      _checkDown(pointer);
311
      _wonArenaForPrimaryPointer = true;
Hixie's avatar
Hixie committed
312
      _checkUp();
313 314 315
    }
  }

316
  @override
317 318 319
  void rejectGesture(int pointer) {
    super.rejectGesture(pointer);
    if (pointer == primaryPointer) {
320 321
      // Another gesture won the arena.
      assert(state != GestureRecognizerState.possible);
322 323
      if (_sentTapDown)
        _checkCancel('forced ');
324
      _reset();
325 326 327
    }
  }

328
  void _checkDown(int pointer) {
329 330 331 332
    if (_sentTapDown) {
      return;
    }
    final TapDownDetails details = TapDownDetails(
333 334
      globalPosition: initialPosition.global,
      localPosition: initialPosition.local,
335 336 337 338 339 340 341 342 343 344 345 346 347
      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:
Hixie's avatar
Hixie committed
348
    }
349
    _sentTapDown = true;
Hixie's avatar
Hixie committed
350 351 352
  }

  void _checkUp() {
353 354 355 356
    if (!_wonArenaForPrimaryPointer || _finalPosition == null) {
      return;
    }
    final TapUpDetails details = TapUpDetails(
357 358
      globalPosition: _finalPosition.global,
      localPosition: _finalPosition.local,
359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388
    );
    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:
389 390
    }
  }
391 392

  void _reset() {
Hixie's avatar
Hixie committed
393
    _sentTapDown = false;
394
    _wonArenaForPrimaryPointer = false;
395
    _finalPosition = null;
396
    _initialButtons = null;
397
  }
398

399
  @override
400
  String get debugDescription => 'tap';
401 402

  @override
403 404
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
405
    properties.add(FlagProperty('wonArenaForPrimaryPointer', value: _wonArenaForPrimaryPointer, ifTrue: 'won arena'));
406 407
    properties.add(DiagnosticsProperty<Offset>('finalPosition', _finalPosition?.global, defaultValue: null));
    properties.add(DiagnosticsProperty<Offset>('finalLocalPosition', _finalPosition?.local, defaultValue: _finalPosition?.global));
408
    properties.add(FlagProperty('sentTapDown', value: _sentTapDown, ifTrue: 'sent tap down'));
409
    // TODO(tongmu): Add property _initialButtons and update related tests
410
  }
411
}