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:
LongCatIsLooong
2024-12-09 13:47:34 -08:00
committed by GitHub
parent 06b95c3880
commit 9ff7238937
17 changed files with 199 additions and 48 deletions

View File

@@ -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

View File

@@ -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;
}
}
}

View File

@@ -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,

View File

@@ -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));

View File

@@ -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));

View File

@@ -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:

View File

@@ -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(

View File

@@ -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();
});

View File

@@ -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;
}

View File

@@ -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(

View File

@@ -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],

View File

@@ -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:

View File

@@ -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),
),

View File

@@ -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(

View File

@@ -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>[

View File

@@ -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>[

View File

@@ -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 =