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:
parent
aa01970589
commit
18cf7ae0a7
@ -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')
|
||||||
|
@ -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 =
|
||||||
|
@ -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,
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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 {}
|
||||||
|
@ -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.
|
||||||
|
@ -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|";
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user