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

a11y and scrolling with slivers (#11711)

* refactor to assembleSemanticNode

* ++

* cleanup

* fix test

* add note

* review comments

* review feedback

* import fix

* another import fix

* refactor to ensure tag

* tests, tests, tests

* analyzer fixes

* review comments
parent b4f6e567
......@@ -705,30 +705,25 @@ abstract class _InterestingSemanticsFragment extends _SemanticsFragment {
bool dropSemanticsOfPreviousSiblings,
}) : super(renderObjectOwner: renderObjectOwner, annotator: annotator, children: children, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings);
bool get haveConcreteNode => true;
@override
Iterable<SemanticsNode> compile({ _SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics }) sync* {
assert(!_debugCompiled);
assert(() { _debugCompiled = true; return true; });
final SemanticsNode node = establishSemanticsNode(geometry, currentSemantics, parentSemantics);
if (annotator != null)
annotator(node);
final List<SemanticsNode> children = <SemanticsNode>[];
for (_SemanticsFragment child in _children) {
assert(child._ancestorChain.last == renderObjectOwner);
node.addChildren(child.compile(
children.addAll(child.compile(
geometry: createSemanticsGeometryForChild(geometry),
currentSemantics: _children.length > 1 ? null : node,
parentSemantics: node
parentSemantics: node,
));
}
if (haveConcreteNode) {
node.finalizeChildren();
yield node;
}
yield* finalizeSemanticsNode(node, children);
}
SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics);
Iterable<SemanticsNode> finalizeSemanticsNode(SemanticsNode node, List<SemanticsNode> children);
_SemanticsGeometry createSemanticsGeometryForChild(_SemanticsGeometry geometry);
}
......@@ -761,6 +756,15 @@ class _RootSemanticsFragment extends _InterestingSemanticsFragment {
return node;
}
@override
Iterable<SemanticsNode> finalizeSemanticsNode(SemanticsNode node, List<SemanticsNode> children) sync* {
if (annotator != null)
annotator(node);
node.addChildren(children);
node.finalizeChildren();
yield node;
}
@override
_SemanticsGeometry createSemanticsGeometryForChild(_SemanticsGeometry geometry) {
return new _SemanticsGeometry();
......@@ -794,6 +798,12 @@ class _ConcreteSemanticsFragment extends _InterestingSemanticsFragment {
return node;
}
@override
Iterable<SemanticsNode> finalizeSemanticsNode(SemanticsNode node, List<SemanticsNode> children) sync* {
renderObjectOwner.assembleSemanticsNode(node, children);
yield node;
}
@override
_SemanticsGeometry createSemanticsGeometryForChild(_SemanticsGeometry geometry) {
return new _SemanticsGeometry.withClipFrom(geometry);
......@@ -815,16 +825,16 @@ class _ImplicitSemanticsFragment extends _InterestingSemanticsFragment {
bool dropSemanticsOfPreviousSiblings,
}) : super(renderObjectOwner: renderObjectOwner, annotator: annotator, children: children, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings);
@override
bool get haveConcreteNode => _haveConcreteNode;
bool _haveConcreteNode;
// If true, this fragment will introduce its own node into the Semantics Tree.
// If false, a borrowed semantics node from an ancestor is used.
bool _introducesOwnNode;
@override
SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics) {
SemanticsNode node;
assert(_haveConcreteNode == null);
_haveConcreteNode = currentSemantics == null && annotator != null;
if (haveConcreteNode) {
assert(_introducesOwnNode == null);
_introducesOwnNode = currentSemantics == null && annotator != null;
if (_introducesOwnNode) {
renderObjectOwner._semantics ??= new SemanticsNode(
handler: renderObjectOwner is SemanticsActionHandler ? renderObjectOwner as dynamic : null,
showOnScreen: renderObjectOwner.showOnScreen,
......@@ -836,7 +846,7 @@ class _ImplicitSemanticsFragment extends _InterestingSemanticsFragment {
}
if (geometry != null) {
geometry.applyAncestorChain(_ancestorChain);
if (haveConcreteNode)
if (_introducesOwnNode)
geometry.updateSemanticsNode(rendering: renderObjectOwner, semantics: node, parentSemantics: parentSemantics);
} else {
assert(_ancestorChain.length == 1);
......@@ -844,9 +854,23 @@ class _ImplicitSemanticsFragment extends _InterestingSemanticsFragment {
return node;
}
@override
Iterable<SemanticsNode> finalizeSemanticsNode(SemanticsNode node, List<SemanticsNode> children) sync* {
if (annotator != null)
annotator(node);
if (_introducesOwnNode) {
node.addChildren(children);
node.finalizeChildren();
yield node;
} else {
// Transparently forward children to the borrowed node.
yield* children;
}
}
@override
_SemanticsGeometry createSemanticsGeometryForChild(_SemanticsGeometry geometry) {
if (haveConcreteNode)
if (_introducesOwnNode)
return new _SemanticsGeometry.withClipFrom(geometry);
return new _SemanticsGeometry.copy(geometry);
}
......@@ -2519,6 +2543,14 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
});
}
/// Restore the [SemanticsNode]s owned by this render object to its default
/// state.
@mustCallSuper
@protected
void resetSemantics() {
_semantics?.reset();
}
/// Mark this node as needing an update to its semantics
/// description.
///
......@@ -2578,10 +2610,10 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
if (node.parent is! RenderObject)
break;
node._needsSemanticsUpdate = true;
node._semantics?.reset();
node.resetSemantics();
node = node.parent;
} while (node._semantics == null);
node._semantics?.reset();
node.resetSemantics();
if (node != this && _semantics != null && _needsSemanticsUpdate) {
// If [this] node has already been added to [owner._nodesNeedingSemantics]
// remove it as it is no longer guaranteed that its semantics
......@@ -2717,8 +2749,30 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
/// [isSemanticBoundary] isn't true, then the associated call to
/// [markNeedsSemanticsUpdate] must not have `onlyChanges` set, as it is
/// possible that the node should be entirely removed.
///
/// If the annotation should only happen under certain conditions, `null`
/// should be returned if those conditions are currently not met to avoid
/// the creation of an empty [SemanticsNode].
SemanticsAnnotator get semanticsAnnotator => null;
/// Assemble the [SemanticsNode] for this [RenderObject].
///
/// If [isSemanticBoundary] is true, this method is called with the semantics
/// [node] created for this [RenderObject] and its semantics [children].
/// By default, the method will annotate [node] with the [semanticsAnnotator]
/// and add the [children] to it.
///
/// Subclasses can override this method to add additional [SemanticNode]s
/// to the tree. If a subclass adds additional nodes in this method, it also
/// needs to override [resetSemantics] to call [SemanticsNodes.reset] on those
/// additional [SemanticsNode]s.
void assembleSemanticsNode(SemanticsNode node, Iterable<SemanticsNode> children) {
assert(node == _semantics);
if (semanticsAnnotator != null)
semanticsAnnotator(node);
node.addChildren(children);
node.finalizeChildren();
}
// EVENTS
......
......@@ -2773,6 +2773,37 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA
_onVerticalDragUpdate = onVerticalDragUpdate,
super(child);
/// When a [SemanticsNode] that is a direct child of this object's
/// [SemanticsNode] is tagged with [excludeFromScrolling] it will not be
/// part of the scrolling area for semantic purposes.
///
/// This behavior is only active if the [SemanticsNode] of this
/// [RenderSemanticsGestureHandler] is tagged with [useTwoPaneSemantics].
/// Otherwise, the [excludeFromScrolling] tag is ignored.
///
/// As an example, a [RenderSliver] that stays on the screen within a
/// [Scrollable] even though the user has scrolled past it (e.g. a pinned app
/// bar) can tag its [SemanticNode] with [excludeFromScrolling] to indicate
/// that it should no longer be considered for semantic actions related to
/// scrolling.
static const SemanticsTag excludeFromScrolling = const SemanticsTag('RenderSemanticsGestureHandler.excludeFromScrolling');
/// If the [SemanticsNode] of this [RenderSemanticsGestureHandler] is tagged
/// with [useTwoPaneSemantics], two semantics nodes will be used to represent
/// this render object in the semantics tree.
///
/// Two semantics nodes are necessary to exclude certain child nodes (via the
/// [excludeFromScrolling] tag) from the scrollable area for semantic
/// purposes.
///
/// If this tag is used, the first "outer" semantics node is the regular node
/// of this object. The second "inner" node is introduces as a child to that
/// node. All scrollable children are now a child of the inner node, which has
/// the semantic scrolling logic enabled. All children that have been
/// excluded from scrolling with [excludeFromScrolling] are turned into
/// children of the outer node.
static const SemanticsTag useTwoPaneSemantics = const SemanticsTag('RenderSemanticsGestureHandler.twoPane');
/// If non-null, the set of actions to allow. Other actions will be omitted,
/// even if their callback is provided.
///
......@@ -2865,6 +2896,43 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA
@override
SemanticsAnnotator get semanticsAnnotator => isSemanticBoundary ? _annotate : null;
SemanticsNode _innerNode;
@override
void assembleSemanticsNode(SemanticsNode node, Iterable<SemanticsNode> children) {
if (!node.hasTag(useTwoPaneSemantics)) {
super.assembleSemanticsNode(node, children);
return;
}
_innerNode ??= new SemanticsNode(handler: this, showOnScreen: showOnScreen);
_innerNode
..wasAffectedByClip = node.wasAffectedByClip
..rect = Offset.zero & node.rect.size;
semanticsAnnotator(_innerNode);
final List<SemanticsNode> excluded = <SemanticsNode>[];
final List<SemanticsNode> included = <SemanticsNode>[];
for (SemanticsNode child in children) {
if (child.hasTag(excludeFromScrolling))
excluded.add(child);
else
included.add(child);
}
excluded.add(_innerNode);
node.addChildren(excluded);
_innerNode.addChildren(included);
_innerNode.finalizeChildren();
node.finalizeChildren();
}
@override
void resetSemantics() {
_innerNode?.reset();
super.resetSemantics();
}
void _annotate(SemanticsNode node) {
List<SemanticsAction> actions = <SemanticsAction>[];
if (onTap != null)
......
......@@ -46,6 +46,38 @@ typedef void SemanticsAnnotator(SemanticsNode semantics);
/// Used by [SemanticsNode.visitChildren].
typedef bool SemanticsNodeVisitor(SemanticsNode node);
/// A tag for a [SemanticsNode].
///
/// Tags can be interpreted by the parent of a [SemanticsNode]
/// and depending on the presence of a tag the parent can for example decide
/// how to add the tagged note as a child. Tags are not sent to the engine.
///
/// As an example, the [RenderSemanticsGestureHandler] uses tags to determine
/// if a child node should be excluded from the scrollable area for semantic
/// purposes.
///
/// The provided [name] is only used for debugging. Two tags created with the
/// same [name] and the `new` operator are not considered identical. However,
/// two tags created with the same [name] and the `const` operator are always
/// identical.
class SemanticsTag {
/// Creates a [SemanticsTag].
///
/// The provided [name] is only used for debugging. Two tags created with the
/// same [name] and the `new` operator are not considered identical. However,
/// two tags created with the same [name] and the `const` operator are always
/// identical.
const SemanticsTag(this.name);
/// A human-readable name for this tag used for debugging.
///
/// This string is not used to determine if two tags are identical.
final String name;
@override
String toString() => '$runtimeType($name)';
}
/// Summary information about a [SemanticsNode] object.
///
/// A semantics node might [SemanticsNode.mergeAllDescendantsIntoThisNode],
......@@ -64,11 +96,13 @@ class SemanticsData {
@required this.actions,
@required this.label,
@required this.rect,
@required this.tags,
this.transform
}) : assert(flags != null),
assert(actions != null),
assert(label != null),
assert(rect != null);
assert(rect != null),
assert(tags != null);
/// A bit field of [SemanticsFlags] that apply to this node.
final int flags;
......@@ -82,6 +116,9 @@ class SemanticsData {
/// The bounding box for this node in its coordinate system.
final Rect rect;
/// The set of [SemanticsTag]s associated with this node.
final Set<SemanticsTag> tags;
/// The transform from this node's coordinate system to its parent's coordinate system.
///
/// By default, the transform is null, which represents the identity
......@@ -124,11 +161,12 @@ class SemanticsData {
&& typedOther.actions == actions
&& typedOther.label == label
&& typedOther.rect == rect
&& setEquals(typedOther.tags, tags)
&& typedOther.transform == transform;
}
@override
int get hashCode => hashValues(flags, actions, label, rect, transform);
int get hashCode => hashValues(flags, actions, label, rect, tags, transform);
}
/// A node that represents some semantic data.
......@@ -312,6 +350,37 @@ class SemanticsNode extends AbstractNode {
}
}
Set<SemanticsTag> _tags = new Set<SemanticsTag>();
/// Ensures that the [SemanticsNode] is or is not tagged with [tag].
///
/// If [isPresent] is `true` it will ensure that the tag is present. If
/// [isPresent] is `false` it will ensure that the node is not tagged with
/// [tag].
///
/// Tags are not sent to the engine. They can be used by a parent
/// [SemanticsNode] to figure out how to add the node as a child.
///
/// See also:
///
/// * [SemanticsTag], whose documentation discusses the purposes of tags.
/// * [hasTag] to check if the node has a certain tag.
void ensureTag(SemanticsTag tag, { bool isPresent: true }) {
if (isPresent)
_tags.add(tag);
else
_tags.remove(tag);
}
/// Check if the [SemanticsNode] is tagged with [tag].
///
/// Tags can be added and removed with [ensureTag].
///
/// See also:
///
/// * [SemanticsTag], whose documentation discusses the purposes of tags.
bool hasTag(SemanticsTag tag) => _tags.contains(tag);
/// Restore this node to its default state.
void reset() {
final bool hadInheritedMergeAllDescendantsIntoThisNode = _inheritedMergeAllDescendantsIntoThisNode;
......@@ -326,6 +395,9 @@ class SemanticsNode extends AbstractNode {
List<SemanticsNode> _newChildren;
/// Append the given children as children of this node.
///
/// The [finalizeChildren] method must be called after all children have been
/// added.
void addChildren(Iterable<SemanticsNode> children) {
_newChildren ??= <SemanticsNode>[];
_newChildren.addAll(children);
......@@ -515,11 +587,13 @@ class SemanticsNode extends AbstractNode {
int flags = _flags;
int actions = _actions;
String label = _label;
final Set<SemanticsTag> tags = new Set<SemanticsTag>.from(_tags);
if (mergeAllDescendantsIntoThisNode) {
_visitDescendants((SemanticsNode node) {
flags |= node._flags;
actions |= node._actions;
tags.addAll(node._tags);
if (node.label.isNotEmpty) {
if (label.isEmpty)
label = node.label;
......@@ -535,7 +609,8 @@ class SemanticsNode extends AbstractNode {
actions: actions,
label: label,
rect: rect,
transform: transform
transform: transform,
tags: tags,
);
}
......@@ -598,6 +673,8 @@ class SemanticsNode extends AbstractNode {
if ((_actions & action.index) != 0)
buffer.write('; $action');
}
for (SemanticsTag tag in _tags)
buffer.write('; $tag');
if (hasCheckedState) {
if (isChecked)
buffer.write('; checked');
......
......@@ -13,6 +13,8 @@ import 'package:vector_math/vector_math_64.dart';
import 'binding.dart';
import 'box.dart';
import 'object.dart';
import 'proxy_box.dart';
import 'semantics.dart';
import 'sliver.dart';
import 'viewport_offset.dart';
......@@ -203,6 +205,28 @@ abstract class RenderSliverPersistentHeader extends RenderSliver with RenderObje
}
}
/// Whether the [SemanticsNode]s associated with this [RenderSliver] should
/// be excluded from the semantic scrolling area.
///
/// [RenderSliver]s that stay on the screen even though the user has scrolled
/// past them (e.g. a pinned app bar) should set this to `true`.
@protected
bool get excludeFromSemanticsScrolling => _excludeFromSemanticsScrolling;
bool _excludeFromSemanticsScrolling = false;
set excludeFromSemanticsScrolling(bool value) {
if (_excludeFromSemanticsScrolling == value)
return;
_excludeFromSemanticsScrolling = value;
markNeedsSemanticsUpdate();
}
@override
SemanticsAnnotator get semanticsAnnotator => _excludeFromSemanticsScrolling ? _annotate : null;
void _annotate(SemanticsNode node) {
node.ensureTag(RenderSemanticsGestureHandler.excludeFromScrolling);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
......@@ -264,7 +288,9 @@ abstract class RenderSliverPinnedPersistentHeader extends RenderSliverPersistent
@override
void performLayout() {
final double maxExtent = this.maxExtent;
layoutChild(constraints.scrollOffset, maxExtent, overlapsContent: constraints.overlap > 0.0);
final bool overlapsContent = constraints.overlap > 0.0;
excludeFromSemanticsScrolling = overlapsContent || (constraints.scrollOffset > maxExtent - minExtent);
layoutChild(constraints.scrollOffset, maxExtent, overlapsContent: overlapsContent);
geometry = new SliverGeometry(
scrollExtent: maxExtent,
paintOrigin: constraints.overlap,
......@@ -445,7 +471,9 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste
} else {
_effectiveScrollOffset = constraints.scrollOffset;
}
layoutChild(_effectiveScrollOffset, maxExtent, overlapsContent: _effectiveScrollOffset < constraints.scrollOffset);
final bool overlapsContent = _effectiveScrollOffset < constraints.scrollOffset;
excludeFromSemanticsScrolling = overlapsContent;
layoutChild(_effectiveScrollOffset, maxExtent, overlapsContent: overlapsContent);
_childPosition = updateGeometry();
_lastActualScrollOffset = constraints.scrollOffset;
}
......
......@@ -11,6 +11,8 @@ import 'package:vector_math/vector_math_64.dart';
import 'binding.dart';
import 'box.dart';
import 'object.dart';
import 'proxy_box.dart';
import 'semantics.dart';
import 'sliver.dart';
import 'viewport_offset.dart';
......@@ -85,6 +87,13 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
_axisDirection = axisDirection,
_offset = offset;
@override
SemanticsAnnotator get semanticsAnnotator => _annotate;
void _annotate(SemanticsNode node) {
node.ensureTag(RenderSemanticsGestureHandler.useTwoPaneSemantics);
}
/// The direction in which the [SliverConstraints.scrollOffset] increases.
///
/// For example, if the [axisDirection] is [AxisDirection.down], a scroll
......
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/rendering.dart';
import 'package:test/test.dart';
void main() {
group('SemanticsNode', () {
const SemanticsTag tag1 = const SemanticsTag('Tag One');
const SemanticsTag tag2 = const SemanticsTag('Tag Two');
const SemanticsTag tag3 = const SemanticsTag('Tag Three');
test('tagging', () {
final SemanticsNode node = new SemanticsNode();
expect(node.hasTag(tag1), isFalse);
expect(node.hasTag(tag2), isFalse);
node.ensureTag(tag1);
expect(node.hasTag(tag1), isTrue);
expect(node.hasTag(tag2), isFalse);
node.ensureTag(tag2, isPresent: false);
expect(node.hasTag(tag1), isTrue);
expect(node.hasTag(tag2), isFalse);
node.ensureTag(tag2, isPresent: true);
expect(node.hasTag(tag1), isTrue);
expect(node.hasTag(tag2), isTrue);
node.ensureTag(tag2, isPresent: true);
expect(node.hasTag(tag1), isTrue);
expect(node.hasTag(tag2), isTrue);
node.ensureTag(tag1, isPresent: false);
expect(node.hasTag(tag1), isFalse);
expect(node.hasTag(tag2), isTrue);
node.ensureTag(tag2, isPresent: false);
expect(node.hasTag(tag1), isFalse);
expect(node.hasTag(tag2), isFalse);
});
test('getSemanticsData includes tags', () {
final SemanticsNode node = new SemanticsNode()
..ensureTag(tag1)
..ensureTag(tag2);
final Set<SemanticsTag> expected = new Set<SemanticsTag>()
..add(tag1)
..add(tag2);
expect(node.getSemanticsData().tags, expected);
node.mergeAllDescendantsIntoThisNode = true;
node.addChildren(<SemanticsNode>[
new SemanticsNode()..ensureTag(tag3)
]);
node.finalizeChildren();
expected.add(tag3);
expect(node.getSemanticsData().tags, expected);
});
});
}
......@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:meta/meta.dart';
......@@ -32,14 +33,15 @@ class TestSemantics {
this.flags: 0,
this.actions: 0,
this.label: '',
@required this.rect,
this.rect,
this.transform,
this.children: const <TestSemantics>[],
Iterable<SemanticsTag> tags,
}) : assert(id != null),
assert(flags != null),
assert(label != null),
assert(rect != null),
assert(children != null);
assert(children != null),
tags = tags?.toSet() ?? new Set<SemanticsTag>();
/// Creates an object with some test semantics data, with the [id] and [rect]
/// set to the appropriate values for the root node.
......@@ -49,11 +51,13 @@ class TestSemantics {
this.label: '',
this.transform,
this.children: const <TestSemantics>[],
Iterable<SemanticsTag> tags,
}) : id = 0,
assert(flags != null),
assert(label != null),
rect = TestSemantics.rootRect,
assert(children != null);
assert(children != null),
tags = tags?.toSet() ?? new Set<SemanticsTag>();
/// Creates an object with some test semantics data, with the [id] and [rect]
/// set to the appropriate values for direct children of the root node.
......@@ -69,13 +73,15 @@ class TestSemantics {
this.flags: 0,
this.actions: 0,
this.label: '',
@required this.rect,
this.rect,
Matrix4 transform,
this.children: const <TestSemantics>[],
Iterable<SemanticsTag> tags,
}) : assert(flags != null),
assert(label != null),
transform = _applyRootChildScale(transform),
assert(children != null);
assert(children != null),
tags = tags?.toSet() ?? new Set<SemanticsTag>();
/// The unique identifier for this node.
///
......@@ -131,19 +137,18 @@ class TestSemantics {
/// The children of this node.
final List<TestSemantics> children;
SemanticsData _getSemanticsData() {
return new SemanticsData(
flags: flags,
actions: actions,
label: label,
rect: rect,
transform: transform,
);
}
/// The tags of this node.
final Set<SemanticsTag> tags;
bool _matches(SemanticsNode node, Map<dynamic, dynamic> matchState) {
bool _matches(SemanticsNode node, Map<dynamic, dynamic> matchState, { bool ignoreRect: false, bool ignoreTransform: false }) {
final SemanticsData nodeData = node.getSemanticsData();
if (node == null || id != node.id
|| _getSemanticsData() != node.getSemanticsData()
|| flags != nodeData.flags
|| actions != nodeData.actions
|| label != nodeData.label
|| !setEquals(tags, nodeData.tags)
|| (!ignoreRect && rect != nodeData.rect)
|| (!ignoreTransform && transform != nodeData.transform)
|| children.length != (node.mergeAllDescendantsIntoThisNode ? 0 : node.childrenCount)) {
matchState[TestSemantics] = this;
matchState[SemanticsNode] = node;
......@@ -155,7 +160,7 @@ class TestSemantics {
final Iterator<TestSemantics> it = children.iterator;
node.visitChildren((SemanticsNode node) {
it.moveNext();
if (!it.current._matches(node, matchState)) {
if (!it.current._matches(node, matchState, ignoreRect: ignoreRect, ignoreTransform: ignoreTransform)) {
result = false;
return false;
}
......@@ -197,13 +202,15 @@ class SemanticsTester {
const String _matcherHelp = 'Try dumping the semantics with debugDumpSemanticsTree() from the rendering library to see what the semantics tree looks like.';
class _HasSemantics extends Matcher {
const _HasSemantics(this._semantics) : assert(_semantics != null);
const _HasSemantics(this._semantics, { this.ignoreRect: false, this.ignoreTransform: false }) : assert(_semantics != null), assert(ignoreRect != null), assert(ignoreTransform != null);
final TestSemantics _semantics;
final bool ignoreRect;
final bool ignoreTransform;
@override
bool matches(covariant SemanticsTester item, Map<dynamic, dynamic> matchState) {
return _semantics._matches(item.tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode, matchState);
return _semantics._matches(item.tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode, matchState, ignoreTransform: ignoreTransform, ignoreRect: ignoreRect);
}
@override
......@@ -226,9 +233,9 @@ class _HasSemantics extends Matcher {
return mismatchDescription.add('expected node id ${testNode.id} to have actions ${testNode.actions} but found actions ${data.actions}.\n$_matcherHelp');
if (testNode.label != data.label)
return mismatchDescription.add('expected node id ${testNode.id} to have label "${testNode.label}" but found label "${data.label}".\n$_matcherHelp');
if (testNode.rect != data.rect)
if (!ignoreRect && testNode.rect != data.rect)
return mismatchDescription.add('expected node id ${testNode.id} to have rect ${testNode.rect} but found rect ${data.rect}.\n$_matcherHelp');
if (testNode.transform != data.transform)
if (!ignoreTransform && testNode.transform != data.transform)
return mismatchDescription.add('expected node id ${testNode.id} to have transform ${testNode.transform} but found transform:.\n${data.transform}.\n$_matcherHelp');
final int childrenCount = node.mergeAllDescendantsIntoThisNode ? 0 : node.childrenCount;
if (testNode.children.length != childrenCount)
......@@ -238,7 +245,10 @@ class _HasSemantics extends Matcher {
}
/// Asserts that a [SemanticsTester] has a semantics tree that exactly matches the given semantics.
Matcher hasSemantics(TestSemantics semantics) => new _HasSemantics(semantics);
Matcher hasSemantics(TestSemantics semantics, {
bool ignoreRect: false,
bool ignoreTransform: false,
}) => new _HasSemantics(semantics, ignoreRect: ignoreRect, ignoreTransform: ignoreTransform);
class _IncludesNodeWith extends Matcher {
const _IncludesNodeWith({
......
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
import 'semantics_tester.dart';
void main() {
testWidgets('excludeFromScrollable works correctly', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
const double appBarExpandedHeight = 200.0;
final ScrollController scrollController = new ScrollController();
final List<Widget> listChildren = new List<Widget>.generate(30, (int i) {
return new Container(
height: appBarExpandedHeight,
child: new Text('Item $i'),
);
});
await tester.pumpWidget(
new MediaQuery(
data: const MediaQueryData(),
child: new CustomScrollView(
controller: scrollController,
slivers: <Widget>[
new SliverAppBar(
pinned: true,
expandedHeight: appBarExpandedHeight,
title: const Text('Semantics Test with Slivers'),
),
new SliverList(
delegate: new SliverChildListDelegate(listChildren),
),
],
),
));
// AppBar is child of node with semantic scroll actions.
expect(semantics, hasSemantics(
new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics],
children: <TestSemantics>[
new TestSemantics(
id: 5,
actions: SemanticsAction.scrollUp.index,
children: <TestSemantics>[
new TestSemantics(
id: 2,
label: 'Semantics Test with Slivers',
),
new TestSemantics(
id: 3,
label: 'Item 0',
),
new TestSemantics(
id: 4,
label: 'Item 1',
),
],
),
],
)
],
),
ignoreRect: true,
ignoreTransform: true,
));
// Scroll down far enough to reach the pinned state of the app bar.
scrollController.jumpTo(appBarExpandedHeight);
await tester.pump();
// App bar is NOT a child of node with semantic scroll actions.
expect(semantics, hasSemantics(
new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics],
children: <TestSemantics>[
new TestSemantics(
id: 6,
label: 'Semantics Test with Slivers',
tags: <SemanticsTag>[RenderSemanticsGestureHandler.excludeFromScrolling],
),
new TestSemantics(
id: 5,
actions: SemanticsAction.scrollUp.index | SemanticsAction.scrollDown.index,
children: <TestSemantics>[
new TestSemantics(
id: 3,
label: 'Item 0',
),
new TestSemantics(
id: 4,
label: 'Item 1',
),
new TestSemantics(
id: 7,
label: 'Item 2',
),
],
),
],
)
],
),
ignoreRect: true,
ignoreTransform: true,
));
// Scroll halfway back to the top, app bar is no longer in pinned state.
scrollController.jumpTo(appBarExpandedHeight / 2);
await tester.pump();
// AppBar is child of node with semantic scroll actions.
expect(semantics, hasSemantics(
new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
tags: <SemanticsTag>[RenderSemanticsGestureHandler.useTwoPaneSemantics],
children: <TestSemantics>[
new TestSemantics(
id: 5,
actions: SemanticsAction.scrollUp.index | SemanticsAction.scrollDown.index,
children: <TestSemantics>[
new TestSemantics(
id: 8,
label: 'Semantics Test with Slivers',
),
new TestSemantics(
id: 3,
label: 'Item 0',
),
new TestSemantics(
id: 4,
label: 'Item 1',
),
new TestSemantics(
id: 7,
label: 'Item 2',
),
],
),
],
)
],
),
ignoreRect: true,
ignoreTransform: true,
));
});
}
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