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
This commit is contained in:
parent
d767ac0be5
commit
b5c461a917
@ -739,7 +739,8 @@ class _RootSemanticsFragment extends _InterestingSemanticsFragment {
|
|||||||
assert(parentSemantics == null);
|
assert(parentSemantics == null);
|
||||||
renderObjectOwner._semantics ??= new SemanticsNode.root(
|
renderObjectOwner._semantics ??= new SemanticsNode.root(
|
||||||
handler: renderObjectOwner is SemanticsActionHandler ? renderObjectOwner as dynamic : null,
|
handler: renderObjectOwner is SemanticsActionHandler ? renderObjectOwner as dynamic : null,
|
||||||
owner: renderObjectOwner.owner.semanticsOwner
|
owner: renderObjectOwner.owner.semanticsOwner,
|
||||||
|
showOnScreen: renderObjectOwner.showOnScreen,
|
||||||
);
|
);
|
||||||
final SemanticsNode node = renderObjectOwner._semantics;
|
final SemanticsNode node = renderObjectOwner._semantics;
|
||||||
assert(MatrixUtils.matrixEquals(node.transform, new Matrix4.identity()));
|
assert(MatrixUtils.matrixEquals(node.transform, new Matrix4.identity()));
|
||||||
@ -768,7 +769,8 @@ class _ConcreteSemanticsFragment extends _InterestingSemanticsFragment {
|
|||||||
@override
|
@override
|
||||||
SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics) {
|
SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics) {
|
||||||
renderObjectOwner._semantics ??= new SemanticsNode(
|
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;
|
final SemanticsNode node = renderObjectOwner._semantics;
|
||||||
if (geometry != null) {
|
if (geometry != null) {
|
||||||
@ -812,7 +814,8 @@ class _ImplicitSemanticsFragment extends _InterestingSemanticsFragment {
|
|||||||
_haveConcreteNode = currentSemantics == null && annotator != null;
|
_haveConcreteNode = currentSemantics == null && annotator != null;
|
||||||
if (haveConcreteNode) {
|
if (haveConcreteNode) {
|
||||||
renderObjectOwner._semantics ??= new SemanticsNode(
|
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;
|
node = renderObjectOwner._semantics;
|
||||||
} else {
|
} else {
|
||||||
@ -2777,6 +2780,17 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
|
|||||||
@protected
|
@protected
|
||||||
String debugDescribeChildren(String prefix) => '';
|
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.
|
/// 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
|
/// Each semantic node has a unique identifier that is assigned when the node
|
||||||
/// is created.
|
/// is created.
|
||||||
SemanticsNode({
|
SemanticsNode({
|
||||||
SemanticsActionHandler handler
|
SemanticsActionHandler handler,
|
||||||
|
VoidCallback showOnScreen,
|
||||||
}) : id = _generateNewId(),
|
}) : id = _generateNewId(),
|
||||||
|
_showOnScreen = showOnScreen,
|
||||||
_actionHandler = handler;
|
_actionHandler = handler;
|
||||||
|
|
||||||
/// Creates a semantic node to represent the root of the semantics tree.
|
/// 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.
|
/// The root node is assigned an identifier of zero.
|
||||||
SemanticsNode.root({
|
SemanticsNode.root({
|
||||||
SemanticsActionHandler handler,
|
SemanticsActionHandler handler,
|
||||||
SemanticsOwner owner
|
VoidCallback showOnScreen,
|
||||||
|
SemanticsOwner owner,
|
||||||
}) : id = 0,
|
}) : id = 0,
|
||||||
|
_showOnScreen = showOnScreen,
|
||||||
_actionHandler = handler {
|
_actionHandler = handler {
|
||||||
attach(owner);
|
attach(owner);
|
||||||
}
|
}
|
||||||
@ -171,6 +175,7 @@ class SemanticsNode extends AbstractNode {
|
|||||||
final int id;
|
final int id;
|
||||||
|
|
||||||
final SemanticsActionHandler _actionHandler;
|
final SemanticsActionHandler _actionHandler;
|
||||||
|
final VoidCallback _showOnScreen;
|
||||||
|
|
||||||
// GEOMETRY
|
// GEOMETRY
|
||||||
// These are automatically handled by RenderObject's own logic
|
// These are automatically handled by RenderObject's own logic
|
||||||
@ -734,7 +739,14 @@ class SemanticsOwner extends ChangeNotifier {
|
|||||||
void performAction(int id, SemanticsAction action) {
|
void performAction(int id, SemanticsAction action) {
|
||||||
assert(action != null);
|
assert(action != null);
|
||||||
final SemanticsActionHandler handler = _getSemanticsActionHandlerForId(id, action);
|
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) {
|
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].
|
/// This should be the reverse order of [childrenInPaintOrder].
|
||||||
@protected
|
@protected
|
||||||
Iterable<RenderSliver> get childrenInHitTestOrder;
|
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.
|
/// 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.
|
/// being called again, though this should be very rare.
|
||||||
void correctBy(double correction);
|
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 direction in which the user is trying to change [pixels], relative to
|
||||||
/// the viewport's [RenderViewport.axisDirection].
|
/// the viewport's [RenderViewport.axisDirection].
|
||||||
///
|
///
|
||||||
@ -208,6 +212,11 @@ class _FixedViewportOffset extends ViewportOffset {
|
|||||||
_pixels += correction;
|
_pixels += correction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void jumpTo(double pixels) {
|
||||||
|
// Do nothing, viewport is fixed.
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ScrollDirection get userScrollDirection => ScrollDirection.idle;
|
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
|
/// If this method changes the scroll position, a sequence of start/update/end
|
||||||
/// scroll notifications will be dispatched. No overscroll notifications can
|
/// scroll notifications will be dispatched. No overscroll notifications can
|
||||||
/// be generated by this method.
|
/// be generated by this method.
|
||||||
|
@override
|
||||||
void jumpTo(double value);
|
void jumpTo(double value);
|
||||||
|
|
||||||
/// Deprecated. Use [jumpTo] or a custom [ScrollPosition] instead.
|
/// Deprecated. Use [jumpTo] or a custom [ScrollPosition] instead.
|
||||||
|
@ -418,4 +418,23 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
|
|||||||
|
|
||||||
return leadingScrollOffset - (mainAxisExtent - targetMainAxisExtent) * alignment;
|
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);
|
await flingDown(tester);
|
||||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollDown]));
|
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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user