Skip to content

Commit 26277de

Browse files
committed
a11y: implement new SemanticsAction "showOnScreen" (v2)
This action is triggered when the user swipes (in accessibility mode) to the last visible item of a scrollable list to bring that item fully on screen. This requires engine rolled to flutter/engine#3856. I am in the process of adding tests, but I'd like to get early feedback to see if this approach is OK.
1 parent 1744e8e commit 26277de

File tree

6 files changed

+71
-5
lines changed

6 files changed

+71
-5
lines changed

packages/flutter/lib/src/rendering/object.dart

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -739,7 +739,8 @@ class _RootSemanticsFragment extends _InterestingSemanticsFragment {
739739
assert(parentSemantics == null);
740740
renderObjectOwner._semantics ??= new SemanticsNode.root(
741741
handler: renderObjectOwner is SemanticsActionHandler ? renderObjectOwner as dynamic : null,
742-
owner: renderObjectOwner.owner.semanticsOwner
742+
owner: renderObjectOwner.owner.semanticsOwner,
743+
showOnScreen: renderObjectOwner.showOnScreen,
743744
);
744745
final SemanticsNode node = renderObjectOwner._semantics;
745746
assert(MatrixUtils.matrixEquals(node.transform, new Matrix4.identity()));
@@ -768,7 +769,8 @@ class _ConcreteSemanticsFragment extends _InterestingSemanticsFragment {
768769
@override
769770
SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics) {
770771
renderObjectOwner._semantics ??= new SemanticsNode(
771-
handler: renderObjectOwner is SemanticsActionHandler ? renderObjectOwner as dynamic : null
772+
handler: renderObjectOwner is SemanticsActionHandler ? renderObjectOwner as dynamic : null,
773+
showOnScreen: renderObjectOwner.showOnScreen,
772774
);
773775
final SemanticsNode node = renderObjectOwner._semantics;
774776
if (geometry != null) {
@@ -812,7 +814,8 @@ class _ImplicitSemanticsFragment extends _InterestingSemanticsFragment {
812814
_haveConcreteNode = currentSemantics == null && annotator != null;
813815
if (haveConcreteNode) {
814816
renderObjectOwner._semantics ??= new SemanticsNode(
815-
handler: renderObjectOwner is SemanticsActionHandler ? renderObjectOwner as dynamic : null
817+
handler: renderObjectOwner is SemanticsActionHandler ? renderObjectOwner as dynamic : null,
818+
showOnScreen: renderObjectOwner.showOnScreen,
816819
);
817820
node = renderObjectOwner._semantics;
818821
} else {
@@ -2777,6 +2780,17 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
27772780
@protected
27782781
String debugDescribeChildren(String prefix) => '';
27792782

2783+
2784+
/// Attempt to make this or a descendant RenderObject visible on screen.
2785+
///
2786+
/// If [child] is provided, that RenderObject is made visible. If [child] is
2787+
/// omitted, this RenderObject is made visible.
2788+
void showOnScreen([RenderObject child]) {
2789+
if (parent is RenderObject) {
2790+
final RenderObject renderParent = parent;
2791+
renderParent.showOnScreen(child ?? this);
2792+
}
2793+
}
27802794
}
27812795

27822796
/// Generic mixin for render objects with one child.

packages/flutter/lib/src/rendering/semantics.dart

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,17 +143,21 @@ class SemanticsNode extends AbstractNode {
143143
/// Each semantic node has a unique identifier that is assigned when the node
144144
/// is created.
145145
SemanticsNode({
146-
SemanticsActionHandler handler
146+
SemanticsActionHandler handler,
147+
VoidCallback showOnScreen,
147148
}) : id = _generateNewId(),
149+
_showOnScreen = showOnScreen,
148150
_actionHandler = handler;
149151

150152
/// Creates a semantic node to represent the root of the semantics tree.
151153
///
152154
/// The root node is assigned an identifier of zero.
153155
SemanticsNode.root({
154156
SemanticsActionHandler handler,
155-
SemanticsOwner owner
157+
VoidCallback showOnScreen,
158+
SemanticsOwner owner,
156159
}) : id = 0,
160+
_showOnScreen = showOnScreen,
157161
_actionHandler = handler {
158162
attach(owner);
159163
}
@@ -171,6 +175,7 @@ class SemanticsNode extends AbstractNode {
171175
final int id;
172176

173177
final SemanticsActionHandler _actionHandler;
178+
final VoidCallback _showOnScreen;
174179

175180
// GEOMETRY
176181
// These are automatically handled by RenderObject's own logic
@@ -735,6 +740,11 @@ class SemanticsOwner extends ChangeNotifier {
735740
assert(action != null);
736741
final SemanticsActionHandler handler = _getSemanticsActionHandlerForId(id, action);
737742
handler?.performAction(action);
743+
744+
// Default actions if no [handler] was provided.
745+
if (handler == null && action == SemanticsAction.showOnScreen && _nodes[id]._showOnScreen != null) {
746+
_nodes[id]._showOnScreen();
747+
}
738748
}
739749

740750
SemanticsActionHandler _getSemanticsActionHandlerForPosition(SemanticsNode node, Offset position, SemanticsAction action) {

packages/flutter/lib/src/rendering/viewport.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,22 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
591591
/// This should be the reverse order of [childrenInPaintOrder].
592592
@protected
593593
Iterable<RenderSliver> get childrenInHitTestOrder;
594+
595+
@override
596+
void showOnScreen([RenderObject child]) {
597+
// Move viewport the smallest distance to bring [child] on screen.
598+
final double leadingEdgeOffset = getOffsetToReveal(child, 0.0);
599+
final double trailingEdgeOffset = getOffsetToReveal(child, 1.0);
600+
final double currentOffset = offset.pixels;
601+
if ((currentOffset - leadingEdgeOffset).abs() < (currentOffset - trailingEdgeOffset).abs()) {
602+
offset.jumpTo(leadingEdgeOffset);
603+
} else {
604+
offset.jumpTo(trailingEdgeOffset);
605+
}
606+
607+
// Make sure the viewport itself is on screen.
608+
super.showOnScreen();
609+
}
594610
}
595611

596612
/// A render object that is bigger on the inside.

packages/flutter/lib/src/rendering/viewport_offset.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,10 @@ abstract class ViewportOffset extends ChangeNotifier {
152152
/// being called again, though this should be very rare.
153153
void correctBy(double correction);
154154

155+
/// Jumps the scroll position from its current value to the given value,
156+
/// without animation, and without checking if the new value is in range.
157+
void jumpTo(double pixels);
158+
155159
/// The direction in which the user is trying to change [pixels], relative to
156160
/// the viewport's [RenderViewport.axisDirection].
157161
///
@@ -208,6 +212,11 @@ class _FixedViewportOffset extends ViewportOffset {
208212
_pixels += correction;
209213
}
210214

215+
@override
216+
void jumpTo(double pixels) {
217+
_pixels = pixels;
218+
}
219+
211220
@override
212221
ScrollDirection get userScrollDirection => ScrollDirection.idle;
213222
}

packages/flutter/lib/src/widgets/scroll_position.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
510510
/// If this method changes the scroll position, a sequence of start/update/end
511511
/// scroll notifications will be dispatched. No overscroll notifications can
512512
/// be generated by this method.
513+
@override
513514
void jumpTo(double value);
514515

515516
/// Deprecated. Use [jumpTo] or a custom [ScrollPosition] instead.

packages/flutter/lib/src/widgets/single_child_scroll_view.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,4 +418,20 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
418418

419419
return leadingScrollOffset - (mainAxisExtent - targetMainAxisExtent) * alignment;
420420
}
421+
422+
@override
423+
void showOnScreen([RenderObject child]) {
424+
// Move viewport the smallest distance to bring [child] on screen.
425+
final double leadingEdgeOffset = getOffsetToReveal(child, 0.0);
426+
final double trailingEdgeOffset = getOffsetToReveal(child, 1.0);
427+
final double currentOffset = offset.pixels;
428+
if ((currentOffset - leadingEdgeOffset).abs() < (currentOffset - trailingEdgeOffset).abs()) {
429+
offset.jumpTo(leadingEdgeOffset);
430+
} else {
431+
offset.jumpTo(trailingEdgeOffset);
432+
}
433+
434+
// Make sure the viewport itself is on screen.
435+
super.showOnScreen();
436+
}
421437
}

0 commit comments

Comments
 (0)