Unverified Commit 1a374d82 authored by Tong Mu's avatar Tong Mu Committed by GitHub

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.
parent d6bd1c05
...@@ -45,6 +45,7 @@ export 'package:flutter/gestures.dart' show ...@@ -45,6 +45,7 @@ export 'package:flutter/gestures.dart' show
TapUpDetails, TapUpDetails,
ForcePressDetails, ForcePressDetails,
Velocity; Velocity;
export 'package:flutter/rendering.dart' show RenderSemanticsGestureHandler;
// Examples can assume: // Examples can assume:
// bool _lights; // bool _lights;
...@@ -764,15 +765,16 @@ class GestureDetector extends StatelessWidget { ...@@ -764,15 +765,16 @@ class GestureDetector extends StatelessWidget {
class RawGestureDetector extends StatefulWidget { class RawGestureDetector extends StatefulWidget {
/// Creates a widget that detects gestures. /// Creates a widget that detects gestures.
/// ///
/// By default, gesture detectors contribute semantic information to the tree /// Gesture detectors can contribute semantic information to the tree that is
/// that is used by assistive technology. This can be controlled using /// used by assistive technology. The behavior can be configured by
/// [excludeFromSemantics]. /// [semantics], or disabled with [excludeFromSemantics].
const RawGestureDetector({ const RawGestureDetector({
Key key, Key key,
this.child, this.child,
this.gestures = const <Type, GestureRecognizerFactory>{}, this.gestures = const <Type, GestureRecognizerFactory>{},
this.behavior, this.behavior,
this.excludeFromSemantics = false, this.excludeFromSemantics = false,
this.semantics,
}) : assert(gestures != null), }) : assert(gestures != null),
assert(excludeFromSemantics != null), assert(excludeFromSemantics != null),
super(key: key); super(key: key);
...@@ -804,6 +806,72 @@ class RawGestureDetector extends StatefulWidget { ...@@ -804,6 +806,72 @@ class RawGestureDetector extends StatefulWidget {
/// duplication of information. /// duplication of information.
final bool excludeFromSemantics; 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 @override
RawGestureDetectorState createState() => RawGestureDetectorState(); RawGestureDetectorState createState() => RawGestureDetectorState();
} }
...@@ -811,16 +879,21 @@ class RawGestureDetector extends StatefulWidget { ...@@ -811,16 +879,21 @@ class RawGestureDetector extends StatefulWidget {
/// State for a [RawGestureDetector]. /// State for a [RawGestureDetector].
class RawGestureDetectorState extends State<RawGestureDetector> { class RawGestureDetectorState extends State<RawGestureDetector> {
Map<Type, GestureRecognizer> _recognizers = const <Type, GestureRecognizer>{}; Map<Type, GestureRecognizer> _recognizers = const <Type, GestureRecognizer>{};
SemanticsGestureDelegate _semantics;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_semantics = widget.semantics ?? _DefaultSemanticsGestureDelegate(this);
_syncAll(widget.gestures); _syncAll(widget.gestures);
} }
@override @override
void didUpdateWidget(RawGestureDetector oldWidget) { void didUpdateWidget(RawGestureDetector oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (!(oldWidget.semantics == null && widget.semantics == null)) {
_semantics = widget.semantics ?? _DefaultSemanticsGestureDelegate(this);
}
_syncAll(widget.gestures); _syncAll(widget.gestures);
} }
...@@ -853,10 +926,7 @@ class RawGestureDetectorState extends State<RawGestureDetector> { ...@@ -853,10 +926,7 @@ class RawGestureDetectorState extends State<RawGestureDetector> {
_syncAll(gestures); _syncAll(gestures);
if (!widget.excludeFromSemantics) { if (!widget.excludeFromSemantics) {
final RenderSemanticsGestureHandler semanticsGestureHandler = context.findRenderObject(); final RenderSemanticsGestureHandler semanticsGestureHandler = context.findRenderObject();
context.visitChildElements((Element element) { _updateSemanticsForRenderObject(semanticsGestureHandler);
final _GestureSemantics widget = element.widget;
widget._updateHandlers(semanticsGestureHandler);
});
} }
} }
...@@ -924,90 +994,10 @@ class RawGestureDetectorState extends State<RawGestureDetector> { ...@@ -924,90 +994,10 @@ class RawGestureDetectorState extends State<RawGestureDetector> {
return widget.child == null ? HitTestBehavior.translucent : HitTestBehavior.deferToChild; return widget.child == null ? HitTestBehavior.translucent : HitTestBehavior.deferToChild;
} }
void _handleSemanticsTap() { void _updateSemanticsForRenderObject(RenderSemanticsGestureHandler renderObject) {
final TapGestureRecognizer recognizer = _recognizers[TapGestureRecognizer]; assert(!widget.excludeFromSemantics);
assert(recognizer != null); assert(_semantics != null);
if (recognizer.onTapDown != null) _semantics.assignSemantics(renderObject);
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 @override
...@@ -1018,7 +1008,10 @@ class RawGestureDetectorState extends State<RawGestureDetector> { ...@@ -1018,7 +1008,10 @@ class RawGestureDetectorState extends State<RawGestureDetector> {
child: widget.child, child: widget.child,
); );
if (!widget.excludeFromSemantics) if (!widget.excludeFromSemantics)
result = _GestureSemantics(owner: this, child: result); result = _GestureSemantics(
child: result,
assignSemantics: _updateSemanticsForRenderObject,
);
return result; return result;
} }
...@@ -1031,58 +1024,202 @@ class RawGestureDetectorState extends State<RawGestureDetector> { ...@@ -1031,58 +1024,202 @@ class RawGestureDetectorState extends State<RawGestureDetector> {
final List<String> gestures = _recognizers.values.map<String>((GestureRecognizer recognizer) => recognizer.debugDescription).toList(); final List<String> gestures = _recognizers.values.map<String>((GestureRecognizer recognizer) => recognizer.debugDescription).toList();
properties.add(IterableProperty<String>('gestures', gestures, ifEmpty: '<none>')); properties.add(IterableProperty<String>('gestures', gestures, ifEmpty: '<none>'));
properties.add(IterableProperty<GestureRecognizer>('recognizers', _recognizers.values, level: DiagnosticLevel.fine)); 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)); properties.add(EnumProperty<HitTestBehavior>('behavior', widget.behavior, defaultValue: null));
} }
} }
typedef _AssignSemantics = void Function(RenderSemanticsGestureHandler);
class _GestureSemantics extends SingleChildRenderObjectWidget { class _GestureSemantics extends SingleChildRenderObjectWidget {
const _GestureSemantics({ const _GestureSemantics({
Key key, Key key,
Widget child, Widget child,
this.owner, @required this.assignSemantics,
}) : super(key: key, child: child); }) : assert(assignSemantics != null),
super(key: key, child: child);
final RawGestureDetectorState owner; final _AssignSemantics assignSemantics;
@override @override
RenderSemanticsGestureHandler createRenderObject(BuildContext context) { RenderSemanticsGestureHandler createRenderObject(BuildContext context) {
return RenderSemanticsGestureHandler( final RenderSemanticsGestureHandler renderObject = RenderSemanticsGestureHandler();
onTap: _onTapHandler, assignSemantics(renderObject);
onLongPress: _onLongPressHandler, return renderObject;
onHorizontalDragUpdate: _onHorizontalDragUpdateHandler,
onVerticalDragUpdate: _onVerticalDragUpdateHandler,
);
} }
void _updateHandlers(RenderSemanticsGestureHandler renderObject) { @override
renderObject void updateRenderObject(BuildContext context, RenderSemanticsGestureHandler renderObject) {
..onTap = _onTapHandler assignSemantics(renderObject);
..onLongPress = _onLongPressHandler
..onHorizontalDragUpdate = _onHorizontalDragUpdateHandler
..onVerticalDragUpdate = _onVerticalDragUpdateHandler;
} }
}
/// 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 @override
void updateRenderObject(BuildContext context, RenderSemanticsGestureHandler renderObject) { String toString() => '$runtimeType()';
_updateHandlers(renderObject); }
// 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 { GestureTapCallback _getTapHandler(Map<Type, GestureRecognizer> recognizers) {
return owner._recognizers.containsKey(TapGestureRecognizer) ? owner._handleSemanticsTap : null; 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 { GestureLongPressCallback _getLongPressHandler(Map<Type, GestureRecognizer> recognizers) {
return owner._recognizers.containsKey(LongPressGestureRecognizer) ? owner._handleSemanticsLongPress : null; 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 { GestureDragUpdateCallback _getHorizontalDragUpdateHandler(Map<Type, GestureRecognizer> recognizers) {
return owner._recognizers.containsKey(HorizontalDragGestureRecognizer) || final HorizontalDragGestureRecognizer horizontal = recognizers[HorizontalDragGestureRecognizer];
owner._recognizers.containsKey(PanGestureRecognizer) ? owner._handleSemanticsHorizontalDragUpdate : null; 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 { GestureDragUpdateCallback _getVerticalDragUpdateHandler(Map<Type, GestureRecognizer> recognizers) {
return owner._recognizers.containsKey(VerticalDragGestureRecognizer) || final VerticalDragGestureRecognizer vertical = recognizers[VerticalDragGestureRecognizer];
owner._recognizers.containsKey(PanGestureRecognizer) ? owner._handleSemanticsVerticalDragUpdate : null; 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);
};
} }
} }
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
...@@ -76,4 +77,686 @@ void main() { ...@@ -76,4 +77,686 @@ void main() {
semantics.dispose(); 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;
}
} }
...@@ -224,7 +224,7 @@ void main() { ...@@ -224,7 +224,7 @@ void main() {
expect(didTap, isFalse); expect(didTap, isFalse);
}); });
testWidgets('cache unchanged callbacks', (WidgetTester tester) async { testWidgets('cache render object', (WidgetTester tester) async {
final GestureTapCallback inputCallback = () { }; final GestureTapCallback inputCallback = () { };
await tester.pumpWidget( await tester.pumpWidget(
...@@ -237,7 +237,6 @@ void main() { ...@@ -237,7 +237,6 @@ void main() {
); );
final RenderSemanticsGestureHandler renderObj1 = tester.renderObject(find.byType(GestureDetector)); final RenderSemanticsGestureHandler renderObj1 = tester.renderObject(find.byType(GestureDetector));
final GestureTapCallback actualCallback1 = renderObj1.onTap;
await tester.pumpWidget( await tester.pumpWidget(
Center( Center(
...@@ -249,10 +248,8 @@ void main() { ...@@ -249,10 +248,8 @@ void main() {
); );
final RenderSemanticsGestureHandler renderObj2 = tester.renderObject(find.byType(GestureDetector)); final RenderSemanticsGestureHandler renderObj2 = tester.renderObject(find.byType(GestureDetector));
final GestureTapCallback actualCallback2 = renderObj2.onTap;
expect(renderObj1, same(renderObj2)); expect(renderObj1, same(renderObj2));
expect(actualCallback1, same(actualCallback2)); // Should be cached.
}); });
testWidgets('Tap down occurs after kPressTimeout', (WidgetTester tester) async { testWidgets('Tap down occurs after kPressTimeout', (WidgetTester tester) async {
...@@ -525,4 +522,90 @@ void main() { ...@@ -525,4 +522,90 @@ void main() {
expect(horizontalDragStart, 1); expect(horizontalDragStart, 1);
expect(forcePressStart, 0); 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) {
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment