diff --git a/lib/ui/semantics.dart b/lib/ui/semantics.dart index 75df7db4f21f7..6f33669e6f473 100644 --- a/lib/ui/semantics.dart +++ b/lib/ui/semantics.dart @@ -290,6 +290,7 @@ class SemanticsFlag { static const int _kHasImplicitScrollingIndex = 1 << 18; static const int _kIsMultilineIndex = 1 << 19; static const int _kIsReadOnlyIndex = 1 << 20; + static const int _kIsFocusableIndex = 1 << 21; const SemanticsFlag._(this.index); @@ -348,6 +349,11 @@ class SemanticsFlag { /// Only applicable when [isTextField] is true. static const SemanticsFlag isReadOnly = SemanticsFlag._(_kIsReadOnlyIndex); + /// Whether the semantic node is able to hold the user's focus. + /// + /// The focused element is usually the current receiver of keyboard inputs. + static const SemanticsFlag isFocusable = SemanticsFlag._(_kIsFocusableIndex); + /// Whether the semantic node currently holds the user's focus. /// /// The focused element is usually the current receiver of keyboard inputs. diff --git a/lib/ui/semantics/semantics_node.h b/lib/ui/semantics/semantics_node.h index bf3d3beaf2a53..c135b58b05501 100644 --- a/lib/ui/semantics/semantics_node.h +++ b/lib/ui/semantics/semantics_node.h @@ -72,6 +72,7 @@ enum class SemanticsFlags : int32_t { // The Dart API defines the following flag but it isn't used in iOS. // kIsMultiline = 1 << 19, kIsReadOnly = 1 << 20, + kIsFocusable = 1 << 21, }; const int kScrollableSemanticsFlags = diff --git a/lib/web_ui/lib/src/ui/semantics.dart b/lib/web_ui/lib/src/ui/semantics.dart index 3a9a163291432..c13892dc68cef 100644 --- a/lib/web_ui/lib/src/ui/semantics.dart +++ b/lib/web_ui/lib/src/ui/semantics.dart @@ -293,6 +293,7 @@ class SemanticsFlag { static const int _kHasImplicitScrollingIndex = 1 << 18; static const int _kIsMultilineIndex = 1 << 19; static const int _kIsReadOnlyIndex = 1 << 20; + static const int _kIsFocusableIndex = 1 << 21; const SemanticsFlag._(this.index); @@ -351,6 +352,11 @@ class SemanticsFlag { /// Only applicable when [isTextField] is true. static const SemanticsFlag isReadOnly = SemanticsFlag._(_kIsReadOnlyIndex); + /// Whether the semantic node is able to hold the user's focus. + /// + /// The focused element is usually the current receiver of keyboard inputs. + static const SemanticsFlag isFocusable = SemanticsFlag._(_kIsFocusableIndex); + /// Whether the semantic node currently holds the user's focus. /// /// The focused element is usually the current receiver of keyboard inputs. @@ -517,6 +523,7 @@ class SemanticsFlag { _kIsSelectedIndex: isSelected, _kIsButtonIndex: isButton, _kIsTextFieldIndex: isTextField, + _kIsFocusableIndex: isFocusable, _kIsFocusedIndex: isFocused, _kHasEnabledStateIndex: hasEnabledState, _kIsEnabledIndex: isEnabled, @@ -548,6 +555,8 @@ class SemanticsFlag { return 'SemanticsFlag.isButton'; case _kIsTextFieldIndex: return 'SemanticsFlag.isTextField'; + case _kIsFocusableIndex: + return 'SemanticsFlag.isFocusable'; case _kIsFocusedIndex: return 'SemanticsFlag.isFocused'; case _kHasEnabledStateIndex: diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 6732c9b445591..8303a9e11ec3e 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -1271,7 +1271,7 @@ void updateSemantics(@NonNull ByteBuffer buffer, @NonNull String[] strings) { } if (lastAdded != null && lastAdded.id != previousRouteId) { previousRouteId = lastAdded.id; - createAndSendWindowChangeEvent(lastAdded); + sendWindowChangeEvent(lastAdded); } flutterNavigationStack.clear(); for (SemanticsNode semanticsNode : newRoutes) { @@ -1290,7 +1290,7 @@ void updateSemantics(@NonNull ByteBuffer buffer, @NonNull String[] strings) { // TODO(goderbauer): Send this event only once (!) for changed subtrees, // see https://github.com/flutter/flutter/issues/14534 - sendAccessibilityEvent(0, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + sendWindowContentChangeEvent(0); for (SemanticsNode object : updated) { if (object.didScroll()) { @@ -1362,13 +1362,13 @@ void updateSemantics(@NonNull ByteBuffer buffer, @NonNull String[] strings) { String label = object.label == null ? "" : object.label; String previousLabel = object.previousLabel == null ? "" : object.label; if (!label.equals(previousLabel) || !object.hadFlag(Flag.IS_LIVE_REGION)) { - sendAccessibilityEvent(object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + sendWindowContentChangeEvent(object.id); } } else if (object.hasFlag(Flag.IS_TEXT_FIELD) && object.didChangeLabel() && inputFocusedSemanticsNode != null && inputFocusedSemanticsNode.id == object.id) { // Text fields should announce when their label changes while focused. We use a live // region tag to do so, and this event triggers that update. - sendAccessibilityEvent(object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + sendWindowContentChangeEvent(object.id); } if (accessibilityFocusedSemanticsNode != null && accessibilityFocusedSemanticsNode.id == object.id && !object.hadFlag(Flag.IS_SELECTED) && object.hasFlag(Flag.IS_SELECTED)) { @@ -1472,13 +1472,13 @@ private void sendAccessibilityEvent(@NonNull AccessibilityEvent event) { } /** - * Factory method that creates a {@link AccessibilityEvent#TYPE_WINDOW_STATE_CHANGED} and sends - * the event to Android's accessibility system. + * Creates a {@link AccessibilityEvent#TYPE_WINDOW_STATE_CHANGED} and sends the event to + * Android's accessibility system. * * The given {@code route} should be a {@link SemanticsNode} that represents a navigation route * in the Flutter app. */ - private void createAndSendWindowChangeEvent(@NonNull SemanticsNode route) { + private void sendWindowChangeEvent(@NonNull SemanticsNode route) { AccessibilityEvent event = obtainAccessibilityEvent( route.id, AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED @@ -1488,6 +1488,27 @@ private void createAndSendWindowChangeEvent(@NonNull SemanticsNode route) { sendAccessibilityEvent(event); } + /** + * Creates a {@link AccessibilityEvent#TYPE_WINDOW_CONTENT_CHANGED} and sends the event to + * Android's accessibility system. + * + * It sets the content change types to {@link AccessibilityEvent#CONTENT_CHANGE_TYPE_SUBTREE} + * when supported by the API level. + * + * The given {@code virtualViewId} should be a {@link SemanticsNode} below which the content has + * changed. + */ + private void sendWindowContentChangeEvent(int virtualViewId) { + AccessibilityEvent event = obtainAccessibilityEvent( + virtualViewId, + AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED + ); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + event.setContentChangeTypes(AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE); + } + sendAccessibilityEvent(event); + } + /** * Factory method that creates a new {@link AccessibilityEvent} that is configured to represent * the Flutter {@link SemanticsNode} represented by the given {@code virtualViewId}, categorized @@ -1559,7 +1580,7 @@ public void reset() { } accessibilityFocusedSemanticsNode = null; hoveredObject = null; - sendAccessibilityEvent(0, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + sendWindowContentChangeEvent(0); } /** @@ -1626,7 +1647,8 @@ private enum Flag { HAS_IMPLICIT_SCROLLING(1 << 18), // The Dart API defines the following flag but it isn't used in Android. // IS_MULTILINE(1 << 19); - IS_READ_ONLY(1 << 20); + IS_READ_ONLY(1 << 20), + IS_FOCUSABLE(1 << 21); final int value; @@ -2000,6 +2022,12 @@ private boolean isFocusable() { if (hasFlag(Flag.SCOPES_ROUTE)) { return false; } + if (hasFlag(Flag.IS_FOCUSABLE)) { + return true; + } + // If not explicitly set as focusable, then use our legacy + // algorithm. Once all focusable widgets have a Focus widget, then + // this won't be needed. int scrollableActions = Action.SCROLL_RIGHT.value | Action.SCROLL_LEFT.value | Action.SCROLL_UP.value | Action.SCROLL_DOWN.value; return (actions & ~scrollableActions) != 0 || flags != 0