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 { ...@@ -705,30 +705,25 @@ abstract class _InterestingSemanticsFragment extends _SemanticsFragment {
bool dropSemanticsOfPreviousSiblings, bool dropSemanticsOfPreviousSiblings,
}) : super(renderObjectOwner: renderObjectOwner, annotator: annotator, children: children, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings); }) : super(renderObjectOwner: renderObjectOwner, annotator: annotator, children: children, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings);
bool get haveConcreteNode => true;
@override @override
Iterable<SemanticsNode> compile({ _SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics }) sync* { Iterable<SemanticsNode> compile({ _SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics }) sync* {
assert(!_debugCompiled); assert(!_debugCompiled);
assert(() { _debugCompiled = true; return true; }); assert(() { _debugCompiled = true; return true; });
final SemanticsNode node = establishSemanticsNode(geometry, currentSemantics, parentSemantics); final SemanticsNode node = establishSemanticsNode(geometry, currentSemantics, parentSemantics);
if (annotator != null) final List<SemanticsNode> children = <SemanticsNode>[];
annotator(node);
for (_SemanticsFragment child in _children) { for (_SemanticsFragment child in _children) {
assert(child._ancestorChain.last == renderObjectOwner); assert(child._ancestorChain.last == renderObjectOwner);
node.addChildren(child.compile( children.addAll(child.compile(
geometry: createSemanticsGeometryForChild(geometry), geometry: createSemanticsGeometryForChild(geometry),
currentSemantics: _children.length > 1 ? null : node, currentSemantics: _children.length > 1 ? null : node,
parentSemantics: node parentSemantics: node,
)); ));
} }
if (haveConcreteNode) { yield* finalizeSemanticsNode(node, children);
node.finalizeChildren();
yield node;
}
} }
SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics); SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics);
Iterable<SemanticsNode> finalizeSemanticsNode(SemanticsNode node, List<SemanticsNode> children);
_SemanticsGeometry createSemanticsGeometryForChild(_SemanticsGeometry geometry); _SemanticsGeometry createSemanticsGeometryForChild(_SemanticsGeometry geometry);
} }
...@@ -761,6 +756,15 @@ class _RootSemanticsFragment extends _InterestingSemanticsFragment { ...@@ -761,6 +756,15 @@ class _RootSemanticsFragment extends _InterestingSemanticsFragment {
return node; 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 @override
_SemanticsGeometry createSemanticsGeometryForChild(_SemanticsGeometry geometry) { _SemanticsGeometry createSemanticsGeometryForChild(_SemanticsGeometry geometry) {
return new _SemanticsGeometry(); return new _SemanticsGeometry();
...@@ -794,6 +798,12 @@ class _ConcreteSemanticsFragment extends _InterestingSemanticsFragment { ...@@ -794,6 +798,12 @@ class _ConcreteSemanticsFragment extends _InterestingSemanticsFragment {
return node; return node;
} }
@override
Iterable<SemanticsNode> finalizeSemanticsNode(SemanticsNode node, List<SemanticsNode> children) sync* {
renderObjectOwner.assembleSemanticsNode(node, children);
yield node;
}
@override @override
_SemanticsGeometry createSemanticsGeometryForChild(_SemanticsGeometry geometry) { _SemanticsGeometry createSemanticsGeometryForChild(_SemanticsGeometry geometry) {
return new _SemanticsGeometry.withClipFrom(geometry); return new _SemanticsGeometry.withClipFrom(geometry);
...@@ -815,16 +825,16 @@ class _ImplicitSemanticsFragment extends _InterestingSemanticsFragment { ...@@ -815,16 +825,16 @@ class _ImplicitSemanticsFragment extends _InterestingSemanticsFragment {
bool dropSemanticsOfPreviousSiblings, bool dropSemanticsOfPreviousSiblings,
}) : super(renderObjectOwner: renderObjectOwner, annotator: annotator, children: children, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings); }) : super(renderObjectOwner: renderObjectOwner, annotator: annotator, children: children, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings);
@override // If true, this fragment will introduce its own node into the Semantics Tree.
bool get haveConcreteNode => _haveConcreteNode; // If false, a borrowed semantics node from an ancestor is used.
bool _haveConcreteNode; bool _introducesOwnNode;
@override @override
SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics) { SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics) {
SemanticsNode node; SemanticsNode node;
assert(_haveConcreteNode == null); assert(_introducesOwnNode == null);
_haveConcreteNode = currentSemantics == null && annotator != null; _introducesOwnNode = currentSemantics == null && annotator != null;
if (haveConcreteNode) { if (_introducesOwnNode) {
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, showOnScreen: renderObjectOwner.showOnScreen,
...@@ -836,7 +846,7 @@ class _ImplicitSemanticsFragment extends _InterestingSemanticsFragment { ...@@ -836,7 +846,7 @@ class _ImplicitSemanticsFragment extends _InterestingSemanticsFragment {
} }
if (geometry != null) { if (geometry != null) {
geometry.applyAncestorChain(_ancestorChain); geometry.applyAncestorChain(_ancestorChain);
if (haveConcreteNode) if (_introducesOwnNode)
geometry.updateSemanticsNode(rendering: renderObjectOwner, semantics: node, parentSemantics: parentSemantics); geometry.updateSemanticsNode(rendering: renderObjectOwner, semantics: node, parentSemantics: parentSemantics);
} else { } else {
assert(_ancestorChain.length == 1); assert(_ancestorChain.length == 1);
...@@ -844,9 +854,23 @@ class _ImplicitSemanticsFragment extends _InterestingSemanticsFragment { ...@@ -844,9 +854,23 @@ class _ImplicitSemanticsFragment extends _InterestingSemanticsFragment {
return node; 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 @override
_SemanticsGeometry createSemanticsGeometryForChild(_SemanticsGeometry geometry) { _SemanticsGeometry createSemanticsGeometryForChild(_SemanticsGeometry geometry) {
if (haveConcreteNode) if (_introducesOwnNode)
return new _SemanticsGeometry.withClipFrom(geometry); return new _SemanticsGeometry.withClipFrom(geometry);
return new _SemanticsGeometry.copy(geometry); return new _SemanticsGeometry.copy(geometry);
} }
...@@ -2519,6 +2543,14 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -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 /// Mark this node as needing an update to its semantics
/// description. /// description.
/// ///
...@@ -2578,10 +2610,10 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2578,10 +2610,10 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
if (node.parent is! RenderObject) if (node.parent is! RenderObject)
break; break;
node._needsSemanticsUpdate = true; node._needsSemanticsUpdate = true;
node._semantics?.reset(); node.resetSemantics();
node = node.parent; node = node.parent;
} while (node._semantics == null); } while (node._semantics == null);
node._semantics?.reset(); node.resetSemantics();
if (node != this && _semantics != null && _needsSemanticsUpdate) { if (node != this && _semantics != null && _needsSemanticsUpdate) {
// If [this] node has already been added to [owner._nodesNeedingSemantics] // If [this] node has already been added to [owner._nodesNeedingSemantics]
// remove it as it is no longer guaranteed that its semantics // remove it as it is no longer guaranteed that its semantics
...@@ -2717,8 +2749,30 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2717,8 +2749,30 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
/// [isSemanticBoundary] isn't true, then the associated call to /// [isSemanticBoundary] isn't true, then the associated call to
/// [markNeedsSemanticsUpdate] must not have `onlyChanges` set, as it is /// [markNeedsSemanticsUpdate] must not have `onlyChanges` set, as it is
/// possible that the node should be entirely removed. /// 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; 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 // EVENTS
......
...@@ -2773,6 +2773,37 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA ...@@ -2773,6 +2773,37 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA
_onVerticalDragUpdate = onVerticalDragUpdate, _onVerticalDragUpdate = onVerticalDragUpdate,
super(child); 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, /// If non-null, the set of actions to allow. Other actions will be omitted,
/// even if their callback is provided. /// even if their callback is provided.
/// ///
...@@ -2865,6 +2896,43 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA ...@@ -2865,6 +2896,43 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA
@override @override
SemanticsAnnotator get semanticsAnnotator => isSemanticBoundary ? _annotate : null; 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) { void _annotate(SemanticsNode node) {
List<SemanticsAction> actions = <SemanticsAction>[]; List<SemanticsAction> actions = <SemanticsAction>[];
if (onTap != null) if (onTap != null)
......
...@@ -46,6 +46,38 @@ typedef void SemanticsAnnotator(SemanticsNode semantics); ...@@ -46,6 +46,38 @@ typedef void SemanticsAnnotator(SemanticsNode semantics);
/// Used by [SemanticsNode.visitChildren]. /// Used by [SemanticsNode.visitChildren].
typedef bool SemanticsNodeVisitor(SemanticsNode node); 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. /// Summary information about a [SemanticsNode] object.
/// ///
/// A semantics node might [SemanticsNode.mergeAllDescendantsIntoThisNode], /// A semantics node might [SemanticsNode.mergeAllDescendantsIntoThisNode],
...@@ -64,11 +96,13 @@ class SemanticsData { ...@@ -64,11 +96,13 @@ class SemanticsData {
@required this.actions, @required this.actions,
@required this.label, @required this.label,
@required this.rect, @required this.rect,
@required this.tags,
this.transform this.transform
}) : assert(flags != null), }) : assert(flags != null),
assert(actions != null), assert(actions != null),
assert(label != null), assert(label != null),
assert(rect != null); assert(rect != null),
assert(tags != null);
/// A bit field of [SemanticsFlags] that apply to this node. /// A bit field of [SemanticsFlags] that apply to this node.
final int flags; final int flags;
...@@ -82,6 +116,9 @@ class SemanticsData { ...@@ -82,6 +116,9 @@ class SemanticsData {
/// The bounding box for this node in its coordinate system. /// The bounding box for this node in its coordinate system.
final Rect rect; 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. /// The transform from this node's coordinate system to its parent's coordinate system.
/// ///
/// By default, the transform is null, which represents the identity /// By default, the transform is null, which represents the identity
...@@ -124,11 +161,12 @@ class SemanticsData { ...@@ -124,11 +161,12 @@ class SemanticsData {
&& typedOther.actions == actions && typedOther.actions == actions
&& typedOther.label == label && typedOther.label == label
&& typedOther.rect == rect && typedOther.rect == rect
&& setEquals(typedOther.tags, tags)
&& typedOther.transform == transform; && typedOther.transform == transform;
} }
@override @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. /// A node that represents some semantic data.
...@@ -312,6 +350,37 @@ class SemanticsNode extends AbstractNode { ...@@ -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. /// Restore this node to its default state.
void reset() { void reset() {
final bool hadInheritedMergeAllDescendantsIntoThisNode = _inheritedMergeAllDescendantsIntoThisNode; final bool hadInheritedMergeAllDescendantsIntoThisNode = _inheritedMergeAllDescendantsIntoThisNode;
...@@ -326,6 +395,9 @@ class SemanticsNode extends AbstractNode { ...@@ -326,6 +395,9 @@ class SemanticsNode extends AbstractNode {
List<SemanticsNode> _newChildren; List<SemanticsNode> _newChildren;
/// Append the given children as children of this node. /// 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) { void addChildren(Iterable<SemanticsNode> children) {
_newChildren ??= <SemanticsNode>[]; _newChildren ??= <SemanticsNode>[];
_newChildren.addAll(children); _newChildren.addAll(children);
...@@ -515,11 +587,13 @@ class SemanticsNode extends AbstractNode { ...@@ -515,11 +587,13 @@ class SemanticsNode extends AbstractNode {
int flags = _flags; int flags = _flags;
int actions = _actions; int actions = _actions;
String label = _label; String label = _label;
final Set<SemanticsTag> tags = new Set<SemanticsTag>.from(_tags);
if (mergeAllDescendantsIntoThisNode) { if (mergeAllDescendantsIntoThisNode) {
_visitDescendants((SemanticsNode node) { _visitDescendants((SemanticsNode node) {
flags |= node._flags; flags |= node._flags;
actions |= node._actions; actions |= node._actions;
tags.addAll(node._tags);
if (node.label.isNotEmpty) { if (node.label.isNotEmpty) {
if (label.isEmpty) if (label.isEmpty)
label = node.label; label = node.label;
...@@ -535,7 +609,8 @@ class SemanticsNode extends AbstractNode { ...@@ -535,7 +609,8 @@ class SemanticsNode extends AbstractNode {
actions: actions, actions: actions,
label: label, label: label,
rect: rect, rect: rect,
transform: transform transform: transform,
tags: tags,
); );
} }
...@@ -598,6 +673,8 @@ class SemanticsNode extends AbstractNode { ...@@ -598,6 +673,8 @@ class SemanticsNode extends AbstractNode {
if ((_actions & action.index) != 0) if ((_actions & action.index) != 0)
buffer.write('; $action'); buffer.write('; $action');
} }
for (SemanticsTag tag in _tags)
buffer.write('; $tag');
if (hasCheckedState) { if (hasCheckedState) {
if (isChecked) if (isChecked)
buffer.write('; checked'); buffer.write('; checked');
......
...@@ -13,6 +13,8 @@ import 'package:vector_math/vector_math_64.dart'; ...@@ -13,6 +13,8 @@ import 'package:vector_math/vector_math_64.dart';
import 'binding.dart'; import 'binding.dart';
import 'box.dart'; import 'box.dart';
import 'object.dart'; import 'object.dart';
import 'proxy_box.dart';
import 'semantics.dart';
import 'sliver.dart'; import 'sliver.dart';
import 'viewport_offset.dart'; import 'viewport_offset.dart';
...@@ -203,6 +205,28 @@ abstract class RenderSliverPersistentHeader extends RenderSliver with RenderObje ...@@ -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 @override
void debugFillProperties(DiagnosticPropertiesBuilder description) { void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description); super.debugFillProperties(description);
...@@ -264,7 +288,9 @@ abstract class RenderSliverPinnedPersistentHeader extends RenderSliverPersistent ...@@ -264,7 +288,9 @@ abstract class RenderSliverPinnedPersistentHeader extends RenderSliverPersistent
@override @override
void performLayout() { void performLayout() {
final double maxExtent = this.maxExtent; 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( geometry = new SliverGeometry(
scrollExtent: maxExtent, scrollExtent: maxExtent,
paintOrigin: constraints.overlap, paintOrigin: constraints.overlap,
...@@ -445,7 +471,9 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste ...@@ -445,7 +471,9 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste
} else { } else {
_effectiveScrollOffset = constraints.scrollOffset; _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(); _childPosition = updateGeometry();
_lastActualScrollOffset = constraints.scrollOffset; _lastActualScrollOffset = constraints.scrollOffset;
} }
......
...@@ -11,6 +11,8 @@ import 'package:vector_math/vector_math_64.dart'; ...@@ -11,6 +11,8 @@ import 'package:vector_math/vector_math_64.dart';
import 'binding.dart'; import 'binding.dart';
import 'box.dart'; import 'box.dart';
import 'object.dart'; import 'object.dart';
import 'proxy_box.dart';
import 'semantics.dart';
import 'sliver.dart'; import 'sliver.dart';
import 'viewport_offset.dart'; import 'viewport_offset.dart';
...@@ -85,6 +87,13 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix ...@@ -85,6 +87,13 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
_axisDirection = axisDirection, _axisDirection = axisDirection,
_offset = offset; _offset = offset;
@override
SemanticsAnnotator get semanticsAnnotator => _annotate;
void _annotate(SemanticsNode node) {
node.ensureTag(RenderSemanticsGestureHandler.useTwoPaneSemantics);
}
/// The direction in which the [SliverConstraints.scrollOffset] increases. /// The direction in which the [SliverConstraints.scrollOffset] increases.
/// ///
/// For example, if the [axisDirection] is [AxisDirection.down], a scroll /// 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 @@ ...@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
...@@ -32,14 +33,15 @@ class TestSemantics { ...@@ -32,14 +33,15 @@ class TestSemantics {
this.flags: 0, this.flags: 0,
this.actions: 0, this.actions: 0,
this.label: '', this.label: '',
@required this.rect, this.rect,
this.transform, this.transform,
this.children: const <TestSemantics>[], this.children: const <TestSemantics>[],
Iterable<SemanticsTag> tags,
}) : assert(id != null), }) : assert(id != null),
assert(flags != null), assert(flags != null),
assert(label != 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] /// Creates an object with some test semantics data, with the [id] and [rect]
/// set to the appropriate values for the root node. /// set to the appropriate values for the root node.
...@@ -49,11 +51,13 @@ class TestSemantics { ...@@ -49,11 +51,13 @@ class TestSemantics {
this.label: '', this.label: '',
this.transform, this.transform,
this.children: const <TestSemantics>[], this.children: const <TestSemantics>[],
Iterable<SemanticsTag> tags,
}) : id = 0, }) : id = 0,
assert(flags != null), assert(flags != null),
assert(label != null), assert(label != null),
rect = TestSemantics.rootRect, 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] /// 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. /// set to the appropriate values for direct children of the root node.
...@@ -69,13 +73,15 @@ class TestSemantics { ...@@ -69,13 +73,15 @@ class TestSemantics {
this.flags: 0, this.flags: 0,
this.actions: 0, this.actions: 0,
this.label: '', this.label: '',
@required this.rect, this.rect,
Matrix4 transform, Matrix4 transform,
this.children: const <TestSemantics>[], this.children: const <TestSemantics>[],
Iterable<SemanticsTag> tags,
}) : assert(flags != null), }) : assert(flags != null),
assert(label != null), assert(label != null),
transform = _applyRootChildScale(transform), transform = _applyRootChildScale(transform),
assert(children != null); assert(children != null),
tags = tags?.toSet() ?? new Set<SemanticsTag>();
/// The unique identifier for this node. /// The unique identifier for this node.
/// ///
...@@ -131,19 +137,18 @@ class TestSemantics { ...@@ -131,19 +137,18 @@ class TestSemantics {
/// The children of this node. /// The children of this node.
final List<TestSemantics> children; final List<TestSemantics> children;
SemanticsData _getSemanticsData() { /// The tags of this node.
return new SemanticsData( final Set<SemanticsTag> tags;
flags: flags,
actions: actions,
label: label,
rect: rect,
transform: transform,
);
}
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 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)) { || children.length != (node.mergeAllDescendantsIntoThisNode ? 0 : node.childrenCount)) {
matchState[TestSemantics] = this; matchState[TestSemantics] = this;
matchState[SemanticsNode] = node; matchState[SemanticsNode] = node;
...@@ -155,7 +160,7 @@ class TestSemantics { ...@@ -155,7 +160,7 @@ class TestSemantics {
final Iterator<TestSemantics> it = children.iterator; final Iterator<TestSemantics> it = children.iterator;
node.visitChildren((SemanticsNode node) { node.visitChildren((SemanticsNode node) {
it.moveNext(); it.moveNext();
if (!it.current._matches(node, matchState)) { if (!it.current._matches(node, matchState, ignoreRect: ignoreRect, ignoreTransform: ignoreTransform)) {
result = false; result = false;
return false; return false;
} }
...@@ -197,13 +202,15 @@ class SemanticsTester { ...@@ -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.'; 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 { 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 TestSemantics _semantics;
final bool ignoreRect;
final bool ignoreTransform;
@override @override
bool matches(covariant SemanticsTester item, Map<dynamic, dynamic> matchState) { 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 @override
...@@ -226,9 +233,9 @@ class _HasSemantics extends Matcher { ...@@ -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'); 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) 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'); 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'); 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'); 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; final int childrenCount = node.mergeAllDescendantsIntoThisNode ? 0 : node.childrenCount;
if (testNode.children.length != childrenCount) if (testNode.children.length != childrenCount)
...@@ -238,7 +245,10 @@ class _HasSemantics extends Matcher { ...@@ -238,7 +245,10 @@ class _HasSemantics extends Matcher {
} }
/// Asserts that a [SemanticsTester] has a semantics tree that exactly matches the given semantics. /// 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 { class _IncludesNodeWith extends Matcher {
const _IncludesNodeWith({ 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