Reland "[iOS] Full keyboard access scrolling (#56606)" (flutter/engine#56842)

Reverts flutter/engine#56802

https://github.com/flutter/flutter/pull/159517 should address the engine roll failure.

I'm not planning to land this until the coming Monday.
This commit is contained in:
LongCatIsLooong 2024-12-02 13:53:21 -08:00 committed by GitHub
parent aa01970589
commit 18cf7ae0a7
15 changed files with 295 additions and 25 deletions

View File

@ -45,6 +45,7 @@ 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
@ -86,6 +87,17 @@ 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.
@ -265,6 +277,7 @@ 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,
@ -764,7 +777,7 @@ base class LocaleStringAttribute extends StringAttribute {
_initLocaleStringAttribute(this, range.start, range.end, locale.toLanguageTag()); _initLocaleStringAttribute(this, range.start, range.end, locale.toLanguageTag());
} }
/// The lanuage of this attribute. /// The language 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,6 +43,7 @@ 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,6 +33,7 @@ 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');
@ -40,6 +41,7 @@ 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');
@ -65,6 +67,7 @@ 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 = 23; const int numSemanticsActions = 24;
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,7 +2120,8 @@ 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,10 +18,30 @@ 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 @interface FlutterSemanticsScrollView : UIScrollView <UIScrollViewDelegate>
@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,6 +15,8 @@ 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;
} }
@ -105,4 +107,14 @@ 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,7 +3,10 @@
// 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
@ -27,10 +30,19 @@ 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 {
@ -49,7 +61,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 ?: self.bridge->view(); return self.parent.focusItem ?: self.bridge->view();
} }
- (NSArray<id<UIFocusEnvironment>>*)preferredFocusEnvironments { - (NSArray<id<UIFocusEnvironment>>*)preferredFocusEnvironments {
@ -71,8 +83,57 @@ 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 {
return self.accessibilityFrame; SkPoint quad[4] = {SkPoint::Make(self.node.rect.left(), self.node.rect.top()),
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
@ -87,16 +148,94 @@ 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<SemanticsObject*>* reversedItems = NSMutableArray<id<UIFocusItem>>* 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) {
[reversedItems SemanticsObject* child = self.childrenInHitTestOrder[self.childrenInHitTestOrder.count - 1 - i];
addObject:self.childrenInHitTestOrder[self.childrenInHitTestOrder.count - 1 - i]]; [reversedItems addObject:child.focusItem];
} }
return reversedItems; return reversedItems;
} }
- (id<UICoordinateSpace>)coordinateSpace { - (id<UICoordinateSpace>)coordinateSpace {
return self.bridge->view(); // A regular SemanticsObject uses the same coordinate space as its parent.
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,6 +10,7 @@
#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;
@ -186,7 +187,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,6 +154,8 @@ 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;
@ -174,7 +176,10 @@ 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];
[self.scrollView setContentOffset:[self contentOffsetInternal] animated:NO]; // See the documentation on `isDoingSystemScrolling`.
if (!self.scrollView.isDoingSystemScrolling) {
[self.scrollView setContentOffset:self.contentOffsetInternal animated:NO];
}
} }
- (id)nativeAccessibility { - (id)nativeAccessibility {

View File

@ -7,6 +7,7 @@
#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"
@ -19,6 +20,10 @@ 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
@ -665,7 +670,7 @@ const float kFloatCompareEpsilon = 0.001;
XCTAssertEqual(container.semanticsObject, parentObject); XCTAssertEqual(container.semanticsObject, parentObject);
} }
- (void)testFlutterScrollableSemanticsObjectHidesScrollBar { - (void)testFlutterScrollableSemanticsObjectNoScrollBarOrContentInsets {
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();
@ -685,6 +690,9 @@ 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 {
@ -1155,6 +1163,19 @@ 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());
@ -1207,17 +1228,57 @@ const float kFloatCompareEpsilon = 0.001;
XCTAssertTrue([itemsInRect containsObject:child2]); XCTAssertTrue([itemsInRect containsObject:child2]);
} }
- (void)testSliderSemanticsObject { - (void)testUIFocusItemScrollableContainerConformance {
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::testing::MockAccessibilityBridge> bridge = factory.GetWeakPtr();
FlutterScrollableSemanticsObject* scrollable =
[[FlutterScrollableSemanticsObject alloc] initWithBridge:bridge uid:5];
flutter::SemanticsNode node; // setContentOffset
node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kIsSlider); CGPoint p = CGPointMake(123.0, 456.0);
SemanticsObject* object = [[SemanticsObject alloc] initWithBridge:bridge uid:0]; [scrollable.scrollView scrollViewWillEndDragging:scrollable.scrollView
[object setSemanticsNode:&node]; withVelocity:CGPointZero
[object accessibilityBridgeDidFinishUpdate]; targetContentOffset:&p];
XCTAssertEqual([object accessibilityActivate], YES); 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;
node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kHasImplicitScrolling);
node.actions = flutter::kVerticalScrollSemanticsActions;
node.rect = SkRect::MakeXYWH(0, 0, 100, 200);
node.scrollExtentMax = 10000;
node.scrollPosition = scrollPosition;
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,10 +15,18 @@ 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) {} : id(observed_id), action(observed_action), args({}) {}
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 {
@ -38,7 +46,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); SemanticsActionObservation observation(id, action, args);
observations.push_back(observation); observations.push_back(observation);
} }
void AccessibilityObjectDidBecomeFocused(int32_t id) override {} void AccessibilityObjectDidBecomeFocused(int32_t id) override {}
@ -67,7 +75,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); SemanticsActionObservation observation(id, action, args);
observations.push_back(observation); observations.push_back(observation);
} }
void AccessibilityObjectDidBecomeFocused(int32_t id) override {} void AccessibilityObjectDidBecomeFocused(int32_t id) override {}

View File

@ -164,6 +164,9 @@ 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,6 +161,9 @@ 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 = 23; const int numSemanticsActions = 24;
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) {