New parameter for RawGestureDetector to customize semantics mapping (#33936)
This PR adds a new parameter to RawGestureDetector, `semantics`, which configures how detectors handle semantics gestures. It has a default delegate that keep the current behavior.
diff --git a/packages/flutter/lib/src/widgets/gesture_detector.dart b/packages/flutter/lib/src/widgets/gesture_detector.dart
index e916ec3..3de687a 100644
--- a/packages/flutter/lib/src/widgets/gesture_detector.dart
+++ b/packages/flutter/lib/src/widgets/gesture_detector.dart
@@ -45,6 +45,7 @@
TapUpDetails,
ForcePressDetails,
Velocity;
+export 'package:flutter/rendering.dart' show RenderSemanticsGestureHandler;
// Examples can assume:
// bool _lights;
@@ -764,15 +765,16 @@
class RawGestureDetector extends StatefulWidget {
/// Creates a widget that detects gestures.
///
- /// By default, gesture detectors contribute semantic information to the tree
- /// that is used by assistive technology. This can be controlled using
- /// [excludeFromSemantics].
+ /// Gesture detectors can contribute semantic information to the tree that is
+ /// used by assistive technology. The behavior can be configured by
+ /// [semantics], or disabled with [excludeFromSemantics].
const RawGestureDetector({
Key key,
this.child,
this.gestures = const <Type, GestureRecognizerFactory>{},
this.behavior,
this.excludeFromSemantics = false,
+ this.semantics,
}) : assert(gestures != null),
assert(excludeFromSemantics != null),
super(key: key);
@@ -804,6 +806,72 @@
/// duplication of information.
final bool excludeFromSemantics;
+ /// Describes the semantics notations that should be added to the underlying
+ /// render object [RenderSemanticsGestureHandler].
+ ///
+ /// It has no effect if [excludeFromSemantics] is true.
+ ///
+ /// When [semantics] is null, [RawGestureDetector] will fall back to a
+ /// default delegate which checks if the detector owns certain gesture
+ /// recognizers and calls their callbacks if they exist:
+ ///
+ /// * During a semantic tap, it calls [TapGestureRecognizer]'s
+ /// `onTapDown`, `onTapUp`, and `onTap`.
+ /// * During a semantic long press, it calls [LongPressGestureRecognizer]'s
+ /// `onLongPressStart`, `onLongPress`, `onLongPressEnd` and `onLongPressUp`.
+ /// * During a semantic horizontal drag, it calls [HorizontalDragGestureRecognizer]'s
+ /// `onDown`, `onStart`, `onUpdate` and `onEnd`, then
+ /// [PanGestureRecognizer]'s `onDown`, `onStart`, `onUpdate` and `onEnd`.
+ /// * During a semantic vertical drag, it calls [VerticalDragGestureRecognizer]'s
+ /// `onDown`, `onStart`, `onUpdate` and `onEnd`, then
+ /// [PanGestureRecognizer]'s `onDown`, `onStart`, `onUpdate` and `onEnd`.
+ ///
+ /// {@tool sample}
+ /// This custom gesture detector listens to force presses, while also allows
+ /// the same callback to be triggered by semantic long presses.
+ ///
+ /// ```dart
+ /// class ForcePressGestureDetectorWithSemantics extends StatelessWidget {
+ /// const ForcePressGestureDetectorWithSemantics({
+ /// this.child,
+ /// this.onForcePress,
+ /// });
+ ///
+ /// final Widget child;
+ /// final VoidCallback onForcePress;
+ ///
+ /// @override
+ /// Widget build(BuildContext context) {
+ /// return RawGestureDetector(
+ /// gestures: <Type, GestureRecognizerFactory>{
+ /// ForcePressGestureRecognizer: GestureRecognizerFactoryWithHandlers<ForcePressGestureRecognizer>(
+ /// () => ForcePressGestureRecognizer(debugOwner: this),
+ /// (ForcePressGestureRecognizer instance) {
+ /// instance.onStart = (_) => onForcePress();
+ /// }
+ /// ),
+ /// },
+ /// behavior: HitTestBehavior.opaque,
+ /// semantics: _LongPressSemanticsDelegate(onForcePress),
+ /// child: child,
+ /// );
+ /// }
+ /// }
+ ///
+ /// class _LongPressSemanticsDelegate extends SemanticsGestureDelegate {
+ /// _LongPressSemanticsDelegate(this.onLongPress);
+ ///
+ /// VoidCallback onLongPress;
+ ///
+ /// @override
+ /// void assignSemantics(RenderSemanticsGestureHandler renderObject) {
+ /// renderObject.onLongPress = onLongPress;
+ /// }
+ /// }
+ /// ```
+ /// {@end-tool}
+ final SemanticsGestureDelegate semantics;
+
@override
RawGestureDetectorState createState() => RawGestureDetectorState();
}
@@ -811,16 +879,21 @@
/// State for a [RawGestureDetector].
class RawGestureDetectorState extends State<RawGestureDetector> {
Map<Type, GestureRecognizer> _recognizers = const <Type, GestureRecognizer>{};
+ SemanticsGestureDelegate _semantics;
@override
void initState() {
super.initState();
+ _semantics = widget.semantics ?? _DefaultSemanticsGestureDelegate(this);
_syncAll(widget.gestures);
}
@override
void didUpdateWidget(RawGestureDetector oldWidget) {
super.didUpdateWidget(oldWidget);
+ if (!(oldWidget.semantics == null && widget.semantics == null)) {
+ _semantics = widget.semantics ?? _DefaultSemanticsGestureDelegate(this);
+ }
_syncAll(widget.gestures);
}
@@ -853,10 +926,7 @@
_syncAll(gestures);
if (!widget.excludeFromSemantics) {
final RenderSemanticsGestureHandler semanticsGestureHandler = context.findRenderObject();
- context.visitChildElements((Element element) {
- final _GestureSemantics widget = element.widget;
- widget._updateHandlers(semanticsGestureHandler);
- });
+ _updateSemanticsForRenderObject(semanticsGestureHandler);
}
}
@@ -924,90 +994,10 @@
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;
- }
- }
+ void _updateSemanticsForRenderObject(RenderSemanticsGestureHandler renderObject) {
+ assert(!widget.excludeFromSemantics);
+ assert(_semantics != null);
+ _semantics.assignSemantics(renderObject);
}
@override
@@ -1018,7 +1008,10 @@
child: widget.child,
);
if (!widget.excludeFromSemantics)
- result = _GestureSemantics(owner: this, child: result);
+ result = _GestureSemantics(
+ child: result,
+ assignSemantics: _updateSemanticsForRenderObject,
+ );
return result;
}
@@ -1031,58 +1024,202 @@
final List<String> gestures = _recognizers.values.map<String>((GestureRecognizer recognizer) => recognizer.debugDescription).toList();
properties.add(IterableProperty<String>('gestures', gestures, ifEmpty: '<none>'));
properties.add(IterableProperty<GestureRecognizer>('recognizers', _recognizers.values, level: DiagnosticLevel.fine));
+ properties.add(DiagnosticsProperty<bool>('excludeFromSemantics', widget.excludeFromSemantics, defaultValue: false));
+ if (!widget.excludeFromSemantics) {
+ properties.add(DiagnosticsProperty<SemanticsGestureDelegate>('semantics', widget.semantics, defaultValue: null));
+ }
}
properties.add(EnumProperty<HitTestBehavior>('behavior', widget.behavior, defaultValue: null));
}
}
+typedef _AssignSemantics = void Function(RenderSemanticsGestureHandler);
+
class _GestureSemantics extends SingleChildRenderObjectWidget {
const _GestureSemantics({
Key key,
Widget child,
- this.owner,
- }) : super(key: key, child: child);
+ @required this.assignSemantics,
+ }) : assert(assignSemantics != null),
+ super(key: key, child: child);
- final RawGestureDetectorState owner;
+ final _AssignSemantics assignSemantics;
@override
RenderSemanticsGestureHandler createRenderObject(BuildContext context) {
- return RenderSemanticsGestureHandler(
- onTap: _onTapHandler,
- onLongPress: _onLongPressHandler,
- onHorizontalDragUpdate: _onHorizontalDragUpdateHandler,
- onVerticalDragUpdate: _onVerticalDragUpdateHandler,
- );
- }
-
- void _updateHandlers(RenderSemanticsGestureHandler renderObject) {
- renderObject
- ..onTap = _onTapHandler
- ..onLongPress = _onLongPressHandler
- ..onHorizontalDragUpdate = _onHorizontalDragUpdateHandler
- ..onVerticalDragUpdate = _onVerticalDragUpdateHandler;
+ final RenderSemanticsGestureHandler renderObject = RenderSemanticsGestureHandler();
+ assignSemantics(renderObject);
+ return renderObject;
}
@override
void updateRenderObject(BuildContext context, RenderSemanticsGestureHandler renderObject) {
- _updateHandlers(renderObject);
+ assignSemantics(renderObject);
+ }
+}
+
+/// A base class that describes what semantics notations a [RawGestureDetector]
+/// should add to the render object [RenderSemanticsGestureHandler].
+///
+/// It is used to allow custom [GestureDetector]s to add semantics notations.
+abstract class SemanticsGestureDelegate {
+ /// Create a delegate of gesture semantics.
+ const SemanticsGestureDelegate();
+
+ /// Assigns semantics notations to the [RenderSemanticsGestureHandler] render
+ /// object of the gesture detector.
+ ///
+ /// This method is called when the widget is created, updated, or during
+ /// [RawGestureDetector.replaceGestureRecognizers].
+ void assignSemantics(RenderSemanticsGestureHandler renderObject);
+
+ @override
+ String toString() => '$runtimeType()';
+}
+
+// The default semantics delegate of [RawGestureDetector]. Its behavior is
+// described in [RawGestureDetector.semantics].
+//
+// For readers who come here to learn how to write custom semantics delegates:
+// this is not a proper sample code. It has access to the detector state as well
+// as its private properties, which are inaccessible normally. It is designed
+// this way in order to work independenly in a [RawGestureRecognizer] to
+// preserve existing behavior.
+//
+// Instead, a normal delegate will store callbacks as properties, and use them
+// in `assignSemantics`.
+class _DefaultSemanticsGestureDelegate extends SemanticsGestureDelegate {
+ _DefaultSemanticsGestureDelegate(this.detectorState);
+
+ final RawGestureDetectorState detectorState;
+
+ @override
+ void assignSemantics(RenderSemanticsGestureHandler renderObject) {
+ assert(!detectorState.widget.excludeFromSemantics);
+ final Map<Type, GestureRecognizer> recognizers = detectorState._recognizers;
+ renderObject
+ ..onTap = _getTapHandler(recognizers)
+ ..onLongPress = _getLongPressHandler(recognizers)
+ ..onHorizontalDragUpdate = _getHorizontalDragUpdateHandler(recognizers)
+ ..onVerticalDragUpdate = _getVerticalDragUpdateHandler(recognizers);
}
- GestureTapCallback get _onTapHandler {
- return owner._recognizers.containsKey(TapGestureRecognizer) ? owner._handleSemanticsTap : null;
+ GestureTapCallback _getTapHandler(Map<Type, GestureRecognizer> recognizers) {
+ final TapGestureRecognizer tap = recognizers[TapGestureRecognizer];
+ if (tap == null)
+ return null;
+ assert(tap is TapGestureRecognizer);
+
+ return () {
+ assert(tap != null);
+ if (tap.onTapDown != null)
+ tap.onTapDown(TapDownDetails());
+ if (tap.onTapUp != null)
+ tap.onTapUp(TapUpDetails());
+ if (tap.onTap != null)
+ tap.onTap();
+ };
}
- GestureTapCallback get _onLongPressHandler {
- return owner._recognizers.containsKey(LongPressGestureRecognizer) ? owner._handleSemanticsLongPress : null;
+ GestureLongPressCallback _getLongPressHandler(Map<Type, GestureRecognizer> recognizers) {
+ final LongPressGestureRecognizer longPress = recognizers[LongPressGestureRecognizer];
+ if (longPress == null)
+ return null;
+
+ return () {
+ assert(longPress is LongPressGestureRecognizer);
+ if (longPress.onLongPressStart != null)
+ longPress.onLongPressStart(const LongPressStartDetails());
+ if (longPress.onLongPress != null)
+ longPress.onLongPress();
+ if (longPress.onLongPressEnd != null)
+ longPress.onLongPressEnd(const LongPressEndDetails());
+ if (longPress.onLongPressUp != null)
+ longPress.onLongPressUp();
+ };
}
- GestureDragUpdateCallback get _onHorizontalDragUpdateHandler {
- return owner._recognizers.containsKey(HorizontalDragGestureRecognizer) ||
- owner._recognizers.containsKey(PanGestureRecognizer) ? owner._handleSemanticsHorizontalDragUpdate : null;
+ GestureDragUpdateCallback _getHorizontalDragUpdateHandler(Map<Type, GestureRecognizer> recognizers) {
+ final HorizontalDragGestureRecognizer horizontal = recognizers[HorizontalDragGestureRecognizer];
+ final PanGestureRecognizer pan = recognizers[PanGestureRecognizer];
+
+ final GestureDragUpdateCallback horizontalHandler = horizontal == null ?
+ null :
+ (DragUpdateDetails details) {
+ assert(horizontal is HorizontalDragGestureRecognizer);
+ if (horizontal.onDown != null)
+ horizontal.onDown(DragDownDetails());
+ if (horizontal.onStart != null)
+ horizontal.onStart(DragStartDetails());
+ if (horizontal.onUpdate != null)
+ horizontal.onUpdate(details);
+ if (horizontal.onEnd != null)
+ horizontal.onEnd(DragEndDetails(primaryVelocity: 0.0));
+ };
+
+ final GestureDragUpdateCallback panHandler = pan == null ?
+ null :
+ (DragUpdateDetails details) {
+ assert(pan is PanGestureRecognizer);
+ if (pan.onDown != null)
+ pan.onDown(DragDownDetails());
+ if (pan.onStart != null)
+ pan.onStart(DragStartDetails());
+ if (pan.onUpdate != null)
+ pan.onUpdate(details);
+ if (pan.onEnd != null)
+ pan.onEnd(DragEndDetails());
+ };
+
+ if (horizontalHandler == null && panHandler == null)
+ return null;
+ return (DragUpdateDetails details) {
+ if (horizontalHandler != null)
+ horizontalHandler(details);
+ if (panHandler != null)
+ panHandler(details);
+ };
}
- GestureDragUpdateCallback get _onVerticalDragUpdateHandler {
- return owner._recognizers.containsKey(VerticalDragGestureRecognizer) ||
- owner._recognizers.containsKey(PanGestureRecognizer) ? owner._handleSemanticsVerticalDragUpdate : null;
+ GestureDragUpdateCallback _getVerticalDragUpdateHandler(Map<Type, GestureRecognizer> recognizers) {
+ final VerticalDragGestureRecognizer vertical = recognizers[VerticalDragGestureRecognizer];
+ final PanGestureRecognizer pan = recognizers[PanGestureRecognizer];
+
+ final GestureDragUpdateCallback verticalHandler = vertical == null ?
+ null :
+ (DragUpdateDetails details) {
+ assert(vertical is VerticalDragGestureRecognizer);
+ if (vertical.onDown != null)
+ vertical.onDown(DragDownDetails());
+ if (vertical.onStart != null)
+ vertical.onStart(DragStartDetails());
+ if (vertical.onUpdate != null)
+ vertical.onUpdate(details);
+ if (vertical.onEnd != null)
+ vertical.onEnd(DragEndDetails(primaryVelocity: 0.0));
+ };
+
+ final GestureDragUpdateCallback panHandler = pan == null ?
+ null :
+ (DragUpdateDetails details) {
+ assert(pan is PanGestureRecognizer);
+ if (pan.onDown != null)
+ pan.onDown(DragDownDetails());
+ if (pan.onStart != null)
+ pan.onStart(DragStartDetails());
+ if (pan.onUpdate != null)
+ pan.onUpdate(details);
+ if (pan.onEnd != null)
+ pan.onEnd(DragEndDetails());
+ };
+
+ if (verticalHandler == null && panHandler == null)
+ return null;
+ return (DragUpdateDetails details) {
+ if (verticalHandler != null)
+ verticalHandler(details);
+ if (panHandler != null)
+ panHandler(details);
+ };
}
}
diff --git a/packages/flutter/test/widgets/gesture_detector_semantics_test.dart b/packages/flutter/test/widgets/gesture_detector_semantics_test.dart
index e616be3..dc9796f 100644
--- a/packages/flutter/test/widgets/gesture_detector_semantics_test.dart
+++ b/packages/flutter/test/widgets/gesture_detector_semantics_test.dart
@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
@@ -76,4 +77,686 @@
semantics.dispose();
});
+
+ testWidgets('All registered handlers for the gesture kind are called', (WidgetTester tester) async {
+ final SemanticsTester semantics = SemanticsTester(tester);
+
+ final Set<String> logs = <String>{};
+ 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, <String>{'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:
+ //
+ // * In the base state, RawGestureDetector's recognizer is a HorizontalGR
+ // * Calling `introduceLayoutPerformer()` adds a `_TestLayoutPerformer` as
+ // child of RawGestureDetector, which invokes a given callback during
+ // layout phase.
+ // * The aforementioned callback replaces the detector's recognizer with a
+ // TapGR.
+ // * This test makes sure the replacement correctly updates semantics.
+
+ final Set<String> logs = <String>{};
+ final GlobalKey<RawGestureDetectorState> detectorKey = GlobalKey();
+ final VoidCallback performLayout = () {
+ detectorKey.currentState.replaceGestureRecognizers(<Type, GestureRecognizerFactory>{
+ TapGestureRecognizer: GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
+ () => 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: <Type, GestureRecognizerFactory>{
+ HorizontalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>(
+ () => 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, <String>{'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, <String>{'tap'});
+ logs.clear();
+
+ semantics.dispose();
+ });
+
+ group('RawGestureDetector\'s custom semantics delegate', () {
+ testWidgets('should update semantics notations when switching from the default delegate', (WidgetTester tester) async {
+ final SemanticsTester semantics = SemanticsTester(tester);
+ final Map<Type, GestureRecognizerFactory> gestures =
+ _buildGestureMap(() => LongPressGestureRecognizer(), null)
+ ..addAll( _buildGestureMap(() => TapGestureRecognizer(), null));
+ await tester.pumpWidget(
+ Center(
+ child: RawGestureDetector(
+ gestures: gestures,
+ child: Container(),
+ ),
+ )
+ );
+
+ expect(semantics, includesNodeWith(
+ actions: <SemanticsAction>[SemanticsAction.longPress, SemanticsAction.tap]),
+ );
+
+ await tester.pumpWidget(
+ Center(
+ child: RawGestureDetector(
+ gestures: gestures,
+ semantics: _TestSemanticsGestureDelegate(onTap: () {}),
+ child: Container(),
+ ),
+ )
+ );
+
+ expect(semantics, includesNodeWith(
+ actions: <SemanticsAction>[SemanticsAction.tap]),
+ );
+
+ semantics.dispose();
+ });
+
+ testWidgets('should update semantics notations when switching to the default delegate', (WidgetTester tester) async {
+ final SemanticsTester semantics = SemanticsTester(tester);
+ final Map<Type, GestureRecognizerFactory> gestures =
+ _buildGestureMap(() => LongPressGestureRecognizer(), null)
+ ..addAll( _buildGestureMap(() => TapGestureRecognizer(), null));
+ await tester.pumpWidget(
+ Center(
+ child: RawGestureDetector(
+ gestures: gestures,
+ semantics: _TestSemanticsGestureDelegate(onTap: () {}),
+ child: Container(),
+ ),
+ )
+ );
+
+ expect(semantics, includesNodeWith(
+ actions: <SemanticsAction>[SemanticsAction.tap]),
+ );
+
+ await tester.pumpWidget(
+ Center(
+ child: RawGestureDetector(
+ gestures: gestures,
+ child: Container(),
+ ),
+ )
+ );
+
+ expect(semantics, includesNodeWith(
+ actions: <SemanticsAction>[SemanticsAction.longPress, SemanticsAction.tap]),
+ );
+
+ semantics.dispose();
+ });
+
+ testWidgets('should update semantics notations when switching from a different custom delegate', (WidgetTester tester) async {
+ final SemanticsTester semantics = SemanticsTester(tester);
+ final Map<Type, GestureRecognizerFactory> gestures =
+ _buildGestureMap(() => LongPressGestureRecognizer(), null)
+ ..addAll( _buildGestureMap(() => TapGestureRecognizer(), null));
+ await tester.pumpWidget(
+ Center(
+ child: RawGestureDetector(
+ gestures: gestures,
+ semantics: _TestSemanticsGestureDelegate(onTap: () {}),
+ child: Container(),
+ ),
+ )
+ );
+
+ expect(semantics, includesNodeWith(
+ actions: <SemanticsAction>[SemanticsAction.tap]),
+ );
+
+ await tester.pumpWidget(
+ Center(
+ child: RawGestureDetector(
+ gestures: gestures,
+ semantics: _TestSemanticsGestureDelegate(onLongPress: () {}),
+ child: Container(),
+ ),
+ )
+ );
+
+ expect(semantics, includesNodeWith(
+ actions: <SemanticsAction>[SemanticsAction.longPress]),
+ );
+
+ semantics.dispose();
+ });
+
+ testWidgets('should correctly call callbacks', (WidgetTester tester) async {
+ final SemanticsTester semantics = SemanticsTester(tester);
+ final List<String> logs = <String>[];
+ final GlobalKey<RawGestureDetectorState> detectorKey = GlobalKey();
+ await tester.pumpWidget(
+ Center(
+ child: RawGestureDetector(
+ key: detectorKey,
+ gestures: const <Type, GestureRecognizerFactory>{},
+ semantics: _TestSemanticsGestureDelegate(
+ onTap: () { logs.add('tap'); },
+ onLongPress: () { logs.add('longPress'); },
+ onHorizontalDragUpdate: (_) { logs.add('horizontal'); },
+ onVerticalDragUpdate: (_) { logs.add('vertical'); },
+ ),
+ child: Container(),
+ ),
+ )
+ );
+
+ final int detectorId = detectorKey.currentContext.findRenderObject().debugSemantics.id;
+ tester.binding.pipelineOwner.semanticsOwner.performAction(detectorId, SemanticsAction.tap);
+ expect(logs, <String>['tap']);
+ logs.clear();
+
+ tester.binding.pipelineOwner.semanticsOwner.performAction(detectorId, SemanticsAction.longPress);
+ expect(logs, <String>['longPress']);
+ logs.clear();
+
+ tester.binding.pipelineOwner.semanticsOwner.performAction(detectorId, SemanticsAction.scrollLeft);
+ expect(logs, <String>['horizontal']);
+ logs.clear();
+
+ tester.binding.pipelineOwner.semanticsOwner.performAction(detectorId, SemanticsAction.scrollUp);
+ expect(logs, <String>['vertical']);
+ logs.clear();
+
+ semantics.dispose();
+ });
+ });
+
+ group('RawGestureDetector\'s default semantics delegate', () {
+ group('should map onTap to', () {
+ testWidgets('null when there is no TapGR', (WidgetTester tester) async {
+ final SemanticsTester semantics = SemanticsTester(tester);
+ await tester.pumpWidget(
+ Center(
+ child: RawGestureDetector(
+ gestures: _buildGestureMap(null, null),
+ child: Container(),
+ ),
+ )
+ );
+
+ expect(semantics, isNot(includesNodeWith(
+ actions: <SemanticsAction>[SemanticsAction.tap],
+ )));
+
+ semantics.dispose();
+ });
+
+ testWidgets('non-null when there is TapGR with no callbacks', (WidgetTester tester) async {
+ final SemanticsTester semantics = SemanticsTester(tester);
+ await tester.pumpWidget(
+ Center(
+ child: RawGestureDetector(
+ gestures: _buildGestureMap(
+ () => TapGestureRecognizer(),
+ null,
+ ),
+ child: Container(),
+ ),
+ )
+ );
+
+ expect(semantics, includesNodeWith(
+ actions: <SemanticsAction>[SemanticsAction.tap],
+ ));
+
+ semantics.dispose();
+ });
+
+ testWidgets('a callback that correctly calls callbacks', (WidgetTester tester) async {
+ final SemanticsTester semantics = SemanticsTester(tester);
+ final GlobalKey detectorKey = GlobalKey();
+ final List<String> logs = <String>[];
+ await tester.pumpWidget(
+ Center(
+ child: RawGestureDetector(
+ key: detectorKey,
+ gestures: _buildGestureMap(
+ () => TapGestureRecognizer(),
+ (TapGestureRecognizer tap) {
+ tap
+ ..onTap = () {logs.add('tap');}
+ ..onTapUp = (_) {logs.add('tapUp');}
+ ..onTapDown = (_) {logs.add('tapDown');}
+ ..onTapCancel = () {logs.add('WRONG');}
+ ..onSecondaryTapDown = (_) {logs.add('WRONG');};
+ }
+ ),
+ child: Container(),
+ ),
+ )
+ );
+
+ final int detectorId = detectorKey.currentContext.findRenderObject().debugSemantics.id;
+ tester.binding.pipelineOwner.semanticsOwner.performAction(detectorId, SemanticsAction.tap);
+ expect(logs, <String>['tapDown', 'tapUp', 'tap']);
+
+ semantics.dispose();
+ });
+ });
+
+ group('should map onLongPress to', () {
+ testWidgets('null when there is no LongPressGR ', (WidgetTester tester) async {
+ final SemanticsTester semantics = SemanticsTester(tester);
+ await tester.pumpWidget(
+ Center(
+ child: RawGestureDetector(
+ gestures: _buildGestureMap(null, null),
+ child: Container(),
+ ),
+ )
+ );
+
+ expect(semantics, isNot(includesNodeWith(
+ actions: <SemanticsAction>[SemanticsAction.longPress],
+ )));
+
+ semantics.dispose();
+ });
+
+ testWidgets('non-null when there is LongPressGR with no callbacks', (WidgetTester tester) async {
+ final SemanticsTester semantics = SemanticsTester(tester);
+ await tester.pumpWidget(
+ Center(
+ child: RawGestureDetector(
+ gestures: _buildGestureMap(
+ () => LongPressGestureRecognizer(),
+ null,
+ ),
+ child: Container(),
+ ),
+ )
+ );
+
+ expect(semantics, includesNodeWith(
+ actions: <SemanticsAction>[SemanticsAction.longPress],
+ ));
+
+ semantics.dispose();
+ });
+
+ testWidgets('a callback that correctly calls callbacks', (WidgetTester tester) async {
+ final SemanticsTester semantics = SemanticsTester(tester);
+ final GlobalKey detectorKey = GlobalKey();
+ final List<String> logs = <String>[];
+ await tester.pumpWidget(
+ Center(
+ child: RawGestureDetector(
+ key: detectorKey,
+ gestures: _buildGestureMap(
+ () => LongPressGestureRecognizer(),
+ (LongPressGestureRecognizer longPress) {
+ longPress
+ ..onLongPress = () {logs.add('LP');}
+ ..onLongPressStart = (_) {logs.add('LPStart');}
+ ..onLongPressUp = () {logs.add('LPUp');}
+ ..onLongPressEnd = (_) {logs.add('LPEnd');}
+ ..onLongPressMoveUpdate = (_) {logs.add('WRONG');};
+ }
+ ),
+ child: Container(),
+ ),
+ )
+ );
+
+ final int detectorId = detectorKey.currentContext.findRenderObject().debugSemantics.id;
+ tester.binding.pipelineOwner.semanticsOwner.performAction(detectorId, SemanticsAction.longPress);
+ expect(logs, <String>['LPStart', 'LP', 'LPEnd', 'LPUp']);
+
+ semantics.dispose();
+ });
+ });
+
+ group('should map onHorizontalDragUpdate to', () {
+ testWidgets('null when there is no matching recognizers ', (WidgetTester tester) async {
+ final SemanticsTester semantics = SemanticsTester(tester);
+ await tester.pumpWidget(
+ Center(
+ child: RawGestureDetector(
+ gestures: _buildGestureMap(null, null),
+ child: Container(),
+ ),
+ )
+ );
+
+ expect(semantics, isNot(includesNodeWith(
+ actions: <SemanticsAction>[SemanticsAction.scrollLeft, SemanticsAction.scrollRight],
+ )));
+
+ semantics.dispose();
+ });
+
+ testWidgets('non-null when there is either matching recognizer with no callbacks', (WidgetTester tester) async {
+ final SemanticsTester semantics = SemanticsTester(tester);
+ await tester.pumpWidget(
+ Center(
+ child: RawGestureDetector(
+ gestures: _buildGestureMap(
+ () => HorizontalDragGestureRecognizer(),
+ null,
+ ),
+ child: Container(),
+ ),
+ )
+ );
+
+ expect(semantics, includesNodeWith(
+ actions: <SemanticsAction>[SemanticsAction.scrollLeft, SemanticsAction.scrollRight],
+ ));
+
+ await tester.pumpWidget(
+ Center(
+ child: RawGestureDetector(
+ gestures: _buildGestureMap(
+ () => PanGestureRecognizer(),
+ null,
+ ),
+ child: Container(),
+ ),
+ )
+ );
+
+ expect(semantics, includesNodeWith(
+ actions: <SemanticsAction>[SemanticsAction.scrollLeft, SemanticsAction.scrollRight,
+ SemanticsAction.scrollDown, SemanticsAction.scrollUp],
+ ));
+
+ semantics.dispose();
+ });
+
+ testWidgets('a callback that correctly calls callbacks', (WidgetTester tester) async {
+ final SemanticsTester semantics = SemanticsTester(tester);
+ final GlobalKey detectorKey = GlobalKey();
+ final List<String> logs = <String>[];
+ final Map<Type, GestureRecognizerFactory> gestures = _buildGestureMap(
+ () => HorizontalDragGestureRecognizer(),
+ (HorizontalDragGestureRecognizer horizontal) {
+ horizontal
+ ..onStart = (_) {logs.add('HStart');}
+ ..onDown = (_) {logs.add('HDown');}
+ ..onEnd = (_) {logs.add('HEnd');}
+ ..onUpdate = (_) {logs.add('HUpdate');}
+ ..onCancel = () {logs.add('WRONG');};
+ }
+ )..addAll(_buildGestureMap(
+ () => PanGestureRecognizer(),
+ (PanGestureRecognizer pan) {
+ pan
+ ..onStart = (_) {logs.add('PStart');}
+ ..onDown = (_) {logs.add('PDown');}
+ ..onEnd = (_) {logs.add('PEnd');}
+ ..onUpdate = (_) {logs.add('PUpdate');}
+ ..onCancel = () {logs.add('WRONG');};
+ }
+ ));
+ await tester.pumpWidget(
+ Center(
+ child: RawGestureDetector(
+ key: detectorKey,
+ gestures: gestures,
+ child: Container(),
+ ),
+ )
+ );
+
+ final int detectorId = detectorKey.currentContext.findRenderObject().debugSemantics.id;
+ tester.binding.pipelineOwner.semanticsOwner.performAction(detectorId, SemanticsAction.scrollLeft);
+ expect(logs, <String>['HDown', 'HStart', 'HUpdate', 'HEnd',
+ 'PDown', 'PStart', 'PUpdate', 'PEnd',]);
+ logs.clear();
+
+ tester.binding.pipelineOwner.semanticsOwner.performAction(detectorId, SemanticsAction.scrollLeft);
+ expect(logs, <String>['HDown', 'HStart', 'HUpdate', 'HEnd',
+ 'PDown', 'PStart', 'PUpdate', 'PEnd',]);
+
+ semantics.dispose();
+ });
+ });
+
+ group('should map onVerticalDragUpdate to', () {
+ testWidgets('null when there is no matching recognizers ', (WidgetTester tester) async {
+ final SemanticsTester semantics = SemanticsTester(tester);
+ await tester.pumpWidget(
+ Center(
+ child: RawGestureDetector(
+ gestures: _buildGestureMap(null, null),
+ child: Container(),
+ ),
+ )
+ );
+
+ expect(semantics, isNot(includesNodeWith(
+ actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollDown],
+ )));
+
+ semantics.dispose();
+ });
+
+ testWidgets('non-null when there is either matching recognizer with no callbacks', (WidgetTester tester) async {
+ final SemanticsTester semantics = SemanticsTester(tester);
+ await tester.pumpWidget(
+ Center(
+ child: RawGestureDetector(
+ gestures: _buildGestureMap(
+ () => VerticalDragGestureRecognizer(),
+ null,
+ ),
+ child: Container(),
+ ),
+ )
+ );
+
+ expect(semantics, includesNodeWith(
+ actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollDown],
+ ));
+
+ // Pan has bene tested in Horizontal
+
+ semantics.dispose();
+ });
+
+ testWidgets('a callback that correctly calls callbacks', (WidgetTester tester) async {
+ final SemanticsTester semantics = SemanticsTester(tester);
+ final GlobalKey detectorKey = GlobalKey();
+ final List<String> logs = <String>[];
+ final Map<Type, GestureRecognizerFactory> gestures = _buildGestureMap(
+ () => VerticalDragGestureRecognizer(),
+ (VerticalDragGestureRecognizer horizontal) {
+ horizontal
+ ..onStart = (_) {logs.add('VStart');}
+ ..onDown = (_) {logs.add('VDown');}
+ ..onEnd = (_) {logs.add('VEnd');}
+ ..onUpdate = (_) {logs.add('VUpdate');}
+ ..onCancel = () {logs.add('WRONG');};
+ }
+ )..addAll(_buildGestureMap(
+ () => PanGestureRecognizer(),
+ (PanGestureRecognizer pan) {
+ pan
+ ..onStart = (_) {logs.add('PStart');}
+ ..onDown = (_) {logs.add('PDown');}
+ ..onEnd = (_) {logs.add('PEnd');}
+ ..onUpdate = (_) {logs.add('PUpdate');}
+ ..onCancel = () {logs.add('WRONG');};
+ }
+ ));
+ await tester.pumpWidget(
+ Center(
+ child: RawGestureDetector(
+ key: detectorKey,
+ gestures: gestures,
+ child: Container(),
+ ),
+ )
+ );
+
+ final int detectorId = detectorKey.currentContext.findRenderObject().debugSemantics.id;
+ tester.binding.pipelineOwner.semanticsOwner.performAction(detectorId, SemanticsAction.scrollUp);
+ expect(logs, <String>['VDown', 'VStart', 'VUpdate', 'VEnd',
+ 'PDown', 'PStart', 'PUpdate', 'PEnd',]);
+ logs.clear();
+
+ tester.binding.pipelineOwner.semanticsOwner.performAction(detectorId, SemanticsAction.scrollDown);
+ expect(logs, <String>['VDown', 'VStart', 'VUpdate', 'VEnd',
+ 'PDown', 'PStart', 'PUpdate', 'PEnd',]);
+
+ semantics.dispose();
+ });
+ });
+
+ testWidgets('should update semantics notations when receiving new gestures', (WidgetTester tester) async {
+ final SemanticsTester semantics = SemanticsTester(tester);
+ await tester.pumpWidget(
+ Center(
+ child: RawGestureDetector(
+ gestures: _buildGestureMap(() => LongPressGestureRecognizer(), null),
+ child: Container(),
+ ),
+ )
+ );
+
+ expect(semantics, includesNodeWith(
+ actions: <SemanticsAction>[SemanticsAction.longPress]),
+ );
+
+ await tester.pumpWidget(
+ Center(
+ child: RawGestureDetector(
+ gestures: _buildGestureMap(() => TapGestureRecognizer(), null),
+ child: Container(),
+ ),
+ )
+ );
+
+ expect(semantics, includesNodeWith(
+ actions: <SemanticsAction>[SemanticsAction.tap]),
+ );
+
+ 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();
+ }
+}
+
+Map<Type, GestureRecognizerFactory> _buildGestureMap<T extends GestureRecognizer>(
+ GestureRecognizerFactoryConstructor<T> constructor,
+ GestureRecognizerFactoryInitializer<T> initializer,
+) {
+ if (constructor == null)
+ return <Type, GestureRecognizerFactory>{};
+ return <Type, GestureRecognizerFactory>{
+ T: GestureRecognizerFactoryWithHandlers<T>(
+ constructor,
+ initializer ?? (T o) {},
+ ),
+ };
+}
+
+class _TestSemanticsGestureDelegate extends SemanticsGestureDelegate {
+ const _TestSemanticsGestureDelegate({
+ this.onTap,
+ this.onLongPress,
+ this.onHorizontalDragUpdate,
+ this.onVerticalDragUpdate,
+ });
+
+ final GestureTapCallback onTap;
+ final GestureLongPressCallback onLongPress;
+ final GestureDragUpdateCallback onHorizontalDragUpdate;
+ final GestureDragUpdateCallback onVerticalDragUpdate;
+
+ @override
+ void assignSemantics(RenderSemanticsGestureHandler renderObject) {
+ renderObject
+ ..onTap = onTap
+ ..onLongPress = onLongPress
+ ..onHorizontalDragUpdate = onHorizontalDragUpdate
+ ..onVerticalDragUpdate = onVerticalDragUpdate;
+ }
}
diff --git a/packages/flutter/test/widgets/gesture_detector_test.dart b/packages/flutter/test/widgets/gesture_detector_test.dart
index b762687..aee62a5 100644
--- a/packages/flutter/test/widgets/gesture_detector_test.dart
+++ b/packages/flutter/test/widgets/gesture_detector_test.dart
@@ -224,7 +224,7 @@
expect(didTap, isFalse);
});
- testWidgets('cache unchanged callbacks', (WidgetTester tester) async {
+ testWidgets('cache render object', (WidgetTester tester) async {
final GestureTapCallback inputCallback = () { };
await tester.pumpWidget(
@@ -237,7 +237,6 @@
);
final RenderSemanticsGestureHandler renderObj1 = tester.renderObject(find.byType(GestureDetector));
- final GestureTapCallback actualCallback1 = renderObj1.onTap;
await tester.pumpWidget(
Center(
@@ -249,10 +248,8 @@
);
final RenderSemanticsGestureHandler renderObj2 = tester.renderObject(find.byType(GestureDetector));
- final GestureTapCallback actualCallback2 = renderObj2.onTap;
expect(renderObj1, same(renderObj2));
- expect(actualCallback1, same(actualCallback2)); // Should be cached.
});
testWidgets('Tap down occurs after kPressTimeout', (WidgetTester tester) async {
@@ -525,4 +522,90 @@
expect(horizontalDragStart, 1);
expect(forcePressStart, 0);
});
+
+ group('RawGestureDetectorState\'s debugFillProperties', () {
+ testWidgets('when default', (WidgetTester tester) async {
+ final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
+ final GlobalKey key = GlobalKey();
+ await tester.pumpWidget(RawGestureDetector(
+ key: key,
+ ));
+ key.currentState.debugFillProperties(builder);
+
+ final List<String> description = builder.properties
+ .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
+ .map((DiagnosticsNode node) => node.toString())
+ .toList();
+
+ expect(description, <String>[
+ 'gestures: <none>',
+ ]);
+ });
+
+ testWidgets('should show gestures, custom semantics and behavior', (WidgetTester tester) async {
+ final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
+ final GlobalKey key = GlobalKey();
+ await tester.pumpWidget(RawGestureDetector(
+ key: key,
+ behavior: HitTestBehavior.deferToChild,
+ gestures: <Type, GestureRecognizerFactory>{
+ TapGestureRecognizer: GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
+ () => TapGestureRecognizer(),
+ (TapGestureRecognizer recognizer) {
+ recognizer.onTap = () {};
+ },
+ ),
+ LongPressGestureRecognizer: GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
+ () => LongPressGestureRecognizer(),
+ (LongPressGestureRecognizer recognizer) {
+ recognizer.onLongPress = () {};
+ },
+ ),
+ },
+ child: Container(),
+ semantics: _EmptySemanticsGestureDelegate(),
+ ));
+ key.currentState.debugFillProperties(builder);
+
+ final List<String> description = builder.properties
+ .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
+ .map((DiagnosticsNode node) => node.toString())
+ .toList();
+
+ expect(description, <String>[
+ 'gestures: tap, long press',
+ 'semantics: _EmptySemanticsGestureDelegate()',
+ 'behavior: deferToChild',
+ ]);
+ });
+
+ testWidgets('should not show semantics when excludeFromSemantics is true', (WidgetTester tester) async {
+ final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
+ final GlobalKey key = GlobalKey();
+ await tester.pumpWidget(RawGestureDetector(
+ key: key,
+ gestures: const <Type, GestureRecognizerFactory>{},
+ child: Container(),
+ semantics: _EmptySemanticsGestureDelegate(),
+ excludeFromSemantics: true,
+ ));
+ key.currentState.debugFillProperties(builder);
+
+ final List<String> description = builder.properties
+ .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
+ .map((DiagnosticsNode node) => node.toString())
+ .toList();
+
+ expect(description, <String>[
+ 'gestures: <none>',
+ 'excludeFromSemantics: true',
+ ]);
+ });
+ });
+}
+
+class _EmptySemanticsGestureDelegate extends SemanticsGestureDelegate {
+ @override
+ void assignSemantics(RenderSemanticsGestureHandler renderObject) {
+ }
}