gesture_detector.dart 19.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 [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
  /// The pointer that previously triggered [onVerticalDragDown] did not
  /// complete.
143 144
  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
  /// The pointer that previously triggered [onHorizontalDragDown] did not
  /// complete.
162 163
  final GestureDragCancelCallback onHorizontalDragCancel;

164
  /// A pointer has contacted the screen and might begin to move.
165
  final GesturePanDownCallback onPanDown;
166 167

  /// A pointer has contacted the screen and has begun to move.
168
  final GesturePanStartCallback onPanStart;
169 170

  /// A pointer that is in contact with the screen and moving has moved again.
171
  final GesturePanUpdateCallback onPanUpdate;
172 173 174 175

  /// A pointer that was previously in contact with the screen and moving
  /// is no longer in contact with the screen and was moving at a specific
  /// velocity when it stopped contacting the screen.
176
  final GesturePanEndCallback onPanEnd;
177 178

  /// The pointer that previously triggered [onPanDown] did not complete.
179
  final GesturePanCancelCallback onPanCancel;
180

181 182
  /// The pointers in contact with the screen have established a focal point and
  /// initial scale of 1.0.
183
  final GestureScaleStartCallback onScaleStart;
184 185 186

  /// The pointers in contact with the screen have indicated a new focal point
  /// and/or scale.
187
  final GestureScaleUpdateCallback onScaleUpdate;
188 189

  /// The pointers are no longer in contact with the screen.
190 191
  final GestureScaleEndCallback onScaleEnd;

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

Hixie's avatar
Hixie committed
195 196 197 198 199 200 201
  /// 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;

202
  @override
203 204 205 206
  Widget build(BuildContext context) {
    Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};

    if (onTapDown != null || onTapUp != null || onTap != null || onTapCancel != null) {
207 208
      gestures[TapGestureRecognizer] = (TapGestureRecognizer recognizer) {
        return (recognizer ??= new TapGestureRecognizer())
209 210 211 212 213 214
          ..onTapDown = onTapDown
          ..onTapUp = onTapUp
          ..onTap = onTap
          ..onTapCancel = onTapCancel;
      };
    }
215

216
    if (onDoubleTap != null) {
217 218
      gestures[DoubleTapGestureRecognizer] = (DoubleTapGestureRecognizer recognizer) {
        return (recognizer ??= new DoubleTapGestureRecognizer())
219 220 221
          ..onDoubleTap = onDoubleTap;
      };
    }
222

223
    if (onLongPress != null) {
224 225
      gestures[LongPressGestureRecognizer] = (LongPressGestureRecognizer recognizer) {
        return (recognizer ??= new LongPressGestureRecognizer())
226 227 228
          ..onLongPress = onLongPress;
      };
    }
229

230 231 232 233 234
    if (onVerticalDragDown != null ||
        onVerticalDragStart != null ||
        onVerticalDragUpdate != null ||
        onVerticalDragEnd != null ||
        onVerticalDragCancel != null) {
235 236
      gestures[VerticalDragGestureRecognizer] = (VerticalDragGestureRecognizer recognizer) {
        return (recognizer ??= new VerticalDragGestureRecognizer())
237
          ..onDown = onVerticalDragDown
238 239
          ..onStart = onVerticalDragStart
          ..onUpdate = onVerticalDragUpdate
240 241
          ..onEnd = onVerticalDragEnd
          ..onCancel = onVerticalDragCancel;
242 243
      };
    }
244

245 246 247 248 249
    if (onHorizontalDragDown != null ||
        onHorizontalDragStart != null ||
        onHorizontalDragUpdate != null ||
        onHorizontalDragEnd != null ||
        onHorizontalDragCancel != null) {
250 251
      gestures[HorizontalDragGestureRecognizer] = (HorizontalDragGestureRecognizer recognizer) {
        return (recognizer ??= new HorizontalDragGestureRecognizer())
252
          ..onDown = onHorizontalDragDown
253 254
          ..onStart = onHorizontalDragStart
          ..onUpdate = onHorizontalDragUpdate
255 256
          ..onEnd = onHorizontalDragEnd
          ..onCancel = onHorizontalDragCancel;
257 258
      };
    }
259

260 261 262 263 264
    if (onPanDown != null ||
        onPanStart != null ||
        onPanUpdate != null ||
        onPanEnd != null ||
        onPanCancel != null) {
265 266
      gestures[PanGestureRecognizer] = (PanGestureRecognizer recognizer) {
        return (recognizer ??= new PanGestureRecognizer())
267
          ..onDown = onPanDown
268 269
          ..onStart = onPanStart
          ..onUpdate = onPanUpdate
270 271
          ..onEnd = onPanEnd
          ..onCancel = onPanCancel;
272
      };
273
    }
274

275
    if (onScaleStart != null || onScaleUpdate != null || onScaleEnd != null) {
276 277
      gestures[ScaleGestureRecognizer] = (ScaleGestureRecognizer recognizer) {
        return (recognizer ??= new ScaleGestureRecognizer())
278 279 280 281
          ..onStart = onScaleStart
          ..onUpdate = onScaleUpdate
          ..onEnd = onScaleEnd;
      };
282
    }
283

284 285 286 287 288 289
    return new RawGestureDetector(
      gestures: gestures,
      behavior: behavior,
      excludeFromSemantics: excludeFromSemantics,
      child: child
    );
290
  }
291
}
292

293 294 295 296 297 298
/// 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.
299
class RawGestureDetector extends StatefulWidget {
300 301 302 303 304 305 306 307 308
  RawGestureDetector({
    Key key,
    this.child,
    this.gestures: const <Type, GestureRecognizerFactory>{},
    this.behavior,
    this.excludeFromSemantics: false
  }) : super(key: key) {
    assert(gestures != null);
    assert(excludeFromSemantics != null);
309 310
  }

311
  /// The widget below this widget in the tree.
312 313 314 315 316 317 318 319 320 321 322 323 324 325
  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;

326
  @override
327 328 329 330 331 332 333
  RawGestureDetectorState createState() => new RawGestureDetectorState();
}

class RawGestureDetectorState extends State<RawGestureDetector> {

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

334
  @override
335 336 337
  void initState() {
    super.initState();
    _syncAll(config.gestures);
338 339
  }

340
  @override
341 342
  void didUpdateConfig(RawGestureDetector oldConfig) {
    _syncAll(config.gestures);
343 344
  }

345 346 347 348 349 350 351 352 353 354 355
  /// 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(() {
356 357 358 359
      // TODO kgiesing This assert will trigger if the owner of the current
      // tree is different from the owner assigned to the renderer instance.
      // Once elements have a notion of owners this assertion can be written
      // more clearly.
360
      if (!RendererBinding.instance.pipelineOwner.debugDoingLayout) {
361
        throw new FlutterError(
362 363 364 365 366 367 368
          '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.'
        );
      }
369 370 371 372
      return true;
    });
    _syncAll(gestures);
    if (!config.excludeFromSemantics) {
373 374
      RenderSemanticsGestureHandler semanticsGestureHandler = context.findRenderObject();
      context.visitChildElements((RenderObjectElement element) {
375
        element.widget.updateRenderObject(context, semanticsGestureHandler);
376
      });
377 378 379
    }
  }

380
  @override
381 382 383 384 385
  void dispose() {
    for (GestureRecognizer recognizer in _recognizers.values)
      recognizer.dispose();
    _recognizers = null;
    super.dispose();
386 387
  }

388 389 390 391 392 393
  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));
394
      _recognizers[type] = gestures[type](oldRecognizers[type]);
395 396 397 398 399 400
      assert(_recognizers[type].runtimeType == type);
    }
    for (Type type in oldRecognizers.keys) {
      if (!_recognizers.containsKey(type))
        oldRecognizers[type].dispose();
    }
401 402
  }

Ian Hickson's avatar
Ian Hickson committed
403
  void _handlePointerDown(PointerDownEvent event) {
404 405 406
    assert(_recognizers != null);
    for (GestureRecognizer recognizer in _recognizers.values)
      recognizer.addPointer(event);
407 408
  }

409 410 411 412
  HitTestBehavior get _defaultBehavior {
    return config.child == null ? HitTestBehavior.translucent : HitTestBehavior.deferToChild;
  }

413
  @override
414
  Widget build(BuildContext context) {
Hixie's avatar
Hixie committed
415
    Widget result = new Listener(
416
      onPointerDown: _handlePointerDown,
417
      behavior: config.behavior ?? _defaultBehavior,
418
      child: config.child
419
    );
420 421
    if (!config.excludeFromSemantics)
      result = new _GestureSemantics(owner: this, child: result);
Hixie's avatar
Hixie committed
422
    return result;
423
  }
Hixie's avatar
Hixie committed
424

425
  @override
Hixie's avatar
Hixie committed
426
  void debugFillDescription(List<String> description) {
Ian Hickson's avatar
Ian Hickson committed
427
    super.debugFillDescription(description);
428 429 430
    if (_recognizers == null) {
      description.add('DISPOSED');
    } else {
Ian Hickson's avatar
Ian Hickson committed
431
      List<String> gestures = _recognizers.values.map/*<String>*/((GestureRecognizer recognizer) => recognizer.toStringShort()).toList();
432 433 434 435
      if (gestures.isEmpty)
        gestures.add('<none>');
      description.add('gestures: ${gestures.join(", ")}');
    }
436 437 438 439 440 441 442 443 444 445 446
    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
447
  }
448
}
Hixie's avatar
Hixie committed
449

450
class _GestureSemantics extends SingleChildRenderObjectWidget {
Hixie's avatar
Hixie committed
451 452
  _GestureSemantics({
    Key key,
453 454
    Widget child,
    this.owner
Hixie's avatar
Hixie committed
455 456
  }) : super(key: key, child: child);

457
  final RawGestureDetectorState owner;
Hixie's avatar
Hixie committed
458

459 460 461 462 463 464 465 466 467
  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
468 469
  }

470 471 472 473 474
  void _handleLongPress() {
    LongPressGestureRecognizer recognizer = owner._recognizers[LongPressGestureRecognizer];
    assert(recognizer != null);
    if (recognizer.onLongPress != null)
      recognizer.onLongPress();
Hixie's avatar
Hixie committed
475 476 477
  }

  void _handleHorizontalDragUpdate(double delta) {
478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500
    {
      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
501
    }
502
    assert(false);
Hixie's avatar
Hixie committed
503 504 505
  }

  void _handleVerticalDragUpdate(double delta) {
506 507 508 509 510 511 512 513 514 515 516
    {
      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
517
    }
518 519 520 521 522 523 524 525 526 527 528 529 530
    {
      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
531 532
  }

533
  @override
534
  RenderSemanticsGestureHandler createRenderObject(BuildContext context) {
535
    RenderSemanticsGestureHandler result = new RenderSemanticsGestureHandler();
536
    updateRenderObject(context, result);
537 538
    return result;
  }
Hixie's avatar
Hixie committed
539

540
  @override
541
  void updateRenderObject(BuildContext context, RenderSemanticsGestureHandler renderObject) {
542
    Map<Type, GestureRecognizer> recognizers = owner._recognizers;
543 544 545 546 547 548 549
    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
550 551
  }
}