forked from firka/flutter
Add handler for SemanticsAction.scrollToOffset (#159811)
Also closes https://github.com/flutter/flutter/issues/159515 ~Looking at the google testing failures~ cl/703169955 ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
@@ -49,6 +49,10 @@ typedef SetSelectionHandler = void Function(TextSelection selection);
|
||||
/// current text with the input `text`.
|
||||
typedef SetTextHandler = void Function(String text);
|
||||
|
||||
/// Signature for the [SemanticsAction.scrollToOffset] handlers to scroll the
|
||||
/// scrollable container to the given `targetOffset`.
|
||||
typedef ScrollToOffsetHandler = void Function(Offset targetOffset);
|
||||
|
||||
/// Signature for a handler of a [SemanticsAction].
|
||||
///
|
||||
/// Returned by [SemanticsConfiguration.getActionHandler].
|
||||
@@ -3920,6 +3924,29 @@ class SemanticsConfiguration {
|
||||
_onScrollDown = value;
|
||||
}
|
||||
|
||||
/// The handler for [SemanticsAction.scrollToOffset].
|
||||
///
|
||||
/// This handler is only called on iOS by UIKit, when the iOS focus engine
|
||||
/// switches its focus to an item too close to a scrollable edge of a
|
||||
/// scrollable container, to make sure the focused item is always fully
|
||||
/// visible.
|
||||
///
|
||||
/// The callback, if not `null`, should typically set the scroll offset of
|
||||
/// the associated scrollable container to the given `targetOffset` without
|
||||
/// animation as it is already animated by the caller: the iOS focus engine
|
||||
/// invokes [onScrollToOffset] every frame during the scroll animation with
|
||||
/// animated scroll offset values.
|
||||
ScrollToOffsetHandler? get onScrollToOffset => _onScrollToOffset;
|
||||
ScrollToOffsetHandler? _onScrollToOffset;
|
||||
set onScrollToOffset(ScrollToOffsetHandler? value) {
|
||||
assert(value != null);
|
||||
_addAction(SemanticsAction.scrollToOffset, (Object? args) {
|
||||
final Float64List list = args! as Float64List;
|
||||
value!(Offset(list[0], list[1]));
|
||||
});
|
||||
_onScrollToOffset = value;
|
||||
}
|
||||
|
||||
/// The handler for [SemanticsAction.increase].
|
||||
///
|
||||
/// This is a request to increase the value represented by the widget. For
|
||||
|
||||
@@ -611,12 +611,10 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
|
||||
// Only call this from places that will definitely trigger a rebuild.
|
||||
void _updatePosition() {
|
||||
_configuration = widget.scrollBehavior ?? ScrollConfiguration.of(context);
|
||||
_physics = _configuration.getScrollPhysics(context);
|
||||
if (widget.physics != null) {
|
||||
_physics = widget.physics!.applyTo(_physics);
|
||||
} else if (widget.scrollBehavior != null) {
|
||||
_physics = widget.scrollBehavior!.getScrollPhysics(context).applyTo(_physics);
|
||||
}
|
||||
final ScrollPhysics? physicsFromWidget = widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context);
|
||||
_physics = _configuration.getScrollPhysics(context);
|
||||
_physics = physicsFromWidget?.applyTo(_physics) ?? _physics;
|
||||
|
||||
final ScrollPosition? oldPosition = _position;
|
||||
if (oldPosition != null) {
|
||||
_effectiveScrollController.detach(oldPosition);
|
||||
@@ -1032,6 +1030,7 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
|
||||
key: _scrollSemanticsKey,
|
||||
position: position,
|
||||
allowImplicitScrolling: _physics!.allowImplicitScrolling,
|
||||
axis: widget.axis,
|
||||
semanticChildCount: widget.semanticChildCount,
|
||||
child: result,
|
||||
)
|
||||
@@ -1556,6 +1555,7 @@ class _ScrollSemantics extends SingleChildRenderObjectWidget {
|
||||
super.key,
|
||||
required this.position,
|
||||
required this.allowImplicitScrolling,
|
||||
required this.axis,
|
||||
required this.semanticChildCount,
|
||||
super.child,
|
||||
}) : assert(semanticChildCount == null || semanticChildCount >= 0);
|
||||
@@ -1563,6 +1563,7 @@ class _ScrollSemantics extends SingleChildRenderObjectWidget {
|
||||
final ScrollPosition position;
|
||||
final bool allowImplicitScrolling;
|
||||
final int? semanticChildCount;
|
||||
final Axis axis;
|
||||
|
||||
@override
|
||||
_RenderScrollSemantics createRenderObject(BuildContext context) {
|
||||
@@ -1570,6 +1571,7 @@ class _ScrollSemantics extends SingleChildRenderObjectWidget {
|
||||
position: position,
|
||||
allowImplicitScrolling: allowImplicitScrolling,
|
||||
semanticChildCount: semanticChildCount,
|
||||
axis: axis,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1577,6 +1579,7 @@ class _ScrollSemantics extends SingleChildRenderObjectWidget {
|
||||
void updateRenderObject(BuildContext context, _RenderScrollSemantics renderObject) {
|
||||
renderObject
|
||||
..allowImplicitScrolling = allowImplicitScrolling
|
||||
..axis = axis
|
||||
..position = position
|
||||
..semanticChildCount = semanticChildCount;
|
||||
}
|
||||
@@ -1586,6 +1589,7 @@ class _RenderScrollSemantics extends RenderProxyBox {
|
||||
_RenderScrollSemantics({
|
||||
required ScrollPosition position,
|
||||
required bool allowImplicitScrolling,
|
||||
required this.axis,
|
||||
required int? semanticChildCount,
|
||||
RenderBox? child,
|
||||
}) : _position = position,
|
||||
@@ -1619,6 +1623,8 @@ class _RenderScrollSemantics extends RenderProxyBox {
|
||||
markNeedsSemanticsUpdate();
|
||||
}
|
||||
|
||||
Axis axis;
|
||||
|
||||
int? get semanticChildCount => _semanticChildCount;
|
||||
int? _semanticChildCount;
|
||||
set semanticChildCount(int? value) {
|
||||
@@ -1629,6 +1635,14 @@ class _RenderScrollSemantics extends RenderProxyBox {
|
||||
markNeedsSemanticsUpdate();
|
||||
}
|
||||
|
||||
void _onScrollToOffset(Offset targetOffset) {
|
||||
final double offset = switch (axis) {
|
||||
Axis.horizontal => targetOffset.dx,
|
||||
Axis.vertical => targetOffset.dy,
|
||||
};
|
||||
_position.jumpTo(offset);
|
||||
}
|
||||
|
||||
@override
|
||||
void describeSemanticsConfiguration(SemanticsConfiguration config) {
|
||||
super.describeSemanticsConfiguration(config);
|
||||
@@ -1640,6 +1654,9 @@ class _RenderScrollSemantics extends RenderProxyBox {
|
||||
..scrollExtentMax = _position.maxScrollExtent
|
||||
..scrollExtentMin = _position.minScrollExtent
|
||||
..scrollChildCount = semanticChildCount;
|
||||
if (position.maxScrollExtent > position.minScrollExtent && allowImplicitScrolling) {
|
||||
config.onScrollToOffset = _onScrollToOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -333,7 +333,7 @@ void main() {
|
||||
id: 14,
|
||||
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
|
||||
rect: TestSemantics.fullScreen,
|
||||
actions: <SemanticsAction>[SemanticsAction.scrollUp],
|
||||
actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollToOffset],
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
id: 5,
|
||||
@@ -441,7 +441,7 @@ void main() {
|
||||
id: 14,
|
||||
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
|
||||
rect: TestSemantics.fullScreen,
|
||||
actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollDown],
|
||||
actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollDown, SemanticsAction.scrollToOffset],
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
id: 5,
|
||||
@@ -598,7 +598,7 @@ void main() {
|
||||
id: 14,
|
||||
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
|
||||
rect: TestSemantics.fullScreen,
|
||||
actions: <SemanticsAction>[SemanticsAction.scrollUp],
|
||||
actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollToOffset],
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
id: 5,
|
||||
@@ -706,7 +706,7 @@ void main() {
|
||||
id: 14,
|
||||
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
|
||||
rect: TestSemantics.fullScreen,
|
||||
actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollDown],
|
||||
actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollDown, SemanticsAction.scrollToOffset],
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
id: 5,
|
||||
|
||||
@@ -3677,7 +3677,7 @@ void main() {
|
||||
const String tab10title = 'This is a very wide tab #10\nTab 11 of 20';
|
||||
|
||||
const List<SemanticsFlag> hiddenFlags = <SemanticsFlag>[SemanticsFlag.isHidden, SemanticsFlag.isFocusable, SemanticsFlag.hasSelectedState];
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollLeft]));
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollLeft, SemanticsAction.scrollToOffset]));
|
||||
expect(semantics, includesNodeWith(label: tab0title));
|
||||
expect(semantics, includesNodeWith(label: tab10title, flags: hiddenFlags));
|
||||
|
||||
@@ -3685,18 +3685,18 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(semantics, includesNodeWith(label: tab0title, flags: hiddenFlags));
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollLeft, SemanticsAction.scrollRight]));
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollLeft, SemanticsAction.scrollRight, SemanticsAction.scrollToOffset]));
|
||||
expect(semantics, includesNodeWith(label: tab10title));
|
||||
|
||||
controller.index = 19;
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollRight]));
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollRight, SemanticsAction.scrollToOffset]));
|
||||
|
||||
controller.index = 0;
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollLeft]));
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollLeft, SemanticsAction.scrollToOffset]));
|
||||
expect(semantics, includesNodeWith(label: tab0title));
|
||||
expect(semantics, includesNodeWith(label: tab10title, flags: hiddenFlags));
|
||||
|
||||
|
||||
@@ -908,6 +908,7 @@ void main() {
|
||||
expect(config.onScrollUp, isNull);
|
||||
expect(config.onScrollLeft, isNull);
|
||||
expect(config.onScrollRight, isNull);
|
||||
expect(config.onScrollToOffset, isNull);
|
||||
expect(config.onLongPress, isNull);
|
||||
expect(config.onDecrease, isNull);
|
||||
expect(config.onIncrease, isNull);
|
||||
@@ -932,6 +933,7 @@ void main() {
|
||||
void onScrollUp() { }
|
||||
void onScrollLeft() { }
|
||||
void onScrollRight() { }
|
||||
void onScrollToOffset(Offset _) { }
|
||||
void onLongPress() { }
|
||||
void onDecrease() { }
|
||||
void onIncrease() { }
|
||||
@@ -945,6 +947,7 @@ void main() {
|
||||
config.onScrollUp = onScrollUp;
|
||||
config.onScrollLeft = onScrollLeft;
|
||||
config.onScrollRight = onScrollRight;
|
||||
config.onScrollToOffset = onScrollToOffset;
|
||||
config.onLongPress = onLongPress;
|
||||
config.onDecrease = onDecrease;
|
||||
config.onIncrease = onIncrease;
|
||||
@@ -969,6 +972,7 @@ void main() {
|
||||
expect(config.onScrollUp, same(onScrollUp));
|
||||
expect(config.onScrollLeft, same(onScrollLeft));
|
||||
expect(config.onScrollRight, same(onScrollRight));
|
||||
expect(config.onScrollToOffset, same(onScrollToOffset));
|
||||
expect(config.onLongPress, same(onLongPress));
|
||||
expect(config.onDecrease, same(onDecrease));
|
||||
expect(config.onIncrease, same(onIncrease));
|
||||
|
||||
@@ -356,9 +356,7 @@ void _defineTests() {
|
||||
final Set<SemanticsAction> allActions = SemanticsAction.values.toSet()
|
||||
..remove(SemanticsAction.customAction) // customAction is not user-exposed.
|
||||
..remove(SemanticsAction.showOnScreen) // showOnScreen is not user-exposed
|
||||
// TODO(LongCatIsLooong): change to `SemanticsAction.scrollToOffset` when available.
|
||||
// https://github.com/flutter/flutter/issues/159515.
|
||||
..removeWhere((SemanticsAction action) => action.index == 1 << 23);
|
||||
..remove(SemanticsAction.scrollToOffset); // scrollToOffset is not user-exposed
|
||||
|
||||
const int expectedId = 2;
|
||||
final TestSemantics expectedSemantics = TestSemantics.root(
|
||||
@@ -381,9 +379,6 @@ void _defineTests() {
|
||||
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
|
||||
int expectedLength = 1;
|
||||
for (final SemanticsAction action in allActions) {
|
||||
// TODO(LongCatIsLooong): remove after `SemanticsAction.scrollToOffset` is added to dart:ui.
|
||||
// https://github.com/flutter/flutter/issues/159515.
|
||||
// ignore: exhaustive_cases
|
||||
switch (action) {
|
||||
case SemanticsAction.moveCursorBackwardByCharacter:
|
||||
case SemanticsAction.moveCursorForwardByCharacter:
|
||||
@@ -411,6 +406,7 @@ void _defineTests() {
|
||||
case SemanticsAction.scrollLeft:
|
||||
case SemanticsAction.scrollRight:
|
||||
case SemanticsAction.scrollUp:
|
||||
case SemanticsAction.scrollToOffset:
|
||||
case SemanticsAction.showOnScreen:
|
||||
case SemanticsAction.tap:
|
||||
case SemanticsAction.focus:
|
||||
|
||||
@@ -3362,7 +3362,7 @@ void main() {
|
||||
TestSemantics(
|
||||
id: 9,
|
||||
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
|
||||
actions: <SemanticsAction>[SemanticsAction.scrollLeft],
|
||||
actions: <SemanticsAction>[SemanticsAction.scrollLeft, SemanticsAction.scrollToOffset],
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
id: 5,
|
||||
@@ -3430,6 +3430,7 @@ void main() {
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
id: 9,
|
||||
actions: <SemanticsAction>[SemanticsAction.scrollToOffset],
|
||||
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
|
||||
@@ -35,12 +35,12 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollUp]));
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollToOffset]));
|
||||
|
||||
// Jump to the end.
|
||||
controller.jumpTo(itemCount * itemHeight);
|
||||
await tester.pumpAndSettle();
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollDown]));
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollDown, SemanticsAction.scrollToOffset]));
|
||||
|
||||
semantics.dispose();
|
||||
});
|
||||
@@ -67,12 +67,12 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollDown]));
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollDown, SemanticsAction.scrollToOffset]));
|
||||
|
||||
// Jump to the end.
|
||||
controller.jumpTo(itemCount * itemHeight);
|
||||
await tester.pumpAndSettle();
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollUp]));
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollToOffset]));
|
||||
|
||||
semantics.dispose();
|
||||
});
|
||||
@@ -99,12 +99,12 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollLeft]));
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollLeft, SemanticsAction.scrollToOffset]));
|
||||
|
||||
// Jump to the end.
|
||||
controller.jumpTo(itemCount * itemHeight);
|
||||
await tester.pumpAndSettle();
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollRight]));
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollRight, SemanticsAction.scrollToOffset]));
|
||||
|
||||
semantics.dispose();
|
||||
});
|
||||
@@ -132,12 +132,12 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollRight]));
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollRight, SemanticsAction.scrollToOffset]));
|
||||
|
||||
// Jump to the end.
|
||||
controller.jumpTo(itemCount * itemHeight);
|
||||
await tester.pumpAndSettle();
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollLeft]));
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollLeft, SemanticsAction.scrollToOffset]));
|
||||
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
@@ -25,23 +26,99 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
expect(semantics,includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollUp]));
|
||||
expect(semantics,includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollToOffset]));
|
||||
|
||||
await flingUp(tester);
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollDown]));
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollDown, SemanticsAction.scrollToOffset]));
|
||||
|
||||
await flingDown(tester, repetitions: 2);
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollUp]));
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollToOffset]));
|
||||
|
||||
await flingUp(tester, repetitions: 5);
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollDown]));
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollDown, SemanticsAction.scrollToOffset]));
|
||||
|
||||
await flingDown(tester);
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollDown]));
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollDown, SemanticsAction.scrollToOffset]));
|
||||
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets('Vertical scrollable responds to scrollToOffset', (WidgetTester tester) async {
|
||||
semantics = SemanticsTester(tester);
|
||||
final ScrollController controller = ScrollController();
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: ListView(
|
||||
controller: controller,
|
||||
children: List<Widget>.generate(60, (int i) => Text('$i')),
|
||||
),
|
||||
),
|
||||
);
|
||||
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
|
||||
final int scrollableId = semantics.nodesWith(actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollToOffset]).single.id;
|
||||
|
||||
assert(controller.offset == 0);
|
||||
semanticsOwner.performAction(scrollableId, SemanticsAction.scrollToOffset, Float64List.fromList(<double>[123.0, 456.0]));
|
||||
expect(controller.offset, 456.0);
|
||||
controller.dispose();
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets('Horizontal scrollable responds to scrollToOffset', (WidgetTester tester) async {
|
||||
semantics = SemanticsTester(tester);
|
||||
final ScrollController controller = ScrollController();
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: ListView(
|
||||
controller: controller,
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: List<Widget>.generate(60, (int i) => Text('$i')),
|
||||
),
|
||||
),
|
||||
);
|
||||
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
|
||||
final int scrollableId = semantics.nodesWith(actions: <SemanticsAction>[SemanticsAction.scrollLeft, SemanticsAction.scrollToOffset]).single.id;
|
||||
|
||||
assert(controller.offset == 0);
|
||||
semanticsOwner.performAction(scrollableId, SemanticsAction.scrollToOffset, Float64List.fromList(<double>[123.0, 456.0]));
|
||||
expect(controller.offset, 123.0);
|
||||
controller.dispose();
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets('Unscrollable scrollable does not respond to scrollToOffset', (WidgetTester tester) async {
|
||||
semantics = SemanticsTester(tester);
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: ListView(
|
||||
children: List<Widget>.generate(3, (int i) => Text('$i')),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(semantics.nodesWith(actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollToOffset]), isEmpty);
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets('scrollToOffset respects implicit scrolling configuration', (WidgetTester tester) async {
|
||||
semantics = SemanticsTester(tester);
|
||||
final ScrollPhysics physics = _NoImplicitScrollingScrollPhysics();
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: ListView(
|
||||
physics: physics,
|
||||
children: List<Widget>.generate(60, (int i) => Text('$i')),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(semantics.nodesWith(actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollToOffset]), isEmpty);
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
|
||||
testWidgets('showOnScreen works in scrollable', (WidgetTester tester) async {
|
||||
semantics = SemanticsTester(tester); // enables semantics tree generation
|
||||
|
||||
@@ -227,6 +304,7 @@ void main() {
|
||||
scrollExtentMax: 520.0,
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.scrollUp,
|
||||
SemanticsAction.scrollToOffset,
|
||||
],
|
||||
));
|
||||
|
||||
@@ -239,6 +317,7 @@ void main() {
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.scrollUp,
|
||||
SemanticsAction.scrollDown,
|
||||
SemanticsAction.scrollToOffset,
|
||||
],
|
||||
));
|
||||
|
||||
@@ -250,6 +329,7 @@ void main() {
|
||||
scrollExtentMax: 520.0,
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.scrollDown,
|
||||
SemanticsAction.scrollToOffset,
|
||||
],
|
||||
));
|
||||
|
||||
@@ -276,6 +356,7 @@ void main() {
|
||||
scrollExtentMax: double.infinity,
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.scrollUp,
|
||||
SemanticsAction.scrollToOffset,
|
||||
],
|
||||
));
|
||||
|
||||
@@ -288,6 +369,7 @@ void main() {
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.scrollUp,
|
||||
SemanticsAction.scrollDown,
|
||||
SemanticsAction.scrollToOffset,
|
||||
],
|
||||
));
|
||||
|
||||
@@ -300,6 +382,7 @@ void main() {
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.scrollUp,
|
||||
SemanticsAction.scrollDown,
|
||||
SemanticsAction.scrollToOffset,
|
||||
],
|
||||
));
|
||||
|
||||
@@ -354,7 +437,7 @@ void main() {
|
||||
flags: <SemanticsFlag>[
|
||||
SemanticsFlag.hasImplicitScrolling,
|
||||
],
|
||||
actions: <SemanticsAction>[SemanticsAction.scrollUp],
|
||||
actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollToOffset],
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
label: r'item 0',
|
||||
@@ -614,7 +697,7 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
final SemanticsNode rootScrollNode = semantics.nodesWith(actions: <SemanticsAction>[SemanticsAction.scrollUp]).single;
|
||||
final SemanticsNode rootScrollNode = semantics.nodesWith(actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollToOffset]).single;
|
||||
final SemanticsNode innerListPane = semantics.nodesWith(ancestor: rootScrollNode, scrollExtentMax: 0).single;
|
||||
final SemanticsNode outerListPane = innerListPane.parent!;
|
||||
final List<SemanticsNode> hiddenNodes = semantics.nodesWith(flags: <SemanticsFlag>[SemanticsFlag.isHidden]).toList();
|
||||
@@ -664,3 +747,11 @@ Rect nodeGlobalRect(SemanticsNode node) {
|
||||
}
|
||||
return MatrixUtils.transformRect(globalTransform, node.rect);
|
||||
}
|
||||
|
||||
class _NoImplicitScrollingScrollPhysics extends ScrollPhysics {
|
||||
@override
|
||||
bool get allowImplicitScrolling => false;
|
||||
|
||||
@override
|
||||
ScrollPhysics applyTo(ScrollPhysics? ancestor) => this;
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ void main() {
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.scrollUp,
|
||||
SemanticsAction.scrollDown,
|
||||
SemanticsAction.scrollToOffset,
|
||||
],
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
@@ -245,6 +246,7 @@ void main() {
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.scrollUp,
|
||||
SemanticsAction.scrollDown,
|
||||
SemanticsAction.scrollToOffset,
|
||||
],
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
@@ -376,6 +378,7 @@ void main() {
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.scrollUp,
|
||||
SemanticsAction.scrollDown,
|
||||
SemanticsAction.scrollToOffset,
|
||||
],
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
@@ -515,6 +518,7 @@ void main() {
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.scrollUp,
|
||||
SemanticsAction.scrollDown,
|
||||
SemanticsAction.scrollToOffset,
|
||||
],
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
@@ -671,6 +675,7 @@ void main() {
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.scrollUp,
|
||||
SemanticsAction.scrollDown,
|
||||
SemanticsAction.scrollToOffset,
|
||||
],
|
||||
children: children,
|
||||
),
|
||||
@@ -744,6 +749,7 @@ void main() {
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.scrollUp,
|
||||
SemanticsAction.scrollDown,
|
||||
SemanticsAction.scrollToOffset,
|
||||
],
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
|
||||
@@ -2625,7 +2625,7 @@ void main() {
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
|
||||
actions: <SemanticsAction>[SemanticsAction.scrollUp],
|
||||
actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollToOffset],
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.longPress],
|
||||
|
||||
@@ -525,9 +525,7 @@ void main() {
|
||||
..remove(SemanticsAction.moveCursorBackwardByWord)
|
||||
..remove(SemanticsAction.customAction) // customAction is not user-exposed.
|
||||
..remove(SemanticsAction.showOnScreen) // showOnScreen is not user-exposed
|
||||
// TODO(LongCatIsLooong): change to `SemanticsAction.scrollToOffset` when available.
|
||||
// https://github.com/flutter/flutter/issues/159515.
|
||||
..removeWhere((SemanticsAction action) => action.index == 1 << 23);
|
||||
..remove(SemanticsAction.scrollToOffset); // scrollToOffset is not user-exposed
|
||||
|
||||
const int expectedId = 1;
|
||||
final TestSemantics expectedSemantics = TestSemantics.root(
|
||||
@@ -545,9 +543,6 @@ void main() {
|
||||
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
|
||||
int expectedLength = 1;
|
||||
for (final SemanticsAction action in allActions) {
|
||||
// TODO(LongCatIsLooong): remove after `SemanticsAction.scrollToOffset` is added to dart:ui.
|
||||
// https://github.com/flutter/flutter/issues/159515.
|
||||
// ignore: exhaustive_cases
|
||||
switch (action) {
|
||||
case SemanticsAction.moveCursorBackwardByCharacter:
|
||||
case SemanticsAction.moveCursorForwardByCharacter:
|
||||
@@ -575,6 +570,7 @@ void main() {
|
||||
case SemanticsAction.scrollLeft:
|
||||
case SemanticsAction.scrollRight:
|
||||
case SemanticsAction.scrollUp:
|
||||
case SemanticsAction.scrollToOffset:
|
||||
case SemanticsAction.showOnScreen:
|
||||
case SemanticsAction.tap:
|
||||
case SemanticsAction.focus:
|
||||
|
||||
@@ -354,6 +354,7 @@ void main() {
|
||||
],
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.scrollUp,
|
||||
SemanticsAction.scrollToOffset,
|
||||
],
|
||||
children: generateSemanticsChildren(endHidden: 3),
|
||||
),
|
||||
@@ -375,6 +376,7 @@ void main() {
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.scrollUp,
|
||||
SemanticsAction.scrollDown,
|
||||
SemanticsAction.scrollToOffset,
|
||||
],
|
||||
children: generateSemanticsChildren(startHidden: 14, endHidden: 18),
|
||||
),
|
||||
@@ -395,6 +397,7 @@ void main() {
|
||||
],
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.scrollDown,
|
||||
SemanticsAction.scrollToOffset,
|
||||
],
|
||||
children: generateSemanticsChildren(startHidden: 26),
|
||||
),
|
||||
|
||||
@@ -94,7 +94,7 @@ void _tests() {
|
||||
flags: <SemanticsFlag>[
|
||||
SemanticsFlag.hasImplicitScrolling,
|
||||
],
|
||||
actions: <SemanticsAction>[SemanticsAction.scrollUp],
|
||||
actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollToOffset],
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
id: 3,
|
||||
@@ -165,6 +165,7 @@ void _tests() {
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.scrollUp,
|
||||
SemanticsAction.scrollDown,
|
||||
SemanticsAction.scrollToOffset,
|
||||
],
|
||||
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
|
||||
children: <TestSemantics>[
|
||||
@@ -244,6 +245,7 @@ void _tests() {
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.scrollUp,
|
||||
SemanticsAction.scrollDown,
|
||||
SemanticsAction.scrollToOffset,
|
||||
],
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
@@ -339,6 +341,7 @@ void _tests() {
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.scrollUp,
|
||||
SemanticsAction.scrollDown,
|
||||
SemanticsAction.scrollToOffset,
|
||||
],
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
@@ -521,6 +524,7 @@ void _tests() {
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.scrollUp,
|
||||
SemanticsAction.scrollDown,
|
||||
SemanticsAction.scrollToOffset,
|
||||
],
|
||||
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
|
||||
children: <TestSemantics>[
|
||||
@@ -633,6 +637,7 @@ void _tests() {
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.scrollUp,
|
||||
SemanticsAction.scrollDown,
|
||||
SemanticsAction.scrollToOffset,
|
||||
],
|
||||
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
|
||||
children: <TestSemantics>[
|
||||
@@ -736,6 +741,7 @@ void _tests() {
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.scrollUp,
|
||||
SemanticsAction.scrollDown,
|
||||
SemanticsAction.scrollToOffset,
|
||||
],
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
@@ -851,6 +857,7 @@ void _tests() {
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.scrollUp,
|
||||
SemanticsAction.scrollDown,
|
||||
SemanticsAction.scrollToOffset,
|
||||
],
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
@@ -1007,6 +1014,7 @@ void _tests() {
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.scrollUp,
|
||||
SemanticsAction.scrollDown,
|
||||
SemanticsAction.scrollToOffset,
|
||||
],
|
||||
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
|
||||
children: <TestSemantics>[
|
||||
@@ -1069,6 +1077,7 @@ void _tests() {
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.scrollUp,
|
||||
SemanticsAction.scrollDown,
|
||||
SemanticsAction.scrollToOffset,
|
||||
],
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
|
||||
@@ -106,7 +106,7 @@ void main() {
|
||||
],
|
||||
),
|
||||
TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.scrollUp],
|
||||
actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollToOffset],
|
||||
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
|
||||
scrollIndex: 0,
|
||||
children: <TestSemantics>[
|
||||
@@ -171,7 +171,7 @@ void main() {
|
||||
],
|
||||
),
|
||||
TestSemantics(
|
||||
actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollDown],
|
||||
actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollDown, SemanticsAction.scrollToOffset],
|
||||
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
|
||||
scrollIndex: 11,
|
||||
children: <TestSemantics>[
|
||||
|
||||
@@ -674,7 +674,7 @@ void main() {
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
|
||||
actions: <SemanticsAction>[SemanticsAction.scrollUp],
|
||||
actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollToOffset],
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
children: <TestSemantics>[
|
||||
|
||||
@@ -71,7 +71,8 @@ class SemanticsController {
|
||||
SemanticsAction.scrollUp.index |
|
||||
SemanticsAction.scrollDown.index |
|
||||
SemanticsAction.scrollLeft.index |
|
||||
SemanticsAction.scrollRight.index;
|
||||
SemanticsAction.scrollRight.index |
|
||||
SemanticsAction.scrollToOffset.index;
|
||||
|
||||
/// Based on Android's FOCUSABLE_FLAGS. See [flutter/engine/AccessibilityBridge.java](https://github.com/flutter/engine/blob/main/shell/platform/android/io/flutter/view/AccessibilityBridge.java).
|
||||
static final int _importantFlagsForAccessibility =
|
||||
|
||||
Reference in New Issue
Block a user