mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Add announce support to the engine (#169685)
Partly of https://github.com/flutter/flutter/issues/165510 ⤵️ Child PR: https://github.com/flutter/flutter/pull/168992 Partly re-lands https://github.com/flutter/flutter/pull/165531 The PR was originally reverted due to an issue with an internal Google test. I split re-land PR into two separate ones so that we can individually revert in case it fails again. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
parent
5c4edc244f
commit
694600a9f8
@ -931,6 +931,7 @@ class AccessibilityFeatures {
|
||||
static const int _kReduceMotionIndex = 1 << 4;
|
||||
static const int _kHighContrastIndex = 1 << 5;
|
||||
static const int _kOnOffSwitchLabelsIndex = 1 << 6;
|
||||
static const int _kNoAnnounceIndex = 1 << 7;
|
||||
|
||||
// A bitfield which represents each enabled feature.
|
||||
final int _index;
|
||||
@ -968,6 +969,20 @@ class AccessibilityFeatures {
|
||||
/// Only supported on iOS.
|
||||
bool get onOffSwitchLabels => _kOnOffSwitchLabelsIndex & _index != 0;
|
||||
|
||||
/// Whether accessibility announcements (like [SemanticsService.announce])
|
||||
/// are supported on the current platform.
|
||||
///
|
||||
/// Returns `false` on platforms where announcements are deprecated or
|
||||
/// unsupported by the underlying platform.
|
||||
///
|
||||
/// Returns `true` on platforms where such announcements are
|
||||
/// generally supported without discouragement. (iOS, web etc)
|
||||
///
|
||||
/// Use this flag to conditionally avoid making announcements on Android.
|
||||
// This index check is inverted (== 0 vs != 0); far more platforms support
|
||||
// "announce" than discourage it.
|
||||
bool get announce => _kNoAnnounceIndex & _index == 0;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final List<String> features = <String>[];
|
||||
@ -992,6 +1007,9 @@ class AccessibilityFeatures {
|
||||
if (onOffSwitchLabels) {
|
||||
features.add('onOffSwitchLabels');
|
||||
}
|
||||
if (announce) {
|
||||
features.add('announce');
|
||||
}
|
||||
return 'AccessibilityFeatures$features';
|
||||
}
|
||||
|
||||
|
@ -48,6 +48,7 @@ enum class AccessibilityFeatureFlag : int32_t {
|
||||
kReduceMotion = 1 << 4,
|
||||
kHighContrast = 1 << 5,
|
||||
kOnOffSwitchLabels = 1 << 6,
|
||||
kNoAnnounce = 1 << 7,
|
||||
};
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
@ -54,6 +54,7 @@ class EngineAccessibilityFeatures implements ui.AccessibilityFeatures {
|
||||
static const int _kReduceMotionIndex = 1 << 4;
|
||||
static const int _kHighContrastIndex = 1 << 5;
|
||||
static const int _kOnOffSwitchLabelsIndex = 1 << 6;
|
||||
static const int _kNoAnnounceIndex = 1 << 7;
|
||||
|
||||
// A bitfield which represents each enabled feature.
|
||||
final int _index;
|
||||
@ -72,6 +73,10 @@ class EngineAccessibilityFeatures implements ui.AccessibilityFeatures {
|
||||
bool get highContrast => _kHighContrastIndex & _index != 0;
|
||||
@override
|
||||
bool get onOffSwitchLabels => _kOnOffSwitchLabelsIndex & _index != 0;
|
||||
// This index check is inverted (== 0 vs != 0); far more platforms support
|
||||
// "announce" than discourage it.
|
||||
@override
|
||||
bool get announce => _kNoAnnounceIndex & _index == 0;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@ -97,6 +102,9 @@ class EngineAccessibilityFeatures implements ui.AccessibilityFeatures {
|
||||
if (onOffSwitchLabels) {
|
||||
features.add('onOffSwitchLabels');
|
||||
}
|
||||
if (announce) {
|
||||
features.add('announce');
|
||||
}
|
||||
return 'AccessibilityFeatures$features';
|
||||
}
|
||||
|
||||
@ -119,6 +127,7 @@ class EngineAccessibilityFeatures implements ui.AccessibilityFeatures {
|
||||
bool? reduceMotion,
|
||||
bool? highContrast,
|
||||
bool? onOffSwitchLabels,
|
||||
bool? announce,
|
||||
}) {
|
||||
final EngineAccessibilityFeaturesBuilder builder = EngineAccessibilityFeaturesBuilder(0);
|
||||
|
||||
@ -129,6 +138,7 @@ class EngineAccessibilityFeatures implements ui.AccessibilityFeatures {
|
||||
builder.reduceMotion = reduceMotion ?? this.reduceMotion;
|
||||
builder.highContrast = highContrast ?? this.highContrast;
|
||||
builder.onOffSwitchLabels = onOffSwitchLabels ?? this.onOffSwitchLabels;
|
||||
builder.announce = announce ?? this.announce;
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
@ -146,6 +156,9 @@ class EngineAccessibilityFeaturesBuilder {
|
||||
bool get reduceMotion => EngineAccessibilityFeatures._kReduceMotionIndex & _index != 0;
|
||||
bool get highContrast => EngineAccessibilityFeatures._kHighContrastIndex & _index != 0;
|
||||
bool get onOffSwitchLabels => EngineAccessibilityFeatures._kOnOffSwitchLabelsIndex & _index != 0;
|
||||
// This index check is inverted (== 0 vs != 0); far more platforms support
|
||||
// "announce" than discourage it.
|
||||
bool get announce => EngineAccessibilityFeatures._kNoAnnounceIndex & _index == 0;
|
||||
|
||||
set accessibleNavigation(bool value) {
|
||||
const int accessibleNavigation = EngineAccessibilityFeatures._kAccessibleNavigation;
|
||||
@ -182,6 +195,12 @@ class EngineAccessibilityFeaturesBuilder {
|
||||
_index = value ? _index | onOffSwitchLabels : _index & ~onOffSwitchLabels;
|
||||
}
|
||||
|
||||
set announce(bool value) {
|
||||
const int noAnnounce = EngineAccessibilityFeatures._kNoAnnounceIndex;
|
||||
// Since we are using noAnnounce for the embedder, we need to flip the value.
|
||||
_index = !value ? _index | noAnnounce : _index & ~noAnnounce;
|
||||
}
|
||||
|
||||
/// Creates and returns an instance of EngineAccessibilityFeatures based on the value of _index
|
||||
EngineAccessibilityFeatures build() {
|
||||
return EngineAccessibilityFeatures(_index);
|
||||
|
@ -115,6 +115,7 @@ abstract class AccessibilityFeatures {
|
||||
bool get reduceMotion;
|
||||
bool get highContrast;
|
||||
bool get onOffSwitchLabels;
|
||||
bool get announce;
|
||||
}
|
||||
|
||||
enum Brightness { dark, light }
|
||||
|
@ -299,6 +299,14 @@ void _testEngineAccessibilityBuilder() {
|
||||
expect(features.onOffSwitchLabels, isTrue);
|
||||
});
|
||||
|
||||
test('announce', () {
|
||||
// By default this starts off true, see EngineAccessibilityFeatures.announce
|
||||
expect(features.announce, isTrue);
|
||||
builder.announce = false;
|
||||
features = builder.build();
|
||||
expect(features.announce, isFalse);
|
||||
});
|
||||
|
||||
test('reduce motion', () {
|
||||
expect(features.reduceMotion, isFalse);
|
||||
builder.reduceMotion = true;
|
||||
@ -391,7 +399,10 @@ void _testEngineSemanticsOwner() {
|
||||
});
|
||||
|
||||
test('accessibilityFeatures copyWith function works', () {
|
||||
const EngineAccessibilityFeatures original = EngineAccessibilityFeatures(0);
|
||||
// Announce is an inverted check, see EngineAccessibilityFeatures.announce.
|
||||
// Therefore, we need to ensure that the original copy starts with false (1 << 7).
|
||||
const EngineAccessibilityFeatures original = EngineAccessibilityFeatures(0 | 1 << 7);
|
||||
|
||||
EngineAccessibilityFeatures copy = original.copyWith(accessibleNavigation: true);
|
||||
expect(copy.accessibleNavigation, true);
|
||||
expect(copy.boldText, false);
|
||||
@ -399,6 +410,7 @@ void _testEngineSemanticsOwner() {
|
||||
expect(copy.highContrast, false);
|
||||
expect(copy.invertColors, false);
|
||||
expect(copy.onOffSwitchLabels, false);
|
||||
expect(copy.announce, false);
|
||||
expect(copy.reduceMotion, false);
|
||||
|
||||
copy = original.copyWith(boldText: true);
|
||||
@ -417,6 +429,7 @@ void _testEngineSemanticsOwner() {
|
||||
expect(copy.highContrast, false);
|
||||
expect(copy.invertColors, false);
|
||||
expect(copy.onOffSwitchLabels, false);
|
||||
expect(copy.announce, false);
|
||||
expect(copy.reduceMotion, false);
|
||||
|
||||
copy = original.copyWith(highContrast: true);
|
||||
@ -426,6 +439,7 @@ void _testEngineSemanticsOwner() {
|
||||
expect(copy.highContrast, true);
|
||||
expect(copy.invertColors, false);
|
||||
expect(copy.onOffSwitchLabels, false);
|
||||
expect(copy.announce, false);
|
||||
expect(copy.reduceMotion, false);
|
||||
|
||||
copy = original.copyWith(invertColors: true);
|
||||
@ -435,6 +449,7 @@ void _testEngineSemanticsOwner() {
|
||||
expect(copy.highContrast, false);
|
||||
expect(copy.invertColors, true);
|
||||
expect(copy.onOffSwitchLabels, false);
|
||||
expect(copy.announce, false);
|
||||
expect(copy.reduceMotion, false);
|
||||
|
||||
copy = original.copyWith(onOffSwitchLabels: true);
|
||||
@ -446,6 +461,16 @@ void _testEngineSemanticsOwner() {
|
||||
expect(copy.onOffSwitchLabels, true);
|
||||
expect(copy.reduceMotion, false);
|
||||
|
||||
copy = original.copyWith(announce: true);
|
||||
expect(copy.accessibleNavigation, false);
|
||||
expect(copy.boldText, false);
|
||||
expect(copy.disableAnimations, false);
|
||||
expect(copy.highContrast, false);
|
||||
expect(copy.invertColors, false);
|
||||
expect(copy.onOffSwitchLabels, false);
|
||||
expect(copy.announce, true);
|
||||
expect(copy.reduceMotion, false);
|
||||
|
||||
copy = original.copyWith(reduceMotion: true);
|
||||
expect(copy.accessibleNavigation, false);
|
||||
expect(copy.boldText, false);
|
||||
@ -453,6 +478,7 @@ void _testEngineSemanticsOwner() {
|
||||
expect(copy.highContrast, false);
|
||||
expect(copy.invertColors, false);
|
||||
expect(copy.onOffSwitchLabels, false);
|
||||
expect(copy.announce, false);
|
||||
expect(copy.reduceMotion, true);
|
||||
});
|
||||
|
||||
|
@ -490,6 +490,7 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
|
||||
this.accessibilityManager.addTouchExplorationStateChangeListener(
|
||||
touchExplorationStateChangeListener);
|
||||
|
||||
accessibilityFeatureFlags |= AccessibilityFeature.NO_ANNOUNCE.value;
|
||||
// Tell Flutter whether animations should initially be enabled or disabled. Then register a
|
||||
// listener to be notified of changes in the future.
|
||||
animationScaleObserver.onChange(false);
|
||||
@ -2174,7 +2175,8 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
|
||||
BOLD_TEXT(1 << 3), // NOT SUPPORTED
|
||||
REDUCE_MOTION(1 << 4), // NOT SUPPORTED
|
||||
HIGH_CONTRAST(1 << 5), // NOT SUPPORTED
|
||||
ON_OFF_SWITCH_LABELS(1 << 6); // NOT SUPPORTED
|
||||
ON_OFF_SWITCH_LABELS(1 << 6), // NOT SUPPORTED
|
||||
NO_ANNOUNCE(1 << 7);
|
||||
|
||||
final int value;
|
||||
|
||||
|
@ -66,6 +66,11 @@ import org.robolectric.annotation.Config;
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class AccessibilityBridgeTest {
|
||||
|
||||
private static final int ACCESSIBILITY_FEATURE_NAVIGATION = 1 << 0;
|
||||
private static final int ACCESSIBILITY_FEATURE_DISABLE_ANIMATIONS = 1 << 2;
|
||||
private static final int ACCESSIBILITY_FEATURE_BOLD_TEXT = 1 << 3;
|
||||
private static final int ACCESSIBILITY_FEATURE_NO_ANNOUNCE = 1 << 7;
|
||||
|
||||
@Test
|
||||
public void itDescribesNonTextFieldsWithAContentDescription() {
|
||||
AccessibilityBridge accessibilityBridge = setUpBridge();
|
||||
@ -135,6 +140,26 @@ public class AccessibilityBridgeTest {
|
||||
assertEquals(position, outBoundsInScreen.top);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void itSetsNoAnnounceAccessibleFlagByDefault() {
|
||||
AccessibilityChannel mockChannel = mock(AccessibilityChannel.class);
|
||||
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
|
||||
AccessibilityManager mockManager = mock(AccessibilityManager.class);
|
||||
View mockRootView = mock(View.class);
|
||||
Context context = mock(Context.class);
|
||||
when(mockRootView.getContext()).thenReturn(context);
|
||||
when(context.getPackageName()).thenReturn("test");
|
||||
when(mockManager.isTouchExplorationEnabled()).thenReturn(false);
|
||||
setUpBridge(
|
||||
/*rootAccessibilityView=*/ mockRootView,
|
||||
/*accessibilityChannel=*/ mockChannel,
|
||||
/*accessibilityManager=*/ mockManager,
|
||||
/*contentResolver=*/ null,
|
||||
/*accessibilityViewEmbedder=*/ mockViewEmbedder,
|
||||
/*platformViewsAccessibilityDelegate=*/ null);
|
||||
verify(mockChannel).setAccessibilityFeatures(ACCESSIBILITY_FEATURE_NO_ANNOUNCE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void itSetsAccessibleNavigation() {
|
||||
AccessibilityChannel mockChannel = mock(AccessibilityChannel.class);
|
||||
@ -158,18 +183,20 @@ public class AccessibilityBridgeTest {
|
||||
verify(mockManager).addTouchExplorationStateChangeListener(listenerCaptor.capture());
|
||||
|
||||
assertEquals(accessibilityBridge.getAccessibleNavigation(), false);
|
||||
verify(mockChannel).setAccessibilityFeatures(0);
|
||||
verify(mockChannel).setAccessibilityFeatures(ACCESSIBILITY_FEATURE_NO_ANNOUNCE);
|
||||
reset(mockChannel);
|
||||
|
||||
// Simulate assistive technology accessing accessibility tree.
|
||||
accessibilityBridge.createAccessibilityNodeInfo(0);
|
||||
verify(mockChannel).setAccessibilityFeatures(1);
|
||||
verify(mockChannel)
|
||||
.setAccessibilityFeatures(
|
||||
ACCESSIBILITY_FEATURE_NAVIGATION | ACCESSIBILITY_FEATURE_NO_ANNOUNCE);
|
||||
assertEquals(accessibilityBridge.getAccessibleNavigation(), true);
|
||||
|
||||
// Simulate turning off TalkBack.
|
||||
reset(mockChannel);
|
||||
listenerCaptor.getValue().onTouchExplorationStateChanged(false);
|
||||
verify(mockChannel).setAccessibilityFeatures(0);
|
||||
verify(mockChannel).setAccessibilityFeatures(ACCESSIBILITY_FEATURE_NO_ANNOUNCE);
|
||||
assertEquals(accessibilityBridge.getAccessibleNavigation(), false);
|
||||
}
|
||||
|
||||
@ -1157,7 +1184,9 @@ public class AccessibilityBridgeTest {
|
||||
/*accessibilityViewEmbedder=*/ mockViewEmbedder,
|
||||
/*platformViewsAccessibilityDelegate=*/ null);
|
||||
|
||||
verify(mockChannel).setAccessibilityFeatures(1 << 3);
|
||||
verify(mockChannel)
|
||||
.setAccessibilityFeatures(
|
||||
ACCESSIBILITY_FEATURE_BOLD_TEXT | ACCESSIBILITY_FEATURE_NO_ANNOUNCE);
|
||||
reset(mockChannel);
|
||||
|
||||
// Now verify that clearing the BOLD_TEXT flag doesn't touch any of the other flags.
|
||||
@ -1179,7 +1208,9 @@ public class AccessibilityBridgeTest {
|
||||
// constructor, verify that the latest argument is correct
|
||||
ArgumentCaptor<Integer> captor = ArgumentCaptor.forClass(Integer.class);
|
||||
verify(mockChannel, atLeastOnce()).setAccessibilityFeatures(captor.capture());
|
||||
assertEquals(1 << 2 /* DISABLE_ANIMATION */, captor.getValue().intValue());
|
||||
assertEquals(
|
||||
ACCESSIBILITY_FEATURE_DISABLE_ANIMATIONS | ACCESSIBILITY_FEATURE_NO_ANNOUNCE,
|
||||
captor.getValue().intValue());
|
||||
|
||||
// Set back to default
|
||||
Settings.Global.putFloat(null, "transition_animation_scale", 1.0f);
|
||||
@ -1874,19 +1905,21 @@ public class AccessibilityBridgeTest {
|
||||
ContentObserver observer = observerCaptor.getValue();
|
||||
|
||||
// Initial state
|
||||
verify(mockChannel).setAccessibilityFeatures(0);
|
||||
verify(mockChannel).setAccessibilityFeatures(ACCESSIBILITY_FEATURE_NO_ANNOUNCE);
|
||||
reset(mockChannel);
|
||||
|
||||
// Animations are disabled
|
||||
Settings.Global.putFloat(mockContentResolver, "transition_animation_scale", 0.0f);
|
||||
observer.onChange(false);
|
||||
verify(mockChannel).setAccessibilityFeatures(1 << 2);
|
||||
verify(mockChannel)
|
||||
.setAccessibilityFeatures(
|
||||
ACCESSIBILITY_FEATURE_DISABLE_ANIMATIONS | ACCESSIBILITY_FEATURE_NO_ANNOUNCE);
|
||||
reset(mockChannel);
|
||||
|
||||
// Animations are enabled
|
||||
Settings.Global.putFloat(mockContentResolver, "transition_animation_scale", 1.0f);
|
||||
observer.onChange(false);
|
||||
verify(mockChannel).setAccessibilityFeatures(0);
|
||||
verify(mockChannel).setAccessibilityFeatures(ACCESSIBILITY_FEATURE_NO_ANNOUNCE);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -105,6 +105,8 @@ typedef enum {
|
||||
kFlutterAccessibilityFeatureHighContrast = 1 << 5,
|
||||
/// Request to show on/off labels inside switches.
|
||||
kFlutterAccessibilityFeatureOnOffSwitchLabels = 1 << 6,
|
||||
/// Indicate the platform does not support announcements.
|
||||
kFlutterAccessibilityFeatureNoAnnounce = 1 << 7,
|
||||
} FlutterAccessibilityFeature;
|
||||
|
||||
/// The set of possible actions that can be conveyed to a semantics node.
|
||||
|
@ -32,6 +32,7 @@ class FakeAccessibilityFeatures implements AccessibilityFeatures {
|
||||
this.reduceMotion = false,
|
||||
this.highContrast = false,
|
||||
this.onOffSwitchLabels = false,
|
||||
this.announce = false,
|
||||
});
|
||||
|
||||
/// An instance of [AccessibilityFeatures] where all the features are enabled.
|
||||
@ -43,6 +44,7 @@ class FakeAccessibilityFeatures implements AccessibilityFeatures {
|
||||
reduceMotion: true,
|
||||
highContrast: true,
|
||||
onOffSwitchLabels: true,
|
||||
announce: true,
|
||||
);
|
||||
|
||||
@override
|
||||
@ -66,6 +68,9 @@ class FakeAccessibilityFeatures implements AccessibilityFeatures {
|
||||
@override
|
||||
final bool onOffSwitchLabels;
|
||||
|
||||
@override
|
||||
final bool announce;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other.runtimeType != runtimeType) {
|
||||
@ -78,7 +83,8 @@ class FakeAccessibilityFeatures implements AccessibilityFeatures {
|
||||
other.boldText == boldText &&
|
||||
other.reduceMotion == reduceMotion &&
|
||||
other.highContrast == highContrast &&
|
||||
other.onOffSwitchLabels == onOffSwitchLabels;
|
||||
other.onOffSwitchLabels == onOffSwitchLabels &&
|
||||
other.announce == announce;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -91,6 +97,7 @@ class FakeAccessibilityFeatures implements AccessibilityFeatures {
|
||||
reduceMotion,
|
||||
highContrast,
|
||||
onOffSwitchLabels,
|
||||
announce,
|
||||
);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user