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;
|
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> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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 {
|
|||||||
|
|
||||||
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 {
|
|||||||
applyNewDimensions();
|
applyNewDimensions();
|
||||||
_didChangeViewportDimension = false;
|
_didChangeViewportDimension = false;
|
||||||
}
|
}
|
||||||
_updateSemanticActions();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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 {
|
|||||||
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
|
|||||||
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
|
|||||||
@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() {
|
|||||||
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(),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user