Reverts "[iOS] Full keyboard access scrolling (#56606)" (flutter/engine#56802)

Reverts: flutter/engine#56606
Initiated by: LongCatIsLooong
Reason for reverting: https://github.com/flutter/flutter/issues/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: https://github.com/flutter/engine/pull/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
This commit is contained in:
auto-submit[bot] 2024-11-26 00:39:15 +00:00 committed by GitHub
parent 569d30910b
commit 57b102520b
15 changed files with 25 additions and 295 deletions

View File

@ -45,7 +45,6 @@ class SemanticsAction {
static const int _kMoveCursorBackwardByWordIndex = 1 << 20; static const int _kMoveCursorBackwardByWordIndex = 1 << 20;
static const int _kSetTextIndex = 1 << 21; static const int _kSetTextIndex = 1 << 21;
static const int _kFocusIndex = 1 << 22; static const int _kFocusIndex = 1 << 22;
static const int _kScrollToOffsetIndex = 1 << 23;
// READ THIS: if you add an action here, you MUST update the // READ THIS: if you add an action here, you MUST update the
// numSemanticsActions value in testing/dart/semantics_test.dart and // numSemanticsActions value in testing/dart/semantics_test.dart and
// lib/web_ui/test/engine/semantics/semantics_api_test.dart, or tests // lib/web_ui/test/engine/semantics/semantics_api_test.dart, or tests
@ -87,17 +86,6 @@ class SemanticsAction {
/// scrollable. /// scrollable.
static const SemanticsAction scrollDown = SemanticsAction._(_kScrollDownIndex, 'scrollDown'); static const SemanticsAction scrollDown = SemanticsAction._(_kScrollDownIndex, 'scrollDown');
/// A request to scroll the scrollable container to a given scroll offset.
///
/// The payload of this [SemanticsAction] is a flutter-standard-encoded
/// [Float64List] of length 2 containing the target horizontal and vertical
/// offsets (in logical pixels) the receiving scrollable container should
/// scroll to.
///
/// This action is used by iOS Full Keyboard Access to reveal contents that
/// are currently not visible in the viewport.
static const SemanticsAction scrollToOffset = SemanticsAction._(_kScrollToOffsetIndex, 'scrollToOffset');
/// A request to increase the value represented by the semantics node. /// A request to increase the value represented by the semantics node.
/// ///
/// For example, this action might be recognized by a slider control. /// For example, this action might be recognized by a slider control.
@ -277,7 +265,6 @@ class SemanticsAction {
_kScrollRightIndex: scrollRight, _kScrollRightIndex: scrollRight,
_kScrollUpIndex: scrollUp, _kScrollUpIndex: scrollUp,
_kScrollDownIndex: scrollDown, _kScrollDownIndex: scrollDown,
_kScrollToOffsetIndex: scrollToOffset,
_kIncreaseIndex: increase, _kIncreaseIndex: increase,
_kDecreaseIndex: decrease, _kDecreaseIndex: decrease,
_kShowOnScreenIndex: showOnScreen, _kShowOnScreenIndex: showOnScreen,
@ -777,7 +764,7 @@ base class LocaleStringAttribute extends StringAttribute {
_initLocaleStringAttribute(this, range.start, range.end, locale.toLanguageTag()); _initLocaleStringAttribute(this, range.start, range.end, locale.toLanguageTag());
} }
/// The language of this attribute. /// The lanuage of this attribute.
final Locale locale; final Locale locale;
@Native<Void Function(Handle, Int32, Int32, Handle)>(symbol: 'NativeStringAttribute::initLocaleStringAttribute') @Native<Void Function(Handle, Int32, Int32, Handle)>(symbol: 'NativeStringAttribute::initLocaleStringAttribute')

View File

@ -43,7 +43,6 @@ enum class SemanticsAction : int32_t {
kMoveCursorBackwardByWord = 1 << 20, kMoveCursorBackwardByWord = 1 << 20,
kSetText = 1 << 21, kSetText = 1 << 21,
kFocus = 1 << 22, kFocus = 1 << 22,
kScrollToOffset = 1 << 23,
}; };
const int kVerticalScrollSemanticsActions = const int kVerticalScrollSemanticsActions =

View File

@ -33,7 +33,6 @@ class SemanticsAction {
static const int _kMoveCursorBackwardByWordIndex = 1 << 20; static const int _kMoveCursorBackwardByWordIndex = 1 << 20;
static const int _kSetTextIndex = 1 << 21; static const int _kSetTextIndex = 1 << 21;
static const int _kFocusIndex = 1 << 22; static const int _kFocusIndex = 1 << 22;
static const int _kScrollToOffsetIndex = 1 << 23;
static const SemanticsAction tap = SemanticsAction._(_kTapIndex, 'tap'); static const SemanticsAction tap = SemanticsAction._(_kTapIndex, 'tap');
static const SemanticsAction longPress = SemanticsAction._(_kLongPressIndex, 'longPress'); static const SemanticsAction longPress = SemanticsAction._(_kLongPressIndex, 'longPress');
@ -41,7 +40,6 @@ class SemanticsAction {
static const SemanticsAction scrollRight = SemanticsAction._(_kScrollRightIndex, 'scrollRight'); static const SemanticsAction scrollRight = SemanticsAction._(_kScrollRightIndex, 'scrollRight');
static const SemanticsAction scrollUp = SemanticsAction._(_kScrollUpIndex, 'scrollUp'); static const SemanticsAction scrollUp = SemanticsAction._(_kScrollUpIndex, 'scrollUp');
static const SemanticsAction scrollDown = SemanticsAction._(_kScrollDownIndex, 'scrollDown'); static const SemanticsAction scrollDown = SemanticsAction._(_kScrollDownIndex, 'scrollDown');
static const SemanticsAction scrollToOffset = SemanticsAction._(_kScrollToOffsetIndex, 'scrollToOffset');
static const SemanticsAction increase = SemanticsAction._(_kIncreaseIndex, 'increase'); static const SemanticsAction increase = SemanticsAction._(_kIncreaseIndex, 'increase');
static const SemanticsAction decrease = SemanticsAction._(_kDecreaseIndex, 'decrease'); static const SemanticsAction decrease = SemanticsAction._(_kDecreaseIndex, 'decrease');
static const SemanticsAction showOnScreen = SemanticsAction._(_kShowOnScreenIndex, 'showOnScreen'); static const SemanticsAction showOnScreen = SemanticsAction._(_kShowOnScreenIndex, 'showOnScreen');
@ -67,7 +65,6 @@ class SemanticsAction {
_kScrollRightIndex: scrollRight, _kScrollRightIndex: scrollRight,
_kScrollUpIndex: scrollUp, _kScrollUpIndex: scrollUp,
_kScrollDownIndex: scrollDown, _kScrollDownIndex: scrollDown,
_kScrollToOffsetIndex: scrollToOffset,
_kIncreaseIndex: increase, _kIncreaseIndex: increase,
_kDecreaseIndex: decrease, _kDecreaseIndex: decrease,
_kShowOnScreenIndex: showOnScreen, _kShowOnScreenIndex: showOnScreen,

View File

@ -29,7 +29,7 @@ void testMain() {
}); });
// This must match the number of actions in lib/ui/semantics.dart // This must match the number of actions in lib/ui/semantics.dart
const int numSemanticsActions = 24; const int numSemanticsActions = 23;
test('SemanticsAction.values refers to all actions.', () async { test('SemanticsAction.values refers to all actions.', () async {
expect(SemanticsAction.values.length, equals(numSemanticsActions)); expect(SemanticsAction.values.length, equals(numSemanticsActions));
for (int index = 0; index < numSemanticsActions; ++index) { for (int index = 0; index < numSemanticsActions; ++index) {

View File

@ -2120,8 +2120,7 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
MOVE_CURSOR_FORWARD_BY_WORD(1 << 19), MOVE_CURSOR_FORWARD_BY_WORD(1 << 19),
MOVE_CURSOR_BACKWARD_BY_WORD(1 << 20), MOVE_CURSOR_BACKWARD_BY_WORD(1 << 20),
SET_TEXT(1 << 21), SET_TEXT(1 << 21),
FOCUS(1 << 22), FOCUS(1 << 22);
SCROLL_TO_OFFSET(1 << 23);
public final int value; public final int value;

View File

@ -18,30 +18,10 @@ NS_ASSUME_NONNULL_BEGIN
* sends all of selector calls from accessibility services to the * sends all of selector calls from accessibility services to the
* owner SemanticsObject. * owner SemanticsObject.
*/ */
@interface FlutterSemanticsScrollView : UIScrollView <UIScrollViewDelegate> @interface FlutterSemanticsScrollView : UIScrollView
@property(nonatomic, weak, nullable) SemanticsObject* semanticsObject; @property(nonatomic, weak, nullable) SemanticsObject* semanticsObject;
/// Whether this scroll view's content offset is actively being updated by UIKit
/// or other the system services.
///
/// This flag is set by the `FlutterSemanticsScrollView` itself, typically in
/// one of the `UIScrollViewDelegate` methods.
///
/// When this flag is true, the `SemanticsObject` implementation ignores all
/// content offset updates coming from the Flutter framework, to prevent
/// potential feedback loops (especially when the framework is only echoing
/// the new content offset back to this scroll view).
///
/// For example, to scroll a scrollable container with iOS full keyboard access,
/// the iOS focus system uses a display link to scroll the container to the
/// desired offset animatedly. If the user changes the scroll offset during the
/// animation, the display link will be invalidated and the scrolling animation
/// will be interrupted. For simplicity, content offset updates coming from the
/// framework will be ignored in the relatively short animation duration (~1s),
/// allowing the scrolling animation to finish.
@property(nonatomic, readonly) BOOL isDoingSystemScrolling;
- (instancetype)init NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE; - (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;
- (instancetype)initWithCoder:(NSCoder*)coder NS_UNAVAILABLE; - (instancetype)initWithCoder:(NSCoder*)coder NS_UNAVAILABLE;

View File

@ -15,8 +15,6 @@ FLUTTER_ASSERT_ARC
self = [super initWithFrame:CGRectZero]; self = [super initWithFrame:CGRectZero];
if (self) { if (self) {
_semanticsObject = semanticsObject; _semanticsObject = semanticsObject;
_isDoingSystemScrolling = NO;
self.delegate = self;
} }
return self; return self;
} }
@ -107,14 +105,4 @@ FLUTTER_ASSERT_ARC
return self.semanticsObject.children.count; return self.semanticsObject.children.count;
} }
- (void)scrollViewWillEndDragging:(UIScrollView*)scrollView
withVelocity:(CGPoint)velocity
targetContentOffset:(inout CGPoint*)targetContentOffset {
_isDoingSystemScrolling = YES;
}
- (void)scrollViewDidEndDecelerating:(UIScrollView*)scrollView {
_isDoingSystemScrolling = NO;
}
@end @end

View File

@ -3,10 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
#import "SemanticsObject.h" #import "SemanticsObject.h"
#include "flutter/lib/ui/semantics/semantics_node.h"
#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterCodecs.h"
#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.h"
FLUTTER_ASSERT_ARC FLUTTER_ASSERT_ARC
@ -30,19 +27,10 @@ FLUTTER_ASSERT_ARC
// translated to calls such as -[NSObject accessibilityActivate]), while most // translated to calls such as -[NSObject accessibilityActivate]), while most
// other key events are dispatched to the framework. // other key events are dispatched to the framework.
@interface SemanticsObject (UIFocusSystem) <UIFocusItem, UIFocusItemContainer> @interface SemanticsObject (UIFocusSystem) <UIFocusItem, UIFocusItemContainer>
/// The `UIFocusItem` that represents this SemanticsObject.
///
/// For regular `SemanticsObject`s, this method returns `self`,
/// for `FlutterScrollableSemanticsObject`s, this method returns its scroll view.
- (id<UIFocusItem>)focusItem;
@end @end
@implementation SemanticsObject (UIFocusSystem) @implementation SemanticsObject (UIFocusSystem)
- (id<UIFocusItem>)focusItem {
return self;
}
#pragma mark - UIFocusEnvironment Conformance #pragma mark - UIFocusEnvironment Conformance
- (void)setNeedsFocusUpdate { - (void)setNeedsFocusUpdate {
@ -61,7 +49,7 @@ FLUTTER_ASSERT_ARC
- (id<UIFocusEnvironment>)parentFocusEnvironment { - (id<UIFocusEnvironment>)parentFocusEnvironment {
// The root SemanticsObject node's parent is the FlutterView. // The root SemanticsObject node's parent is the FlutterView.
return self.parent.focusItem ?: self.bridge->view(); return self.parent ?: self.bridge->view();
} }
- (NSArray<id<UIFocusEnvironment>>*)preferredFocusEnvironments { - (NSArray<id<UIFocusEnvironment>>*)preferredFocusEnvironments {
@ -83,57 +71,8 @@ FLUTTER_ASSERT_ARC
return self.node.HasAction(flutter::SemanticsAction::kTap); return self.node.HasAction(flutter::SemanticsAction::kTap);
} }
// The frame is described in the `coordinateSpace` of the
// `parentFocusEnvironment` (all `parentFocusEnvironment`s are `UIFocusItem`s).
//
// See also the `coordinateSpace` implementation.
// TODO(LongCatIsLooong): use CoreGraphics types.
- (CGRect)frame { - (CGRect)frame {
SkPoint quad[4] = {SkPoint::Make(self.node.rect.left(), self.node.rect.top()), return self.accessibilityFrame;
SkPoint::Make(self.node.rect.left(), self.node.rect.bottom()),
SkPoint::Make(self.node.rect.right(), self.node.rect.top()),
SkPoint::Make(self.node.rect.right(), self.node.rect.bottom())};
SkM44 transform = self.node.transform;
FlutterSemanticsScrollView* scrollView;
for (SemanticsObject* ancestor = self.parent; ancestor; ancestor = ancestor.parent) {
if ([ancestor isKindOfClass:[FlutterScrollableSemanticsObject class]]) {
scrollView = ((FlutterScrollableSemanticsObject*)ancestor).scrollView;
break;
}
transform = ancestor.node.transform * transform;
}
for (auto& vertex : quad) {
SkV4 vector = transform.map(vertex.x(), vertex.y(), 0, 1);
vertex = SkPoint::Make(vector.x / vector.w, vector.y / vector.w);
}
SkRect rect;
rect.setBounds(quad, 4);
// If this UIFocusItemContainer's coordinateSpace is a UIScrollView, offset
// the rect by `contentOffset` because the contentOffset translation is
// incorporated into the paint transform at different node depth in UIKit
// and Flutter. In Flutter, the translation is added to the cells
// while in UIKit the viewport's bounds is manipulated (IOW, each cell's frame
// in the UIScrollView coordinateSpace does not change when the UIScrollView
// scrolls).
CGRect unscaledRect =
CGRectMake(rect.x() + scrollView.bounds.origin.x, rect.y() + scrollView.bounds.origin.y,
rect.width(), rect.height());
if (scrollView) {
return unscaledRect;
}
// `rect` could be in physical pixels since the root RenderObject ("RenderView")
// applies a transform that turns logical pixels to physical pixels. Undo the
// transform by dividing the coordinates by the screen's scale factor, if this
// UIFocusItem's reported `coordinateSpace` is the root view (which means this
// UIFocusItem is not inside of a scroll view).
//
// Screen can be nil if the FlutterView is covered by another native view.
CGFloat scale = (self.bridge->view().window.screen ?: UIScreen.mainScreen).scale;
return CGRectMake(unscaledRect.origin.x / scale, unscaledRect.origin.y / scale,
unscaledRect.size.width / scale, unscaledRect.size.height / scale);
} }
#pragma mark - UIFocusItemContainer Conformance #pragma mark - UIFocusItemContainer Conformance
@ -148,94 +87,16 @@ FLUTTER_ASSERT_ARC
// //
// This method is only supposed to return items within the given // This method is only supposed to return items within the given
// rect but returning everything in the subtree seems to work fine. // rect but returning everything in the subtree seems to work fine.
NSMutableArray<id<UIFocusItem>>* reversedItems = NSMutableArray<SemanticsObject*>* reversedItems =
[[NSMutableArray alloc] initWithCapacity:self.childrenInHitTestOrder.count]; [[NSMutableArray alloc] initWithCapacity:self.childrenInHitTestOrder.count];
for (NSUInteger i = 0; i < self.childrenInHitTestOrder.count; ++i) { for (NSUInteger i = 0; i < self.childrenInHitTestOrder.count; ++i) {
SemanticsObject* child = self.childrenInHitTestOrder[self.childrenInHitTestOrder.count - 1 - i]; [reversedItems
[reversedItems addObject:child.focusItem]; addObject:self.childrenInHitTestOrder[self.childrenInHitTestOrder.count - 1 - i]];
} }
return reversedItems; return reversedItems;
} }
- (id<UICoordinateSpace>)coordinateSpace { - (id<UICoordinateSpace>)coordinateSpace {
// A regular SemanticsObject uses the same coordinate space as its parent. return self.bridge->view();
return self.parent.coordinateSpace ?: self.bridge->view();
}
@end
/// Scrollable containers interact with the iOS focus engine using the
/// `UIFocusItemScrollableContainer` protocol. The said protocol (and other focus-related protocols)
/// does not provide means to inform the focus system of layout changes. In order for the focus
/// highlight to update properly as the scroll view scrolls, this implementation incorporates a
/// UIScrollView into the focus hierarchy to workaround the highlight update problem.
///
/// As a result, in the current implementation only scrollable containers and the root node
/// establish their own `coordinateSpace`s. All other `UIFocusItemContainter`s use the same
/// `coordinateSpace` as the containing UIScrollView, or the root `FlutterView`, whichever is
/// closer.
///
/// See also the `frame` method implementation.
#pragma mark - Scrolling
@interface FlutterScrollableSemanticsObject (CoordinateSpace)
@end
@implementation FlutterScrollableSemanticsObject (CoordinateSpace)
- (id<UICoordinateSpace>)coordinateSpace {
// A scrollable SemanticsObject uses the same coordinate space as the scroll view.
// This may not work very well in nested scroll views.
return self.scrollView;
}
- (id<UIFocusItem>)focusItem {
return self.scrollView;
}
@end
@interface FlutterSemanticsScrollView (UIFocusItemScrollableContainer) <
UIFocusItemScrollableContainer>
@end
@implementation FlutterSemanticsScrollView (UIFocusItemScrollableContainer)
#pragma mark - FlutterSemanticsScrollView UIFocusItemScrollableContainer Conformance
- (CGSize)visibleSize {
return self.frame.size;
}
- (void)setContentOffset:(CGPoint)contentOffset {
[super setContentOffset:contentOffset];
// Do no send flutter::SemanticsAction::kScrollToOffset if it's triggered
// by a framework update.
if (![self.semanticsObject isAccessibilityBridgeAlive] || !self.isDoingSystemScrolling) {
return;
}
double offset[2] = {contentOffset.x, contentOffset.y};
FlutterStandardTypedData* offsetData = [FlutterStandardTypedData
typedDataWithFloat64:[NSData dataWithBytes:&offset length:sizeof(offset)]];
NSData* encoded = [[FlutterStandardMessageCodec sharedInstance] encode:offsetData];
self.semanticsObject.bridge->DispatchSemanticsAction(
self.semanticsObject.uid, flutter::SemanticsAction::kScrollToOffset,
fml::MallocMapping::Copy(encoded.bytes, encoded.length));
}
- (BOOL)canBecomeFocused {
return NO;
}
- (id<UIFocusEnvironment>)parentFocusEnvironment {
return self.semanticsObject.parentFocusEnvironment;
}
- (NSArray<id<UIFocusEnvironment>>*)preferredFocusEnvironments {
return nil;
}
- (NSArray<id<UIFocusItem>>*)focusItemsInRect:(CGRect)rect {
return [self.semanticsObject focusItemsInRect:rect];
} }
@end @end

View File

@ -10,7 +10,6 @@
#include "flutter/fml/macros.h" #include "flutter/fml/macros.h"
#include "flutter/fml/memory/weak_ptr.h" #include "flutter/fml/memory/weak_ptr.h"
#include "flutter/lib/ui/semantics/semantics_node.h" #include "flutter/lib/ui/semantics/semantics_node.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge_ios.h" #import "flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge_ios.h"
constexpr int32_t kRootNodeId = 0; constexpr int32_t kRootNodeId = 0;
@ -187,7 +186,7 @@ constexpr float kScrollExtentMaxForInf = 1000;
/// The semantics object for scrollable. This class creates an UIScrollView to interact with the /// The semantics object for scrollable. This class creates an UIScrollView to interact with the
/// iOS. /// iOS.
@interface FlutterScrollableSemanticsObject : SemanticsObject @interface FlutterScrollableSemanticsObject : SemanticsObject
@property(nonatomic, readonly) FlutterSemanticsScrollView* scrollView;
@end @end
/** /**

View File

@ -154,8 +154,6 @@ CGRect ConvertRectToGlobal(SemanticsObject* reference, CGRect local_rect) {
_scrollView = [[FlutterSemanticsScrollView alloc] initWithSemanticsObject:self]; _scrollView = [[FlutterSemanticsScrollView alloc] initWithSemanticsObject:self];
[_scrollView setShowsHorizontalScrollIndicator:NO]; [_scrollView setShowsHorizontalScrollIndicator:NO];
[_scrollView setShowsVerticalScrollIndicator:NO]; [_scrollView setShowsVerticalScrollIndicator:NO];
[_scrollView setContentInset:UIEdgeInsetsZero];
[_scrollView setContentInsetAdjustmentBehavior:UIScrollViewContentInsetAdjustmentNever];
[self.bridge->view() addSubview:_scrollView]; [self.bridge->view() addSubview:_scrollView];
} }
return self; return self;
@ -176,10 +174,7 @@ CGRect ConvertRectToGlobal(SemanticsObject* reference, CGRect local_rect) {
// contentOffset is 0.0, only the scroll down action is available. // contentOffset is 0.0, only the scroll down action is available.
self.scrollView.frame = self.accessibilityFrame; self.scrollView.frame = self.accessibilityFrame;
self.scrollView.contentSize = [self contentSizeInternal]; self.scrollView.contentSize = [self contentSizeInternal];
// See the documentation on `isDoingSystemScrolling`. [self.scrollView setContentOffset:[self contentOffsetInternal] animated:NO];
if (!self.scrollView.isDoingSystemScrolling) {
[self.scrollView setContentOffset:self.contentOffsetInternal animated:NO];
}
} }
- (id)nativeAccessibility { - (id)nativeAccessibility {

View File

@ -7,7 +7,6 @@
#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTouchInterceptingView_Test.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTouchInterceptingView_Test.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.h" #import "flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/SemanticsObjectTestMocks.h" #import "flutter/shell/platform/darwin/ios/framework/Source/SemanticsObjectTestMocks.h"
@ -20,10 +19,6 @@ const float kFloatCompareEpsilon = 0.001;
@interface SemanticsObject (UIFocusSystem) <UIFocusItem, UIFocusItemContainer> @interface SemanticsObject (UIFocusSystem) <UIFocusItem, UIFocusItemContainer>
@end @end
@interface FlutterScrollableSemanticsObject (UIFocusItemScrollableContainer) <
UIFocusItemScrollableContainer>
@end
@interface TextInputSemanticsObject (Test) @interface TextInputSemanticsObject (Test)
- (UIView<UITextInput>*)textInputSurrogate; - (UIView<UITextInput>*)textInputSurrogate;
@end @end
@ -670,7 +665,7 @@ const float kFloatCompareEpsilon = 0.001;
XCTAssertEqual(container.semanticsObject, parentObject); XCTAssertEqual(container.semanticsObject, parentObject);
} }
- (void)testFlutterScrollableSemanticsObjectNoScrollBarOrContentInsets { - (void)testFlutterScrollableSemanticsObjectHidesScrollBar {
fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory( fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
new flutter::testing::MockAccessibilityBridge()); new flutter::testing::MockAccessibilityBridge());
fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr(); fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
@ -690,9 +685,6 @@ const float kFloatCompareEpsilon = 0.001;
XCTAssertFalse(scrollView.showsHorizontalScrollIndicator); XCTAssertFalse(scrollView.showsHorizontalScrollIndicator);
XCTAssertFalse(scrollView.showsVerticalScrollIndicator); XCTAssertFalse(scrollView.showsVerticalScrollIndicator);
XCTAssertEqual(scrollView.contentInsetAdjustmentBehavior,
UIScrollViewContentInsetAdjustmentNever);
XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(scrollView.contentInset, UIEdgeInsetsZero));
} }
- (void)testSemanticsObjectBuildsAttributedString { - (void)testSemanticsObjectBuildsAttributedString {
@ -1163,19 +1155,6 @@ const float kFloatCompareEpsilon = 0.001;
[self waitForExpectationsWithTimeout:1 handler:nil]; [self waitForExpectationsWithTimeout:1 handler:nil];
} }
- (void)testSliderSemanticsObject {
fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
new flutter::testing::MockAccessibilityBridge());
fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
flutter::SemanticsNode node;
node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kIsSlider);
SemanticsObject* object = [[SemanticsObject alloc] initWithBridge:bridge uid:0];
[object setSemanticsNode:&node];
[object accessibilityBridgeDidFinishUpdate];
XCTAssertEqual([object accessibilityActivate], YES);
}
- (void)testUIFocusItemConformance { - (void)testUIFocusItemConformance {
fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory( fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
new flutter::testing::MockAccessibilityBridge()); new flutter::testing::MockAccessibilityBridge());
@ -1228,57 +1207,17 @@ const float kFloatCompareEpsilon = 0.001;
XCTAssertTrue([itemsInRect containsObject:child2]); XCTAssertTrue([itemsInRect containsObject:child2]);
} }
- (void)testUIFocusItemScrollableContainerConformance { - (void)testSliderSemanticsObject {
fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory( fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
new flutter::testing::MockAccessibilityBridge()); new flutter::testing::MockAccessibilityBridge());
fml::WeakPtr<flutter::testing::MockAccessibilityBridge> bridge = factory.GetWeakPtr(); fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
FlutterScrollableSemanticsObject* scrollable =
[[FlutterScrollableSemanticsObject alloc] initWithBridge:bridge uid:5];
// setContentOffset
CGPoint p = CGPointMake(123.0, 456.0);
[scrollable.scrollView scrollViewWillEndDragging:scrollable.scrollView
withVelocity:CGPointZero
targetContentOffset:&p];
scrollable.scrollView.contentOffset = p;
[scrollable.scrollView scrollViewDidEndDecelerating:scrollable.scrollView];
XCTAssertEqual(bridge->observations.size(), (size_t)1);
XCTAssertEqual(bridge->observations[0].id, 5);
XCTAssertEqual(bridge->observations[0].action, flutter::SemanticsAction::kScrollToOffset);
std::vector<uint8_t> args = bridge->observations[0].args;
XCTAssertEqual(args.size(), 3 * sizeof(CGFloat));
NSData* encoded = [NSData dataWithBytes:args.data() length:args.size()];
FlutterStandardTypedData* decoded = [[FlutterStandardMessageCodec sharedInstance] decode:encoded];
CGPoint point = CGPointZero;
memcpy(&point, decoded.data.bytes, decoded.data.length);
XCTAssertTrue(CGPointEqualToPoint(point, p));
}
- (void)testUIFocusItemScrollableContainerNoFeedbackLoops {
fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
new flutter::testing::MockAccessibilityBridge());
fml::WeakPtr<flutter::testing::MockAccessibilityBridge> bridge = factory.GetWeakPtr();
FlutterScrollableSemanticsObject* scrollable =
[[FlutterScrollableSemanticsObject alloc] initWithBridge:bridge uid:5];
// setContentOffset
const CGPoint p = CGPointMake(0.0, 456.0);
scrollable.scrollView.contentOffset = p;
bridge->observations.clear();
const SkScalar scrollPosition = p.y + 0.0000000000000001;
flutter::SemanticsNode node; flutter::SemanticsNode node;
node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kHasImplicitScrolling); node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kIsSlider);
node.actions = flutter::kVerticalScrollSemanticsActions; SemanticsObject* object = [[SemanticsObject alloc] initWithBridge:bridge uid:0];
node.rect = SkRect::MakeXYWH(0, 0, 100, 200); [object setSemanticsNode:&node];
node.scrollExtentMax = 10000; [object accessibilityBridgeDidFinishUpdate];
node.scrollPosition = scrollPosition; XCTAssertEqual([object accessibilityActivate], YES);
node.transform = {1.0, 0, 0, 0, 0, 1.0, 0, 0, 0, 0, 1.0, 0, 0, scrollPosition, 0, 1.0};
[scrollable setSemanticsNode:&node];
[scrollable accessibilityBridgeDidFinishUpdate];
XCTAssertEqual(bridge->observations.size(), (size_t)0);
} }
@end @end

View File

@ -15,18 +15,10 @@ namespace testing {
class SemanticsActionObservation { class SemanticsActionObservation {
public: public:
SemanticsActionObservation(int32_t observed_id, SemanticsAction observed_action) SemanticsActionObservation(int32_t observed_id, SemanticsAction observed_action)
: id(observed_id), action(observed_action), args({}) {} : id(observed_id), action(observed_action) {}
SemanticsActionObservation(int32_t observed_id,
SemanticsAction observed_action,
fml::MallocMapping& args)
: id(observed_id),
action(observed_action),
args(args.GetMapping(), args.GetMapping() + args.GetSize()) {}
int32_t id; int32_t id;
SemanticsAction action; SemanticsAction action;
std::vector<uint8_t> args;
}; };
class MockAccessibilityBridge : public AccessibilityBridgeIos { class MockAccessibilityBridge : public AccessibilityBridgeIos {
@ -46,7 +38,7 @@ class MockAccessibilityBridge : public AccessibilityBridgeIos {
void DispatchSemanticsAction(int32_t id, void DispatchSemanticsAction(int32_t id,
SemanticsAction action, SemanticsAction action,
fml::MallocMapping args) override { fml::MallocMapping args) override {
SemanticsActionObservation observation(id, action, args); SemanticsActionObservation observation(id, action);
observations.push_back(observation); observations.push_back(observation);
} }
void AccessibilityObjectDidBecomeFocused(int32_t id) override {} void AccessibilityObjectDidBecomeFocused(int32_t id) override {}
@ -77,7 +69,7 @@ class MockAccessibilityBridgeNoWindow : public AccessibilityBridgeIos {
void DispatchSemanticsAction(int32_t id, void DispatchSemanticsAction(int32_t id,
SemanticsAction action, SemanticsAction action,
fml::MallocMapping args) override { fml::MallocMapping args) override {
SemanticsActionObservation observation(id, action, args); SemanticsActionObservation observation(id, action);
observations.push_back(observation); observations.push_back(observation);
} }
void AccessibilityObjectDidBecomeFocused(int32_t id) override {} void AccessibilityObjectDidBecomeFocused(int32_t id) override {}

View File

@ -164,9 +164,6 @@ typedef enum {
kFlutterSemanticsActionSetText = 1 << 21, kFlutterSemanticsActionSetText = 1 << 21,
/// Request that the respective focusable widget gain input focus. /// Request that the respective focusable widget gain input focus.
kFlutterSemanticsActionFocus = 1 << 22, kFlutterSemanticsActionFocus = 1 << 22,
/// Request that scrolls the current scrollable container to a given scroll
/// offset.
kFlutterSemanticsActionScrollToOffset = 1 << 23,
} FlutterSemanticsAction; } FlutterSemanticsAction;
/// The set of properties that may be associated with a semantics node. /// The set of properties that may be associated with a semantics node.

View File

@ -161,9 +161,6 @@ std::string NodeActionsToString(const flutter::SemanticsNode& node) {
if (node.HasAction(flutter::SemanticsAction::kScrollUp)) { if (node.HasAction(flutter::SemanticsAction::kScrollUp)) {
output += "kScrollUp|"; output += "kScrollUp|";
} }
if (node.HasAction(flutter::SemanticsAction::kScrollToOffset)) {
output += "kScrollToOffset|";
}
if (node.HasAction(flutter::SemanticsAction::kSetSelection)) { if (node.HasAction(flutter::SemanticsAction::kSetSelection)) {
output += "kSetSelection|"; output += "kSetSelection|";
} }

View File

@ -22,7 +22,7 @@ void main() {
}); });
// This must match the number of actions in lib/ui/semantics.dart // This must match the number of actions in lib/ui/semantics.dart
const int numSemanticsActions = 24; const int numSemanticsActions = 23;
test('SemanticsAction.values refers to all actions.', () async { test('SemanticsAction.values refers to all actions.', () async {
expect(SemanticsAction.values.length, equals(numSemanticsActions)); expect(SemanticsAction.values.length, equals(numSemanticsActions));
for (int index = 0; index < numSemanticsActions; ++index) { for (int index = 0; index < numSemanticsActions; ++index) {