mirror of
https://github.com/flutter/flutter
synced 2024-09-21 09:11:56 +00:00
Add longPress/Tap event to SemanticService (#16945)
This commit is contained in:
parent
57dd51a301
commit
50bd39a913
|
@ -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:
|
||||
|
|
|
@ -285,6 +285,7 @@ abstract class RenderToggleable extends RenderConstrainedBox {
|
|||
onChanged(false);
|
||||
break;
|
||||
}
|
||||
sendSemanticsEvent(const TapSemanticEvent());
|
||||
}
|
||||
|
||||
void _handleTapUp(TapUpDetails details) {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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>{};
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
Loading…
Reference in a new issue