Move declaration of semantic handlers from detectors to recognizers (#33475)

- A refactor that moves the semantics declaration from detectors to recognizers to allow custom recognizers to respond to semantic gectures.
- Renames all handlers related to semantics from Gesture* to Semantics*.
This commit is contained in:
Tong Mu 2019-06-03 15:07:23 -07:00 committed by GitHub
parent 90500a5dc4
commit 572f349f5c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 591 additions and 115 deletions

View file

@ -28,6 +28,7 @@ export 'src/gestures/pointer_router.dart';
export 'src/gestures/pointer_signal_resolver.dart';
export 'src/gestures/recognizer.dart';
export 'src/gestures/scale.dart';
export 'src/gestures/semantics.dart';
export 'src/gestures/tap.dart';
export 'src/gestures/team.dart';
export 'src/gestures/velocity_tracker.dart';

View file

@ -6,6 +6,7 @@ import 'arena.dart';
import 'constants.dart';
import 'events.dart';
import 'recognizer.dart';
import 'semantics.dart';
/// Callback signature for [LongPressGestureRecognizer.onLongPress].
///
@ -214,6 +215,23 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
/// callback.
GestureLongPressEndCallback onLongPressEnd;
@override
SemanticsGestureConfiguration get semanticsConfiguration {
return _semanticsConfiguration ??= SemanticsGestureConfiguration(
onLongPress: () {
if (onLongPressStart != null)
onLongPressStart(const LongPressStartDetails());
if (onLongPress != null)
onLongPress();
if (onLongPressEnd != null)
onLongPressEnd(const LongPressEndDetails());
if (onLongPressUp != null)
onLongPressUp();
},
);
}
SemanticsGestureConfiguration _semanticsConfiguration;
@override
bool isPointerAllowed(PointerDownEvent event) {
switch (event.buttons) {

View file

@ -10,6 +10,7 @@ import 'constants.dart';
import 'drag_details.dart';
import 'events.dart';
import 'recognizer.dart';
import 'semantics.dart';
import 'velocity_tracker.dart';
enum _DragState {
@ -456,6 +457,23 @@ class VerticalDragGestureRecognizer extends DragGestureRecognizer {
PointerDeviceKind kind,
}) : super(debugOwner: debugOwner, kind: kind);
@override
SemanticsGestureConfiguration get semanticsConfiguration {
return _semanticsConfiguration ??= SemanticsGestureConfiguration(
onVerticalDragUpdate: (DragUpdateDetails updateDetails) {
if (onDown != null)
onDown(DragDownDetails());
if (onStart != null)
onStart(DragStartDetails());
if (onUpdate != null)
onUpdate(updateDetails);
if (onEnd != null)
onEnd(DragEndDetails(primaryVelocity: 0.0));
},
);
}
SemanticsGestureConfiguration _semanticsConfiguration;
@override
bool _isFlingGesture(VelocityEstimate estimate) {
final double minVelocity = minFlingVelocity ?? kMinFlingVelocity;
@ -495,6 +513,23 @@ class HorizontalDragGestureRecognizer extends DragGestureRecognizer {
PointerDeviceKind kind,
}) : super(debugOwner: debugOwner, kind: kind);
@override
SemanticsGestureConfiguration get semanticsConfiguration {
return _semanticsConfiguration ??= SemanticsGestureConfiguration(
onHorizontalDragUpdate: (DragUpdateDetails updateDetails) {
if (onDown != null)
onDown(DragDownDetails());
if (onStart != null)
onStart(DragStartDetails());
if (onUpdate != null)
onUpdate(updateDetails);
if (onEnd != null)
onEnd(DragEndDetails(primaryVelocity: 0.0));
},
);
}
SemanticsGestureConfiguration _semanticsConfiguration;
@override
bool _isFlingGesture(VelocityEstimate estimate) {
final double minVelocity = minFlingVelocity ?? kMinFlingVelocity;
@ -528,6 +563,33 @@ class PanGestureRecognizer extends DragGestureRecognizer {
/// Create a gesture recognizer for tracking movement on a plane.
PanGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
@override
SemanticsGestureConfiguration get semanticsConfiguration {
return _semanticsConfiguration ??= SemanticsGestureConfiguration(
onHorizontalDragUpdate: (DragUpdateDetails updateDetails) {
if (onDown != null)
onDown(DragDownDetails());
if (onStart != null)
onStart(DragStartDetails());
if (onUpdate != null)
onUpdate(updateDetails);
if (onEnd != null)
onEnd(DragEndDetails());
},
onVerticalDragUpdate: (DragUpdateDetails updateDetails) {
if (onDown != null)
onDown(DragDownDetails());
if (onStart != null)
onStart(DragStartDetails());
if (onUpdate != null)
onUpdate(updateDetails);
if (onEnd != null)
onEnd(DragEndDetails());
},
);
}
SemanticsGestureConfiguration _semanticsConfiguration;
@override
bool _isFlingGesture(VelocityEstimate estimate) {
final double minVelocity = minFlingVelocity ?? kMinFlingVelocity;

View file

@ -15,6 +15,7 @@ import 'constants.dart';
import 'debug.dart';
import 'events.dart';
import 'pointer_router.dart';
import 'semantics.dart';
import 'team.dart';
export 'pointer_router.dart' show PointerRouter;
@ -142,6 +143,21 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT
return _pointerToKind[pointer];
}
/// Returns the semantics configuration of this gesture recognizer, for example
/// for accessibility purposes. It is queried by the recognizer's
/// [RawGestureDetector] to build a collective semantics annotation.
///
/// This method should be overridden by subclasses that are interested in
/// semantic gestures.
///
/// The returned [SemanticsGestureConfiguration] object should be annotated in
/// a manner that describes the current state, and should remain consistant
/// behaviors across different calls. It is recommended to cache the returned
/// object.
SemanticsGestureConfiguration get semanticsConfiguration {
return null;
}
/// Releases any resources used by the object.
///
/// This method is called by the owner of this gesture recognizer

View file

@ -0,0 +1,69 @@
// Copyright 2019 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'drag_details.dart';
/// Called when the user taps with a semantics device.
typedef SemanticsTapCallback = void Function();
/// Called when the user presses for a long period of time with a semantics
/// device.
typedef SemanticsLongPressCallback = void Function();
/// Called when the user drags with a semantics device.
typedef SemanticsDragUpdateCallback = void Function(DragUpdateDetails details);
/// Describes the semantics configuration of a gesture recognizer, for example
/// for accessibility purposes. It is queried by the recognizer's
/// [RawGestureDetector] to build a collective semantics annotation.
///
/// When a [RawGestureDetector] receives a semantics gesture, it will invoke
/// the corresponding method that each recognizer reports in the configuration.
///
/// See also:
///
/// * [GestureRecognizer.semanticsConfiguration], a method that returns this
/// class.
class SemanticsGestureConfiguration {
/// Initialize the semantics handler configuration by declaring the handlers
/// for each kind of semantics events.
SemanticsGestureConfiguration({
this.onTap,
this.onLongPress,
this.onHorizontalDragUpdate,
this.onVerticalDragUpdate,
});
/// Called when the user taps with a semantics device.
///
/// See also:
///
/// * [RenderSemanticsGestureHandler.onTap], which calls this handler with
/// the help of [RawGestureRecognizer].
final SemanticsTapCallback onTap;
/// Called when the user presses for a long period of time with a semantics
/// device.
///
/// See also:
///
/// * [RenderSemanticsGestureHandler.onLongPress], which calls this handler
/// with the help of [RawGestureRecognizer].
final SemanticsLongPressCallback onLongPress;
/// Called when the user scrolls to the left or to the right with a semantics
/// device.
///
/// See also:
///
/// * [RenderSemanticsGestureHandler.onHorizontalDragUpdate], which calls
/// this handler with the help of [RawGestureRecognizer].
final SemanticsDragUpdateCallback onHorizontalDragUpdate;
/// Called when the user scrolls up or down with a semantics device.
///
/// See also:
///
/// * [RenderSemanticsGestureHandler.onVerticalDragUpdate], which calls
/// this handler with the help of [RawGestureRecognizer].
final SemanticsDragUpdateCallback onVerticalDragUpdate;
}

View file

@ -8,6 +8,7 @@ import 'arena.dart';
import 'constants.dart';
import 'events.dart';
import 'recognizer.dart';
import 'semantics.dart';
/// Details for [GestureTapDownCallback], such as position
///
@ -237,6 +238,25 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
// different set of buttons, the gesture is canceled.
int _initialButtons;
@override
SemanticsGestureConfiguration get semanticsConfiguration {
// This method must always return a non-null configuration with a non-null
// onTap, even when there are no handlers related to semantics, because the
// handler properties are mutable, and assigning properties will not notify
// RawGestureDetector to update its combined configuration.
return _semanticsConfiguration ??= SemanticsGestureConfiguration(
onTap: () {
if (onTapDown != null)
onTapDown(TapDownDetails());
if (onTapUp != null)
onTapUp(TapUpDetails());
if (onTap != null)
onTap();
},
);
}
SemanticsGestureConfiguration _semanticsConfiguration;
@override
bool isPointerAllowed(PointerDownEvent event) {
switch (event.buttons) {

View file

@ -3234,10 +3234,10 @@ class RenderSemanticsGestureHandler extends RenderProxyBox {
/// The [scrollFactor] argument must not be null.
RenderSemanticsGestureHandler({
RenderBox child,
GestureTapCallback onTap,
GestureLongPressCallback onLongPress,
GestureDragUpdateCallback onHorizontalDragUpdate,
GestureDragUpdateCallback onVerticalDragUpdate,
SemanticsTapCallback onTap,
SemanticsLongPressCallback onLongPress,
SemanticsDragUpdateCallback onHorizontalDragUpdate,
SemanticsDragUpdateCallback onVerticalDragUpdate,
this.scrollFactor = 0.8,
}) : assert(scrollFactor != null),
_onTap = onTap,
@ -3269,9 +3269,9 @@ class RenderSemanticsGestureHandler extends RenderProxyBox {
}
/// Called when the user taps on the render object.
GestureTapCallback get onTap => _onTap;
GestureTapCallback _onTap;
set onTap(GestureTapCallback value) {
SemanticsTapCallback get onTap => _onTap;
SemanticsTapCallback _onTap;
set onTap(SemanticsTapCallback value) {
if (_onTap == value)
return;
final bool hadHandler = _onTap != null;
@ -3281,9 +3281,9 @@ class RenderSemanticsGestureHandler extends RenderProxyBox {
}
/// Called when the user presses on the render object for a long period of time.
GestureLongPressCallback get onLongPress => _onLongPress;
GestureLongPressCallback _onLongPress;
set onLongPress(GestureLongPressCallback value) {
SemanticsLongPressCallback get onLongPress => _onLongPress;
SemanticsLongPressCallback _onLongPress;
set onLongPress(SemanticsLongPressCallback value) {
if (_onLongPress == value)
return;
final bool hadHandler = _onLongPress != null;
@ -3293,9 +3293,9 @@ class RenderSemanticsGestureHandler extends RenderProxyBox {
}
/// Called when the user scrolls to the left or to the right.
GestureDragUpdateCallback get onHorizontalDragUpdate => _onHorizontalDragUpdate;
GestureDragUpdateCallback _onHorizontalDragUpdate;
set onHorizontalDragUpdate(GestureDragUpdateCallback value) {
SemanticsDragUpdateCallback get onHorizontalDragUpdate => _onHorizontalDragUpdate;
SemanticsDragUpdateCallback _onHorizontalDragUpdate;
set onHorizontalDragUpdate(SemanticsDragUpdateCallback value) {
if (_onHorizontalDragUpdate == value)
return;
final bool hadHandler = _onHorizontalDragUpdate != null;
@ -3305,9 +3305,9 @@ class RenderSemanticsGestureHandler extends RenderProxyBox {
}
/// Called when the user scrolls up or down.
GestureDragUpdateCallback get onVerticalDragUpdate => _onVerticalDragUpdate;
GestureDragUpdateCallback _onVerticalDragUpdate;
set onVerticalDragUpdate(GestureDragUpdateCallback value) {
SemanticsDragUpdateCallback get onVerticalDragUpdate => _onVerticalDragUpdate;
SemanticsDragUpdateCallback _onVerticalDragUpdate;
set onVerticalDragUpdate(SemanticsDragUpdateCallback value) {
if (_onVerticalDragUpdate == value)
return;
final bool hadHandler = _onVerticalDragUpdate != null;

View file

@ -105,6 +105,87 @@ class GestureRecognizerFactoryWithHandlers<T extends GestureRecognizer> extends
void initializer(T instance) => _initializer(instance);
}
// Retrieve semantics handlers from the given recognizers and combine them into
// collective callbacks, one for each kind of semantics gesture.
//
// This class stores the handlers instead of the recognizers, therefore any
// change to the recognzier list needs to be followed by [buildHandlers].
class _CombinedSemanticsHandlers {
_CombinedSemanticsHandlers({
Iterable<GestureRecognizer> recognizers,
}) {
buildHandlers(recognizers);
}
final List<SemanticsTapCallback> _tapHandlers = <SemanticsTapCallback>[];
final List<SemanticsLongPressCallback> _longPressHandlers =
<SemanticsLongPressCallback>[];
final List<SemanticsDragUpdateCallback> _horizontalHandlers =
<SemanticsDragUpdateCallback>[];
final List<SemanticsDragUpdateCallback> _verticalHandlers =
<SemanticsDragUpdateCallback>[];
// The following getters of handlers return null unless any recognizers
// show interest in the corresponding kind of semantics gestures.
SemanticsTapCallback get onTap {
return _tapHandlers.isEmpty ? null : _handleTap;
}
SemanticsLongPressCallback get onLongPress {
return _longPressHandlers.isEmpty ? null : _handleLongPress;
}
SemanticsDragUpdateCallback get onHorizontalDragUpdate {
return _horizontalHandlers.isEmpty ? null : _handleHorizontalDragUpdate;
}
SemanticsDragUpdateCallback get onVerticalDragUpdate {
return _verticalHandlers.isEmpty ? null : _handleVerticalDragUpdate;
}
// Clear internal cache of lists of callbacks, and build anew from the given
// recognziers.
void buildHandlers(Iterable<GestureRecognizer> recognizers) {
_tapHandlers.clear();
_longPressHandlers.clear();
_horizontalHandlers.clear();
_verticalHandlers.clear();
if (recognizers == null)
return;
for (GestureRecognizer recognizer in recognizers) {
final SemanticsGestureConfiguration configuration =
recognizer.semanticsConfiguration;
if (configuration == null)
continue;
if (configuration.onTap != null)
_tapHandlers.add(configuration.onTap);
if (configuration.onLongPress != null)
_longPressHandlers.add(configuration.onLongPress);
if (configuration.onHorizontalDragUpdate != null)
_horizontalHandlers.add(configuration.onHorizontalDragUpdate);
if (configuration.onVerticalDragUpdate != null)
_verticalHandlers.add(configuration.onVerticalDragUpdate);
}
}
void _handleTap() {
for (SemanticsTapCallback callback in _tapHandlers)
callback();
}
void _handleLongPress() {
for (SemanticsTapCallback callback in _longPressHandlers)
callback();
}
void _handleHorizontalDragUpdate(DragUpdateDetails details) {
for (SemanticsDragUpdateCallback callback in _horizontalHandlers)
callback(details);
}
void _handleVerticalDragUpdate(DragUpdateDetails details) {
for (SemanticsDragUpdateCallback callback in _verticalHandlers)
callback(details);
}
}
/// A widget that detects gestures.
///
/// Attempts to recognize gestures that correspond to its non-null callbacks.
@ -811,6 +892,8 @@ class RawGestureDetector extends StatefulWidget {
/// State for a [RawGestureDetector].
class RawGestureDetectorState extends State<RawGestureDetector> {
Map<Type, GestureRecognizer> _recognizers = const <Type, GestureRecognizer>{};
final _CombinedSemanticsHandlers _semanticsHandlers =
_CombinedSemanticsHandlers();
@override
void initState() {
@ -912,6 +995,7 @@ class RawGestureDetectorState extends State<RawGestureDetector> {
if (!_recognizers.containsKey(type))
oldRecognizers[type].dispose();
}
_semanticsHandlers.buildHandlers(_recognizers.values);
}
void _handlePointerDown(PointerDownEvent event) {
@ -924,92 +1008,6 @@ class RawGestureDetectorState extends State<RawGestureDetector> {
return widget.child == null ? HitTestBehavior.translucent : HitTestBehavior.deferToChild;
}
void _handleSemanticsTap() {
final TapGestureRecognizer recognizer = _recognizers[TapGestureRecognizer];
assert(recognizer != null);
if (recognizer.onTapDown != null)
recognizer.onTapDown(TapDownDetails());
if (recognizer.onTapUp != null)
recognizer.onTapUp(TapUpDetails());
if (recognizer.onTap != null)
recognizer.onTap();
}
void _handleSemanticsLongPress() {
final LongPressGestureRecognizer recognizer = _recognizers[LongPressGestureRecognizer];
assert(recognizer != null);
if (recognizer.onLongPressStart != null)
recognizer.onLongPressStart(const LongPressStartDetails());
if (recognizer.onLongPress != null)
recognizer.onLongPress();
if (recognizer.onLongPressEnd != null)
recognizer.onLongPressEnd(const LongPressEndDetails());
if (recognizer.onLongPressUp != null)
recognizer.onLongPressUp();
}
void _handleSemanticsHorizontalDragUpdate(DragUpdateDetails updateDetails) {
{
final HorizontalDragGestureRecognizer recognizer = _recognizers[HorizontalDragGestureRecognizer];
if (recognizer != null) {
if (recognizer.onDown != null)
recognizer.onDown(DragDownDetails());
if (recognizer.onStart != null)
recognizer.onStart(DragStartDetails());
if (recognizer.onUpdate != null)
recognizer.onUpdate(updateDetails);
if (recognizer.onEnd != null)
recognizer.onEnd(DragEndDetails(primaryVelocity: 0.0));
return;
}
}
{
final PanGestureRecognizer recognizer = _recognizers[PanGestureRecognizer];
if (recognizer != null) {
if (recognizer.onDown != null)
recognizer.onDown(DragDownDetails());
if (recognizer.onStart != null)
recognizer.onStart(DragStartDetails());
if (recognizer.onUpdate != null)
recognizer.onUpdate(updateDetails);
if (recognizer.onEnd != null)
recognizer.onEnd(DragEndDetails());
return;
}
}
}
void _handleSemanticsVerticalDragUpdate(DragUpdateDetails updateDetails) {
{
final VerticalDragGestureRecognizer recognizer = _recognizers[VerticalDragGestureRecognizer];
if (recognizer != null) {
if (recognizer.onDown != null)
recognizer.onDown(DragDownDetails());
if (recognizer.onStart != null)
recognizer.onStart(DragStartDetails());
if (recognizer.onUpdate != null)
recognizer.onUpdate(updateDetails);
if (recognizer.onEnd != null)
recognizer.onEnd(DragEndDetails(primaryVelocity: 0.0));
return;
}
}
{
final PanGestureRecognizer recognizer = _recognizers[PanGestureRecognizer];
if (recognizer != null) {
if (recognizer.onDown != null)
recognizer.onDown(DragDownDetails());
if (recognizer.onStart != null)
recognizer.onStart(DragStartDetails());
if (recognizer.onUpdate != null)
recognizer.onUpdate(updateDetails);
if (recognizer.onEnd != null)
recognizer.onEnd(DragEndDetails());
return;
}
}
}
@override
Widget build(BuildContext context) {
Widget result = Listener(
@ -1068,21 +1066,16 @@ class _GestureSemantics extends SingleChildRenderObjectWidget {
_updateHandlers(renderObject);
}
GestureTapCallback get _onTapHandler {
return owner._recognizers.containsKey(TapGestureRecognizer) ? owner._handleSemanticsTap : null;
SemanticsTapCallback get _onTapHandler {
return owner._semanticsHandlers.onTap;
}
GestureTapCallback get _onLongPressHandler {
return owner._recognizers.containsKey(LongPressGestureRecognizer) ? owner._handleSemanticsLongPress : null;
SemanticsTapCallback get _onLongPressHandler {
return owner._semanticsHandlers.onLongPress;
}
GestureDragUpdateCallback get _onHorizontalDragUpdateHandler {
return owner._recognizers.containsKey(HorizontalDragGestureRecognizer) ||
owner._recognizers.containsKey(PanGestureRecognizer) ? owner._handleSemanticsHorizontalDragUpdate : null;
SemanticsDragUpdateCallback get _onHorizontalDragUpdateHandler {
return owner._semanticsHandlers.onHorizontalDragUpdate;
}
GestureDragUpdateCallback get _onVerticalDragUpdateHandler {
return owner._recognizers.containsKey(VerticalDragGestureRecognizer) ||
owner._recognizers.containsKey(PanGestureRecognizer) ? owner._handleSemanticsVerticalDragUpdate : null;
SemanticsDragUpdateCallback get _onVerticalDragUpdateHandler {
return owner._semanticsHandlers.onVerticalDragUpdate;
}
}

View file

@ -845,4 +845,120 @@ void main() {
tap.dispose();
recognized.clear();
});
group('Semantic gesture configuration:', () {
test('HorizontalDragGestureRecognizer\'s semantic handlers are correctly called', () {
final List<String> logs = <String>[];
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer()
..onDown = (_) { logs.add('onDown'); }
..onStart = (_) { logs.add('onStart'); }
..onUpdate = (_) { logs.add('onUpdate'); }
..onEnd = (_) { logs.add('onEnd'); };
final SemanticsGestureConfiguration configuration = drag.semanticsConfiguration;
expect(configuration, isNotNull);
expect(configuration.onTap, isNull);
expect(configuration.onLongPress, isNull);
expect(configuration.onHorizontalDragUpdate, isNotNull);
expect(configuration.onVerticalDragUpdate, isNull);
configuration.onHorizontalDragUpdate(DragUpdateDetails(
globalPosition: const Offset(11, 13),
));
expect(logs, <String>['onDown', 'onStart', 'onUpdate', 'onEnd']);
logs.clear();
// Assigning handler should update the configuration handler's behavior but
// keep the configuration object
drag..onDown = null
..onStart = null
..onUpdate = null
..onEnd = null;
final SemanticsGestureConfiguration configuration2 = drag.semanticsConfiguration;
expect(configuration, same(configuration2));
configuration.onHorizontalDragUpdate(DragUpdateDetails(
globalPosition: const Offset(12, 14),
));
expect(logs, <String>[]);
});
test('VerticalDragGestureRecognizer\'s semantic handlers are correctly called', () {
final List<String> logs = <String>[];
final VerticalDragGestureRecognizer drag = VerticalDragGestureRecognizer()
..onDown = (_) { logs.add('onDown'); }
..onStart = (_) { logs.add('onStart'); }
..onUpdate = (_) { logs.add('onUpdate'); }
..onEnd = (_) { logs.add('onEnd'); };
final SemanticsGestureConfiguration configuration = drag.semanticsConfiguration;
expect(configuration, isNotNull);
expect(configuration.onTap, isNull);
expect(configuration.onLongPress, isNull);
expect(configuration.onHorizontalDragUpdate, isNull);
expect(configuration.onVerticalDragUpdate, isNotNull);
configuration.onVerticalDragUpdate(DragUpdateDetails(
globalPosition: const Offset(11, 13),
));
expect(logs, <String>['onDown', 'onStart', 'onUpdate', 'onEnd']);
logs.clear();
// Assigning handler should update the configuration handler's behavior but
// keep the configuration object
drag..onDown = null
..onStart = null
..onUpdate = null
..onEnd = null;
final SemanticsGestureConfiguration configuration2 = drag.semanticsConfiguration;
expect(configuration, same(configuration2));
configuration.onVerticalDragUpdate(DragUpdateDetails(
globalPosition: const Offset(12, 14),
));
expect(logs, <String>[]);
});
test('PanGestureRecognizer\'s semantic handlers are correctly called', () {
final List<String> logs = <String>[];
final PanGestureRecognizer drag = PanGestureRecognizer()
..onDown = (_) { logs.add('onDown'); }
..onStart = (_) { logs.add('onStart'); }
..onUpdate = (_) { logs.add('onUpdate'); }
..onEnd = (_) { logs.add('onEnd'); };
final SemanticsGestureConfiguration configuration = drag.semanticsConfiguration;
expect(configuration, isNotNull);
expect(configuration.onTap, isNull);
expect(configuration.onLongPress, isNull);
expect(configuration.onHorizontalDragUpdate, isNotNull);
expect(configuration.onVerticalDragUpdate, isNotNull);
configuration.onVerticalDragUpdate(DragUpdateDetails(
globalPosition: const Offset(11, 13),
));
expect(logs, <String>['onDown', 'onStart', 'onUpdate', 'onEnd']);
logs.clear();
configuration.onHorizontalDragUpdate(DragUpdateDetails(
globalPosition: const Offset(11, 13),
));
expect(logs, <String>['onDown', 'onStart', 'onUpdate', 'onEnd']);
logs.clear();
// Assigning handler should update the configuration handler's behavior but
// keep the configuration object
drag..onDown = null
..onStart = null
..onUpdate = null
..onEnd = null;
final SemanticsGestureConfiguration configuration2 = drag.semanticsConfiguration;
expect(configuration, same(configuration2));
configuration.onVerticalDragUpdate(DragUpdateDetails(
globalPosition: const Offset(12, 14),
));
configuration.onHorizontalDragUpdate(DragUpdateDetails(
globalPosition: const Offset(12, 14),
));
expect(logs, <String>[]);
});
});
}

View file

@ -588,4 +588,35 @@ void main() {
longPress.dispose();
recognized.clear();
});
test('Semantic onLongPress correctly calls handlers', () {
final List<String> logs = <String>[];
final LongPressGestureRecognizer longPress = LongPressGestureRecognizer()
..onLongPressStart = (_) { logs.add('onLongPressStart'); }
..onLongPress = () { logs.add('onLongPress'); }
..onLongPressEnd = (_) { logs.add('onLongPressEnd'); }
..onLongPressUp = () { logs.add('onLongPressUp'); };
final SemanticsGestureConfiguration configuration = longPress.semanticsConfiguration;
expect(configuration, isNotNull);
expect(configuration.onTap, isNull);
expect(configuration.onLongPress, isNotNull);
expect(configuration.onHorizontalDragUpdate, isNull);
expect(configuration.onVerticalDragUpdate, isNull);
configuration.onLongPress();
expect(logs, <String>['onLongPressStart', 'onLongPress', 'onLongPressEnd', 'onLongPressUp']);
logs.clear();
// Assigning handler should update the configuration handler's behavior but
// keep the configuration object
longPress..onLongPressStart = null
..onLongPress = null
..onLongPressEnd = null
..onLongPressUp = null;
final SemanticsGestureConfiguration configuration2 = longPress.semanticsConfiguration;
expect(configuration, same(configuration2));
configuration.onLongPress();
expect(logs, <String>[]);
});
}

View file

@ -799,4 +799,33 @@ void main() {
expect(recognized, <String>['secondaryCancel']);
});
});
test('Semantic onTap correctly calls handlers', () {
final List<String> logs = <String>[];
final TapGestureRecognizer tap = TapGestureRecognizer()
..onTapDown = (_) { logs.add('onTapDown'); }
..onTapUp = (_) { logs.add('onTapUp'); }
..onTap = () { logs.add('onTap'); };
final SemanticsGestureConfiguration configuration = tap.semanticsConfiguration;
expect(configuration, isNotNull);
expect(configuration.onTap, isNotNull);
expect(configuration.onLongPress, isNull);
expect(configuration.onHorizontalDragUpdate, isNull);
expect(configuration.onVerticalDragUpdate, isNull);
configuration.onTap();
expect(logs, <String>['onTapDown', 'onTapUp', 'onTap']);
logs.clear();
// Assigning handler should update the configuration handler's behavior but
// keep the configuration object
tap..onTapDown = null
..onTapUp = null
..onTap = null;
final SemanticsGestureConfiguration configuration2 = tap.semanticsConfiguration;
expect(configuration, same(configuration2));
configuration.onTap();
expect(logs, <String>[]);
});
}

View file

@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/gestures.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
@ -76,4 +77,124 @@ void main() {
semantics.dispose();
});
testWidgets('All registered handlers for the gesture kind are called', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final Set<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:
// - Base state: RawGestureDetector with a HorizontalGR
// - Calling `introduceLayoutPerformer()` adds a `TestLayoutPerformer` as
// child of RawGestureDetector.
// - TestLayoutPerformer calls RawGestureDetector.replaceGestureRecognizers
// during layout phase, which replaces the recognizers with a TapGR.
final Set<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();
});
}
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();
}
}