Commit b5c461a9 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

a11y: implement new SemanticsAction "showOnScreen" (v2) (#11156)

* 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.

* fix null check

* review comments

* review comments

* Add test

* fix analyzer warning
parent d767ac0b
......@@ -739,7 +739,8 @@ class _RootSemanticsFragment extends _InterestingSemanticsFragment {
assert(parentSemantics == null);
renderObjectOwner._semantics ??= new SemanticsNode.root(
handler: renderObjectOwner is SemanticsActionHandler ? renderObjectOwner as dynamic : null,
owner: renderObjectOwner.owner.semanticsOwner
owner: renderObjectOwner.owner.semanticsOwner,
showOnScreen: renderObjectOwner.showOnScreen,
);
final SemanticsNode node = renderObjectOwner._semantics;
assert(MatrixUtils.matrixEquals(node.transform, new Matrix4.identity()));
......@@ -768,7 +769,8 @@ class _ConcreteSemanticsFragment extends _InterestingSemanticsFragment {
@override
SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics) {
renderObjectOwner._semantics ??= new SemanticsNode(
handler: renderObjectOwner is SemanticsActionHandler ? renderObjectOwner as dynamic : null
handler: renderObjectOwner is SemanticsActionHandler ? renderObjectOwner as dynamic : null,
showOnScreen: renderObjectOwner.showOnScreen,
);
final SemanticsNode node = renderObjectOwner._semantics;
if (geometry != null) {
......@@ -812,7 +814,8 @@ class _ImplicitSemanticsFragment extends _InterestingSemanticsFragment {
_haveConcreteNode = currentSemantics == null && annotator != null;
if (haveConcreteNode) {
renderObjectOwner._semantics ??= new SemanticsNode(
handler: renderObjectOwner is SemanticsActionHandler ? renderObjectOwner as dynamic : null
handler: renderObjectOwner is SemanticsActionHandler ? renderObjectOwner as dynamic : null,
showOnScreen: renderObjectOwner.showOnScreen,
);
node = renderObjectOwner._semantics;
} else {
......@@ -2777,6 +2780,17 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
@protected
String debugDescribeChildren(String prefix) => '';
/// Attempt to make this or a descendant RenderObject visible on screen.
///
/// If [child] is provided, that [RenderObject] is made visible. If [child] is
/// omitted, this [RenderObject] is made visible.
void showOnScreen([RenderObject child]) {
if (parent is RenderObject) {
final RenderObject renderParent = parent;
renderParent.showOnScreen(child ?? this);
}
}
}
/// Generic mixin for render objects with one child.
......
......@@ -143,8 +143,10 @@ class SemanticsNode extends AbstractNode {
/// Each semantic node has a unique identifier that is assigned when the node
/// is created.
SemanticsNode({
SemanticsActionHandler handler
SemanticsActionHandler handler,
VoidCallback showOnScreen,
}) : id = _generateNewId(),
_showOnScreen = showOnScreen,
_actionHandler = handler;
/// Creates a semantic node to represent the root of the semantics tree.
......@@ -152,8 +154,10 @@ class SemanticsNode extends AbstractNode {
/// The root node is assigned an identifier of zero.
SemanticsNode.root({
SemanticsActionHandler handler,
SemanticsOwner owner
VoidCallback showOnScreen,
SemanticsOwner owner,
}) : id = 0,
_showOnScreen = showOnScreen,
_actionHandler = handler {
attach(owner);
}
......@@ -171,6 +175,7 @@ class SemanticsNode extends AbstractNode {
final int id;
final SemanticsActionHandler _actionHandler;
final VoidCallback _showOnScreen;
// GEOMETRY
// These are automatically handled by RenderObject's own logic
......@@ -734,7 +739,14 @@ class SemanticsOwner extends ChangeNotifier {
void performAction(int id, SemanticsAction action) {
assert(action != null);
final SemanticsActionHandler handler = _getSemanticsActionHandlerForId(id, action);
handler?.performAction(action);
if (handler != null) {
handler.performAction(action);
return;
}
// Default actions if no [handler] was provided.
if (action == SemanticsAction.showOnScreen && _nodes[id]._showOnScreen != null)
_nodes[id]._showOnScreen();
}
SemanticsActionHandler _getSemanticsActionHandlerForPosition(SemanticsNode node, Offset position, SemanticsAction action) {
......
......@@ -591,6 +591,24 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
/// This should be the reverse order of [childrenInPaintOrder].
@protected
Iterable<RenderSliver> get childrenInHitTestOrder;
@override
void showOnScreen([RenderObject child]) {
// Logic duplicated in [_RenderSingleChildViewport.showOnScreen].
if (child != null) {
// Move viewport the smallest distance to bring [child] on screen.
final double leadingEdgeOffset = getOffsetToReveal(child, 0.0);
final double trailingEdgeOffset = getOffsetToReveal(child, 1.0);
final double currentOffset = offset.pixels;
if ((currentOffset - leadingEdgeOffset).abs() < (currentOffset - trailingEdgeOffset).abs()) {
offset.jumpTo(leadingEdgeOffset);
} else {
offset.jumpTo(trailingEdgeOffset);
}
}
// Make sure the viewport itself is on screen.
super.showOnScreen();
}
}
/// A render object that is bigger on the inside.
......
......@@ -152,6 +152,10 @@ abstract class ViewportOffset extends ChangeNotifier {
/// being called again, though this should be very rare.
void correctBy(double correction);
/// Jumps the scroll position from its current value to the given value,
/// without animation, and without checking if the new value is in range.
void jumpTo(double pixels);
/// The direction in which the user is trying to change [pixels], relative to
/// the viewport's [RenderViewport.axisDirection].
///
......@@ -208,6 +212,11 @@ class _FixedViewportOffset extends ViewportOffset {
_pixels += correction;
}
@override
void jumpTo(double pixels) {
// Do nothing, viewport is fixed.
}
@override
ScrollDirection get userScrollDirection => ScrollDirection.idle;
}
......@@ -510,6 +510,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
/// If this method changes the scroll position, a sequence of start/update/end
/// scroll notifications will be dispatched. No overscroll notifications can
/// be generated by this method.
@override
void jumpTo(double value);
/// Deprecated. Use [jumpTo] or a custom [ScrollPosition] instead.
......
......@@ -418,4 +418,23 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
return leadingScrollOffset - (mainAxisExtent - targetMainAxisExtent) * alignment;
}
@override
void showOnScreen([RenderObject child]) {
// Logic duplicated in [RenderViewportBase.showOnScreen].
if (child != null) {
// Move viewport the smallest distance to bring [child] on screen.
final double leadingEdgeOffset = getOffsetToReveal(child, 0.0);
final double trailingEdgeOffset = getOffsetToReveal(child, 1.0);
final double currentOffset = offset.pixels;
if ((currentOffset - leadingEdgeOffset).abs() < (currentOffset - trailingEdgeOffset).abs()) {
offset.jumpTo(leadingEdgeOffset);
} else {
offset.jumpTo(trailingEdgeOffset);
}
}
// Make sure the viewport itself is on screen.
super.showOnScreen();
}
}
......@@ -30,7 +30,37 @@ void main() {
await flingDown(tester);
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollDown]));
});
testWidgets('showOnScreen works in scrollable', (WidgetTester tester) async {
new SemanticsTester(tester); // enables semantics tree generation
const double kItemHeight = 40.0;
final List<Widget> containers = <Widget>[];
for (int i = 0; i < 80; i++)
containers.add(new MergeSemantics(child: new Container(
height: kItemHeight,
child: new Text('container $i'),
)));
final ScrollController scrollController = new ScrollController(
initialScrollOffset: kItemHeight / 2,
);
await tester.pumpWidget(new ListView(
controller: scrollController,
children: containers
));
expect(scrollController.offset, kItemHeight / 2);
final int firstContainerId = tester.renderObject(find.byWidget(containers.first)).debugSemantics.id;
tester.binding.pipelineOwner.semanticsOwner.performAction(firstContainerId, SemanticsAction.showOnScreen);
await tester.pump();
await tester.pump(const Duration(seconds: 5));
expect(scrollController.offset, 0.0);
});
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment