tap.dart 13.3 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 24 25
  TapDownDetails({
    this.globalPosition = Offset.zero,
    this.kind,
  }) : assert(globalPosition != null);
26 27

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

  /// The kind of the device that initiated the event.
  final PointerDeviceKind kind;
32 33 34 35 36 37 38
}

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

/// Details for [GestureTapUpCallback], such as position.
47 48 49 50 51
///
/// See also:
///
///  * [GestureDetector.onTapUp], which receives this information.
///  * [TapGestureRecognizer], which passes this information to one of its callbacks.
52 53
class TapUpDetails {
  /// The [globalPosition] argument must not be null.
54
  TapUpDetails({ this.globalPosition = Offset.zero })
55
    : assert(globalPosition != null);
56 57

  /// The global position at which the pointer contacted the screen.
58
  final Offset globalPosition;
59
}
60 61

/// Signature for when a pointer that will trigger a tap has stopped contacting
62 63 64 65
/// the screen.
///
/// The position at which the pointer stopped contacting the screen is available
/// in the `details`.
66 67 68 69 70
///
/// See also:
///
///  * [GestureDetector.onTapUp], which matches this signature.
///  * [TapGestureRecognizer], which uses this signature in one of its callbacks.
71
typedef GestureTapUpCallback = void Function(TapUpDetails details);
72 73

/// Signature for when a tap has occurred.
74 75 76 77 78
///
/// See also:
///
///  * [GestureDetector.onTap], which matches this signature.
///  * [TapGestureRecognizer], which uses this signature in one of its callbacks.
79
typedef GestureTapCallback = void Function();
80 81 82

/// Signature for when the pointer that previously triggered a
/// [GestureTapDownCallback] will not end up causing a tap.
83 84 85 86 87
///
/// See also:
///
///  * [GestureDetector.onTapCancel], which matches this signature.
///  * [TapGestureRecognizer], which uses this signature in one of its callbacks.
88
typedef GestureTapCancelCallback = void Function();
89

90 91
/// Recognizes taps.
///
92 93 94 95
/// Gesture recognizers take part in gesture arenas to enable potential gestures
/// to be disambiguated from each other. This process is managed by a
/// [GestureArenaManager] (q.v.).
///
96 97 98
/// [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
99
/// taps. For example, down-1, down-2, up-1, up-2 produces only one tap on up-1.
100
///
101 102 103 104
/// [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.
105
///
106 107
/// See also:
///
108
///  * [GestureDetector.onTap], which uses this recognizer.
109
///  * [MultiTapGestureRecognizer]
110
class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
111
  /// Creates a tap gesture recognizer.
112
  TapGestureRecognizer({ Object debugOwner }) : super(deadline: kPressTimeout, debugOwner: debugOwner);
113

114 115
  /// A pointer that might cause a tap of a primary button has contacted the
  /// screen at a particular location.
116
  ///
117 118
  /// This triggers once a short timeout ([deadline]) has elapsed, or once
  /// the gestures has won the arena, whichever comes first.
119 120 121 122 123 124
  ///
  /// If the gesture doesn't win the arena, [onTapCancel] is called next.
  /// Otherwise, [onTapUp] is called next.
  ///
  /// See also:
  ///
125 126 127
  ///  * [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.
128
  ///  * [GestureDetector.onTapDown], which exposes this callback.
129
  GestureTapDownCallback onTapDown;
130

131 132
  /// A pointer that will trigger a tap of a primary button has stopped
  /// contacting the screen at a particular location.
133 134 135 136 137 138 139 140
  ///
  /// 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:
  ///
141 142
  ///  * [kPrimaryButton], the button this callback responds to.
  ///  * [onSecondaryTapUp], a similar callback but for a secondary button.
143
  ///  * [TapUpDetails], which is passed as an argument to this callback.
144
  ///  * [GestureDetector.onTapUp], which exposes this callback.
Hixie's avatar
Hixie committed
145
  GestureTapUpCallback onTapUp;
146

147
  /// A tap of a primary button has occurred.
148 149 150 151 152 153 154 155
  ///
  /// 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:
  ///
156 157
  ///  * [kPrimaryButton], the button this callback responds to.
  ///  * [onTapUp], which has the same timing but with details.
158
  ///  * [GestureDetector.onTap], which exposes this callback.
159
  GestureTapCallback onTap;
160 161 162

  /// The pointer that previously triggered [onTapDown] will not end up causing
  /// a tap.
163 164 165 166 167 168 169
  ///
  /// This triggers if the gesture loses the arena.
  ///
  /// If the gesture wins the arena, [onTapUp] and [onTap] are called instead.
  ///
  /// See also:
  ///
170 171
  ///  * [kPrimaryButton], the button this callback responds to.
  ///  * [onSecondaryTapCancel], a similar callback but for a secondary button.
172
  ///  * [GestureDetector.onTapCancel], which exposes this callback.
173
  GestureTapCancelCallback onTapCancel;
174

175 176 177 178 179 180 181 182 183 184 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
  /// 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
222
  bool _sentTapDown = false;
223
  bool _wonArenaForPrimaryPointer = false;
224
  Offset _finalPosition;
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258
  // 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;
  }
259

260
  @override
Ian Hickson's avatar
Ian Hickson committed
261 262
  void handlePrimaryPointer(PointerEvent event) {
    if (event is PointerUpEvent) {
263
      _finalPosition = event.position;
264
      _checkUp();
265
    } else if (event is PointerCancelEvent) {
266 267 268
      resolve(GestureDisposition.rejected);
      if (_sentTapDown) {
        _checkCancel('');
269
      }
270
      _reset();
271 272 273
    } else if (event.buttons != _initialButtons) {
      resolve(GestureDisposition.rejected);
      stopTrackingPointer(primaryPointer);
274 275 276
    }
  }

277
  @override
Ian Hickson's avatar
Ian Hickson committed
278
  void resolve(GestureDisposition disposition) {
279
    if (_wonArenaForPrimaryPointer && disposition == GestureDisposition.rejected) {
280 281 282
      // 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.
283
      assert(_sentTapDown);
284
      _checkCancel('spontaneous ');
Ian Hickson's avatar
Ian Hickson committed
285
      _reset();
286
    }
Ian Hickson's avatar
Ian Hickson committed
287 288 289
    super.resolve(disposition);
  }

290
  @override
291 292
  void didExceedDeadlineWithEvent(PointerDownEvent event) {
    _checkDown(event.pointer);
Hixie's avatar
Hixie committed
293 294
  }

295
  @override
296 297 298
  void acceptGesture(int pointer) {
    super.acceptGesture(pointer);
    if (pointer == primaryPointer) {
299
      _checkDown(pointer);
300
      _wonArenaForPrimaryPointer = true;
Hixie's avatar
Hixie committed
301
      _checkUp();
302 303 304
    }
  }

305
  @override
306 307 308
  void rejectGesture(int pointer) {
    super.rejectGesture(pointer);
    if (pointer == primaryPointer) {
309 310
      // Another gesture won the arena.
      assert(state != GestureRecognizerState.possible);
311 312
      if (_sentTapDown)
        _checkCancel('forced ');
313
      _reset();
314 315 316
    }
  }

317
  void _checkDown(int pointer) {
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335
    if (_sentTapDown) {
      return;
    }
    final TapDownDetails details = TapDownDetails(
      globalPosition: initialPosition,
      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
336
    }
337
    _sentTapDown = true;
Hixie's avatar
Hixie committed
338 339 340
  }

  void _checkUp() {
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375
    if (!_wonArenaForPrimaryPointer || _finalPosition == null) {
      return;
    }
    final TapUpDetails details = TapUpDetails(
      globalPosition: _finalPosition,
    );
    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:
376 377
    }
  }
378 379

  void _reset() {
Hixie's avatar
Hixie committed
380
    _sentTapDown = false;
381
    _wonArenaForPrimaryPointer = false;
382
    _finalPosition = null;
383
    _initialButtons = null;
384
  }
385

386
  @override
387
  String get debugDescription => 'tap';
388 389

  @override
390 391
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
392 393 394
    properties.add(FlagProperty('wonArenaForPrimaryPointer', value: _wonArenaForPrimaryPointer, ifTrue: 'won arena'));
    properties.add(DiagnosticsProperty<Offset>('finalPosition', _finalPosition, defaultValue: null));
    properties.add(FlagProperty('sentTapDown', value: _sentTapDown, ifTrue: 'sent tap down'));
395
    // TODO(tongmu): Add property _initialButtons and update related tests
396
  }
397
}