Add longPress/Tap event to SemanticService (#16945)

This commit is contained in:
Jonah Williams 2018-05-03 11:04:43 -07:00 committed by GitHub
parent 57dd51a301
commit 50bd39a913
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 294 additions and 2 deletions

View file

@ -4,6 +4,8 @@
import 'dart:async';
import 'package:flutter/rendering.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
@ -86,6 +88,7 @@ class Feedback {
/// * [wrapForTap] to trigger platform-specific feedback before executing a
/// [GestureTapCallback].
static Future<Null> forTap(BuildContext context) async {
context.findRenderObject().sendSemanticsEvent(const TapSemanticEvent());
switch (_platform(context)) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
@ -124,6 +127,7 @@ class Feedback {
/// * [wrapForLongPress] to trigger platform-specific feedback before
/// executing a [GestureLongPressCallback].
static Future<Null> forLongPress(BuildContext context) {
context.findRenderObject().sendSemanticsEvent(const LongPressSemanticsEvent());
switch (_platform(context)) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:

View file

@ -285,6 +285,7 @@ abstract class RenderToggleable extends RenderConstrainedBox {
onChanged(false);
break;
}
sendSemanticsEvent(const TapSemanticEvent());
}
void _handleTapUp(TapUpDetails details) {

View file

@ -2165,6 +2165,25 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
// Nothing to do by default.
}
/// Sends a [SemanticsEvent] associated with this render object's [SemanticsNode].
///
/// If this render object has no semantics information, the first parent
/// render object with a non-null semantic node is used.
///
/// If semantics are disabled, no events are dispatched.
///
/// See [SemanticsNode.sendEvent] for a full description of the behavior.
void sendSemanticsEvent(SemanticsEvent semanticsEvent) {
if (owner.semanticsOwner == null)
return;
if (_semantics != null) {
_semantics.sendEvent(semanticsEvent);
} else if (parent != null) {
final RenderObject renderParent = parent;
renderParent.sendSemanticsEvent(semanticsEvent);
}
}
// Use [_semanticsConfiguration] to access.
SemanticsConfiguration _cachedSemanticsConfiguration;

View file

@ -90,7 +90,7 @@ class AnnounceSemanticsEvent extends SemanticsEvent {
}
/// An event for a semantic announcement of a tooltip.
///
///
/// This is only used by Android to announce tooltip values.
class TooltipSemanticsEvent extends SemanticsEvent {
@ -107,3 +107,29 @@ class TooltipSemanticsEvent extends SemanticsEvent {
};
}
}
/// An event which triggers long press semantic feedback.
///
/// Currently only honored on Android. Triggers a long-press specific sound
/// when TalkBack is enabled.
class LongPressSemanticsEvent extends SemanticsEvent {
/// Constructs an event that triggers a long-press semantic feedback by the platform.
const LongPressSemanticsEvent() : super('longPress');
@override
Map<String, dynamic> getDataMap() => const <String, dynamic>{};
}
/// An event which triggers tap semantic feedback.
///
/// Currently only honored on Android. Triggers a tap specific sound when
/// TalkBack is enabled.
class TapSemanticEvent extends SemanticsEvent {
/// Constructs an event that triggers a long-press semantic feedback by the platform.
const TapSemanticEvent() : super('tap');
@override
Map<String, dynamic> getDataMap() => const <String, dynamic>{};
}

View file

@ -34,7 +34,8 @@ class SemanticsService {
/// Sends a semantic announcement of a tooltip.
///
/// This is only used by Android.
/// Currently only honored on Android. The contents of [message] will be
/// read by TalkBack.
static Future<Null> tooltip(String message) async {
final TooltipSemanticsEvent event = new TooltipSemanticsEvent(message);
await SystemChannels.accessibility.send(event.toMap());

View file

@ -5,6 +5,7 @@
import 'dart:ui';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
@ -185,4 +186,44 @@ void main() {
await tester.pumpAndSettle();
expect(checkBoxValue, null);
});
testWidgets('has semantic events', (WidgetTester tester) async {
dynamic semanticEvent;
bool checkboxValue = false;
SystemChannels.accessibility.setMockMessageHandler((dynamic message) {
semanticEvent = message;
});
final SemanticsTester semanticsTester = new SemanticsTester(tester);
await tester.pumpWidget(
new Material(
child: new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return new Checkbox(
value: checkboxValue,
onChanged: (bool value) {
setState(() {
checkboxValue = value;
});
},
);
},
),
),
);
await tester.tap(find.byType(Checkbox));
final RenderObject object = tester.firstRenderObject(find.byType(Checkbox));
expect(checkboxValue, true);
expect(semanticEvent, <String, dynamic>{
'type': 'tap',
'nodeId': object.debugSemantics.id,
'data': <String, dynamic>{},
});
expect(object.debugSemantics.getSemanticsData().hasAction(SemanticsAction.tap), true);
SystemChannels.accessibility.setMockMessageHandler(null);
semanticsTester.dispose();
});
}

View file

@ -1,10 +1,15 @@
import 'dart:ui';
// Copyright 2017 The Chromium 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 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../widgets/semantics_tester.dart';
import 'feedback_tester.dart';
void main () {
@ -21,8 +26,23 @@ void main () {
});
group('Feedback on Android', () {
List<Map<String, Object>> semanticEvents;
setUp(() {
semanticEvents = <Map<String, Object>>[];
SystemChannels.accessibility.setMockMessageHandler((dynamic message) {
final Map<dynamic, dynamic> typedMessage = message;
semanticEvents.add(typedMessage.cast<String, Object>());
});
});
tearDown(() {
SystemChannels.accessibility.setMockMessageHandler(null);
});
testWidgets('forTap', (WidgetTester tester) async {
final SemanticsTester semanticsTester = new SemanticsTester(tester);
await tester.pumpWidget(new TestWidget(
tapHandler: (BuildContext context) {
return () => Feedback.forTap(context);
@ -31,14 +51,27 @@ void main () {
await tester.pumpAndSettle(kWaitDuration);
expect(feedback.hapticCount, 0);
expect(feedback.clickSoundCount, 0);
expect(semanticEvents, isEmpty);
await tester.tap(find.text('X'));
await tester.pumpAndSettle(kWaitDuration);
final RenderObject object = tester.firstRenderObject(find.byType(GestureDetector));
expect(feedback.hapticCount, 0);
expect(feedback.clickSoundCount, 1);
expect(semanticEvents.single, <String, dynamic>{
'type': 'tap',
'nodeId': object.debugSemantics.id,
'data': <String, dynamic>{},
});
expect(object.debugSemantics.getSemanticsData().hasAction(SemanticsAction.tap), true);
semanticsTester.dispose();
});
testWidgets('forTap Wrapper', (WidgetTester tester) async {
final SemanticsTester semanticsTester = new SemanticsTester(tester);
int callbackCount = 0;
final VoidCallback callback = () {
callbackCount++;
@ -49,6 +82,7 @@ void main () {
return Feedback.wrapForTap(callback, context);
},
));
await tester.pumpAndSettle(kWaitDuration);
expect(feedback.hapticCount, 0);
expect(feedback.clickSoundCount, 0);
@ -56,12 +90,24 @@ void main () {
await tester.tap(find.text('X'));
await tester.pumpAndSettle(kWaitDuration);
final RenderObject object = tester.firstRenderObject(find.byType(GestureDetector));
expect(feedback.hapticCount, 0);
expect(feedback.clickSoundCount, 1);
expect(callbackCount, 1);
expect(semanticEvents.single, <String, dynamic>{
'type': 'tap',
'nodeId': object.debugSemantics.id,
'data': <String, dynamic>{},
});
expect(object.debugSemantics.getSemanticsData().hasAction(SemanticsAction.tap), true);
semanticsTester.dispose();
});
testWidgets('forLongPress', (WidgetTester tester) async {
final SemanticsTester semanticsTester = new SemanticsTester(tester);
await tester.pumpWidget(new TestWidget(
longPressHandler: (BuildContext context) {
return () => Feedback.forLongPress(context);
@ -73,11 +119,23 @@ void main () {
await tester.longPress(find.text('X'));
await tester.pumpAndSettle(kWaitDuration);
final RenderObject object = tester.firstRenderObject(find.byType(GestureDetector));
expect(feedback.hapticCount, 1);
expect(feedback.clickSoundCount, 0);
expect(semanticEvents.single, <String, dynamic>{
'type': 'longPress',
'nodeId': object.debugSemantics.id,
'data': <String, dynamic>{},
});
expect(object.debugSemantics.getSemanticsData().hasAction(SemanticsAction.longPress), true);
semanticsTester.dispose();
});
testWidgets('forLongPress Wrapper', (WidgetTester tester) async {
final SemanticsTester semanticsTester = new SemanticsTester(tester);
int callbackCount = 0;
final VoidCallback callback = () {
callbackCount++;
@ -89,15 +147,26 @@ void main () {
},
));
await tester.pumpAndSettle(kWaitDuration);
final RenderObject object = tester.firstRenderObject(find.byType(GestureDetector));
expect(feedback.hapticCount, 0);
expect(feedback.clickSoundCount, 0);
expect(callbackCount, 0);
await tester.longPress(find.text('X'));
await tester.pumpAndSettle(kWaitDuration);
expect(feedback.hapticCount, 1);
expect(feedback.clickSoundCount, 0);
expect(callbackCount, 1);
expect(semanticEvents.single, <String, dynamic>{
'type': 'longPress',
'nodeId': object.debugSemantics.id,
'data': <String, dynamic>{},
});
expect(object.debugSemantics.getSemanticsData().hasAction(SemanticsAction.longPress), true);
semanticsTester.dispose();
});
});

View file

@ -5,6 +5,7 @@
import 'dart:ui';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
@ -162,4 +163,39 @@ void main() {
semantics.dispose();
});
testWidgets('has semantic events', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
final Key key = new UniqueKey();
dynamic semanticEvent;
int radioValue = 2;
SystemChannels.accessibility.setMockMessageHandler((dynamic message) {
semanticEvent = message;
});
await tester.pumpWidget(new Material(
child: new Radio<int>(
key: key,
value: 1,
groupValue: radioValue,
onChanged: (int i) {
radioValue = i;
},
),
));
await tester.tap(find.byKey(key));
final RenderObject object = tester.firstRenderObject(find.byKey(key));
expect(radioValue, 1);
expect(semanticEvent, <String, dynamic>{
'type': 'tap',
'nodeId': object.debugSemantics.id,
'data': <String, dynamic>{},
});
expect(object.debugSemantics.getSemanticsData().hasAction(SemanticsAction.tap), true);
semantics.dispose();
SystemChannels.accessibility.setMockMessageHandler(null);
});
}

View file

@ -4,9 +4,11 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
import '../widgets/semantics_tester.dart';
void main() {
testWidgets('Switch can toggle on tap', (WidgetTester tester) async {
@ -187,4 +189,48 @@ void main() {
..circle(color: Colors.red[500])
);
});
testWidgets('switch has semantic events', (WidgetTester tester) async {
dynamic semanticEvent;
bool value = false;
SystemChannels.accessibility.setMockMessageHandler((dynamic message) {
semanticEvent = message;
});
final SemanticsTester semanticsTester = new SemanticsTester(tester);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return new Material(
child: new Center(
child: new Switch(
value: value,
onChanged: (bool newValue) {
setState(() {
value = newValue;
});
},
),
),
);
},
),
),
);
await tester.tap(find.byType(Switch));
final RenderObject object = tester.firstRenderObject(find.byType(Switch));
expect(value, true);
expect(semanticEvent, <String, dynamic>{
'type': 'tap',
'nodeId': object.debugSemantics.id,
'data': <String, dynamic>{},
});
expect(object.debugSemantics.getSemanticsData().hasAction(SemanticsAction.tap), true);
semanticsTester.dispose();
SystemChannels.accessibility.setMockMessageHandler(null);
});
}

View file

@ -1,5 +1,6 @@
import 'dart:ui';
import 'package:flutter/services.dart';
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
@ -605,6 +606,48 @@ void main() {
feedback.dispose();
});
testWidgets('has semantic events', (WidgetTester tester) async {
final List<dynamic> semanticEvents = <dynamic>[];
SystemChannels.accessibility.setMockMessageHandler((dynamic message) {
semanticEvents.add(message);
});
final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget(
new MaterialApp(
home: new Center(
child: new Tooltip(
message: 'Foo',
child: new Container(
width: 100.0,
height: 100.0,
color: Colors.green[500],
),
),
),
),
);
await tester.longPress(find.byType(Tooltip));
final RenderObject object = tester.firstRenderObject(find.byType(Tooltip));
expect(semanticEvents, unorderedEquals(<dynamic>[
<String, dynamic>{
'type': 'longPress',
'nodeId': findDebugSemantics(object).id,
'data': <String, dynamic>{},
},
<String, dynamic>{
'type': 'tooltip',
'data': <String, dynamic>{
'message': 'Foo',
},
},
]));
semantics.dispose();
SystemChannels.accessibility.setMockMessageHandler(null);
});
testWidgets('Semantics included', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
@ -677,3 +720,9 @@ void main() {
});
}
SemanticsNode findDebugSemantics(RenderObject object) {
if (object.debugSemantics != null)
return object.debugSemantics;
return findDebugSemantics(object.parent);
}