From a83dafe0e00849d933a9288d80440631d17e68a2 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Fri, 27 Sep 2019 15:19:08 -0700 Subject: [PATCH 1/2] Send TYPE_VIEW_FOCUSED for views with input focus. This reverts commit b3f2aad1c64d9f0a4d15cf1f8e392419cd04d5f4. --- .../io/flutter/view/AccessibilityBridge.java | 46 +++++++++++++++---- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 6732c9b445591..3f848a48f4d68 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -519,6 +519,10 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { result.setClassName("android.view.View"); result.setSource(rootAccessibilityView, virtualViewId); result.setFocusable(semanticsNode.isFocusable()); + if (semanticsNode.isFocusable()) { + result.addAction(AccessibilityNodeInfo.ACTION_FOCUS); + } + if (inputFocusedSemanticsNode != null) { result.setFocused(inputFocusedSemanticsNode.id == virtualViewId); } @@ -1228,6 +1232,11 @@ void updateSemantics(@NonNull ByteBuffer buffer, @NonNull String[] strings) { } if (semanticsNode.hasFlag(Flag.IS_FOCUSED)) { inputFocusedSemanticsNode = semanticsNode; + AccessibilityEvent event = obtainAccessibilityEvent( + inputFocusedSemanticsNode.id, + AccessibilityEvent.TYPE_VIEW_FOCUSED + ); + sendAccessibilityEvent(event); } if (semanticsNode.hadPreviousConfig) { updated.add(semanticsNode); @@ -1271,7 +1280,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 +1299,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 +1371,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 +1481,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 +1497,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 +1589,7 @@ public void reset() { } accessibilityFocusedSemanticsNode = null; hoveredObject = null; - sendAccessibilityEvent(0, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + sendWindowContentChangeEvent(0); } /** From c902c0735b73e0d633f4d83bb22bb7ba05725e38 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Fri, 27 Sep 2019 17:33:14 -0700 Subject: [PATCH 2/2] Add isFocusable --- lib/ui/semantics.dart | 6 ++++++ lib/ui/semantics/semantics_node.h | 1 + lib/web_ui/lib/src/ui/semantics.dart | 9 +++++++++ .../io/flutter/view/AccessibilityBridge.java | 18 ++++++++---------- 4 files changed, 24 insertions(+), 10 deletions(-) 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 3f848a48f4d68..8303a9e11ec3e 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -519,10 +519,6 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { result.setClassName("android.view.View"); result.setSource(rootAccessibilityView, virtualViewId); result.setFocusable(semanticsNode.isFocusable()); - if (semanticsNode.isFocusable()) { - result.addAction(AccessibilityNodeInfo.ACTION_FOCUS); - } - if (inputFocusedSemanticsNode != null) { result.setFocused(inputFocusedSemanticsNode.id == virtualViewId); } @@ -1232,11 +1228,6 @@ void updateSemantics(@NonNull ByteBuffer buffer, @NonNull String[] strings) { } if (semanticsNode.hasFlag(Flag.IS_FOCUSED)) { inputFocusedSemanticsNode = semanticsNode; - AccessibilityEvent event = obtainAccessibilityEvent( - inputFocusedSemanticsNode.id, - AccessibilityEvent.TYPE_VIEW_FOCUSED - ); - sendAccessibilityEvent(event); } if (semanticsNode.hadPreviousConfig) { updated.add(semanticsNode); @@ -1656,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; @@ -2030,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