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) {
+  }
 }