diff --git a/packages/flutter/lib/gestures.dart b/packages/flutter/lib/gestures.dart index 7e308941fd..356d0d7fa6 100644 --- a/packages/flutter/lib/gestures.dart +++ b/packages/flutter/lib/gestures.dart @@ -28,6 +28,7 @@ export 'src/gestures/pointer_router.dart'; export 'src/gestures/pointer_signal_resolver.dart'; export 'src/gestures/recognizer.dart'; export 'src/gestures/scale.dart'; +export 'src/gestures/semantics.dart'; export 'src/gestures/tap.dart'; export 'src/gestures/team.dart'; export 'src/gestures/velocity_tracker.dart'; diff --git a/packages/flutter/lib/src/gestures/long_press.dart b/packages/flutter/lib/src/gestures/long_press.dart index 0c4b66c726..1bacb2ce73 100644 --- a/packages/flutter/lib/src/gestures/long_press.dart +++ b/packages/flutter/lib/src/gestures/long_press.dart @@ -6,6 +6,7 @@ import 'arena.dart'; import 'constants.dart'; import 'events.dart'; import 'recognizer.dart'; +import 'semantics.dart'; /// Callback signature for [LongPressGestureRecognizer.onLongPress]. /// @@ -214,6 +215,23 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer { /// callback. GestureLongPressEndCallback onLongPressEnd; + @override + SemanticsGestureConfiguration get semanticsConfiguration { + return _semanticsConfiguration ??= SemanticsGestureConfiguration( + onLongPress: () { + if (onLongPressStart != null) + onLongPressStart(const LongPressStartDetails()); + if (onLongPress != null) + onLongPress(); + if (onLongPressEnd != null) + onLongPressEnd(const LongPressEndDetails()); + if (onLongPressUp != null) + onLongPressUp(); + }, + ); + } + SemanticsGestureConfiguration _semanticsConfiguration; + @override bool isPointerAllowed(PointerDownEvent event) { switch (event.buttons) { diff --git a/packages/flutter/lib/src/gestures/monodrag.dart b/packages/flutter/lib/src/gestures/monodrag.dart index af235ea69a..38329ae884 100644 --- a/packages/flutter/lib/src/gestures/monodrag.dart +++ b/packages/flutter/lib/src/gestures/monodrag.dart @@ -10,6 +10,7 @@ import 'constants.dart'; import 'drag_details.dart'; import 'events.dart'; import 'recognizer.dart'; +import 'semantics.dart'; import 'velocity_tracker.dart'; enum _DragState { @@ -456,6 +457,23 @@ class VerticalDragGestureRecognizer extends DragGestureRecognizer { PointerDeviceKind kind, }) : super(debugOwner: debugOwner, kind: kind); + @override + SemanticsGestureConfiguration get semanticsConfiguration { + return _semanticsConfiguration ??= SemanticsGestureConfiguration( + onVerticalDragUpdate: (DragUpdateDetails updateDetails) { + if (onDown != null) + onDown(DragDownDetails()); + if (onStart != null) + onStart(DragStartDetails()); + if (onUpdate != null) + onUpdate(updateDetails); + if (onEnd != null) + onEnd(DragEndDetails(primaryVelocity: 0.0)); + }, + ); + } + SemanticsGestureConfiguration _semanticsConfiguration; + @override bool _isFlingGesture(VelocityEstimate estimate) { final double minVelocity = minFlingVelocity ?? kMinFlingVelocity; @@ -495,6 +513,23 @@ class HorizontalDragGestureRecognizer extends DragGestureRecognizer { PointerDeviceKind kind, }) : super(debugOwner: debugOwner, kind: kind); + @override + SemanticsGestureConfiguration get semanticsConfiguration { + return _semanticsConfiguration ??= SemanticsGestureConfiguration( + onHorizontalDragUpdate: (DragUpdateDetails updateDetails) { + if (onDown != null) + onDown(DragDownDetails()); + if (onStart != null) + onStart(DragStartDetails()); + if (onUpdate != null) + onUpdate(updateDetails); + if (onEnd != null) + onEnd(DragEndDetails(primaryVelocity: 0.0)); + }, + ); + } + SemanticsGestureConfiguration _semanticsConfiguration; + @override bool _isFlingGesture(VelocityEstimate estimate) { final double minVelocity = minFlingVelocity ?? kMinFlingVelocity; @@ -528,6 +563,33 @@ class PanGestureRecognizer extends DragGestureRecognizer { /// Create a gesture recognizer for tracking movement on a plane. PanGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner); + @override + SemanticsGestureConfiguration get semanticsConfiguration { + return _semanticsConfiguration ??= SemanticsGestureConfiguration( + onHorizontalDragUpdate: (DragUpdateDetails updateDetails) { + if (onDown != null) + onDown(DragDownDetails()); + if (onStart != null) + onStart(DragStartDetails()); + if (onUpdate != null) + onUpdate(updateDetails); + if (onEnd != null) + onEnd(DragEndDetails()); + }, + onVerticalDragUpdate: (DragUpdateDetails updateDetails) { + if (onDown != null) + onDown(DragDownDetails()); + if (onStart != null) + onStart(DragStartDetails()); + if (onUpdate != null) + onUpdate(updateDetails); + if (onEnd != null) + onEnd(DragEndDetails()); + }, + ); + } + SemanticsGestureConfiguration _semanticsConfiguration; + @override bool _isFlingGesture(VelocityEstimate estimate) { final double minVelocity = minFlingVelocity ?? kMinFlingVelocity; diff --git a/packages/flutter/lib/src/gestures/recognizer.dart b/packages/flutter/lib/src/gestures/recognizer.dart index 53f9bd6ba5..6492ff7136 100644 --- a/packages/flutter/lib/src/gestures/recognizer.dart +++ b/packages/flutter/lib/src/gestures/recognizer.dart @@ -15,6 +15,7 @@ import 'constants.dart'; import 'debug.dart'; import 'events.dart'; import 'pointer_router.dart'; +import 'semantics.dart'; import 'team.dart'; export 'pointer_router.dart' show PointerRouter; @@ -142,6 +143,21 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT return _pointerToKind[pointer]; } + /// Returns the semantics configuration of this gesture recognizer, for example + /// for accessibility purposes. It is queried by the recognizer's + /// [RawGestureDetector] to build a collective semantics annotation. + /// + /// This method should be overridden by subclasses that are interested in + /// semantic gestures. + /// + /// The returned [SemanticsGestureConfiguration] object should be annotated in + /// a manner that describes the current state, and should remain consistant + /// behaviors across different calls. It is recommended to cache the returned + /// object. + SemanticsGestureConfiguration get semanticsConfiguration { + return null; + } + /// Releases any resources used by the object. /// /// This method is called by the owner of this gesture recognizer diff --git a/packages/flutter/lib/src/gestures/semantics.dart b/packages/flutter/lib/src/gestures/semantics.dart new file mode 100644 index 0000000000..ff63f6269f --- /dev/null +++ b/packages/flutter/lib/src/gestures/semantics.dart @@ -0,0 +1,69 @@ +// Copyright 2019 The Flutter 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 'drag_details.dart'; + +/// Called when the user taps with a semantics device. +typedef SemanticsTapCallback = void Function(); +/// Called when the user presses for a long period of time with a semantics +/// device. +typedef SemanticsLongPressCallback = void Function(); +/// Called when the user drags with a semantics device. +typedef SemanticsDragUpdateCallback = void Function(DragUpdateDetails details); + +/// Describes the semantics configuration of a gesture recognizer, for example +/// for accessibility purposes. It is queried by the recognizer's +/// [RawGestureDetector] to build a collective semantics annotation. +/// +/// When a [RawGestureDetector] receives a semantics gesture, it will invoke +/// the corresponding method that each recognizer reports in the configuration. +/// +/// See also: +/// +/// * [GestureRecognizer.semanticsConfiguration], a method that returns this +/// class. +class SemanticsGestureConfiguration { + /// Initialize the semantics handler configuration by declaring the handlers + /// for each kind of semantics events. + SemanticsGestureConfiguration({ + this.onTap, + this.onLongPress, + this.onHorizontalDragUpdate, + this.onVerticalDragUpdate, + }); + + /// Called when the user taps with a semantics device. + /// + /// See also: + /// + /// * [RenderSemanticsGestureHandler.onTap], which calls this handler with + /// the help of [RawGestureRecognizer]. + final SemanticsTapCallback onTap; + + /// Called when the user presses for a long period of time with a semantics + /// device. + /// + /// See also: + /// + /// * [RenderSemanticsGestureHandler.onLongPress], which calls this handler + /// with the help of [RawGestureRecognizer]. + final SemanticsLongPressCallback onLongPress; + + /// Called when the user scrolls to the left or to the right with a semantics + /// device. + /// + /// See also: + /// + /// * [RenderSemanticsGestureHandler.onHorizontalDragUpdate], which calls + /// this handler with the help of [RawGestureRecognizer]. + final SemanticsDragUpdateCallback onHorizontalDragUpdate; + + /// Called when the user scrolls up or down with a semantics device. + /// + /// See also: + /// + /// * [RenderSemanticsGestureHandler.onVerticalDragUpdate], which calls + /// this handler with the help of [RawGestureRecognizer]. + final SemanticsDragUpdateCallback onVerticalDragUpdate; +} diff --git a/packages/flutter/lib/src/gestures/tap.dart b/packages/flutter/lib/src/gestures/tap.dart index f23a64466d..b4ab40808d 100644 --- a/packages/flutter/lib/src/gestures/tap.dart +++ b/packages/flutter/lib/src/gestures/tap.dart @@ -8,6 +8,7 @@ import 'arena.dart'; import 'constants.dart'; import 'events.dart'; import 'recognizer.dart'; +import 'semantics.dart'; /// Details for [GestureTapDownCallback], such as position /// @@ -237,6 +238,25 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer { // different set of buttons, the gesture is canceled. int _initialButtons; + @override + SemanticsGestureConfiguration get semanticsConfiguration { + // This method must always return a non-null configuration with a non-null + // onTap, even when there are no handlers related to semantics, because the + // handler properties are mutable, and assigning properties will not notify + // RawGestureDetector to update its combined configuration. + return _semanticsConfiguration ??= SemanticsGestureConfiguration( + onTap: () { + if (onTapDown != null) + onTapDown(TapDownDetails()); + if (onTapUp != null) + onTapUp(TapUpDetails()); + if (onTap != null) + onTap(); + }, + ); + } + SemanticsGestureConfiguration _semanticsConfiguration; + @override bool isPointerAllowed(PointerDownEvent event) { switch (event.buttons) { diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index 7a6bf1ed51..a984f4a40c 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -3234,10 +3234,10 @@ class RenderSemanticsGestureHandler extends RenderProxyBox { /// The [scrollFactor] argument must not be null. RenderSemanticsGestureHandler({ RenderBox child, - GestureTapCallback onTap, - GestureLongPressCallback onLongPress, - GestureDragUpdateCallback onHorizontalDragUpdate, - GestureDragUpdateCallback onVerticalDragUpdate, + SemanticsTapCallback onTap, + SemanticsLongPressCallback onLongPress, + SemanticsDragUpdateCallback onHorizontalDragUpdate, + SemanticsDragUpdateCallback onVerticalDragUpdate, this.scrollFactor = 0.8, }) : assert(scrollFactor != null), _onTap = onTap, @@ -3269,9 +3269,9 @@ class RenderSemanticsGestureHandler extends RenderProxyBox { } /// Called when the user taps on the render object. - GestureTapCallback get onTap => _onTap; - GestureTapCallback _onTap; - set onTap(GestureTapCallback value) { + SemanticsTapCallback get onTap => _onTap; + SemanticsTapCallback _onTap; + set onTap(SemanticsTapCallback value) { if (_onTap == value) return; final bool hadHandler = _onTap != null; @@ -3281,9 +3281,9 @@ class RenderSemanticsGestureHandler extends RenderProxyBox { } /// Called when the user presses on the render object for a long period of time. - GestureLongPressCallback get onLongPress => _onLongPress; - GestureLongPressCallback _onLongPress; - set onLongPress(GestureLongPressCallback value) { + SemanticsLongPressCallback get onLongPress => _onLongPress; + SemanticsLongPressCallback _onLongPress; + set onLongPress(SemanticsLongPressCallback value) { if (_onLongPress == value) return; final bool hadHandler = _onLongPress != null; @@ -3293,9 +3293,9 @@ class RenderSemanticsGestureHandler extends RenderProxyBox { } /// Called when the user scrolls to the left or to the right. - GestureDragUpdateCallback get onHorizontalDragUpdate => _onHorizontalDragUpdate; - GestureDragUpdateCallback _onHorizontalDragUpdate; - set onHorizontalDragUpdate(GestureDragUpdateCallback value) { + SemanticsDragUpdateCallback get onHorizontalDragUpdate => _onHorizontalDragUpdate; + SemanticsDragUpdateCallback _onHorizontalDragUpdate; + set onHorizontalDragUpdate(SemanticsDragUpdateCallback value) { if (_onHorizontalDragUpdate == value) return; final bool hadHandler = _onHorizontalDragUpdate != null; @@ -3305,9 +3305,9 @@ class RenderSemanticsGestureHandler extends RenderProxyBox { } /// Called when the user scrolls up or down. - GestureDragUpdateCallback get onVerticalDragUpdate => _onVerticalDragUpdate; - GestureDragUpdateCallback _onVerticalDragUpdate; - set onVerticalDragUpdate(GestureDragUpdateCallback value) { + SemanticsDragUpdateCallback get onVerticalDragUpdate => _onVerticalDragUpdate; + SemanticsDragUpdateCallback _onVerticalDragUpdate; + set onVerticalDragUpdate(SemanticsDragUpdateCallback value) { if (_onVerticalDragUpdate == value) return; final bool hadHandler = _onVerticalDragUpdate != null; diff --git a/packages/flutter/lib/src/widgets/gesture_detector.dart b/packages/flutter/lib/src/widgets/gesture_detector.dart index e916ec3554..4afb68062c 100644 --- a/packages/flutter/lib/src/widgets/gesture_detector.dart +++ b/packages/flutter/lib/src/widgets/gesture_detector.dart @@ -105,6 +105,87 @@ class GestureRecognizerFactoryWithHandlers extends void initializer(T instance) => _initializer(instance); } +// Retrieve semantics handlers from the given recognizers and combine them into +// collective callbacks, one for each kind of semantics gesture. +// +// This class stores the handlers instead of the recognizers, therefore any +// change to the recognzier list needs to be followed by [buildHandlers]. +class _CombinedSemanticsHandlers { + _CombinedSemanticsHandlers({ + Iterable recognizers, + }) { + buildHandlers(recognizers); + } + + final List _tapHandlers = []; + final List _longPressHandlers = + []; + final List _horizontalHandlers = + []; + final List _verticalHandlers = + []; + + // The following getters of handlers return null unless any recognizers + // show interest in the corresponding kind of semantics gestures. + SemanticsTapCallback get onTap { + return _tapHandlers.isEmpty ? null : _handleTap; + } + SemanticsLongPressCallback get onLongPress { + return _longPressHandlers.isEmpty ? null : _handleLongPress; + } + SemanticsDragUpdateCallback get onHorizontalDragUpdate { + return _horizontalHandlers.isEmpty ? null : _handleHorizontalDragUpdate; + } + SemanticsDragUpdateCallback get onVerticalDragUpdate { + return _verticalHandlers.isEmpty ? null : _handleVerticalDragUpdate; + } + + // Clear internal cache of lists of callbacks, and build anew from the given + // recognziers. + void buildHandlers(Iterable recognizers) { + _tapHandlers.clear(); + _longPressHandlers.clear(); + _horizontalHandlers.clear(); + _verticalHandlers.clear(); + if (recognizers == null) + return; + for (GestureRecognizer recognizer in recognizers) { + final SemanticsGestureConfiguration configuration = + recognizer.semanticsConfiguration; + if (configuration == null) + continue; + if (configuration.onTap != null) + _tapHandlers.add(configuration.onTap); + if (configuration.onLongPress != null) + _longPressHandlers.add(configuration.onLongPress); + if (configuration.onHorizontalDragUpdate != null) + _horizontalHandlers.add(configuration.onHorizontalDragUpdate); + if (configuration.onVerticalDragUpdate != null) + _verticalHandlers.add(configuration.onVerticalDragUpdate); + } + } + + void _handleTap() { + for (SemanticsTapCallback callback in _tapHandlers) + callback(); + } + + void _handleLongPress() { + for (SemanticsTapCallback callback in _longPressHandlers) + callback(); + } + + void _handleHorizontalDragUpdate(DragUpdateDetails details) { + for (SemanticsDragUpdateCallback callback in _horizontalHandlers) + callback(details); + } + + void _handleVerticalDragUpdate(DragUpdateDetails details) { + for (SemanticsDragUpdateCallback callback in _verticalHandlers) + callback(details); + } +} + /// A widget that detects gestures. /// /// Attempts to recognize gestures that correspond to its non-null callbacks. @@ -811,6 +892,8 @@ class RawGestureDetector extends StatefulWidget { /// State for a [RawGestureDetector]. class RawGestureDetectorState extends State { Map _recognizers = const {}; + final _CombinedSemanticsHandlers _semanticsHandlers = + _CombinedSemanticsHandlers(); @override void initState() { @@ -912,6 +995,7 @@ class RawGestureDetectorState extends State { if (!_recognizers.containsKey(type)) oldRecognizers[type].dispose(); } + _semanticsHandlers.buildHandlers(_recognizers.values); } void _handlePointerDown(PointerDownEvent event) { @@ -924,92 +1008,6 @@ class RawGestureDetectorState extends State { return widget.child == null ? HitTestBehavior.translucent : HitTestBehavior.deferToChild; } - void _handleSemanticsTap() { - final TapGestureRecognizer recognizer = _recognizers[TapGestureRecognizer]; - assert(recognizer != null); - if (recognizer.onTapDown != null) - recognizer.onTapDown(TapDownDetails()); - if (recognizer.onTapUp != null) - recognizer.onTapUp(TapUpDetails()); - if (recognizer.onTap != null) - recognizer.onTap(); - } - - void _handleSemanticsLongPress() { - final LongPressGestureRecognizer recognizer = _recognizers[LongPressGestureRecognizer]; - assert(recognizer != null); - if (recognizer.onLongPressStart != null) - recognizer.onLongPressStart(const LongPressStartDetails()); - if (recognizer.onLongPress != null) - recognizer.onLongPress(); - if (recognizer.onLongPressEnd != null) - recognizer.onLongPressEnd(const LongPressEndDetails()); - if (recognizer.onLongPressUp != null) - recognizer.onLongPressUp(); - } - - void _handleSemanticsHorizontalDragUpdate(DragUpdateDetails updateDetails) { - { - final HorizontalDragGestureRecognizer recognizer = _recognizers[HorizontalDragGestureRecognizer]; - if (recognizer != null) { - if (recognizer.onDown != null) - recognizer.onDown(DragDownDetails()); - if (recognizer.onStart != null) - recognizer.onStart(DragStartDetails()); - if (recognizer.onUpdate != null) - recognizer.onUpdate(updateDetails); - if (recognizer.onEnd != null) - recognizer.onEnd(DragEndDetails(primaryVelocity: 0.0)); - return; - } - } - { - final PanGestureRecognizer recognizer = _recognizers[PanGestureRecognizer]; - if (recognizer != null) { - if (recognizer.onDown != null) - recognizer.onDown(DragDownDetails()); - if (recognizer.onStart != null) - recognizer.onStart(DragStartDetails()); - if (recognizer.onUpdate != null) - recognizer.onUpdate(updateDetails); - if (recognizer.onEnd != null) - recognizer.onEnd(DragEndDetails()); - return; - } - } - } - - void _handleSemanticsVerticalDragUpdate(DragUpdateDetails updateDetails) { - { - final VerticalDragGestureRecognizer recognizer = _recognizers[VerticalDragGestureRecognizer]; - if (recognizer != null) { - if (recognizer.onDown != null) - recognizer.onDown(DragDownDetails()); - if (recognizer.onStart != null) - recognizer.onStart(DragStartDetails()); - if (recognizer.onUpdate != null) - recognizer.onUpdate(updateDetails); - if (recognizer.onEnd != null) - recognizer.onEnd(DragEndDetails(primaryVelocity: 0.0)); - return; - } - } - { - final PanGestureRecognizer recognizer = _recognizers[PanGestureRecognizer]; - if (recognizer != null) { - if (recognizer.onDown != null) - recognizer.onDown(DragDownDetails()); - if (recognizer.onStart != null) - recognizer.onStart(DragStartDetails()); - if (recognizer.onUpdate != null) - recognizer.onUpdate(updateDetails); - if (recognizer.onEnd != null) - recognizer.onEnd(DragEndDetails()); - return; - } - } - } - @override Widget build(BuildContext context) { Widget result = Listener( @@ -1068,21 +1066,16 @@ class _GestureSemantics extends SingleChildRenderObjectWidget { _updateHandlers(renderObject); } - GestureTapCallback get _onTapHandler { - return owner._recognizers.containsKey(TapGestureRecognizer) ? owner._handleSemanticsTap : null; + SemanticsTapCallback get _onTapHandler { + return owner._semanticsHandlers.onTap; } - - GestureTapCallback get _onLongPressHandler { - return owner._recognizers.containsKey(LongPressGestureRecognizer) ? owner._handleSemanticsLongPress : null; + SemanticsTapCallback get _onLongPressHandler { + return owner._semanticsHandlers.onLongPress; } - - GestureDragUpdateCallback get _onHorizontalDragUpdateHandler { - return owner._recognizers.containsKey(HorizontalDragGestureRecognizer) || - owner._recognizers.containsKey(PanGestureRecognizer) ? owner._handleSemanticsHorizontalDragUpdate : null; + SemanticsDragUpdateCallback get _onHorizontalDragUpdateHandler { + return owner._semanticsHandlers.onHorizontalDragUpdate; } - - GestureDragUpdateCallback get _onVerticalDragUpdateHandler { - return owner._recognizers.containsKey(VerticalDragGestureRecognizer) || - owner._recognizers.containsKey(PanGestureRecognizer) ? owner._handleSemanticsVerticalDragUpdate : null; + SemanticsDragUpdateCallback get _onVerticalDragUpdateHandler { + return owner._semanticsHandlers.onVerticalDragUpdate; } } diff --git a/packages/flutter/test/gestures/drag_test.dart b/packages/flutter/test/gestures/drag_test.dart index 35a8197171..5efc4045d9 100644 --- a/packages/flutter/test/gestures/drag_test.dart +++ b/packages/flutter/test/gestures/drag_test.dart @@ -845,4 +845,120 @@ void main() { tap.dispose(); recognized.clear(); }); + + group('Semantic gesture configuration:', () { + test('HorizontalDragGestureRecognizer\'s semantic handlers are correctly called', () { + final List logs = []; + final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer() + ..onDown = (_) { logs.add('onDown'); } + ..onStart = (_) { logs.add('onStart'); } + ..onUpdate = (_) { logs.add('onUpdate'); } + ..onEnd = (_) { logs.add('onEnd'); }; + final SemanticsGestureConfiguration configuration = drag.semanticsConfiguration; + + expect(configuration, isNotNull); + expect(configuration.onTap, isNull); + expect(configuration.onLongPress, isNull); + expect(configuration.onHorizontalDragUpdate, isNotNull); + expect(configuration.onVerticalDragUpdate, isNull); + + configuration.onHorizontalDragUpdate(DragUpdateDetails( + globalPosition: const Offset(11, 13), + )); + expect(logs, ['onDown', 'onStart', 'onUpdate', 'onEnd']); + logs.clear(); + + // Assigning handler should update the configuration handler's behavior but + // keep the configuration object + drag..onDown = null + ..onStart = null + ..onUpdate = null + ..onEnd = null; + final SemanticsGestureConfiguration configuration2 = drag.semanticsConfiguration; + expect(configuration, same(configuration2)); + configuration.onHorizontalDragUpdate(DragUpdateDetails( + globalPosition: const Offset(12, 14), + )); + expect(logs, []); + }); + + test('VerticalDragGestureRecognizer\'s semantic handlers are correctly called', () { + final List logs = []; + final VerticalDragGestureRecognizer drag = VerticalDragGestureRecognizer() + ..onDown = (_) { logs.add('onDown'); } + ..onStart = (_) { logs.add('onStart'); } + ..onUpdate = (_) { logs.add('onUpdate'); } + ..onEnd = (_) { logs.add('onEnd'); }; + final SemanticsGestureConfiguration configuration = drag.semanticsConfiguration; + + expect(configuration, isNotNull); + expect(configuration.onTap, isNull); + expect(configuration.onLongPress, isNull); + expect(configuration.onHorizontalDragUpdate, isNull); + expect(configuration.onVerticalDragUpdate, isNotNull); + + configuration.onVerticalDragUpdate(DragUpdateDetails( + globalPosition: const Offset(11, 13), + )); + expect(logs, ['onDown', 'onStart', 'onUpdate', 'onEnd']); + logs.clear(); + + // Assigning handler should update the configuration handler's behavior but + // keep the configuration object + drag..onDown = null + ..onStart = null + ..onUpdate = null + ..onEnd = null; + final SemanticsGestureConfiguration configuration2 = drag.semanticsConfiguration; + expect(configuration, same(configuration2)); + configuration.onVerticalDragUpdate(DragUpdateDetails( + globalPosition: const Offset(12, 14), + )); + expect(logs, []); + }); + + test('PanGestureRecognizer\'s semantic handlers are correctly called', () { + final List logs = []; + final PanGestureRecognizer drag = PanGestureRecognizer() + ..onDown = (_) { logs.add('onDown'); } + ..onStart = (_) { logs.add('onStart'); } + ..onUpdate = (_) { logs.add('onUpdate'); } + ..onEnd = (_) { logs.add('onEnd'); }; + final SemanticsGestureConfiguration configuration = drag.semanticsConfiguration; + + expect(configuration, isNotNull); + expect(configuration.onTap, isNull); + expect(configuration.onLongPress, isNull); + expect(configuration.onHorizontalDragUpdate, isNotNull); + expect(configuration.onVerticalDragUpdate, isNotNull); + + configuration.onVerticalDragUpdate(DragUpdateDetails( + globalPosition: const Offset(11, 13), + )); + expect(logs, ['onDown', 'onStart', 'onUpdate', 'onEnd']); + logs.clear(); + + configuration.onHorizontalDragUpdate(DragUpdateDetails( + globalPosition: const Offset(11, 13), + )); + expect(logs, ['onDown', 'onStart', 'onUpdate', 'onEnd']); + logs.clear(); + + // Assigning handler should update the configuration handler's behavior but + // keep the configuration object + drag..onDown = null + ..onStart = null + ..onUpdate = null + ..onEnd = null; + final SemanticsGestureConfiguration configuration2 = drag.semanticsConfiguration; + expect(configuration, same(configuration2)); + configuration.onVerticalDragUpdate(DragUpdateDetails( + globalPosition: const Offset(12, 14), + )); + configuration.onHorizontalDragUpdate(DragUpdateDetails( + globalPosition: const Offset(12, 14), + )); + expect(logs, []); + }); + }); } diff --git a/packages/flutter/test/gestures/long_press_test.dart b/packages/flutter/test/gestures/long_press_test.dart index 746ba6b304..7a6630d4ff 100644 --- a/packages/flutter/test/gestures/long_press_test.dart +++ b/packages/flutter/test/gestures/long_press_test.dart @@ -588,4 +588,35 @@ void main() { longPress.dispose(); recognized.clear(); }); + + test('Semantic onLongPress correctly calls handlers', () { + final List logs = []; + final LongPressGestureRecognizer longPress = LongPressGestureRecognizer() + ..onLongPressStart = (_) { logs.add('onLongPressStart'); } + ..onLongPress = () { logs.add('onLongPress'); } + ..onLongPressEnd = (_) { logs.add('onLongPressEnd'); } + ..onLongPressUp = () { logs.add('onLongPressUp'); }; + final SemanticsGestureConfiguration configuration = longPress.semanticsConfiguration; + + expect(configuration, isNotNull); + expect(configuration.onTap, isNull); + expect(configuration.onLongPress, isNotNull); + expect(configuration.onHorizontalDragUpdate, isNull); + expect(configuration.onVerticalDragUpdate, isNull); + + configuration.onLongPress(); + expect(logs, ['onLongPressStart', 'onLongPress', 'onLongPressEnd', 'onLongPressUp']); + logs.clear(); + + // Assigning handler should update the configuration handler's behavior but + // keep the configuration object + longPress..onLongPressStart = null + ..onLongPress = null + ..onLongPressEnd = null + ..onLongPressUp = null; + final SemanticsGestureConfiguration configuration2 = longPress.semanticsConfiguration; + expect(configuration, same(configuration2)); + configuration.onLongPress(); + expect(logs, []); + }); } diff --git a/packages/flutter/test/gestures/tap_test.dart b/packages/flutter/test/gestures/tap_test.dart index 2d377d6506..50b4f006ea 100644 --- a/packages/flutter/test/gestures/tap_test.dart +++ b/packages/flutter/test/gestures/tap_test.dart @@ -799,4 +799,33 @@ void main() { expect(recognized, ['secondaryCancel']); }); }); + + test('Semantic onTap correctly calls handlers', () { + final List logs = []; + final TapGestureRecognizer tap = TapGestureRecognizer() + ..onTapDown = (_) { logs.add('onTapDown'); } + ..onTapUp = (_) { logs.add('onTapUp'); } + ..onTap = () { logs.add('onTap'); }; + final SemanticsGestureConfiguration configuration = tap.semanticsConfiguration; + + expect(configuration, isNotNull); + expect(configuration.onTap, isNotNull); + expect(configuration.onLongPress, isNull); + expect(configuration.onHorizontalDragUpdate, isNull); + expect(configuration.onVerticalDragUpdate, isNull); + + configuration.onTap(); + expect(logs, ['onTapDown', 'onTapUp', 'onTap']); + logs.clear(); + + // Assigning handler should update the configuration handler's behavior but + // keep the configuration object + tap..onTapDown = null + ..onTapUp = null + ..onTap = null; + final SemanticsGestureConfiguration configuration2 = tap.semanticsConfiguration; + expect(configuration, same(configuration2)); + configuration.onTap(); + expect(logs, []); + }); } diff --git a/packages/flutter/test/widgets/gesture_detector_semantics_test.dart b/packages/flutter/test/widgets/gesture_detector_semantics_test.dart index e616be36e3..7a7550ce33 100644 --- a/packages/flutter/test/widgets/gesture_detector_semantics_test.dart +++ b/packages/flutter/test/widgets/gesture_detector_semantics_test.dart @@ -2,6 +2,7 @@ // 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_test/flutter_test.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; @@ -76,4 +77,124 @@ void main() { semantics.dispose(); }); + + testWidgets('All registered handlers for the gesture kind are called', (WidgetTester tester) async { + final SemanticsTester semantics = SemanticsTester(tester); + + final Set logs = {}; + final GlobalKey detectorKey = GlobalKey(); + + await tester.pumpWidget( + Center( + child: GestureDetector( + key: detectorKey, + onHorizontalDragStart: (_) { logs.add('horizontal'); }, + onPanStart: (_) { logs.add('pan'); }, + child: Container(), + ), + ), + ); + + final int detectorId = detectorKey.currentContext.findRenderObject().debugSemantics.id; + tester.binding.pipelineOwner.semanticsOwner.performAction(detectorId, SemanticsAction.scrollLeft); + expect(logs, {'horizontal', 'pan'}); + + semantics.dispose(); + }); + + testWidgets('Replacing recognizers should update semantic handlers', (WidgetTester tester) async { + final SemanticsTester semantics = SemanticsTester(tester); + + // How the test is set up: + // - Base state: RawGestureDetector with a HorizontalGR + // - Calling `introduceLayoutPerformer()` adds a `TestLayoutPerformer` as + // child of RawGestureDetector. + // - TestLayoutPerformer calls RawGestureDetector.replaceGestureRecognizers + // during layout phase, which replaces the recognizers with a TapGR. + + final Set logs = {}; + final GlobalKey detectorKey = GlobalKey(); + final VoidCallback performLayout = () { + detectorKey.currentState.replaceGestureRecognizers({ + TapGestureRecognizer: GestureRecognizerFactoryWithHandlers( + () => TapGestureRecognizer(), + (TapGestureRecognizer instance) { + instance + ..onTap = () { logs.add('tap'); }; + }, + ) + }); + }; + + bool hasLayoutPerformer = false; + VoidCallback introduceLayoutPerformer; + await tester.pumpWidget( + StatefulBuilder( + builder: (BuildContext context, StateSetter setter) { + introduceLayoutPerformer = () { + setter(() { + hasLayoutPerformer = true; + }); + }; + return Center( + child: RawGestureDetector( + key: detectorKey, + gestures: { + HorizontalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers( + () => HorizontalDragGestureRecognizer(), + (HorizontalDragGestureRecognizer instance) { + instance + ..onStart = (_) { logs.add('horizontal'); }; + }, + ) + }, + child: hasLayoutPerformer ? TestLayoutPerformer(performLayout: performLayout) : null, + ), + ); + }, + ), + ); + + final int detectorId = detectorKey.currentContext.findRenderObject().debugSemantics.id; + tester.binding.pipelineOwner.semanticsOwner.performAction(detectorId, SemanticsAction.scrollLeft); + expect(logs, {'horizontal'}); + logs.clear(); + + introduceLayoutPerformer(); + await tester.pumpAndSettle(); + + tester.binding.pipelineOwner.semanticsOwner.performAction(detectorId, SemanticsAction.scrollLeft); + tester.binding.pipelineOwner.semanticsOwner.performAction(detectorId, SemanticsAction.tap); + expect(logs, {'tap'}); + logs.clear(); + + semantics.dispose(); + }); +} + +class TestLayoutPerformer extends SingleChildRenderObjectWidget { + const TestLayoutPerformer({ + Key key, + this.performLayout, + }) : super(key: key); + + final VoidCallback performLayout; + + @override + RenderTestLayoutPerformer createRenderObject(BuildContext context) { + return RenderTestLayoutPerformer(performLayout: performLayout); + } +} + +class RenderTestLayoutPerformer extends RenderBox { + RenderTestLayoutPerformer({VoidCallback performLayout}) : _performLayout = performLayout; + + VoidCallback _performLayout; + + @override + void performLayout() { + size = const Size(1, 1); + if (_performLayout != null) + _performLayout(); + } }