Update Semantics for SingleChildScrollViews (#12376)
* Update Semantics for SingleChildScrollViews * refactor * review feedback * added assert and comments * doc
This commit is contained in:
parent
51e4e9f9a5
commit
f07170b45b
@ -2145,6 +2145,11 @@ class BuildOwner {
|
||||
|
||||
int _debugStateLockLevel = 0;
|
||||
bool get _debugStateLocked => _debugStateLockLevel > 0;
|
||||
|
||||
/// Whether this widget tree is in the build phase.
|
||||
///
|
||||
/// Only valid when asserts are enabled.
|
||||
bool get debugBuilding => _debugBuilding;
|
||||
bool _debugBuilding = false;
|
||||
Element _debugCurrentBuildTarget;
|
||||
|
||||
|
@ -535,29 +535,31 @@ class RawGestureDetectorState extends State<RawGestureDetector> {
|
||||
}
|
||||
}
|
||||
|
||||
/// This method can be called after the build phase, during the layout of the
|
||||
/// nearest descendant [RenderObjectWidget] of the gesture detector, to filter
|
||||
/// the list of available semantic actions.
|
||||
/// This method can be called outside of the build phase to filter the list of
|
||||
/// available semantic actions.
|
||||
///
|
||||
/// The actual filtering is happening in the next frame and a frame will be
|
||||
/// scheduled if non is pending.
|
||||
///
|
||||
/// This is used by [Scrollable] to configure system accessibility tools so
|
||||
/// that they know in which direction a particular list can be scrolled.
|
||||
///
|
||||
/// If this is never called, then the actions are not filtered. If the list of
|
||||
/// actions to filter changes, it must be called again (during the layout of
|
||||
/// the nearest descendant [RenderObjectWidget] of the gesture detector).
|
||||
/// actions to filter changes, it must be called again.
|
||||
void replaceSemanticsActions(Set<SemanticsAction> actions) {
|
||||
assert(() {
|
||||
if (!context.findRenderObject().owner.debugDoingLayout) {
|
||||
final Element element = context;
|
||||
if (element.owner.debugBuilding) {
|
||||
throw new FlutterError(
|
||||
'Unexpected call to replaceSemanticsActions() method of RawGestureDetectorState.\n'
|
||||
'The replaceSemanticsActions() method can only be called during the layout phase.'
|
||||
'The replaceSemanticsActions() method can only be called outside of the build phase.'
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
if (!widget.excludeFromSemantics) {
|
||||
final RenderSemanticsGestureHandler semanticsGestureHandler = context.findRenderObject();
|
||||
semanticsGestureHandler.validActions = actions;
|
||||
semanticsGestureHandler.validActions = actions; // will call _markNeedsSemanticsUpdate(), if required.
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -371,6 +371,18 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
|
||||
|
||||
Set<SemanticsAction> _semanticActions;
|
||||
|
||||
/// Called whenever the scroll position or the dimensions of the scroll view
|
||||
/// change to schedule an update of the available semantics actions. The
|
||||
/// actual update will be performed in the nxt frame. If non is pending
|
||||
/// a frame will be scheduled.
|
||||
///
|
||||
/// For example: If the scroll view has been scrolled all the way to the top,
|
||||
/// the action to scroll further up needs to be removed as the scroll view
|
||||
/// cannot be scrolled in that direction anymore.
|
||||
///
|
||||
/// This method is potentially called twice per frame (if scroll position and
|
||||
/// scroll view dimensions both change) and therefore shouldn't do anything
|
||||
/// expensive.
|
||||
void _updateSemanticActions() {
|
||||
SemanticsAction forward;
|
||||
SemanticsAction backward;
|
||||
@ -409,7 +421,6 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
|
||||
applyNewDimensions();
|
||||
_didChangeViewportDimension = false;
|
||||
}
|
||||
_updateSemanticActions();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -437,6 +448,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
|
||||
void applyNewDimensions() {
|
||||
assert(pixels != null);
|
||||
activity.applyNewDimensions();
|
||||
_updateSemanticActions(); // will potentially request a semantics update.
|
||||
}
|
||||
|
||||
/// Animates the position such that the given object is as visible as possible
|
||||
@ -613,6 +625,12 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void notifyListeners() {
|
||||
_updateSemanticActions(); // will potentially request a semantics update.
|
||||
super.notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillDescription(List<String> description) {
|
||||
if (debugLabel != null)
|
||||
|
@ -205,13 +205,18 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
|
||||
if (value == _offset)
|
||||
return;
|
||||
if (attached)
|
||||
_offset.removeListener(markNeedsPaint);
|
||||
_offset.removeListener(_hasScrolled);
|
||||
_offset = value;
|
||||
if (attached)
|
||||
_offset.addListener(markNeedsPaint);
|
||||
_offset.addListener(_hasScrolled);
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
void _hasScrolled() {
|
||||
markNeedsPaint();
|
||||
markNeedsSemanticsUpdate();
|
||||
}
|
||||
|
||||
@override
|
||||
void setupParentData(RenderObject child) {
|
||||
// We don't actually use the offset argument in BoxParentData, so let's
|
||||
@ -223,12 +228,12 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
|
||||
@override
|
||||
void attach(PipelineOwner owner) {
|
||||
super.attach(owner);
|
||||
_offset.addListener(markNeedsPaint);
|
||||
_offset.addListener(_hasScrolled);
|
||||
}
|
||||
|
||||
@override
|
||||
void detach() {
|
||||
_offset.removeListener(markNeedsPaint);
|
||||
_offset.removeListener(_hasScrolled);
|
||||
super.detach();
|
||||
}
|
||||
|
||||
|
@ -1038,6 +1038,56 @@ void main() {
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets('correct scrolling semantics', (WidgetTester tester) async {
|
||||
final SemanticsTester semantics = new SemanticsTester(tester);
|
||||
|
||||
final List<Tab> tabs = new List<Tab>.generate(20, (int index) {
|
||||
return new Tab(text: 'This is a very wide tab #$index');
|
||||
});
|
||||
|
||||
final TabController controller = new TabController(
|
||||
vsync: const TestVSync(),
|
||||
length: tabs.length,
|
||||
initialIndex: 0,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
boilerplate(
|
||||
child: new Semantics(
|
||||
container: true,
|
||||
child: new TabBar(
|
||||
isScrollable: true,
|
||||
controller: controller,
|
||||
tabs: tabs,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollLeft]));
|
||||
expect(semantics, isNot(includesNodeWith(label: 'This is a very wide tab #10')));
|
||||
|
||||
controller.index = 10;
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(semantics, isNot(includesNodeWith(label: 'This is a very wide tab #0')));
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollLeft, SemanticsAction.scrollRight]));
|
||||
expect(semantics, includesNodeWith(label: 'This is a very wide tab #10'));
|
||||
|
||||
controller.index = 19;
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollRight]));
|
||||
|
||||
controller.index = 0;
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollLeft]));
|
||||
expect(semantics, includesNodeWith(label: 'This is a very wide tab #0'));
|
||||
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets('TabBar etc with zero tabs', (WidgetTester tester) async {
|
||||
final TabController controller = new TabController(
|
||||
vsync: const TestVSync(),
|
||||
|
Loading…
x
Reference in New Issue
Block a user