// 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/gestures.dart'; import 'package:flutter/rendering.dart'; import 'basic.dart'; import 'framework.dart'; export 'package:flutter/gestures.dart' show GestureTapDownCallback, GestureTapUpCallback, GestureTapCallback, GestureTapCancelCallback, GestureLongPressCallback, GestureDragStartCallback, GestureDragUpdateCallback, GestureDragEndCallback, GestureDragStartCallback, GestureDragUpdateCallback, GestureDragEndCallback, GesturePanStartCallback, GesturePanUpdateCallback, GesturePanEndCallback, GestureScaleStartCallback, GestureScaleUpdateCallback, GestureScaleEndCallback; /// A widget that detects gestures. /// /// Attempts to recognize gestures that correspond to its non-null callbacks. /// /// GestureDetector also listens for accessibility events and maps /// them to the callbacks. To ignore accessibility events, set /// [excludeFromSemantics] to true. /// /// See http://flutter.io/gestures/ for additional information. class GestureDetector extends StatefulComponent { const GestureDetector({ Key key, this.child, this.onTapDown, this.onTapUp, this.onTap, this.onTapCancel, this.onDoubleTap, this.onLongPress, this.onVerticalDragStart, this.onVerticalDragUpdate, this.onVerticalDragEnd, this.onHorizontalDragStart, this.onHorizontalDragUpdate, this.onHorizontalDragEnd, this.onPanStart, this.onPanUpdate, this.onPanEnd, this.onScaleStart, this.onScaleUpdate, this.onScaleEnd, this.behavior, this.excludeFromSemantics: false }) : super(key: key); final Widget child; /// A pointer that might cause a tap has contacted the screen at a particular /// location. final GestureTapDownCallback onTapDown; /// A pointer that will trigger a tap has stopped contacting the screen at a /// particular location. final GestureTapUpCallback onTapUp; /// A tap has occurred. final GestureTapCallback onTap; /// The pointer that previously triggered the [onTapDown] will not end up /// causing a tap. final GestureTapCancelCallback onTapCancel; /// The user has tapped the screen at the same location twice in quick /// succession. final GestureTapCallback onDoubleTap; /// A pointer has remained in contact with the screen at the same location for /// a long period of time. final GestureLongPressCallback onLongPress; /// A pointer has contacted the screen and might begin to move vertically. final GestureDragStartCallback onVerticalDragStart; /// A pointer that is in contact with the screen and moving vertically has /// moved in the vertical direction. final GestureDragUpdateCallback onVerticalDragUpdate; /// 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. final GestureDragEndCallback onVerticalDragEnd; /// A pointer has contacted the screen and might begin to move horizontally. final GestureDragStartCallback onHorizontalDragStart; /// A pointer that is in contact with the screen and moving horizontally has /// moved in the horizontal direction. final GestureDragUpdateCallback onHorizontalDragUpdate; /// 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. final GestureDragEndCallback onHorizontalDragEnd; final GesturePanStartCallback onPanStart; final GesturePanUpdateCallback onPanUpdate; final GesturePanEndCallback onPanEnd; final GestureScaleStartCallback onScaleStart; final GestureScaleUpdateCallback onScaleUpdate; final GestureScaleEndCallback onScaleEnd; /// 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; _GestureDetectorState createState() => new _GestureDetectorState(); } class _GestureDetectorState extends State<GestureDetector> { PointerRouter get _router => Gesturer.instance.pointerRouter; TapGestureRecognizer _tap; DoubleTapGestureRecognizer _doubleTap; LongPressGestureRecognizer _longPress; VerticalDragGestureRecognizer _verticalDrag; HorizontalDragGestureRecognizer _horizontalDrag; PanGestureRecognizer _pan; ScaleGestureRecognizer _scale; void initState() { super.initState(); _syncAll(); } void didUpdateConfig(GestureDetector oldConfig) { _syncAll(); } void dispose() { _tap = _ensureDisposed(_tap); _doubleTap = _ensureDisposed(_doubleTap); _longPress = _ensureDisposed(_longPress); _verticalDrag = _ensureDisposed(_verticalDrag); _horizontalDrag = _ensureDisposed(_horizontalDrag); _pan = _ensureDisposed(_pan); _scale = _ensureDisposed(_scale); super.dispose(); } void _syncAll() { _syncTap(); _syncDoubleTap(); _syncLongPress(); _syncVerticalDrag(); _syncHorizontalDrag(); _syncPan(); _syncScale(); } void _syncTap() { if (config.onTapDown == null && config.onTapUp == null && config.onTap == null && config.onTapCancel == null) { _tap = _ensureDisposed(_tap); } else { _tap ??= new TapGestureRecognizer(router: _router, gestureArena: Gesturer.instance.gestureArena); _tap ..onTapDown = config.onTapDown ..onTapUp = config.onTapUp ..onTap = config.onTap ..onTapCancel = config.onTapCancel; } } void _syncDoubleTap() { if (config.onDoubleTap == null) { _doubleTap = _ensureDisposed(_doubleTap); } else { _doubleTap ??= new DoubleTapGestureRecognizer(router: _router, gestureArena: Gesturer.instance.gestureArena); _doubleTap.onDoubleTap = config.onDoubleTap; } } void _syncLongPress() { if (config.onLongPress == null) { _longPress = _ensureDisposed(_longPress); } else { _longPress ??= new LongPressGestureRecognizer(router: _router, gestureArena: Gesturer.instance.gestureArena); _longPress.onLongPress = config.onLongPress; } } void _syncVerticalDrag() { if (config.onVerticalDragStart == null && config.onVerticalDragUpdate == null && config.onVerticalDragEnd == null) { _verticalDrag = _ensureDisposed(_verticalDrag); } else { _verticalDrag ??= new VerticalDragGestureRecognizer(router: _router, gestureArena: Gesturer.instance.gestureArena); _verticalDrag ..onStart = config.onVerticalDragStart ..onUpdate = config.onVerticalDragUpdate ..onEnd = config.onVerticalDragEnd; } } void _syncHorizontalDrag() { if (config.onHorizontalDragStart == null && config.onHorizontalDragUpdate == null && config.onHorizontalDragEnd == null) { _horizontalDrag = _ensureDisposed(_horizontalDrag); } else { _horizontalDrag ??= new HorizontalDragGestureRecognizer(router: _router, gestureArena: Gesturer.instance.gestureArena); _horizontalDrag ..onStart = config.onHorizontalDragStart ..onUpdate = config.onHorizontalDragUpdate ..onEnd = config.onHorizontalDragEnd; } } void _syncPan() { if (config.onPanStart == null && config.onPanUpdate == null && config.onPanEnd == null) { _pan = _ensureDisposed(_pan); } else { assert(_scale == null); // Scale is a superset of pan; just use scale _pan ??= new PanGestureRecognizer(router: _router, gestureArena: Gesturer.instance.gestureArena); _pan ..onStart = config.onPanStart ..onUpdate = config.onPanUpdate ..onEnd = config.onPanEnd; } } void _syncScale() { if (config.onScaleStart == null && config.onScaleUpdate == null && config.onScaleEnd == null) { _scale = _ensureDisposed(_scale); } else { assert(_pan == null); // Scale is a superset of pan; just use scale _scale ??= new ScaleGestureRecognizer(router: _router, gestureArena: Gesturer.instance.gestureArena); _scale ..onStart = config.onScaleStart ..onUpdate = config.onScaleUpdate ..onEnd = config.onScaleEnd; } } GestureRecognizer _ensureDisposed(GestureRecognizer recognizer) { recognizer?.dispose(); return null; } void _handlePointerDown(PointerDownEvent event) { if (_tap != null) _tap.addPointer(event); if (_doubleTap != null) _doubleTap.addPointer(event); if (_longPress != null) _longPress.addPointer(event); if (_verticalDrag != null) _verticalDrag.addPointer(event); if (_horizontalDrag != null) _horizontalDrag.addPointer(event); if (_pan != null) _pan.addPointer(event); if (_scale != null) _scale.addPointer(event); } HitTestBehavior get _defaultBehavior { return config.child == null ? HitTestBehavior.translucent : HitTestBehavior.deferToChild; } Widget build(BuildContext context) { Widget result = new Listener( onPointerDown: _handlePointerDown, behavior: config.behavior ?? _defaultBehavior, child: config.child ); if (!config.excludeFromSemantics) { result = new _GestureSemantics( onTapDown: config.onTapDown, onTapUp: config.onTapUp, onTap: config.onTap, onLongPress: config.onLongPress, onVerticalDragStart: config.onVerticalDragStart, onVerticalDragUpdate: config.onVerticalDragUpdate, onVerticalDragEnd: config.onVerticalDragEnd, onHorizontalDragStart: config.onHorizontalDragStart, onHorizontalDragUpdate: config.onHorizontalDragUpdate, onHorizontalDragEnd: config.onHorizontalDragEnd, onPanStart: config.onPanStart, onPanUpdate: config.onPanUpdate, onPanEnd: config.onPanEnd, child: result ); } return result; } void debugFillDescription(List<String> description) { super.debugFillDescription(description); List<String> gestures = <String>[]; if (_tap != null) gestures.add('tap'); if (_doubleTap != null) gestures.add('double tap'); if (_longPress != null) gestures.add('long press'); if (_verticalDrag != null) gestures.add('vertical drag'); if (_horizontalDrag != null) gestures.add('horizontal drag'); if (_pan != null) gestures.add('pan'); if (_scale != null) gestures.add('scale'); if (gestures.isEmpty) gestures.add('<none>'); description.add('gestures: ${gestures.join(", ")}'); 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; } } } class _GestureSemantics extends OneChildRenderObjectWidget { _GestureSemantics({ Key key, this.onTapDown, this.onTapUp, this.onTap, this.onLongPress, this.onVerticalDragStart, this.onVerticalDragUpdate, this.onVerticalDragEnd, this.onHorizontalDragStart, this.onHorizontalDragUpdate, this.onHorizontalDragEnd, this.onPanStart, this.onPanUpdate, this.onPanEnd, Widget child }) : super(key: key, child: child); final GestureTapDownCallback onTapDown; final GestureTapUpCallback onTapUp; final GestureTapCallback onTap; final GestureLongPressCallback onLongPress; final GestureDragStartCallback onVerticalDragStart; final GestureDragUpdateCallback onVerticalDragUpdate; final GestureDragEndCallback onVerticalDragEnd; final GestureDragStartCallback onHorizontalDragStart; final GestureDragUpdateCallback onHorizontalDragUpdate; final GestureDragEndCallback onHorizontalDragEnd; final GesturePanStartCallback onPanStart; final GesturePanUpdateCallback onPanUpdate; final GesturePanEndCallback onPanEnd; bool get _watchingTaps { return onTapDown != null || onTapUp != null || onTap != null; } bool get _watchingHorizontalDrags { return onHorizontalDragStart != null || onHorizontalDragUpdate != null || onHorizontalDragEnd != null; } bool get _watchingVerticalDrags { return onVerticalDragStart != null || onVerticalDragUpdate != null || onVerticalDragEnd != null; } bool get _watchingPans { return onPanStart != null || onPanUpdate != null || onPanEnd != null; } void _handleTap() { if (onTapDown != null) onTapDown(Point.origin); if (onTapUp != null) onTapUp(Point.origin); if (onTap != null) onTap(); } void _handleHorizontalDragUpdate(double delta) { if (_watchingHorizontalDrags) { if (onHorizontalDragStart != null) onHorizontalDragStart(Point.origin); if (onHorizontalDragUpdate != null) onHorizontalDragUpdate(delta); if (onHorizontalDragEnd != null) onHorizontalDragEnd(Offset.zero); } else { assert(_watchingPans); if (onPanStart != null) onPanStart(Point.origin); if (onPanUpdate != null) onPanUpdate(new Offset(delta, 0.0)); if (onPanEnd != null) onPanEnd(Offset.zero); } } void _handleVerticalDragUpdate(double delta) { if (_watchingVerticalDrags) { if (onVerticalDragStart != null) onVerticalDragStart(Point.origin); if (onVerticalDragUpdate != null) onVerticalDragUpdate(delta); if (onVerticalDragEnd != null) onVerticalDragEnd(Offset.zero); } else { assert(_watchingPans); if (onPanStart != null) onPanStart(Point.origin); if (onPanUpdate != null) onPanUpdate(new Offset(0.0, delta)); if (onPanEnd != null) onPanEnd(Offset.zero); } } RenderSemanticsGestureHandler createRenderObject() => new RenderSemanticsGestureHandler( onTap: _watchingTaps ? _handleTap : null, onLongPress: onLongPress, onHorizontalDragUpdate: _watchingHorizontalDrags || _watchingPans ? _handleHorizontalDragUpdate : null, onVerticalDragUpdate: _watchingVerticalDrags || _watchingPans ? _handleVerticalDragUpdate : null ); void updateRenderObject(RenderSemanticsGestureHandler renderObject, _GestureSemantics oldWidget) { renderObject.onTap = _watchingTaps ? _handleTap : null; renderObject.onLongPress = onLongPress; renderObject.onHorizontalDragUpdate = _watchingHorizontalDrags || _watchingPans ? _handleHorizontalDragUpdate : null; renderObject.onVerticalDragUpdate = _watchingVerticalDrags || _watchingPans ? _handleVerticalDragUpdate : null; } }