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

Update Semantics for SingleChildScrollViews (#12376)

* Update Semantics for SingleChildScrollViews

* refactor

* review feedback

* added assert and comments

* doc
parent 51e4e9f9
...@@ -2145,6 +2145,11 @@ class BuildOwner { ...@@ -2145,6 +2145,11 @@ class BuildOwner {
int _debugStateLockLevel = 0; int _debugStateLockLevel = 0;
bool get _debugStateLocked => _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; bool _debugBuilding = false;
Element _debugCurrentBuildTarget; Element _debugCurrentBuildTarget;
......
...@@ -535,29 +535,31 @@ class RawGestureDetectorState extends State<RawGestureDetector> { ...@@ -535,29 +535,31 @@ class RawGestureDetectorState extends State<RawGestureDetector> {
} }
} }
/// This method can be called after the build phase, during the layout of the /// This method can be called outside of the build phase to filter the list of
/// nearest descendant [RenderObjectWidget] of the gesture detector, to filter /// available semantic actions.
/// 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 /// This is used by [Scrollable] to configure system accessibility tools so
/// that they know in which direction a particular list can be scrolled. /// 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 /// 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 /// actions to filter changes, it must be called again.
/// the nearest descendant [RenderObjectWidget] of the gesture detector).
void replaceSemanticsActions(Set<SemanticsAction> actions) { void replaceSemanticsActions(Set<SemanticsAction> actions) {
assert(() { assert(() {
if (!context.findRenderObject().owner.debugDoingLayout) { final Element element = context;
if (element.owner.debugBuilding) {
throw new FlutterError( throw new FlutterError(
'Unexpected call to replaceSemanticsActions() method of RawGestureDetectorState.\n' '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; return true;
}()); }());
if (!widget.excludeFromSemantics) { if (!widget.excludeFromSemantics) {
final RenderSemanticsGestureHandler semanticsGestureHandler = context.findRenderObject(); 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 { ...@@ -371,6 +371,18 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
Set<SemanticsAction> _semanticActions; 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() { void _updateSemanticActions() {
SemanticsAction forward; SemanticsAction forward;
SemanticsAction backward; SemanticsAction backward;
...@@ -409,7 +421,6 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics { ...@@ -409,7 +421,6 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
applyNewDimensions(); applyNewDimensions();
_didChangeViewportDimension = false; _didChangeViewportDimension = false;
} }
_updateSemanticActions();
return true; return true;
} }
...@@ -437,6 +448,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics { ...@@ -437,6 +448,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
void applyNewDimensions() { void applyNewDimensions() {
assert(pixels != null); assert(pixels != null);
activity.applyNewDimensions(); activity.applyNewDimensions();
_updateSemanticActions(); // will potentially request a semantics update.
} }
/// Animates the position such that the given object is as visible as possible /// Animates the position such that the given object is as visible as possible
...@@ -613,6 +625,12 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics { ...@@ -613,6 +625,12 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
super.dispose(); super.dispose();
} }
@override
void notifyListeners() {
_updateSemanticActions(); // will potentially request a semantics update.
super.notifyListeners();
}
@override @override
void debugFillDescription(List<String> description) { void debugFillDescription(List<String> description) {
if (debugLabel != null) if (debugLabel != null)
......
...@@ -205,13 +205,18 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix ...@@ -205,13 +205,18 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
if (value == _offset) if (value == _offset)
return; return;
if (attached) if (attached)
_offset.removeListener(markNeedsPaint); _offset.removeListener(_hasScrolled);
_offset = value; _offset = value;
if (attached) if (attached)
_offset.addListener(markNeedsPaint); _offset.addListener(_hasScrolled);
markNeedsLayout(); markNeedsLayout();
} }
void _hasScrolled() {
markNeedsPaint();
markNeedsSemanticsUpdate();
}
@override @override
void setupParentData(RenderObject child) { void setupParentData(RenderObject child) {
// We don't actually use the offset argument in BoxParentData, so let's // We don't actually use the offset argument in BoxParentData, so let's
...@@ -223,12 +228,12 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix ...@@ -223,12 +228,12 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
@override @override
void attach(PipelineOwner owner) { void attach(PipelineOwner owner) {
super.attach(owner); super.attach(owner);
_offset.addListener(markNeedsPaint); _offset.addListener(_hasScrolled);
} }
@override @override
void detach() { void detach() {
_offset.removeListener(markNeedsPaint); _offset.removeListener(_hasScrolled);
super.detach(); super.detach();
} }
......
...@@ -1038,6 +1038,56 @@ void main() { ...@@ -1038,6 +1038,56 @@ void main() {
semantics.dispose(); 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 { testWidgets('TabBar etc with zero tabs', (WidgetTester tester) async {
final TabController controller = new TabController( final TabController controller = new TabController(
vsync: const TestVSync(), vsync: const TestVSync(),
......
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