Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 50174be

Browse files
Reverts "[iOS] Full keyboard access scrolling (#56606)" (#56802)
Reverts: #56606 Initiated by: LongCatIsLooong Reason for reverting: flutter/flutter#159456 Original PR Author: LongCatIsLooong Reviewed By: {chunhtai, cbracken} This change reverts the following previous change: This PR adds basic FKA scrolling support: when the iOS focus (the focus state is maintained separately from the framework focus, see the previous PR) switches to an item in a scrollable container that is too close to the edge of the viewport, the container will scroll to make sure the next item is visible. Previous PR for context: #55964 https://github.com/user-attachments/assets/84ae5153-f955-4d23-9901-ce942c0e98ac ### Why the UIScrollView subclass in the focus hierarchy The iOS focus system does not provide an API that allows apps to notify it of focus highlight changes. So if we were to keep using the transforms sent by the framework as-is and not introducing any UIViews in the focus hierarchy, the focus highlight will be positioned at the wrong location after scrolling (via FKA or via framework). That does not seem to be part of the public API and the focus system seems to only know how to properly highlight focusable UIViews. ### Things that currently may not work 1. Nested scroll views (have not tried to verify) The `UIScrollView`s are always subviews of the `FlutterView`. If there are nested scrollables the focus system may not be able to properly determine the focus hierarchy (in theory the iOS focus system should never depend on `UIView.parentView` but I haven't tried to verify that). 2. If the next item is too far below the bottom of the screen and there is a tab bar with focusable items, the focus will be transferred to tab bar instead of the next item in the list Video demo (as you can see the scrolling is really finicky): https://github.com/user-attachments/assets/51c2bfe4-d7b3-4614-aa49-4256214f8978 I've tried doing the same thing using a `UITableView` with similar configurations but it seems to have the same problem. I'll try to dig a bit deeper into this and see if there's a workaround. [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
1 parent 6ebfd9a commit 50174be

File tree

15 files changed

+25
-295
lines changed

15 files changed

+25
-295
lines changed

lib/ui/semantics.dart

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ class SemanticsAction {
4545
static const int _kMoveCursorBackwardByWordIndex = 1 << 20;
4646
static const int _kSetTextIndex = 1 << 21;
4747
static const int _kFocusIndex = 1 << 22;
48-
static const int _kScrollToOffsetIndex = 1 << 23;
4948
// READ THIS: if you add an action here, you MUST update the
5049
// numSemanticsActions value in testing/dart/semantics_test.dart and
5150
// lib/web_ui/test/engine/semantics/semantics_api_test.dart, or tests
@@ -87,17 +86,6 @@ class SemanticsAction {
8786
/// scrollable.
8887
static const SemanticsAction scrollDown = SemanticsAction._(_kScrollDownIndex, 'scrollDown');
8988

90-
/// A request to scroll the scrollable container to a given scroll offset.
91-
///
92-
/// The payload of this [SemanticsAction] is a flutter-standard-encoded
93-
/// [Float64List] of length 2 containing the target horizontal and vertical
94-
/// offsets (in logical pixels) the receiving scrollable container should
95-
/// scroll to.
96-
///
97-
/// This action is used by iOS Full Keyboard Access to reveal contents that
98-
/// are currently not visible in the viewport.
99-
static const SemanticsAction scrollToOffset = SemanticsAction._(_kScrollToOffsetIndex, 'scrollToOffset');
100-
10189
/// A request to increase the value represented by the semantics node.
10290
///
10391
/// For example, this action might be recognized by a slider control.
@@ -277,7 +265,6 @@ class SemanticsAction {
277265
_kScrollRightIndex: scrollRight,
278266
_kScrollUpIndex: scrollUp,
279267
_kScrollDownIndex: scrollDown,
280-
_kScrollToOffsetIndex: scrollToOffset,
281268
_kIncreaseIndex: increase,
282269
_kDecreaseIndex: decrease,
283270
_kShowOnScreenIndex: showOnScreen,
@@ -777,7 +764,7 @@ base class LocaleStringAttribute extends StringAttribute {
777764
_initLocaleStringAttribute(this, range.start, range.end, locale.toLanguageTag());
778765
}
779766

780-
/// The language of this attribute.
767+
/// The lanuage of this attribute.
781768
final Locale locale;
782769

783770
@Native<Void Function(Handle, Int32, Int32, Handle)>(symbol: 'NativeStringAttribute::initLocaleStringAttribute')

lib/ui/semantics/semantics_node.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ enum class SemanticsAction : int32_t {
4343
kMoveCursorBackwardByWord = 1 << 20,
4444
kSetText = 1 << 21,
4545
kFocus = 1 << 22,
46-
kScrollToOffset = 1 << 23,
4746
};
4847

4948
const int kVerticalScrollSemanticsActions =

lib/web_ui/lib/semantics.dart

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,13 @@ class SemanticsAction {
3333
static const int _kMoveCursorBackwardByWordIndex = 1 << 20;
3434
static const int _kSetTextIndex = 1 << 21;
3535
static const int _kFocusIndex = 1 << 22;
36-
static const int _kScrollToOffsetIndex = 1 << 23;
3736

3837
static const SemanticsAction tap = SemanticsAction._(_kTapIndex, 'tap');
3938
static const SemanticsAction longPress = SemanticsAction._(_kLongPressIndex, 'longPress');
4039
static const SemanticsAction scrollLeft = SemanticsAction._(_kScrollLeftIndex, 'scrollLeft');
4140
static const SemanticsAction scrollRight = SemanticsAction._(_kScrollRightIndex, 'scrollRight');
4241
static const SemanticsAction scrollUp = SemanticsAction._(_kScrollUpIndex, 'scrollUp');
4342
static const SemanticsAction scrollDown = SemanticsAction._(_kScrollDownIndex, 'scrollDown');
44-
static const SemanticsAction scrollToOffset = SemanticsAction._(_kScrollToOffsetIndex, 'scrollToOffset');
4543
static const SemanticsAction increase = SemanticsAction._(_kIncreaseIndex, 'increase');
4644
static const SemanticsAction decrease = SemanticsAction._(_kDecreaseIndex, 'decrease');
4745
static const SemanticsAction showOnScreen = SemanticsAction._(_kShowOnScreenIndex, 'showOnScreen');
@@ -67,7 +65,6 @@ class SemanticsAction {
6765
_kScrollRightIndex: scrollRight,
6866
_kScrollUpIndex: scrollUp,
6967
_kScrollDownIndex: scrollDown,
70-
_kScrollToOffsetIndex: scrollToOffset,
7168
_kIncreaseIndex: increase,
7269
_kDecreaseIndex: decrease,
7370
_kShowOnScreenIndex: showOnScreen,

lib/web_ui/test/engine/semantics/semantics_api_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ void testMain() {
2929
});
3030

3131
// This must match the number of actions in lib/ui/semantics.dart
32-
const int numSemanticsActions = 24;
32+
const int numSemanticsActions = 23;
3333
test('SemanticsAction.values refers to all actions.', () async {
3434
expect(SemanticsAction.values.length, equals(numSemanticsActions));
3535
for (int index = 0; index < numSemanticsActions; ++index) {

shell/platform/android/io/flutter/view/AccessibilityBridge.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2120,8 +2120,7 @@ public enum Action {
21202120
MOVE_CURSOR_FORWARD_BY_WORD(1 << 19),
21212121
MOVE_CURSOR_BACKWARD_BY_WORD(1 << 20),
21222122
SET_TEXT(1 << 21),
2123-
FOCUS(1 << 22),
2124-
SCROLL_TO_OFFSET(1 << 23);
2123+
FOCUS(1 << 22);
21252124

21262125
public final int value;
21272126

shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.h

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,30 +18,10 @@ NS_ASSUME_NONNULL_BEGIN
1818
* sends all of selector calls from accessibility services to the
1919
* owner SemanticsObject.
2020
*/
21-
@interface FlutterSemanticsScrollView : UIScrollView <UIScrollViewDelegate>
21+
@interface FlutterSemanticsScrollView : UIScrollView
2222

2323
@property(nonatomic, weak, nullable) SemanticsObject* semanticsObject;
2424

25-
/// Whether this scroll view's content offset is actively being updated by UIKit
26-
/// or other the system services.
27-
///
28-
/// This flag is set by the `FlutterSemanticsScrollView` itself, typically in
29-
/// one of the `UIScrollViewDelegate` methods.
30-
///
31-
/// When this flag is true, the `SemanticsObject` implementation ignores all
32-
/// content offset updates coming from the Flutter framework, to prevent
33-
/// potential feedback loops (especially when the framework is only echoing
34-
/// the new content offset back to this scroll view).
35-
///
36-
/// For example, to scroll a scrollable container with iOS full keyboard access,
37-
/// the iOS focus system uses a display link to scroll the container to the
38-
/// desired offset animatedly. If the user changes the scroll offset during the
39-
/// animation, the display link will be invalidated and the scrolling animation
40-
/// will be interrupted. For simplicity, content offset updates coming from the
41-
/// framework will be ignored in the relatively short animation duration (~1s),
42-
/// allowing the scrolling animation to finish.
43-
@property(nonatomic, readonly) BOOL isDoingSystemScrolling;
44-
4525
- (instancetype)init NS_UNAVAILABLE;
4626
- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;
4727
- (instancetype)initWithCoder:(NSCoder*)coder NS_UNAVAILABLE;

shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.mm

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ - (instancetype)initWithSemanticsObject:(SemanticsObject*)semanticsObject {
1515
self = [super initWithFrame:CGRectZero];
1616
if (self) {
1717
_semanticsObject = semanticsObject;
18-
_isDoingSystemScrolling = NO;
19-
self.delegate = self;
2018
}
2119
return self;
2220
}
@@ -107,14 +105,4 @@ - (NSInteger)accessibilityElementCount {
107105
return self.semanticsObject.children.count;
108106
}
109107

110-
- (void)scrollViewWillEndDragging:(UIScrollView*)scrollView
111-
withVelocity:(CGPoint)velocity
112-
targetContentOffset:(inout CGPoint*)targetContentOffset {
113-
_isDoingSystemScrolling = YES;
114-
}
115-
116-
- (void)scrollViewDidEndDecelerating:(UIScrollView*)scrollView {
117-
_isDoingSystemScrolling = NO;
118-
}
119-
120108
@end

shell/platform/darwin/ios/framework/Source/SemanticsObject+UIFocusSystem.mm

Lines changed: 6 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@
33
// found in the LICENSE file.
44

55
#import "SemanticsObject.h"
6-
#include "flutter/lib/ui/semantics/semantics_node.h"
7-
#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterCodecs.h"
86
#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h"
9-
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.h"
107

118
FLUTTER_ASSERT_ARC
129

@@ -30,19 +27,10 @@
3027
// translated to calls such as -[NSObject accessibilityActivate]), while most
3128
// other key events are dispatched to the framework.
3229
@interface SemanticsObject (UIFocusSystem) <UIFocusItem, UIFocusItemContainer>
33-
/// The `UIFocusItem` that represents this SemanticsObject.
34-
///
35-
/// For regular `SemanticsObject`s, this method returns `self`,
36-
/// for `FlutterScrollableSemanticsObject`s, this method returns its scroll view.
37-
- (id<UIFocusItem>)focusItem;
3830
@end
3931

4032
@implementation SemanticsObject (UIFocusSystem)
4133

42-
- (id<UIFocusItem>)focusItem {
43-
return self;
44-
}
45-
4634
#pragma mark - UIFocusEnvironment Conformance
4735

4836
- (void)setNeedsFocusUpdate {
@@ -61,7 +49,7 @@ - (void)didUpdateFocusInContext:(UIFocusUpdateContext*)context
6149

6250
- (id<UIFocusEnvironment>)parentFocusEnvironment {
6351
// The root SemanticsObject node's parent is the FlutterView.
64-
return self.parent.focusItem ?: self.bridge->view();
52+
return self.parent ?: self.bridge->view();
6553
}
6654

6755
- (NSArray<id<UIFocusEnvironment>>*)preferredFocusEnvironments {
@@ -83,57 +71,8 @@ - (BOOL)canBecomeFocused {
8371
return self.node.HasAction(flutter::SemanticsAction::kTap);
8472
}
8573

86-
// The frame is described in the `coordinateSpace` of the
87-
// `parentFocusEnvironment` (all `parentFocusEnvironment`s are `UIFocusItem`s).
88-
//
89-
// See also the `coordinateSpace` implementation.
90-
// TODO(LongCatIsLooong): use CoreGraphics types.
9174
- (CGRect)frame {
92-
SkPoint quad[4] = {SkPoint::Make(self.node.rect.left(), self.node.rect.top()),
93-
SkPoint::Make(self.node.rect.left(), self.node.rect.bottom()),
94-
SkPoint::Make(self.node.rect.right(), self.node.rect.top()),
95-
SkPoint::Make(self.node.rect.right(), self.node.rect.bottom())};
96-
97-
SkM44 transform = self.node.transform;
98-
FlutterSemanticsScrollView* scrollView;
99-
for (SemanticsObject* ancestor = self.parent; ancestor; ancestor = ancestor.parent) {
100-
if ([ancestor isKindOfClass:[FlutterScrollableSemanticsObject class]]) {
101-
scrollView = ((FlutterScrollableSemanticsObject*)ancestor).scrollView;
102-
break;
103-
}
104-
transform = ancestor.node.transform * transform;
105-
}
106-
107-
for (auto& vertex : quad) {
108-
SkV4 vector = transform.map(vertex.x(), vertex.y(), 0, 1);
109-
vertex = SkPoint::Make(vector.x / vector.w, vector.y / vector.w);
110-
}
111-
112-
SkRect rect;
113-
rect.setBounds(quad, 4);
114-
// If this UIFocusItemContainer's coordinateSpace is a UIScrollView, offset
115-
// the rect by `contentOffset` because the contentOffset translation is
116-
// incorporated into the paint transform at different node depth in UIKit
117-
// and Flutter. In Flutter, the translation is added to the cells
118-
// while in UIKit the viewport's bounds is manipulated (IOW, each cell's frame
119-
// in the UIScrollView coordinateSpace does not change when the UIScrollView
120-
// scrolls).
121-
CGRect unscaledRect =
122-
CGRectMake(rect.x() + scrollView.bounds.origin.x, rect.y() + scrollView.bounds.origin.y,
123-
rect.width(), rect.height());
124-
if (scrollView) {
125-
return unscaledRect;
126-
}
127-
// `rect` could be in physical pixels since the root RenderObject ("RenderView")
128-
// applies a transform that turns logical pixels to physical pixels. Undo the
129-
// transform by dividing the coordinates by the screen's scale factor, if this
130-
// UIFocusItem's reported `coordinateSpace` is the root view (which means this
131-
// UIFocusItem is not inside of a scroll view).
132-
//
133-
// Screen can be nil if the FlutterView is covered by another native view.
134-
CGFloat scale = (self.bridge->view().window.screen ?: UIScreen.mainScreen).scale;
135-
return CGRectMake(unscaledRect.origin.x / scale, unscaledRect.origin.y / scale,
136-
unscaledRect.size.width / scale, unscaledRect.size.height / scale);
75+
return self.accessibilityFrame;
13776
}
13877

13978
#pragma mark - UIFocusItemContainer Conformance
@@ -148,94 +87,16 @@ - (CGRect)frame {
14887
//
14988
// This method is only supposed to return items within the given
15089
// rect but returning everything in the subtree seems to work fine.
151-
NSMutableArray<id<UIFocusItem>>* reversedItems =
90+
NSMutableArray<SemanticsObject*>* reversedItems =
15291
[[NSMutableArray alloc] initWithCapacity:self.childrenInHitTestOrder.count];
15392
for (NSUInteger i = 0; i < self.childrenInHitTestOrder.count; ++i) {
154-
SemanticsObject* child = self.childrenInHitTestOrder[self.childrenInHitTestOrder.count - 1 - i];
155-
[reversedItems addObject:child.focusItem];
93+
[reversedItems
94+
addObject:self.childrenInHitTestOrder[self.childrenInHitTestOrder.count - 1 - i]];
15695
}
15796
return reversedItems;
15897
}
15998

16099
- (id<UICoordinateSpace>)coordinateSpace {
161-
// A regular SemanticsObject uses the same coordinate space as its parent.
162-
return self.parent.coordinateSpace ?: self.bridge->view();
163-
}
164-
165-
@end
166-
167-
/// Scrollable containers interact with the iOS focus engine using the
168-
/// `UIFocusItemScrollableContainer` protocol. The said protocol (and other focus-related protocols)
169-
/// does not provide means to inform the focus system of layout changes. In order for the focus
170-
/// highlight to update properly as the scroll view scrolls, this implementation incorporates a
171-
/// UIScrollView into the focus hierarchy to workaround the highlight update problem.
172-
///
173-
/// As a result, in the current implementation only scrollable containers and the root node
174-
/// establish their own `coordinateSpace`s. All other `UIFocusItemContainter`s use the same
175-
/// `coordinateSpace` as the containing UIScrollView, or the root `FlutterView`, whichever is
176-
/// closer.
177-
///
178-
/// See also the `frame` method implementation.
179-
#pragma mark - Scrolling
180-
181-
@interface FlutterScrollableSemanticsObject (CoordinateSpace)
182-
@end
183-
184-
@implementation FlutterScrollableSemanticsObject (CoordinateSpace)
185-
- (id<UICoordinateSpace>)coordinateSpace {
186-
// A scrollable SemanticsObject uses the same coordinate space as the scroll view.
187-
// This may not work very well in nested scroll views.
188-
return self.scrollView;
189-
}
190-
191-
- (id<UIFocusItem>)focusItem {
192-
return self.scrollView;
193-
}
194-
195-
@end
196-
197-
@interface FlutterSemanticsScrollView (UIFocusItemScrollableContainer) <
198-
UIFocusItemScrollableContainer>
199-
@end
200-
201-
@implementation FlutterSemanticsScrollView (UIFocusItemScrollableContainer)
202-
203-
#pragma mark - FlutterSemanticsScrollView UIFocusItemScrollableContainer Conformance
204-
205-
- (CGSize)visibleSize {
206-
return self.frame.size;
207-
}
208-
209-
- (void)setContentOffset:(CGPoint)contentOffset {
210-
[super setContentOffset:contentOffset];
211-
// Do no send flutter::SemanticsAction::kScrollToOffset if it's triggered
212-
// by a framework update.
213-
if (![self.semanticsObject isAccessibilityBridgeAlive] || !self.isDoingSystemScrolling) {
214-
return;
215-
}
216-
217-
double offset[2] = {contentOffset.x, contentOffset.y};
218-
FlutterStandardTypedData* offsetData = [FlutterStandardTypedData
219-
typedDataWithFloat64:[NSData dataWithBytes:&offset length:sizeof(offset)]];
220-
NSData* encoded = [[FlutterStandardMessageCodec sharedInstance] encode:offsetData];
221-
self.semanticsObject.bridge->DispatchSemanticsAction(
222-
self.semanticsObject.uid, flutter::SemanticsAction::kScrollToOffset,
223-
fml::MallocMapping::Copy(encoded.bytes, encoded.length));
224-
}
225-
226-
- (BOOL)canBecomeFocused {
227-
return NO;
228-
}
229-
230-
- (id<UIFocusEnvironment>)parentFocusEnvironment {
231-
return self.semanticsObject.parentFocusEnvironment;
232-
}
233-
234-
- (NSArray<id<UIFocusEnvironment>>*)preferredFocusEnvironments {
235-
return nil;
236-
}
237-
238-
- (NSArray<id<UIFocusItem>>*)focusItemsInRect:(CGRect)rect {
239-
return [self.semanticsObject focusItemsInRect:rect];
100+
return self.bridge->view();
240101
}
241102
@end

shell/platform/darwin/ios/framework/Source/SemanticsObject.h

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
#include "flutter/fml/macros.h"
1111
#include "flutter/fml/memory/weak_ptr.h"
1212
#include "flutter/lib/ui/semantics/semantics_node.h"
13-
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.h"
1413
#import "flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge_ios.h"
1514

1615
constexpr int32_t kRootNodeId = 0;
@@ -187,7 +186,7 @@ constexpr float kScrollExtentMaxForInf = 1000;
187186
/// The semantics object for scrollable. This class creates an UIScrollView to interact with the
188187
/// iOS.
189188
@interface FlutterScrollableSemanticsObject : SemanticsObject
190-
@property(nonatomic, readonly) FlutterSemanticsScrollView* scrollView;
189+
191190
@end
192191

193192
/**

shell/platform/darwin/ios/framework/Source/SemanticsObject.mm

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,6 @@ - (instancetype)initWithBridge:(fml::WeakPtr<flutter::AccessibilityBridgeIos>)br
154154
_scrollView = [[FlutterSemanticsScrollView alloc] initWithSemanticsObject:self];
155155
[_scrollView setShowsHorizontalScrollIndicator:NO];
156156
[_scrollView setShowsVerticalScrollIndicator:NO];
157-
[_scrollView setContentInset:UIEdgeInsetsZero];
158-
[_scrollView setContentInsetAdjustmentBehavior:UIScrollViewContentInsetAdjustmentNever];
159157
[self.bridge->view() addSubview:_scrollView];
160158
}
161159
return self;
@@ -176,10 +174,7 @@ - (void)accessibilityBridgeDidFinishUpdate {
176174
// contentOffset is 0.0, only the scroll down action is available.
177175
self.scrollView.frame = self.accessibilityFrame;
178176
self.scrollView.contentSize = [self contentSizeInternal];
179-
// See the documentation on `isDoingSystemScrolling`.
180-
if (!self.scrollView.isDoingSystemScrolling) {
181-
[self.scrollView setContentOffset:self.contentOffsetInternal animated:NO];
182-
}
177+
[self.scrollView setContentOffset:[self contentOffsetInternal] animated:NO];
183178
}
184179

185180
- (id)nativeAccessibility {

0 commit comments

Comments
 (0)