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:
parent
569d30910b
commit
57b102520b
@ -45,7 +45,6 @@ class SemanticsAction {
|
||||
static const int _kMoveCursorBackwardByWordIndex = 1 << 20;
|
||||
static const int _kSetTextIndex = 1 << 21;
|
||||
static const int _kFocusIndex = 1 << 22;
|
||||
static const int _kScrollToOffsetIndex = 1 << 23;
|
||||
// READ THIS: if you add an action here, you MUST update the
|
||||
// numSemanticsActions value in testing/dart/semantics_test.dart and
|
||||
// lib/web_ui/test/engine/semantics/semantics_api_test.dart, or tests
|
||||
@ -87,17 +86,6 @@ class SemanticsAction {
|
||||
/// scrollable.
|
||||
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.
|
||||
///
|
||||
/// For example, this action might be recognized by a slider control.
|
||||
@ -277,7 +265,6 @@ class SemanticsAction {
|
||||
_kScrollRightIndex: scrollRight,
|
||||
_kScrollUpIndex: scrollUp,
|
||||
_kScrollDownIndex: scrollDown,
|
||||
_kScrollToOffsetIndex: scrollToOffset,
|
||||
_kIncreaseIndex: increase,
|
||||
_kDecreaseIndex: decrease,
|
||||
_kShowOnScreenIndex: showOnScreen,
|
||||
@ -777,7 +764,7 @@ base class LocaleStringAttribute extends StringAttribute {
|
||||
_initLocaleStringAttribute(this, range.start, range.end, locale.toLanguageTag());
|
||||
}
|
||||
|
||||
/// The language of this attribute.
|
||||
/// The lanuage of this attribute.
|
||||
final Locale locale;
|
||||
|
||||
@Native<Void Function(Handle, Int32, Int32, Handle)>(symbol: 'NativeStringAttribute::initLocaleStringAttribute')
|
||||
|
@ -43,7 +43,6 @@ enum class SemanticsAction : int32_t {
|
||||
kMoveCursorBackwardByWord = 1 << 20,
|
||||
kSetText = 1 << 21,
|
||||
kFocus = 1 << 22,
|
||||
kScrollToOffset = 1 << 23,
|
||||
};
|
||||
|
||||
const int kVerticalScrollSemanticsActions =
|
||||
|
@ -33,7 +33,6 @@ class SemanticsAction {
|
||||
static const int _kMoveCursorBackwardByWordIndex = 1 << 20;
|
||||
static const int _kSetTextIndex = 1 << 21;
|
||||
static const int _kFocusIndex = 1 << 22;
|
||||
static const int _kScrollToOffsetIndex = 1 << 23;
|
||||
|
||||
static const SemanticsAction tap = SemanticsAction._(_kTapIndex, 'tap');
|
||||
static const SemanticsAction longPress = SemanticsAction._(_kLongPressIndex, 'longPress');
|
||||
@ -41,7 +40,6 @@ class SemanticsAction {
|
||||
static const SemanticsAction scrollRight = SemanticsAction._(_kScrollRightIndex, 'scrollRight');
|
||||
static const SemanticsAction scrollUp = SemanticsAction._(_kScrollUpIndex, 'scrollUp');
|
||||
static const SemanticsAction scrollDown = SemanticsAction._(_kScrollDownIndex, 'scrollDown');
|
||||
static const SemanticsAction scrollToOffset = SemanticsAction._(_kScrollToOffsetIndex, 'scrollToOffset');
|
||||
static const SemanticsAction increase = SemanticsAction._(_kIncreaseIndex, 'increase');
|
||||
static const SemanticsAction decrease = SemanticsAction._(_kDecreaseIndex, 'decrease');
|
||||
static const SemanticsAction showOnScreen = SemanticsAction._(_kShowOnScreenIndex, 'showOnScreen');
|
||||
@ -67,7 +65,6 @@ class SemanticsAction {
|
||||
_kScrollRightIndex: scrollRight,
|
||||
_kScrollUpIndex: scrollUp,
|
||||
_kScrollDownIndex: scrollDown,
|
||||
_kScrollToOffsetIndex: scrollToOffset,
|
||||
_kIncreaseIndex: increase,
|
||||
_kDecreaseIndex: decrease,
|
||||
_kShowOnScreenIndex: showOnScreen,
|
||||
|
@ -29,7 +29,7 @@ void testMain() {
|
||||
});
|
||||
|
||||
// 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 {
|
||||
expect(SemanticsAction.values.length, equals(numSemanticsActions));
|
||||
for (int index = 0; index < numSemanticsActions; ++index) {
|
||||
|
@ -2120,8 +2120,7 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
|
||||
MOVE_CURSOR_FORWARD_BY_WORD(1 << 19),
|
||||
MOVE_CURSOR_BACKWARD_BY_WORD(1 << 20),
|
||||
SET_TEXT(1 << 21),
|
||||
FOCUS(1 << 22),
|
||||
SCROLL_TO_OFFSET(1 << 23);
|
||||
FOCUS(1 << 22);
|
||||
|
||||
public final int value;
|
||||
|
||||
|
@ -18,30 +18,10 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
* sends all of selector calls from accessibility services to the
|
||||
* owner SemanticsObject.
|
||||
*/
|
||||
@interface FlutterSemanticsScrollView : UIScrollView <UIScrollViewDelegate>
|
||||
@interface FlutterSemanticsScrollView : UIScrollView
|
||||
|
||||
@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)initWithFrame:(CGRect)frame NS_UNAVAILABLE;
|
||||
- (instancetype)initWithCoder:(NSCoder*)coder NS_UNAVAILABLE;
|
||||
|
@ -15,8 +15,6 @@ FLUTTER_ASSERT_ARC
|
||||
self = [super initWithFrame:CGRectZero];
|
||||
if (self) {
|
||||
_semanticsObject = semanticsObject;
|
||||
_isDoingSystemScrolling = NO;
|
||||
self.delegate = self;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@ -107,14 +105,4 @@ FLUTTER_ASSERT_ARC
|
||||
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
|
||||
|
@ -3,10 +3,7 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
#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/ios/framework/Source/FlutterSemanticsScrollView.h"
|
||||
|
||||
FLUTTER_ASSERT_ARC
|
||||
|
||||
@ -30,19 +27,10 @@ FLUTTER_ASSERT_ARC
|
||||
// translated to calls such as -[NSObject accessibilityActivate]), while most
|
||||
// other key events are dispatched to the framework.
|
||||
@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
|
||||
|
||||
@implementation SemanticsObject (UIFocusSystem)
|
||||
|
||||
- (id<UIFocusItem>)focusItem {
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - UIFocusEnvironment Conformance
|
||||
|
||||
- (void)setNeedsFocusUpdate {
|
||||
@ -61,7 +49,7 @@ FLUTTER_ASSERT_ARC
|
||||
|
||||
- (id<UIFocusEnvironment>)parentFocusEnvironment {
|
||||
// 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 {
|
||||
@ -83,57 +71,8 @@ FLUTTER_ASSERT_ARC
|
||||
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 {
|
||||
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);
|
||||
return self.accessibilityFrame;
|
||||
}
|
||||
|
||||
#pragma mark - UIFocusItemContainer Conformance
|
||||
@ -148,94 +87,16 @@ FLUTTER_ASSERT_ARC
|
||||
//
|
||||
// This method is only supposed to return items within the given
|
||||
// rect but returning everything in the subtree seems to work fine.
|
||||
NSMutableArray<id<UIFocusItem>>* reversedItems =
|
||||
NSMutableArray<SemanticsObject*>* reversedItems =
|
||||
[[NSMutableArray alloc] initWithCapacity:self.childrenInHitTestOrder.count];
|
||||
for (NSUInteger i = 0; i < self.childrenInHitTestOrder.count; ++i) {
|
||||
SemanticsObject* child = self.childrenInHitTestOrder[self.childrenInHitTestOrder.count - 1 - i];
|
||||
[reversedItems addObject:child.focusItem];
|
||||
[reversedItems
|
||||
addObject:self.childrenInHitTestOrder[self.childrenInHitTestOrder.count - 1 - i]];
|
||||
}
|
||||
return reversedItems;
|
||||
}
|
||||
|
||||
- (id<UICoordinateSpace>)coordinateSpace {
|
||||
// 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];
|
||||
return self.bridge->view();
|
||||
}
|
||||
@end
|
||||
|
@ -10,7 +10,6 @@
|
||||
#include "flutter/fml/macros.h"
|
||||
#include "flutter/fml/memory/weak_ptr.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"
|
||||
|
||||
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
|
||||
/// iOS.
|
||||
@interface FlutterScrollableSemanticsObject : SemanticsObject
|
||||
@property(nonatomic, readonly) FlutterSemanticsScrollView* scrollView;
|
||||
|
||||
@end
|
||||
|
||||
/**
|
||||
|
@ -154,8 +154,6 @@ CGRect ConvertRectToGlobal(SemanticsObject* reference, CGRect local_rect) {
|
||||
_scrollView = [[FlutterSemanticsScrollView alloc] initWithSemanticsObject:self];
|
||||
[_scrollView setShowsHorizontalScrollIndicator:NO];
|
||||
[_scrollView setShowsVerticalScrollIndicator:NO];
|
||||
[_scrollView setContentInset:UIEdgeInsetsZero];
|
||||
[_scrollView setContentInsetAdjustmentBehavior:UIScrollViewContentInsetAdjustmentNever];
|
||||
[self.bridge->view() addSubview:_scrollView];
|
||||
}
|
||||
return self;
|
||||
@ -176,10 +174,7 @@ CGRect ConvertRectToGlobal(SemanticsObject* reference, CGRect local_rect) {
|
||||
// contentOffset is 0.0, only the scroll down action is available.
|
||||
self.scrollView.frame = self.accessibilityFrame;
|
||||
self.scrollView.contentSize = [self contentSizeInternal];
|
||||
// See the documentation on `isDoingSystemScrolling`.
|
||||
if (!self.scrollView.isDoingSystemScrolling) {
|
||||
[self.scrollView setContentOffset:self.contentOffsetInternal animated:NO];
|
||||
}
|
||||
[self.scrollView setContentOffset:[self contentOffsetInternal] animated:NO];
|
||||
}
|
||||
|
||||
- (id)nativeAccessibility {
|
||||
|
@ -7,7 +7,6 @@
|
||||
|
||||
#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/FlutterSemanticsScrollView.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/SemanticsObjectTestMocks.h"
|
||||
@ -20,10 +19,6 @@ const float kFloatCompareEpsilon = 0.001;
|
||||
@interface SemanticsObject (UIFocusSystem) <UIFocusItem, UIFocusItemContainer>
|
||||
@end
|
||||
|
||||
@interface FlutterScrollableSemanticsObject (UIFocusItemScrollableContainer) <
|
||||
UIFocusItemScrollableContainer>
|
||||
@end
|
||||
|
||||
@interface TextInputSemanticsObject (Test)
|
||||
- (UIView<UITextInput>*)textInputSurrogate;
|
||||
@end
|
||||
@ -670,7 +665,7 @@ const float kFloatCompareEpsilon = 0.001;
|
||||
XCTAssertEqual(container.semanticsObject, parentObject);
|
||||
}
|
||||
|
||||
- (void)testFlutterScrollableSemanticsObjectNoScrollBarOrContentInsets {
|
||||
- (void)testFlutterScrollableSemanticsObjectHidesScrollBar {
|
||||
fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
|
||||
new flutter::testing::MockAccessibilityBridge());
|
||||
fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
|
||||
@ -690,9 +685,6 @@ const float kFloatCompareEpsilon = 0.001;
|
||||
|
||||
XCTAssertFalse(scrollView.showsHorizontalScrollIndicator);
|
||||
XCTAssertFalse(scrollView.showsVerticalScrollIndicator);
|
||||
XCTAssertEqual(scrollView.contentInsetAdjustmentBehavior,
|
||||
UIScrollViewContentInsetAdjustmentNever);
|
||||
XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(scrollView.contentInset, UIEdgeInsetsZero));
|
||||
}
|
||||
|
||||
- (void)testSemanticsObjectBuildsAttributedString {
|
||||
@ -1163,19 +1155,6 @@ const float kFloatCompareEpsilon = 0.001;
|
||||
[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 {
|
||||
fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
|
||||
new flutter::testing::MockAccessibilityBridge());
|
||||
@ -1228,57 +1207,17 @@ const float kFloatCompareEpsilon = 0.001;
|
||||
XCTAssertTrue([itemsInRect containsObject:child2]);
|
||||
}
|
||||
|
||||
- (void)testUIFocusItemScrollableContainerConformance {
|
||||
- (void)testSliderSemanticsObject {
|
||||
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];
|
||||
fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
|
||||
|
||||
// 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;
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
@end
|
||||
|
@ -15,18 +15,10 @@ namespace testing {
|
||||
class SemanticsActionObservation {
|
||||
public:
|
||||
SemanticsActionObservation(int32_t observed_id, SemanticsAction 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()) {}
|
||||
: id(observed_id), action(observed_action) {}
|
||||
|
||||
int32_t id;
|
||||
SemanticsAction action;
|
||||
std::vector<uint8_t> args;
|
||||
};
|
||||
|
||||
class MockAccessibilityBridge : public AccessibilityBridgeIos {
|
||||
@ -46,7 +38,7 @@ class MockAccessibilityBridge : public AccessibilityBridgeIos {
|
||||
void DispatchSemanticsAction(int32_t id,
|
||||
SemanticsAction action,
|
||||
fml::MallocMapping args) override {
|
||||
SemanticsActionObservation observation(id, action, args);
|
||||
SemanticsActionObservation observation(id, action);
|
||||
observations.push_back(observation);
|
||||
}
|
||||
void AccessibilityObjectDidBecomeFocused(int32_t id) override {}
|
||||
@ -77,7 +69,7 @@ class MockAccessibilityBridgeNoWindow : public AccessibilityBridgeIos {
|
||||
void DispatchSemanticsAction(int32_t id,
|
||||
SemanticsAction action,
|
||||
fml::MallocMapping args) override {
|
||||
SemanticsActionObservation observation(id, action, args);
|
||||
SemanticsActionObservation observation(id, action);
|
||||
observations.push_back(observation);
|
||||
}
|
||||
void AccessibilityObjectDidBecomeFocused(int32_t id) override {}
|
||||
|
@ -164,9 +164,6 @@ typedef enum {
|
||||
kFlutterSemanticsActionSetText = 1 << 21,
|
||||
/// Request that the respective focusable widget gain input focus.
|
||||
kFlutterSemanticsActionFocus = 1 << 22,
|
||||
/// Request that scrolls the current scrollable container to a given scroll
|
||||
/// offset.
|
||||
kFlutterSemanticsActionScrollToOffset = 1 << 23,
|
||||
} FlutterSemanticsAction;
|
||||
|
||||
/// The set of properties that may be associated with a semantics node.
|
||||
|
@ -161,9 +161,6 @@ std::string NodeActionsToString(const flutter::SemanticsNode& node) {
|
||||
if (node.HasAction(flutter::SemanticsAction::kScrollUp)) {
|
||||
output += "kScrollUp|";
|
||||
}
|
||||
if (node.HasAction(flutter::SemanticsAction::kScrollToOffset)) {
|
||||
output += "kScrollToOffset|";
|
||||
}
|
||||
if (node.HasAction(flutter::SemanticsAction::kSetSelection)) {
|
||||
output += "kSetSelection|";
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ void main() {
|
||||
});
|
||||
|
||||
// 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 {
|
||||
expect(SemanticsAction.values.length, equals(numSemanticsActions));
|
||||
for (int index = 0; index < numSemanticsActions; ++index) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user