gesture_detector.dart 18.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/gestures.dart';
import 'package:flutter/rendering.dart';
7

8 9
import 'basic.dart';
import 'framework.dart';
10

11
export 'package:flutter/gestures.dart' show
12 13
  GestureTapDownCallback,
  GestureTapUpCallback,
14
  GestureTapCallback,
15
  GestureTapCancelCallback,
16
  GestureLongPressCallback,
17
  GestureDragDownCallback,
18 19 20
  GestureDragStartCallback,
  GestureDragUpdateCallback,
  GestureDragEndCallback,
21 22
  GestureDragCancelCallback,
  GesturePanDownCallback,
23 24 25
  GesturePanStartCallback,
  GesturePanUpdateCallback,
  GesturePanEndCallback,
26
  GesturePanCancelCallback,
27 28
  GestureScaleStartCallback,
  GestureScaleUpdateCallback,
29 30
  GestureScaleEndCallback,
  Velocity;
31

32
typedef GestureRecognizer GestureRecognizerFactory(GestureRecognizer recognizer);
33

34 35
/// A widget that detects gestures.
///
36
/// Attempts to recognize gestures that correspond to its non-null callbacks.
37
///
Hixie's avatar
Hixie committed
38 39 40 41
/// GestureDetector also listens for accessibility events and maps
/// them to the callbacks. To ignore accessibility events, set
/// [excludeFromSemantics] to true.
///
42
/// See http://flutter.io/gestures/ for additional information.
43
class GestureDetector extends StatelessWidget {
44
  GestureDetector({
45 46
    Key key,
    this.child,
47
    this.onTapDown,
48 49
    this.onTapUp,
    this.onTap,
50
    this.onTapCancel,
51
    this.onDoubleTap,
52
    this.onLongPress,
53
    this.onVerticalDragDown,
54 55 56
    this.onVerticalDragStart,
    this.onVerticalDragUpdate,
    this.onVerticalDragEnd,
57 58
    this.onVerticalDragCancel,
    this.onHorizontalDragDown,
59 60 61
    this.onHorizontalDragStart,
    this.onHorizontalDragUpdate,
    this.onHorizontalDragEnd,
62 63
    this.onHorizontalDragCancel,
    this.onPanDown,
64 65
    this.onPanStart,
    this.onPanUpdate,
66
    this.onPanEnd,
67
    this.onPanCancel,
68 69
    this.onScaleStart,
    this.onScaleUpdate,
70
    this.onScaleEnd,
Hixie's avatar
Hixie committed
71 72
    this.behavior,
    this.excludeFromSemantics: false
73 74 75 76 77 78 79 80
  }) : super(key: key) {
    assert(excludeFromSemantics != null);
    assert(() {
      bool haveVerticalDrag = onVerticalDragStart != null || onVerticalDragUpdate != null || onVerticalDragEnd != null;
      bool haveHorizontalDrag = onHorizontalDragStart != null || onHorizontalDragUpdate != null || onHorizontalDragEnd != null;
      bool havePan = onPanStart != null || onPanUpdate != null || onPanEnd != null;
      bool haveScale = onScaleStart != null || onScaleUpdate != null || onScaleEnd != null;
      if (havePan || haveScale) {
81
        if (havePan && haveScale) {
82
          throw new FlutterError(
83 84 85 86
            'Incorrect GestureDetector arguments.\n'
            'Having both a pan gesture recognizer and a scale gesture recognizer is redundant; scale is a superset of pan. Just use the scale gesture recognizer.'
          );
        }
87
        String recognizer = havePan ? 'pan' : 'scale';
88
        if (haveVerticalDrag && haveHorizontalDrag) {
89
          throw new FlutterError(
90 91 92 93 94
            'Incorrect GestureDetector arguments.\n'
            'Simultaneously having a vertical drag gesture recognizer, a horizontal drag gesture recognizer, and a $recognizer gesture recognizer '
            'will result in the $recognizer gesture recognizer being ignored, since the other two will catch all drags.'
          );
        }
95 96 97 98
      }
      return true;
    });
  }
99

100
  /// The widget below this widget in the tree.
101
  final Widget child;
102

103 104
  /// A pointer that might cause a tap has contacted the screen at a particular
  /// location.
105
  final GestureTapDownCallback onTapDown;
106 107 108

  /// A pointer that will trigger a tap has stopped contacting the screen at a
  /// particular location.
Hixie's avatar
Hixie committed
109
  final GestureTapUpCallback onTapUp;
110 111

  /// A tap has occurred.
112
  final GestureTapCallback onTap;
113 114 115

  /// The pointer that previously triggered the [onTapDown] will not end up
  /// causing a tap.
116
  final GestureTapCancelCallback onTapCancel;
117 118 119

  /// The user has tapped the screen at the same location twice in quick
  /// succession.
120
  final GestureTapCallback onDoubleTap;
121

122 123
  /// A pointer has remained in contact with the screen at the same location for
  /// a long period of time.
124
  final GestureLongPressCallback onLongPress;
125

126
  /// A pointer has contacted the screen and might begin to move vertically.
127 128 129
  final GestureDragDownCallback onVerticalDragDown;

  /// A pointer has contacted the screen and has begun to move vertically.
130
  final GestureDragStartCallback onVerticalDragStart;
131 132 133

  /// A pointer that is in contact with the screen and moving vertically has
  /// moved in the vertical direction.
134
  final GestureDragUpdateCallback onVerticalDragUpdate;
135 136 137 138

  /// A pointer that was previously in contact with the screen and moving
  /// vertically is no longer in contact with the screen and was moving at a
  /// specific velocity when it stopped contacting the screen.
139 140
  final GestureDragEndCallback onVerticalDragEnd;

141 142 143 144
  /// The pointer that previously triggered the [onVerticalDragDown] did not
  /// end up moving vertically.
  final GestureDragCancelCallback onVerticalDragCancel;

145
  /// A pointer has contacted the screen and might begin to move horizontally.
146 147 148
  final GestureDragDownCallback onHorizontalDragDown;

  /// A pointer has contacted the screen and has begun to move horizontally.
149
  final GestureDragStartCallback onHorizontalDragStart;
150 151 152

  /// A pointer that is in contact with the screen and moving horizontally has
  /// moved in the horizontal direction.
153
  final GestureDragUpdateCallback onHorizontalDragUpdate;
154 155 156 157

  /// A pointer that was previously in contact with the screen and moving
  /// horizontally is no longer in contact with the screen and was moving at a
  /// specific velocity when it stopped contacting the screen.
158 159
  final GestureDragEndCallback onHorizontalDragEnd;

160 161 162 163 164
  /// The pointer that previously triggered the [onHorizontalDragDown] did not
  /// end up moving horizontally.
  final GestureDragCancelCallback onHorizontalDragCancel;

  final GesturePanDownCallback onPanDown;
165 166 167
  final GesturePanStartCallback onPanStart;
  final GesturePanUpdateCallback onPanUpdate;
  final GesturePanEndCallback onPanEnd;
168
  final GesturePanCancelCallback onPanCancel;
169 170 171 172 173

  final GestureScaleStartCallback onScaleStart;
  final GestureScaleUpdateCallback onScaleUpdate;
  final GestureScaleEndCallback onScaleEnd;

174
  /// How this gesture detector should behave during hit testing.
175 176
  final HitTestBehavior behavior;

Hixie's avatar
Hixie committed
177 178 179 180 181 182 183
  /// Whether to exclude these gestures from the semantics tree. For
  /// example, the long-press gesture for showing a tooltip is
  /// excluded because the tooltip itself is included in the semantics
  /// tree directly and so having a gesture to show it would result in
  /// duplication of information.
  final bool excludeFromSemantics;

184
  @override
185 186 187 188
  Widget build(BuildContext context) {
    Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};

    if (onTapDown != null || onTapUp != null || onTap != null || onTapCancel != null) {
189 190
      gestures[TapGestureRecognizer] = (TapGestureRecognizer recognizer) {
        return (recognizer ??= new TapGestureRecognizer())
191 192 193 194 195 196
          ..onTapDown = onTapDown
          ..onTapUp = onTapUp
          ..onTap = onTap
          ..onTapCancel = onTapCancel;
      };
    }
197

198
    if (onDoubleTap != null) {
199 200
      gestures[DoubleTapGestureRecognizer] = (DoubleTapGestureRecognizer recognizer) {
        return (recognizer ??= new DoubleTapGestureRecognizer())
201 202 203
          ..onDoubleTap = onDoubleTap;
      };
    }
204

205
    if (onLongPress != null) {
206 207
      gestures[LongPressGestureRecognizer] = (LongPressGestureRecognizer recognizer) {
        return (recognizer ??= new LongPressGestureRecognizer())
208 209 210
          ..onLongPress = onLongPress;
      };
    }
211

212 213 214 215 216
    if (onVerticalDragDown != null ||
        onVerticalDragStart != null ||
        onVerticalDragUpdate != null ||
        onVerticalDragEnd != null ||
        onVerticalDragCancel != null) {
217 218
      gestures[VerticalDragGestureRecognizer] = (VerticalDragGestureRecognizer recognizer) {
        return (recognizer ??= new VerticalDragGestureRecognizer())
219
          ..onDown = onVerticalDragDown
220 221
          ..onStart = onVerticalDragStart
          ..onUpdate = onVerticalDragUpdate
222 223
          ..onEnd = onVerticalDragEnd
          ..onCancel = onVerticalDragCancel;
224 225
      };
    }
226

227 228 229 230 231
    if (onHorizontalDragDown != null ||
        onHorizontalDragStart != null ||
        onHorizontalDragUpdate != null ||
        onHorizontalDragEnd != null ||
        onHorizontalDragCancel != null) {
232 233
      gestures[HorizontalDragGestureRecognizer] = (HorizontalDragGestureRecognizer recognizer) {
        return (recognizer ??= new HorizontalDragGestureRecognizer())
234
          ..onDown = onHorizontalDragDown
235 236
          ..onStart = onHorizontalDragStart
          ..onUpdate = onHorizontalDragUpdate
237 238
          ..onEnd = onHorizontalDragEnd
          ..onCancel = onHorizontalDragCancel;
239 240
      };
    }
241

242 243 244 245 246
    if (onPanDown != null ||
        onPanStart != null ||
        onPanUpdate != null ||
        onPanEnd != null ||
        onPanCancel != null) {
247 248
      gestures[PanGestureRecognizer] = (PanGestureRecognizer recognizer) {
        return (recognizer ??= new PanGestureRecognizer())
249
          ..onDown = onPanDown
250 251
          ..onStart = onPanStart
          ..onUpdate = onPanUpdate
252 253
          ..onEnd = onPanEnd
          ..onCancel = onPanCancel;
254
      };
255
    }
256

257
    if (onScaleStart != null || onScaleUpdate != null || onScaleEnd != null) {
258 259
      gestures[ScaleGestureRecognizer] = (ScaleGestureRecognizer recognizer) {
        return (recognizer ??= new ScaleGestureRecognizer())
260 261 262 263
          ..onStart = onScaleStart
          ..onUpdate = onScaleUpdate
          ..onEnd = onScaleEnd;
      };
264
    }
265

266 267 268 269 270 271
    return new RawGestureDetector(
      gestures: gestures,
      behavior: behavior,
      excludeFromSemantics: excludeFromSemantics,
      child: child
    );
272
  }
273
}
274

275 276 277 278 279 280
/// A widget that detects gestures described by the given gesture
/// factories.
///
/// For common gestures, use a [GestureRecognizer].
/// RawGestureDetector is useful primarily when developing your
/// own gesture recognizers.
281
class RawGestureDetector extends StatefulWidget {
282 283 284 285 286 287 288 289 290
  RawGestureDetector({
    Key key,
    this.child,
    this.gestures: const <Type, GestureRecognizerFactory>{},
    this.behavior,
    this.excludeFromSemantics: false
  }) : super(key: key) {
    assert(gestures != null);
    assert(excludeFromSemantics != null);
291 292
  }

293
  /// The widget below this widget in the tree.
294 295 296 297 298 299 300 301 302 303 304 305 306 307
  final Widget child;

  final Map<Type, GestureRecognizerFactory> gestures;

  /// How this gesture detector should behave during hit testing.
  final HitTestBehavior behavior;

  /// Whether to exclude these gestures from the semantics tree. For
  /// example, the long-press gesture for showing a tooltip is
  /// excluded because the tooltip itself is included in the semantics
  /// tree directly and so having a gesture to show it would result in
  /// duplication of information.
  final bool excludeFromSemantics;

308
  @override
309 310 311 312 313 314 315
  RawGestureDetectorState createState() => new RawGestureDetectorState();
}

class RawGestureDetectorState extends State<RawGestureDetector> {

  Map<Type, GestureRecognizer> _recognizers = const <Type, GestureRecognizer>{};

316
  @override
317 318 319
  void initState() {
    super.initState();
    _syncAll(config.gestures);
320 321
  }

322
  @override
323 324
  void didUpdateConfig(RawGestureDetector oldConfig) {
    _syncAll(config.gestures);
325 326
  }

327 328 329 330 331 332 333 334 335 336 337
  /// This method can be called after the build phase, during the
  /// layout of the nearest descendant RenderObjectWidget of the
  /// gesture detector, to update the list of active gesture
  /// recognizers.
  ///
  /// The typical use case is [Scrollable]s, which put their viewport
  /// in their gesture detector, and then need to know the dimensions
  /// of the viewport and the viewport's child to determine whether
  /// the gesture detector should be enabled.
  void replaceGestureRecognizers(Map<Type, GestureRecognizerFactory> gestures) {
    assert(() {
338
      if (!RenderObject.debugDoingLayout) {
339
        throw new FlutterError(
340 341 342 343 344 345 346
          'Unexpected call to replaceGestureRecognizers() method of RawGestureDetectorState.\n'
          'The replaceGestureRecognizers() method can only be called during the layout phase. '
          'To set the gesture recognisers at other times, trigger a new build using setState() '
          'and provide the new gesture recognisers as constructor arguments to the corresponding '
          'RawGestureDetector or GestureDetector object.'
        );
      }
347 348 349 350
      return true;
    });
    _syncAll(gestures);
    if (!config.excludeFromSemantics) {
351 352
      RenderSemanticsGestureHandler semanticsGestureHandler = context.findRenderObject();
      context.visitChildElements((RenderObjectElement element) {
353
        element.widget.updateRenderObject(context, semanticsGestureHandler);
354
      });
355 356 357
    }
  }

358
  @override
359 360 361 362 363
  void dispose() {
    for (GestureRecognizer recognizer in _recognizers.values)
      recognizer.dispose();
    _recognizers = null;
    super.dispose();
364 365
  }

366 367 368 369 370 371
  void _syncAll(Map<Type, GestureRecognizerFactory> gestures) {
    assert(_recognizers != null);
    Map<Type, GestureRecognizer> oldRecognizers = _recognizers;
    _recognizers = <Type, GestureRecognizer>{};
    for (Type type in gestures.keys) {
      assert(!_recognizers.containsKey(type));
372
      _recognizers[type] = gestures[type](oldRecognizers[type]);
373 374 375 376 377 378
      assert(_recognizers[type].runtimeType == type);
    }
    for (Type type in oldRecognizers.keys) {
      if (!_recognizers.containsKey(type))
        oldRecognizers[type].dispose();
    }
379 380
  }

Ian Hickson's avatar
Ian Hickson committed
381
  void _handlePointerDown(PointerDownEvent event) {
382 383 384
    assert(_recognizers != null);
    for (GestureRecognizer recognizer in _recognizers.values)
      recognizer.addPointer(event);
385 386
  }

387 388 389 390
  HitTestBehavior get _defaultBehavior {
    return config.child == null ? HitTestBehavior.translucent : HitTestBehavior.deferToChild;
  }

391
  @override
392
  Widget build(BuildContext context) {
Hixie's avatar
Hixie committed
393
    Widget result = new Listener(
394
      onPointerDown: _handlePointerDown,
395
      behavior: config.behavior ?? _defaultBehavior,
396
      child: config.child
397
    );
398 399
    if (!config.excludeFromSemantics)
      result = new _GestureSemantics(owner: this, child: result);
Hixie's avatar
Hixie committed
400
    return result;
401
  }
Hixie's avatar
Hixie committed
402

403
  @override
Hixie's avatar
Hixie committed
404
  void debugFillDescription(List<String> description) {
Ian Hickson's avatar
Ian Hickson committed
405
    super.debugFillDescription(description);
406 407 408
    if (_recognizers == null) {
      description.add('DISPOSED');
    } else {
Ian Hickson's avatar
Ian Hickson committed
409
      List<String> gestures = _recognizers.values.map/*<String>*/((GestureRecognizer recognizer) => recognizer.toStringShort()).toList();
410 411 412 413
      if (gestures.isEmpty)
        gestures.add('<none>');
      description.add('gestures: ${gestures.join(", ")}');
    }
414 415 416 417 418 419 420 421 422 423 424
    switch (config.behavior) {
      case HitTestBehavior.translucent:
        description.add('behavior: translucent');
        break;
      case HitTestBehavior.opaque:
        description.add('behavior: opaque');
        break;
      case HitTestBehavior.deferToChild:
        description.add('behavior: defer-to-child');
        break;
    }
Hixie's avatar
Hixie committed
425
  }
426
}
Hixie's avatar
Hixie committed
427

428
class _GestureSemantics extends SingleChildRenderObjectWidget {
Hixie's avatar
Hixie committed
429 430
  _GestureSemantics({
    Key key,
431 432
    Widget child,
    this.owner
Hixie's avatar
Hixie committed
433 434
  }) : super(key: key, child: child);

435
  final RawGestureDetectorState owner;
Hixie's avatar
Hixie committed
436

437 438 439 440 441 442 443 444 445
  void _handleTap() {
    TapGestureRecognizer recognizer = owner._recognizers[TapGestureRecognizer];
    assert(recognizer != null);
    if (recognizer.onTapDown != null)
      recognizer.onTapDown(Point.origin);
    if (recognizer.onTapUp != null)
      recognizer.onTapUp(Point.origin);
    if (recognizer.onTap != null)
      recognizer.onTap();
Hixie's avatar
Hixie committed
446 447
  }

448 449 450 451 452
  void _handleLongPress() {
    LongPressGestureRecognizer recognizer = owner._recognizers[LongPressGestureRecognizer];
    assert(recognizer != null);
    if (recognizer.onLongPress != null)
      recognizer.onLongPress();
Hixie's avatar
Hixie committed
453 454 455
  }

  void _handleHorizontalDragUpdate(double delta) {
456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478
    {
      HorizontalDragGestureRecognizer recognizer = owner._recognizers[HorizontalDragGestureRecognizer];
      if (recognizer != null) {
        if (recognizer.onStart != null)
          recognizer.onStart(Point.origin);
        if (recognizer.onUpdate != null)
          recognizer.onUpdate(delta);
        if (recognizer.onEnd != null)
          recognizer.onEnd(Velocity.zero);
        return;
      }
    }
    {
      PanGestureRecognizer recognizer = owner._recognizers[PanGestureRecognizer];
      if (recognizer != null) {
        if (recognizer.onStart != null)
          recognizer.onStart(Point.origin);
        if (recognizer.onUpdate != null)
          recognizer.onUpdate(new Offset(delta, 0.0));
        if (recognizer.onEnd != null)
          recognizer.onEnd(Velocity.zero);
        return;
      }
Hixie's avatar
Hixie committed
479
    }
480
    assert(false);
Hixie's avatar
Hixie committed
481 482 483
  }

  void _handleVerticalDragUpdate(double delta) {
484 485 486 487 488 489 490 491 492 493 494
    {
      VerticalDragGestureRecognizer recognizer = owner._recognizers[VerticalDragGestureRecognizer];
      if (recognizer != null) {
        if (recognizer.onStart != null)
          recognizer.onStart(Point.origin);
        if (recognizer.onUpdate != null)
          recognizer.onUpdate(delta);
        if (recognizer.onEnd != null)
          recognizer.onEnd(Velocity.zero);
        return;
      }
Hixie's avatar
Hixie committed
495
    }
496 497 498 499 500 501 502 503 504 505 506 507 508
    {
      PanGestureRecognizer recognizer = owner._recognizers[PanGestureRecognizer];
      if (recognizer != null) {
        if (recognizer.onStart != null)
          recognizer.onStart(Point.origin);
        if (recognizer.onUpdate != null)
          recognizer.onUpdate(new Offset(0.0, delta));
        if (recognizer.onEnd != null)
          recognizer.onEnd(Velocity.zero);
        return;
      }
    }
    assert(false);
Hixie's avatar
Hixie committed
509 510
  }

511
  @override
512
  RenderSemanticsGestureHandler createRenderObject(BuildContext context) {
513
    RenderSemanticsGestureHandler result = new RenderSemanticsGestureHandler();
514
    updateRenderObject(context, result);
515 516
    return result;
  }
Hixie's avatar
Hixie committed
517

518
  @override
519
  void updateRenderObject(BuildContext context, RenderSemanticsGestureHandler renderObject) {
520
    Map<Type, GestureRecognizer> recognizers = owner._recognizers;
521 522 523 524 525 526 527
    renderObject
      ..onTap = recognizers.containsKey(TapGestureRecognizer) ? _handleTap : null
      ..onLongPress = recognizers.containsKey(LongPressGestureRecognizer) ? _handleLongPress : null
      ..onHorizontalDragUpdate = recognizers.containsKey(VerticalDragGestureRecognizer) ||
          recognizers.containsKey(PanGestureRecognizer) ? _handleHorizontalDragUpdate : null
      ..onVerticalDragUpdate = recognizers.containsKey(VerticalDragGestureRecognizer) ||
          recognizers.containsKey(PanGestureRecognizer) ? _handleVerticalDragUpdate : null;
Hixie's avatar
Hixie committed
528 529
  }
}