diff --git a/examples/flutter_gallery/lib/demo/pesto_demo.dart b/examples/flutter_gallery/lib/demo/pesto_demo.dart index ed59672f233..c92d6737564 100644 --- a/examples/flutter_gallery/lib/demo/pesto_demo.dart +++ b/examples/flutter_gallery/lib/demo/pesto_demo.dart @@ -251,48 +251,46 @@ class RecipeCard extends StatelessWidget { @override Widget build(BuildContext context) { - return new MergeSemantics( - child: new GestureDetector( - onTap: onTap, - child: new Card( - child: new Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - new Hero( - tag: 'packages/$_kGalleryAssetsPackage/${recipe.imagePath}', - child: new Image.asset( - recipe.imagePath, - package: recipe.imagePackage, - fit: BoxFit.contain, - ), + return new GestureDetector( + onTap: onTap, + child: new Card( + child: new Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + new Hero( + tag: 'packages/$_kGalleryAssetsPackage/${recipe.imagePath}', + child: new Image.asset( + recipe.imagePath, + package: recipe.imagePackage, + fit: BoxFit.contain, ), - new Expanded( - child: new Row( - children: [ - new Padding( - padding: const EdgeInsets.all(16.0), - child: new Image.asset( - recipe.ingredientsImagePath, - package: recipe.ingredientsImagePackage, - width: 48.0, - height: 48.0, - ), + ), + new Expanded( + child: new Row( + children: [ + new Padding( + padding: const EdgeInsets.all(16.0), + child: new Image.asset( + recipe.ingredientsImagePath, + package: recipe.ingredientsImagePackage, + width: 48.0, + height: 48.0, ), - new Expanded( - child: new Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - new Text(recipe.name, style: titleStyle, softWrap: false, overflow: TextOverflow.ellipsis), - new Text(recipe.author, style: authorStyle), - ], - ), + ), + new Expanded( + child: new Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + new Text(recipe.name, style: titleStyle, softWrap: false, overflow: TextOverflow.ellipsis), + new Text(recipe.author, style: authorStyle), + ], ), - ], - ), + ), + ], ), - ], - ), + ), + ], ), ), ); diff --git a/examples/flutter_gallery/lib/gallery/item.dart b/examples/flutter_gallery/lib/gallery/item.dart index 36edf14e2b0..8d373e8a30e 100644 --- a/examples/flutter_gallery/lib/gallery/item.dart +++ b/examples/flutter_gallery/lib/gallery/item.dart @@ -31,20 +31,18 @@ class GalleryItem extends StatelessWidget { @override Widget build(BuildContext context) { - return new MergeSemantics( - child: new ListTile( - title: new Text(title), - subtitle: new Text(subtitle), - onTap: () { - if (routeName != null) { - Timeline.instantSync('Start Transition', arguments: { - 'from': '/', - 'to': routeName - }); - Navigator.pushNamed(context, routeName); - } + return new ListTile( + title: new Text(title), + subtitle: new Text(subtitle), + onTap: () { + if (routeName != null) { + Timeline.instantSync('Start Transition', arguments: { + 'from': '/', + 'to': routeName + }); + Navigator.pushNamed(context, routeName); } - ), + } ); } } diff --git a/packages/flutter/lib/src/cupertino/slider.dart b/packages/flutter/lib/src/cupertino/slider.dart index e0d385f99b6..228e27128d0 100644 --- a/packages/flutter/lib/src/cupertino/slider.dart +++ b/packages/flutter/lib/src/cupertino/slider.dart @@ -187,7 +187,7 @@ final Duration _kDiscreteTransitionDuration = const Duration(milliseconds: 500); const double _kAdjustmentUnit = 0.1; // Matches iOS implementation of material slider. -class _RenderCupertinoSlider extends RenderConstrainedBox implements SemanticsActionHandler { +class _RenderCupertinoSlider extends RenderConstrainedBox { _RenderCupertinoSlider({ @required double value, int divisions, @@ -253,7 +253,7 @@ class _RenderCupertinoSlider extends RenderConstrainedBox implements SemanticsAc final bool wasInteractive = isInteractive; _onChanged = value; if (wasInteractive != isInteractive) - markNeedsSemanticsUpdate(noGeometry: true); + markNeedsSemanticsUpdate(); } TextDirection get textDirection => _textDirection; @@ -379,31 +379,25 @@ class _RenderCupertinoSlider extends RenderConstrainedBox implements SemanticsAc } @override - bool get isSemanticBoundary => isInteractive; + void describeSemanticsConfiguration(SemanticsConfiguration config) { + super.describeSemanticsConfiguration(config); - @override - SemanticsAnnotator get semanticsAnnotator => _annotate; - - void _annotate(SemanticsNode semantics) { - if (isInteractive) - semantics.addAdjustmentActions(); - } - - @override - void performAction(SemanticsAction action) { - final double unit = divisions != null ? 1.0 / divisions : _kAdjustmentUnit; - switch (action) { - case SemanticsAction.increase: - if (isInteractive) - onChanged((value + unit).clamp(0.0, 1.0)); - break; - case SemanticsAction.decrease: - if (isInteractive) - onChanged((value - unit).clamp(0.0, 1.0)); - break; - default: - assert(false); - break; + config.isSemanticBoundary = isInteractive; + if (isInteractive) { + config.addAction(SemanticsAction.increase, _increaseAction); + config.addAction(SemanticsAction.decrease, _decreaseAction); } } + + double get _semanticActionUnit => divisions != null ? 1.0 / divisions : _kAdjustmentUnit; + + void _increaseAction() { + if (isInteractive) + onChanged((value + _semanticActionUnit).clamp(0.0, 1.0)); + } + + void _decreaseAction() { + if (isInteractive) + onChanged((value - _semanticActionUnit).clamp(0.0, 1.0)); + } } diff --git a/packages/flutter/lib/src/cupertino/switch.dart b/packages/flutter/lib/src/cupertino/switch.dart index b36b2946bed..e05fb11d9e9 100644 --- a/packages/flutter/lib/src/cupertino/switch.dart +++ b/packages/flutter/lib/src/cupertino/switch.dart @@ -156,7 +156,7 @@ const Color _kTrackColor = CupertinoColors.lightBackgroundGray; const Duration _kReactionDuration = const Duration(milliseconds: 300); const Duration _kToggleDuration = const Duration(milliseconds: 200); -class _RenderCupertinoSwitch extends RenderConstrainedBox implements SemanticsActionHandler { +class _RenderCupertinoSwitch extends RenderConstrainedBox { _RenderCupertinoSwitch({ @required bool value, @required Color activeColor, @@ -214,7 +214,7 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox implements SemanticsAc if (value == _value) return; _value = value; - markNeedsSemanticsUpdate(onlyLocalUpdates: true, noGeometry: true); + markNeedsSemanticsUpdate(onlyLocalUpdates: true); _position ..curve = Curves.ease ..reverseCurve = Curves.ease.flipped; @@ -254,7 +254,7 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox implements SemanticsAc _onChanged = value; if (wasInteractive != isInteractive) { markNeedsPaint(); - markNeedsSemanticsUpdate(noGeometry: true); + markNeedsSemanticsUpdate(); } } @@ -375,23 +375,13 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox implements SemanticsAc } @override - bool get isSemanticBoundary => isInteractive; + void describeSemanticsConfiguration(SemanticsConfiguration config) { + super.describeSemanticsConfiguration(config); - @override - SemanticsAnnotator get semanticsAnnotator => _annotate; - - void _annotate(SemanticsNode semantics) { - semantics - ..hasCheckedState = true - ..isChecked = _value; + config.isSemanticBoundary = isInteractive; if (isInteractive) - semantics.addAction(SemanticsAction.tap); - } - - @override - void performAction(SemanticsAction action) { - if (action == SemanticsAction.tap) - _handleTap(); + config.addAction(SemanticsAction.tap, _handleTap); + config.isChecked = _value; } final CupertinoThumbPainter _thumbPainter = new CupertinoThumbPainter(); diff --git a/packages/flutter/lib/src/material/card.dart b/packages/flutter/lib/src/material/card.dart index 0f54fbabb36..6af0ef1d340 100644 --- a/packages/flutter/lib/src/material/card.dart +++ b/packages/flutter/lib/src/material/card.dart @@ -80,14 +80,17 @@ class Card extends StatelessWidget { @override Widget build(BuildContext context) { - return new Container( - margin: const EdgeInsets.all(4.0), - child: new Material( - color: color, - type: MaterialType.card, - elevation: elevation, - child: child - ) + return new Semantics( + container: true, + child: new Container( + margin: const EdgeInsets.all(4.0), + child: new Material( + color: color, + type: MaterialType.card, + elevation: elevation, + child: child + ) + ), ); } } diff --git a/packages/flutter/lib/src/material/slider.dart b/packages/flutter/lib/src/material/slider.dart index fd8eee8c859..58f85358984 100644 --- a/packages/flutter/lib/src/material/slider.dart +++ b/packages/flutter/lib/src/material/slider.dart @@ -307,7 +307,7 @@ double _getPreferredTotalHeight(String label) { return 2 * _kReactionRadius + _getAdditionalHeightForLabel(label); } -class _RenderSlider extends RenderBox implements SemanticsActionHandler { +class _RenderSlider extends RenderBox { _RenderSlider({ @required double value, int divisions, @@ -441,7 +441,7 @@ class _RenderSlider extends RenderBox implements SemanticsActionHandler { _onChanged = value; if (wasInteractive != isInteractive) { markNeedsPaint(); - markNeedsSemanticsUpdate(noGeometry: true); + markNeedsSemanticsUpdate(); } } @@ -708,31 +708,25 @@ class _RenderSlider extends RenderBox implements SemanticsActionHandler { } @override - bool get isSemanticBoundary => isInteractive; + void describeSemanticsConfiguration(SemanticsConfiguration config) { + super.describeSemanticsConfiguration(config); - @override - SemanticsAnnotator get semanticsAnnotator => _annotate; - - void _annotate(SemanticsNode semantics) { - if (isInteractive) - semantics.addAdjustmentActions(); - } - - @override - void performAction(SemanticsAction action) { - final double unit = divisions != null ? 1.0 / divisions : _kAdjustmentUnit; - switch (action) { - case SemanticsAction.increase: - if (isInteractive) - onChanged((value + unit).clamp(0.0, 1.0)); - break; - case SemanticsAction.decrease: - if (isInteractive) - onChanged((value - unit).clamp(0.0, 1.0)); - break; - default: - assert(false); - break; + config.isSemanticBoundary = isInteractive; + if (isInteractive) { + config.addAction(SemanticsAction.increase, _increaseAction); + config.addAction(SemanticsAction.decrease, _decreaseAction); } } + + double get _semanticActionUnit => divisions != null ? 1.0 / divisions : _kAdjustmentUnit; + + void _increaseAction() { + if (isInteractive) + onChanged((value + _semanticActionUnit).clamp(0.0, 1.0)); + } + + void _decreaseAction() { + if (isInteractive) + onChanged((value - _semanticActionUnit).clamp(0.0, 1.0)); + } } diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart index 872d6c2a2bf..c1be60ffa31 100644 --- a/packages/flutter/lib/src/material/tabs.dart +++ b/packages/flutter/lib/src/material/tabs.dart @@ -754,22 +754,20 @@ class _TabBarState extends State { // reflect the intrinsic width of their labels. final int tabCount = widget.tabs.length; for (int index = 0; index < tabCount; index++) { - wrappedTabs[index] = new MergeSemantics( - child: new Stack( - children: [ - new InkWell( - onTap: () { _handleTap(index); }, - child: new Padding( - padding: new EdgeInsets.only(bottom: widget.indicatorWeight), - child: wrappedTabs[index], + wrappedTabs[index] = new InkWell( + onTap: () { _handleTap(index); }, + child: new Padding( + padding: new EdgeInsets.only(bottom: widget.indicatorWeight), + child: new Stack( + children: [ + wrappedTabs[index], + new Semantics( + selected: index == _currentIndex, + // TODO(goderbauer): I10N-ify + label: 'Tab ${index + 1} of $tabCount', ), - ), - new Semantics( - selected: index == _currentIndex, - // TODO(goderbauer): I10N-ify - label: 'Tab ${index + 1} of $tabCount', - ), - ], + ] + ), ), ); if (!widget.isScrollable) diff --git a/packages/flutter/lib/src/material/toggleable.dart b/packages/flutter/lib/src/material/toggleable.dart index a828e5cc1a3..b590b8837df 100644 --- a/packages/flutter/lib/src/material/toggleable.dart +++ b/packages/flutter/lib/src/material/toggleable.dart @@ -18,7 +18,7 @@ final Tween _kRadialReactionRadiusTween = new Tween(begin: 0.0, /// This class handles storing the current value, dispatching ValueChanged on a /// tap gesture and driving a changed animation. Subclasses are responsible for /// painting. -abstract class RenderToggleable extends RenderConstrainedBox implements SemanticsActionHandler { +abstract class RenderToggleable extends RenderConstrainedBox { /// Creates a toggleable render object. /// /// The [value], [activeColor], and [inactiveColor] arguments must not be @@ -122,7 +122,7 @@ abstract class RenderToggleable extends RenderConstrainedBox implements Semantic if (value == _value) return; _value = value; - markNeedsSemanticsUpdate(onlyLocalUpdates: true, noGeometry: true); + markNeedsSemanticsUpdate(onlyLocalUpdates: true); _position ..curve = Curves.easeIn ..reverseCurve = Curves.easeOut; @@ -178,7 +178,7 @@ abstract class RenderToggleable extends RenderConstrainedBox implements Semantic _onChanged = value; if (wasInteractive != isInteractive) { markNeedsPaint(); - markNeedsSemanticsUpdate(noGeometry: true); + markNeedsSemanticsUpdate(); } } @@ -283,23 +283,13 @@ abstract class RenderToggleable extends RenderConstrainedBox implements Semantic } @override - bool get isSemanticBoundary => isInteractive; + void describeSemanticsConfiguration(SemanticsConfiguration config) { + super.describeSemanticsConfiguration(config); - @override - SemanticsAnnotator get semanticsAnnotator => _annotate; - - void _annotate(SemanticsNode semantics) { - semantics - ..hasCheckedState = true - ..isChecked = _value; + config.isSemanticBoundary = isInteractive; if (isInteractive) - semantics.addAction(SemanticsAction.tap); - } - - @override - void performAction(SemanticsAction action) { - if (action == SemanticsAction.tap) - _handleTap(); + config.addAction(SemanticsAction.tap, _handleTap); + config.isChecked = _value; } @override diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index d25815d76e1..2b1433ab02b 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -546,419 +546,6 @@ typedef void RenderObjectVisitor(RenderObject child); /// Used by [RenderObject.invokeLayoutCallback]. typedef void LayoutCallback(T constraints); -class _SemanticsGeometry { - _SemanticsGeometry() : _transform = new Matrix4.identity(); - - _SemanticsGeometry.withClipFrom(_SemanticsGeometry other) { - _clipRect = other?._clipRect; - _transform = new Matrix4.identity(); - } - - _SemanticsGeometry.copy(_SemanticsGeometry other) { - if (other != null) { - _clipRect = other._clipRect; - _transform = new Matrix4.copy(other._transform); - } else { - _transform = new Matrix4.identity(); - } - } - - Rect _clipRect; - - Rect _intersectClipRect(Rect other) { - if (_clipRect == null) - return other; - if (other == null) - return _clipRect; - return _clipRect.intersect(other); - } - - Matrix4 _transform; - - void applyAncestorChain(List ancestorChain) { - for (int index = ancestorChain.length-1; index > 0; index -= 1) { - final RenderObject parent = ancestorChain[index]; - final RenderObject child = ancestorChain[index-1]; - _clipRect = _intersectClipRect(parent.describeApproximatePaintClip(child)); - if (_clipRect != null) { - if (_clipRect.isEmpty) { - _clipRect = Rect.zero; - } else { - final Matrix4 clipTransform = new Matrix4.identity(); - parent.applyPaintTransform(child, clipTransform); - _clipRect = MatrixUtils.inverseTransformRect(clipTransform, _clipRect); - } - } - parent.applyPaintTransform(child, _transform); - } - } - - void updateSemanticsNode({ - @required RenderObject rendering, - @required SemanticsNode semantics, - }) { - assert(rendering != null); - assert(semantics != null); - semantics.transform = _transform; - final Rect semanticBounds = rendering.semanticBounds; - if (_clipRect != null) { - final Rect rect = _clipRect.intersect(semanticBounds); - semantics.rect = rect; - semantics.wasAffectedByClip = rect != semanticBounds; - } else { - semantics.rect = semanticBounds; - semantics.wasAffectedByClip = false; - } - } -} - -/// Describes the shape of the semantic tree. -/// -/// A [_SemanticsFragment] object is a node in a short-lived tree which is used -/// to create the final [SemanticsNode] tree that is sent to the semantics -/// server. These objects have a [SemanticsAnnotator], and a list of -/// [_SemanticsFragment] children. -abstract class _SemanticsFragment { - _SemanticsFragment({ - @required RenderObject renderObjectOwner, - this.annotator, - List<_SemanticsFragment> children, - this.dropSemanticsOfPreviousSiblings, - }) : assert(renderObjectOwner != null), - assert(() { - if (children == null) - return true; - final Set<_SemanticsFragment> seenChildren = new Set<_SemanticsFragment>(); - for (_SemanticsFragment child in children) - assert(seenChildren.add(child)); // check for duplicate adds - return true; - }()), - _ancestorChain = [renderObjectOwner], - _children = children ?? const <_SemanticsFragment>[]; - - final SemanticsAnnotator annotator; - bool dropSemanticsOfPreviousSiblings; - - bool get producesSemanticNodes => true; - - List _ancestorChain; - void addAncestor(RenderObject ancestor) { - _ancestorChain.add(ancestor); - } - - RenderObject get renderObjectOwner => _ancestorChain.first; - - List<_SemanticsFragment> _children; - - bool _debugCompiled = false; - Iterable compile({ _SemanticsGeometry geometry, SemanticsNode currentSemantics }); - - @override - String toString() => describeIdentity(this); -} - -/// A SemanticsFragment that doesn't produce any [SemanticsNode]s when compiled. -class _EmptySemanticsFragment extends _SemanticsFragment { - _EmptySemanticsFragment({ - @required RenderObject renderObjectOwner, - bool dropSemanticsOfPreviousSiblings, - }) : super(renderObjectOwner: renderObjectOwner, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings); - - @override - Iterable compile({ _SemanticsGeometry geometry, SemanticsNode currentSemantics }) sync* { } - - @override - bool get producesSemanticNodes => false; -} - -/// Represents a [RenderObject] which is in no way dirty. -/// -/// This class has no children and no annotators, and when compiled, it returns -/// the [SemanticsNode] that the [RenderObject] already has. (We still update -/// the matrix, since that comes from the (dirty) ancestors.) -class _CleanSemanticsFragment extends _SemanticsFragment { - _CleanSemanticsFragment({ - @required RenderObject renderObjectOwner, - bool dropSemanticsOfPreviousSiblings, - }) : assert(renderObjectOwner != null), - assert(renderObjectOwner._semantics != null), - super( - renderObjectOwner: renderObjectOwner, - dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings - ); - - @override - Iterable compile({ _SemanticsGeometry geometry, SemanticsNode currentSemantics }) sync* { - assert(!_debugCompiled); - assert(() { _debugCompiled = true; return true; }()); - final SemanticsNode node = renderObjectOwner._semantics; - assert(node != null); - if (geometry != null) { - geometry.applyAncestorChain(_ancestorChain); - geometry.updateSemanticsNode(rendering: renderObjectOwner, semantics: node); - if (node.isInvisible) - return; // drop the node - } else { - assert(_ancestorChain.length == 1); - } - yield node; - } -} - -abstract class _InterestingSemanticsFragment extends _SemanticsFragment { - _InterestingSemanticsFragment({ - RenderObject renderObjectOwner, - SemanticsAnnotator annotator, - Iterable<_SemanticsFragment> children, - bool dropSemanticsOfPreviousSiblings, - }) : super( - renderObjectOwner: renderObjectOwner, - annotator: annotator, - children: children, - dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings, - ); - - @override - Iterable compile({ _SemanticsGeometry geometry, SemanticsNode currentSemantics }) sync* { - assert(!_debugCompiled); - assert(() { _debugCompiled = true; return true; }()); - final SemanticsNode node = establishSemanticsNode(geometry, currentSemantics); - if (node.isInvisible) - return; // drop the node - final List children = []; - for (_SemanticsFragment child in _children) { - assert(child._ancestorChain.last == renderObjectOwner); - children.addAll(child.compile( - geometry: createSemanticsGeometryForChild(geometry), - currentSemantics: _children.length > 1 ? null : node, - )); - } - yield* finalizeSemanticsNode(node, children); - } - - SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics); - Iterable finalizeSemanticsNode(SemanticsNode node, List children); - _SemanticsGeometry createSemanticsGeometryForChild(_SemanticsGeometry geometry); -} - -/// Represents the [RenderObject] found at the top of the render tree. -/// -/// This class always compiles to a [SemanticsNode] with ID 0. -class _RootSemanticsFragment extends _InterestingSemanticsFragment { - _RootSemanticsFragment({ - RenderObject renderObjectOwner, - SemanticsAnnotator annotator, - Iterable<_SemanticsFragment> children, - bool dropSemanticsOfPreviousSiblings, - }) : super( - renderObjectOwner: renderObjectOwner, - annotator: annotator, - children: children, - dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings, - ); - - @override - SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics) { - assert(_ancestorChain.length == 1); - assert(geometry == null); - assert(currentSemantics == null); - renderObjectOwner._semantics ??= new SemanticsNode.root( - handler: renderObjectOwner is SemanticsActionHandler ? renderObjectOwner as dynamic : null, - owner: renderObjectOwner.owner.semanticsOwner, - showOnScreen: renderObjectOwner.showOnScreen, - ); - final SemanticsNode node = renderObjectOwner._semantics; - assert(MatrixUtils.matrixEquals(node.transform, new Matrix4.identity())); - assert(!node.wasAffectedByClip); - node.rect = renderObjectOwner.semanticBounds; - return node; - } - - @override - Iterable finalizeSemanticsNode(SemanticsNode node, List children) sync* { - if (annotator != null) - annotator(node); - node.addChildren(children); - node.finalizeChildren(); - yield node; - } - - @override - _SemanticsGeometry createSemanticsGeometryForChild(_SemanticsGeometry geometry) { - return new _SemanticsGeometry(); - } -} - -/// Represents a RenderObject that has [isSemanticBoundary] set to true. -/// -/// It returns the SemanticsNode for that [RenderObject]. -class _ConcreteSemanticsFragment extends _InterestingSemanticsFragment { - _ConcreteSemanticsFragment({ - RenderObject renderObjectOwner, - SemanticsAnnotator annotator, - Iterable<_SemanticsFragment> children, - bool dropSemanticsOfPreviousSiblings, - bool mergeIntoParent, - bool mergesAllDescendants, - }) : _mergeIntoParent = mergeIntoParent, - _mergesAllDescendants = mergesAllDescendants, - super( - renderObjectOwner: renderObjectOwner, - annotator: annotator, - children: children, - dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings, - ); - - final bool _mergeIntoParent; - final bool _mergesAllDescendants; - - @override - SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics) { - renderObjectOwner._semantics ??= new SemanticsNode( - handler: renderObjectOwner is SemanticsActionHandler ? renderObjectOwner as dynamic : null, - showOnScreen: renderObjectOwner.showOnScreen, - ); - final SemanticsNode node = renderObjectOwner._semantics; - node.isMergedIntoParent = _mergeIntoParent; - node.mergeAllDescendantsIntoThisNode = _mergesAllDescendants; - if (geometry != null) { - geometry.applyAncestorChain(_ancestorChain); - geometry.updateSemanticsNode(rendering: renderObjectOwner, semantics: node); - } else { - assert(_ancestorChain.length == 1); - } - return node; - } - - @override - Iterable finalizeSemanticsNode(SemanticsNode node, List children) sync* { - renderObjectOwner.assembleSemanticsNode(node, children); - yield node; - } - - @override - _SemanticsGeometry createSemanticsGeometryForChild(_SemanticsGeometry geometry) { - return new _SemanticsGeometry.withClipFrom(geometry); - } -} - -/// Represents a RenderObject that does not have [isSemanticBoundary] set to -/// true, but which does have some semantic annotators. -/// -/// When it is compiled, if the nearest ancestor [_SemanticsFragment] that isn't -/// also an [_ImplicitSemanticsFragment] is a [_RootSemanticsFragment] or a -/// [_ConcreteSemanticsFragment], then the [SemanticsNode] from that object is -/// reused. Otherwise, a new one is created. -class _ImplicitSemanticsFragment extends _InterestingSemanticsFragment { - _ImplicitSemanticsFragment({ - RenderObject renderObjectOwner, - SemanticsAnnotator annotator, - Iterable<_SemanticsFragment> children, - bool dropSemanticsOfPreviousSiblings, - bool mergeIntoParent, - bool mergesAllDescendants, - }) : _mergeIntoParent = mergeIntoParent, - _mergesAllDescendants = mergesAllDescendants, - super( - renderObjectOwner: renderObjectOwner, - annotator: annotator, - children: children, - dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings, - ); - - // 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; - - final bool _mergeIntoParent; - final bool _mergesAllDescendants; - - @override - SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics) { - SemanticsNode node; - assert(_introducesOwnNode == null); - assert(annotator != null || _mergesAllDescendants); - _introducesOwnNode = currentSemantics == null; - if (_introducesOwnNode) { - renderObjectOwner._semantics ??= new SemanticsNode( - handler: renderObjectOwner is SemanticsActionHandler ? renderObjectOwner as dynamic : null, - showOnScreen: renderObjectOwner.showOnScreen, - ); - node = renderObjectOwner._semantics; - node.isMergedIntoParent = _mergeIntoParent; - } else { - renderObjectOwner._semantics = null; - node = currentSemantics; - } - if (!node.mergeAllDescendantsIntoThisNode) - node.mergeAllDescendantsIntoThisNode = _mergesAllDescendants; - if (geometry != null) { - geometry.applyAncestorChain(_ancestorChain); - if (_introducesOwnNode) - geometry.updateSemanticsNode(rendering: renderObjectOwner, semantics: node); - } else { - assert(_ancestorChain.length == 1); - } - return node; - } - - @override - Iterable finalizeSemanticsNode(SemanticsNode node, List 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 (_introducesOwnNode) - return new _SemanticsGeometry.withClipFrom(geometry); - return new _SemanticsGeometry.copy(geometry); - } -} - -/// Represents a [RenderObject] that introduces no semantics of its own, but -/// which has two or more descendants that do introduce semantics -/// (and which are not ancestors or descendants of each other). -class _ForkingSemanticsFragment extends _SemanticsFragment { - _ForkingSemanticsFragment({ - RenderObject renderObjectOwner, - @required Iterable<_SemanticsFragment> children, - bool dropSemanticsOfPreviousSiblings, - }) : assert(children != null), - assert(children.length > 1), - super( - renderObjectOwner: renderObjectOwner, - children: children, - dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings - ); - - @override - Iterable compile({ - @required _SemanticsGeometry geometry, - SemanticsNode currentSemantics, - }) sync* { - assert(!_debugCompiled); - assert(() { _debugCompiled = true; return true; }()); - assert(geometry != null); - geometry.applyAncestorChain(_ancestorChain); - for (_SemanticsFragment child in _children) { - assert(child._ancestorChain.last == renderObjectOwner); - yield* child.compile( - geometry: new _SemanticsGeometry.copy(geometry), - currentSemantics: null, - ); - } - } -} - /// A reference to the semantics tree. /// /// The framework maintains the semantics tree (used for accessibility and @@ -1629,7 +1216,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im _needsPaint = false; markNeedsPaint(); } - if (_needsSemanticsUpdate && isSemanticBoundary) { + if (_needsSemanticsUpdate && _semanticsConfiguration.isSemanticBoundary) { // Don't enter this block if we've never updated semantics at all; // scheduleInitialSemantics() will handle it _needsSemanticsUpdate = false; @@ -2528,44 +2115,27 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im owner.requestVisualUpdate(); } - /// Whether this [RenderObject] introduces a new box for accessibility purposes. - /// - /// See also: - /// - /// * [semanticsAnnotator], which fills in the [SemanticsNode] implied by - /// setting [isSemanticBoundary] to true. - bool get isSemanticBoundary => false; + @protected + void describeSemanticsConfiguration(SemanticsConfiguration config) { + // Nothing to do by default. + } - /// Whether this [RenderObject] makes other [RenderObject]s previously painted - /// within the same semantic boundary unreachable for accessibility purposes. - /// - /// If true is returned, the [SemanticsNode]s for all siblings and cousins - /// of this node, that are earlier in a depth-first pre-order traversal, are - /// dropped from the semantics tree up until a semantic boundary (as defined - /// by [isSemanticBoundary]) is reached. - /// - /// If [isSemanticBoundary] and [isBlockingSemanticsOfPreviouslyPaintedNodes] - /// is set on the same node, all previously painted siblings and cousins - /// up until the next ancestor that is a semantic boundary are dropped. - /// - /// Paint order as established by [visitChildrenForSemantics] is used to - /// determine if a node is previous to this one. - bool get isBlockingSemanticsOfPreviouslyPaintedNodes => false; + // Use [_semanticsConfiguration] to access. + SemanticsConfiguration _cachedSemanticsConfiguration; - /// Whether the semantic information provided by this [RenderObject] and all - /// of its descendants should be treated as one logical entity. - /// - /// If true is returned, the descendants of this [RenderObject]'s - /// [SemanticsNode] will merge their semantic information into the - /// [SemanticsNode] representing this [RenderObject]. - bool get isMergingSemanticsOfDescendants => false; + SemanticsConfiguration get _semanticsConfiguration { + if (_cachedSemanticsConfiguration == null) { + _cachedSemanticsConfiguration = new SemanticsConfiguration(); + describeSemanticsConfiguration(_cachedSemanticsConfiguration); + } + return _cachedSemanticsConfiguration; + } /// The bounding box, in the local coordinate system, of this /// object, for accessibility purposes. Rect get semanticBounds; bool _needsSemanticsUpdate = true; - bool _needsSemanticsGeometryUpdate = true; SemanticsNode _semantics; /// The semantics of this render object. @@ -2590,27 +2160,14 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im /// Should only be called on objects whose [parent] is not a [RenderObject]. void clearSemantics() { _needsSemanticsUpdate = true; - _needsSemanticsGeometryUpdate = true; _semantics = null; visitChildren((RenderObject child) { child.clearSemantics(); }); } - /// 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. /// - /// The parameters `onlyLocalUpdates` and `noGeometry` tell the framework - /// how much of the semantics have changed. Bigger changes (indicated by - /// setting one or both parameters to false) are more expansive to compute. - /// /// `onlyLocalUpdates` should be set to true to reduce cost if the semantics /// update does not in any way change the shape of the semantics tree (e.g. /// [SemanticsNode]s will neither be added/removed from the tree nor be moved @@ -2626,22 +2183,14 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im /// 2. [semanticsAnnotator] changed from or to returning null and /// [isSemanticBoundary] isn't true. /// - /// `noGeometry` should be set to true to reduce cost if the geometry (e.g. - /// size and position) of the corresponding [SemanticsNode] has not - /// changed. Examples for such semantic updates that don't require a geometry - /// update are changes to flags, labels, or actions. - /// - /// If `onlyLocalUpdates` or `noGeometry` are incorrectly set to true, asserts + /// If `onlyLocalUpdates` is incorrectly set to true, asserts /// might throw or the computed semantics tree might be out-of-date without /// warning. - void markNeedsSemanticsUpdate({ bool onlyLocalUpdates: false, bool noGeometry: false }) { + void markNeedsSemanticsUpdate({ bool onlyLocalUpdates: false }) { assert(!attached || !owner._debugDoingSemantics); - if ((attached && owner._semanticsOwner == null) || (_needsSemanticsUpdate && onlyLocalUpdates && (_needsSemanticsGeometryUpdate || noGeometry))) + _cachedSemanticsConfiguration = null; + if ((attached && owner._semanticsOwner == null)) return; - if (!noGeometry && (_semantics == null || (_semantics.hasChildren && _semantics.wasAffectedByClip))) { - // Since the geometry might have changed, we need to make sure to reapply any clips. - _needsSemanticsGeometryUpdate = true; - } if (onlyLocalUpdates) { // The shape of the tree didn't change, but the details did. // If we have our own SemanticsNode (our _semantics isn't null) @@ -2651,13 +2200,13 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im while (node._semantics == null && node.parent is RenderObject) { if (node._needsSemanticsUpdate) return; + node._cachedSemanticsConfiguration = null; node._needsSemanticsUpdate = true; - node.resetSemantics(); node = node.parent; } if (!node._needsSemanticsUpdate) { - node.resetSemantics(); node._needsSemanticsUpdate = true; + node._cachedSemanticsConfiguration = null; if (owner != null) { owner._nodesNeedingSemantics.add(node); owner.requestVisualUpdate(); @@ -2674,10 +2223,9 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im if (node.parent is! RenderObject) break; node._needsSemanticsUpdate = true; - node.resetSemantics(); + node._cachedSemanticsConfiguration = null; node = node.parent; } while (node._semantics == null); - 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 @@ -2689,6 +2237,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im owner._nodesNeedingSemantics.remove(this); } if (!node._needsSemanticsUpdate) { + node._cachedSemanticsConfiguration = null; node._needsSemanticsUpdate = true; if (owner != null) { owner._nodesNeedingSemantics.add(node); @@ -2699,122 +2248,119 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im } /// Updates the semantic information of the render object. - /// - /// This is essentially a two-pass walk of the render tree. The first pass - /// determines the shape of the output tree (captured in - /// [_SemanticsFragment]s), and the second creates the nodes of this tree and - /// hooks them together. The second walk is a sparse walk; it only walks the - /// nodes that are interesting for the purpose of semantics. void _updateSemantics() { - try { - assert(_needsSemanticsUpdate); - assert(_semantics != null || parent is! RenderObject); - final _SemanticsFragment fragment = _getSemanticsFragment(mergeIntoParent: _semantics?.parent?.isPartOfNodeMerging ?? false); - assert(fragment is _InterestingSemanticsFragment); - final SemanticsNode node = fragment.compile().single; - assert(node != null); - assert(!node.isInvisible); - assert(node == _semantics); - } catch (e, stack) { - _debugReportException('_updateSemantics', e, stack); - } + assert(_semanticsConfiguration.isSemanticBoundary || parent is! RenderObject); + final _SemanticsFragment fragment = _getSemanticsForParent( + parentClippingRect: _semanticsClippingRect, + mergeIntoParent: _semantics?.parent?.isPartOfNodeMerging ?? false, + ); + assert(fragment is _InterestingSemanticsFragment); + final _InterestingSemanticsFragment interestingFragment = fragment; + final SemanticsNode node = interestingFragment.compileChildren().single; + // Fragment only wants to add this node's SemanticsNode to the parent. + assert(interestingFragment.config == null && node == _semantics); } - /// Core function that walks the render tree to obtain the semantics. + /// Clip that needs to be applied to any [SemanticsNode] owned by this + /// [RenderObject]. /// - /// It collects semantic annotators for this RenderObject, then walks its - /// children collecting [_SemanticsFragments] for them, and then returns an - /// appropriate [_SemanticsFragment] object that describes the RenderObject's - /// semantics. - _SemanticsFragment _getSemanticsFragment({ bool mergeIntoParent: false }) { - // early-exit if we're not dirty and have our own semantics - if (!_needsSemanticsUpdate && isSemanticBoundary) { - assert(_semantics != null); - if (mergeIntoParent == _semantics.isMergedIntoParent) { - return new _CleanSemanticsFragment( - renderObjectOwner: this, - dropSemanticsOfPreviousSiblings: isBlockingSemanticsOfPreviouslyPaintedNodes, - ); + /// Can be null if no clip is to be applied. + /// + /// Updated by [_getSemanticsForParent]. + Rect _semanticsClippingRect; + + /// Returns the semantics that this node would like to add to its parent. + _SemanticsFragment _getSemanticsForParent({ + @required Rect parentClippingRect, + @required bool mergeIntoParent, + }) { + assert(mergeIntoParent != null); + + final SemanticsConfiguration config = _semanticsConfiguration; + bool dropSemanticsOfPreviousSiblings = config.isBlockingSemanticsOfPreviouslyPaintedNodes; + + final _SemanticsGeometry geometry = new _SemanticsGeometry( + owner: this, + parentClippingRect: parentClippingRect, + ); + _semanticsClippingRect = geometry.clipRect; + + // Shortcut if this fragment cannot be exposed to user. + if (_semanticsConfiguration.isSemanticBoundary && !mergeIntoParent && geometry.isInvisible) + return new _ContainerSemanticsFragment(dropsSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings); + + final bool producesForkingFragment = !config.hasBeenAnnotated && !config.isSemanticBoundary; + final List<_InterestingSemanticsFragment> fragments = <_InterestingSemanticsFragment>[]; + final Set<_InterestingSemanticsFragment> toBeMarkedExplicit = new Set<_InterestingSemanticsFragment>(); + final bool childrenMergeIntoParent = mergeIntoParent || config.isMergingSemanticsOfDescendants; + + visitChildrenForSemantics((RenderObject renderChild) { + final _SemanticsFragment fragment = renderChild._getSemanticsForParent( + parentClippingRect: _semanticsClippingRect, + mergeIntoParent: childrenMergeIntoParent, + ); + if (fragment.dropsSemanticsOfPreviousSiblings) { + fragments.clear(); + toBeMarkedExplicit.clear(); + if (!config.isSemanticBoundary) + dropSemanticsOfPreviousSiblings = true; } - } - List<_SemanticsFragment> children; - bool dropSemanticsOfPreviousSiblings = isBlockingSemanticsOfPreviouslyPaintedNodes; - final bool childrenMergeIntoParent = mergeIntoParent || isMergingSemanticsOfDescendants; - visitChildrenForSemantics((RenderObject child) { - if (_needsSemanticsGeometryUpdate) { - // If our geometry changed, make sure the child also does a - // full update so that any changes to the clip are fully - // applied. - child._needsSemanticsUpdate = true; - child._needsSemanticsGeometryUpdate = true; - } - final _SemanticsFragment fragment = child._getSemanticsFragment(mergeIntoParent: childrenMergeIntoParent); - assert(fragment != null); - if (fragment.dropSemanticsOfPreviousSiblings) { - children = null; // throw away all left siblings of [child]. - dropSemanticsOfPreviousSiblings = true; - } - if (fragment.producesSemanticNodes) { - fragment.addAncestor(this); - children ??= <_SemanticsFragment>[]; - assert(!children.contains(fragment)); - children.add(fragment); + // Figure out which child fragments are to be made explicit. + for (_InterestingSemanticsFragment fragment in fragment.interestingFragments) { + fragments.add(fragment); + fragment.geometry.addAncestor(this); + fragment.addTags(config.tagsForChildren); + if (config.explicitChildNodes || parent is! RenderObject) { + fragment.markAsExplicit(); + continue; + } + if (!fragment.hasConfigForParent || producesForkingFragment) + continue; + if (!config.isCompatibleWith(fragment.config)) + toBeMarkedExplicit.add(fragment); + for (_InterestingSemanticsFragment siblingFragment in fragments.sublist(0, fragments.length - 1)) { + if (!fragment.config.isCompatibleWith(siblingFragment.config)) { + toBeMarkedExplicit.add(fragment); + toBeMarkedExplicit.add(siblingFragment); + } + } } }); - if (isSemanticBoundary && !isBlockingSemanticsOfPreviouslyPaintedNodes) { - // Don't propagate [dropSemanticsOfPreviousSiblings] up through a semantic boundary. - dropSemanticsOfPreviousSiblings = false; - } + + for (_InterestingSemanticsFragment fragment in toBeMarkedExplicit) + fragment.markAsExplicit(); + _needsSemanticsUpdate = false; - _needsSemanticsGeometryUpdate = false; - final SemanticsAnnotator annotator = semanticsAnnotator; + + _SemanticsFragment result; if (parent is! RenderObject) { + assert(!config.hasBeenAnnotated); assert(!mergeIntoParent); - assert(!isMergingSemanticsOfDescendants); - return new _RootSemanticsFragment( - renderObjectOwner: this, - annotator: annotator, - children: children, - dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings, + result = new _RootSemanticsFragment( + owner: this, + dropsSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings, ); - } - if (isSemanticBoundary) { - return new _ConcreteSemanticsFragment( - renderObjectOwner: this, - annotator: annotator, - children: children, - dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings, + } else if (producesForkingFragment) { + result = new _ContainerSemanticsFragment( + dropsSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings, + ); + } else { + result = new _SwitchableSemanticsFragment( + config: config, + geometry: geometry, mergeIntoParent: mergeIntoParent, - mergesAllDescendants: isMergingSemanticsOfDescendants, + owner: this, + dropsSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings, ); + if (config.isSemanticBoundary) { + final _SwitchableSemanticsFragment fragment = result; + fragment.markAsExplicit(); + } } - if (annotator != null || isMergingSemanticsOfDescendants) { - return new _ImplicitSemanticsFragment( - renderObjectOwner: this, - annotator: annotator, - children: children, - dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings, - mergeIntoParent: mergeIntoParent, - mergesAllDescendants: isMergingSemanticsOfDescendants, - ); - } - _semantics = null; - if (children == null) { - // Introduces no semantics and has no descendants that introduce semantics. - return new _EmptySemanticsFragment( - renderObjectOwner: this, - dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings, - ); - } - if (children.length > 1) { - return new _ForkingSemanticsFragment( - renderObjectOwner: this, - children: children, - dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings, - ); - } - assert(children.length == 1); - return children.single..dropSemanticsOfPreviousSiblings = dropSemanticsOfPreviousSiblings; + + result.addAll(fragments); + + return result; } /// Called when collecting the semantics of this node. @@ -2829,51 +2375,25 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im visitChildren(visitor); } - /// Returns a function that will annotate a [SemanticsNode] with the semantics - /// of this [RenderObject]. - /// - /// To annotate a [SemanticsNode] for this node, return an annotator that adds - /// the annotations. When the behavior of the annotator would change (e.g. the - /// box is now checked rather than unchecked), call [markNeedsSemanticsUpdate] - /// to indicate to the rendering system that the semantics tree needs to be - /// rebuilt. - /// - /// To introduce a new [SemanticsNode], set [isSemanticBoundary] to true for - /// this object. The function returned by this function will be used to - /// annotate the [SemanticsNode] for this object. - /// - /// Semantic annotations are not persisted between subsequent calls to an - /// annotator. The [SemanticsAnnotator] should always set all options - /// (e.g. flags, labels, actions, etc.) to the values it cares about given - /// the current state of the [RenderObject]. - /// - /// If the return value will change from null to non-null (or vice versa), and - /// [isSemanticBoundary] isn't true, then the associated call to - /// [markNeedsSemanticsUpdate] must not have `onlyLocalUpdates` 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. + /// If [isSemanticBoundary] is true, this method is called with the `node` + /// created for this [RenderObject], the `config` to be applied to that node + /// and the `children` [SemanticNode]s that decedents of this RenderObject + /// have generated. + /// + /// By default, the method will annotate `node` with `config` and add the + /// `children` to it. /// /// Subclasses can override this method to add additional [SemanticsNode]s - /// to the tree. If a subclass adds additional nodes in this method, it also - /// needs to override [resetSemantics] to call [SemanticsNode.reset] on those - /// additional [SemanticsNode]s. - void assembleSemanticsNode(SemanticsNode node, Iterable children) { + /// to the tree. + void assembleSemanticsNode( + SemanticsNode node, + SemanticsConfiguration config, + Iterable children, + ) { assert(node == _semantics); - if (semanticsAnnotator != null) - semanticsAnnotator(node); - node.addChildren(children); - node.finalizeChildren(); + node.updateWith(config: config, childrenInInversePaintOrder: children); } // EVENTS @@ -2978,10 +2498,10 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im description.add(new DiagnosticsProperty('semantics node', _semantics, defaultValue: null)); description.add(new FlagProperty( 'isBlockingSemanticsOfPreviouslyPaintedNodes', - value: isBlockingSemanticsOfPreviouslyPaintedNodes, + value: _semanticsConfiguration.isBlockingSemanticsOfPreviouslyPaintedNodes, ifTrue: 'blocks semantics of earlier render objects below the common boundary', )); - description.add(new FlagProperty('isSemanticBoundary', value: isSemanticBoundary, ifTrue: 'semantic boundary')); + description.add(new FlagProperty('isSemanticBoundary', value: _semanticsConfiguration.isSemanticBoundary, ifTrue: 'semantic boundary')); } @override @@ -3422,3 +2942,362 @@ class FlutterErrorDetailsForRendering extends FlutterErrorDetails { /// The RenderObject that was being processed when the exception was caught. final RenderObject renderObject; } + +/// Describes the semantics information a [RenderObject] wants to add to its +/// parent. +/// +/// It has two notable subclasses: +/// * [_InterestingSemanticsFragment] describing actual semantic information to +/// be added to the parent. +/// * [_ContainerSemanticsFragment]: a container class to transport the semantic +/// information of multiple [_InterestingSemanticsFragment] to a parent. +abstract class _SemanticsFragment { + _SemanticsFragment({@required this.dropsSemanticsOfPreviousSiblings }) + : assert (dropsSemanticsOfPreviousSiblings != null); + + /// Incorporate the fragments of children into this fragment. + void addAll(Iterable<_InterestingSemanticsFragment> fragments); + + /// Whether this fragment wants to make the semantics information of + /// previously painted [RenderObject]s unreachable for accessibility purposes. + /// + /// See also: + /// * [SemanticsConfiguration.isBlockingSemanticsOfPreviouslyPaintedNodes] + /// describes what semantics are dropped in more detail. + final bool dropsSemanticsOfPreviousSiblings; + + /// Returns [_InterestingSemanticsFragment] describing the actual semantic + /// information that this fragment wants to add to the parent. + Iterable<_InterestingSemanticsFragment> get interestingFragments; +} + +/// A container used when a [RenderObject] wants to add multiple independent +/// [_InterestingSemanticsFragment] to its parent. +/// +/// The [_InterestingSemanticsFragment] to be added to the parent can be +/// obtained via [interestingFragments]. +class _ContainerSemanticsFragment extends _SemanticsFragment { + + _ContainerSemanticsFragment({ @required bool dropsSemanticsOfPreviousSiblings }) + : super(dropsSemanticsOfPreviousSiblings: dropsSemanticsOfPreviousSiblings); + + @override + void addAll(Iterable<_InterestingSemanticsFragment> fragments) { + interestingFragments.addAll(fragments); + } + + @override + final List<_InterestingSemanticsFragment> interestingFragments = <_InterestingSemanticsFragment>[]; +} + +/// A [_SemanticsFragment] that describes which concrete semantic information +/// a [RenderObject] wants to add to the [SemanticsNode] of its parent. +/// +/// Specifically, it describes what children (as returned by [compileChildren]) +/// should be added to the parent's [SemanticsNode] and what [config] should be +/// merged into the parent's [SemanticsNode]. +abstract class _InterestingSemanticsFragment extends _SemanticsFragment { + _InterestingSemanticsFragment({ + this.geometry, + @required this.owner, + @required bool dropsSemanticsOfPreviousSiblings + }) : assert(owner != null), + super(dropsSemanticsOfPreviousSiblings: dropsSemanticsOfPreviousSiblings); + + /// The [RenderObject] that owns this fragment (and any new [SemanticNode] + /// introduced by it). + final RenderObject owner; + + /// The children to be added to the parent. + Iterable compileChildren(); + + /// The [SemanticsConfiguration] the child wants to merge into the parent's + /// [SemanticsNode] or null if it doesn't want to merge anything. + SemanticsConfiguration get config; + + /// Disallows this fragment to merge any configuration into its parent's + /// [SemanticsNode]. + /// + /// After calling this the fragment will only produce children to be added + /// to the parent and it will return null for [config]. + void markAsExplicit(); + + /// Consume the fragments of children. + /// + /// For each provided fragment it will add that fragment's children to + /// this fragment's children (as returned by [compileChildren]) and merge that + /// fragment's [config] into this fragment's [config]. + /// + /// If a provided fragment should not merge anything into [config] call + /// [markAsExplicit] before passing the fragment to this method. + @override + void addAll(Iterable<_InterestingSemanticsFragment> fragments); + + final _SemanticsGeometry geometry; + + /// Whether this fragment wants to add any semantic information to the parent + /// [SemanticsNode]. + bool get hasConfigForParent => config != null; + + @override + Iterable<_InterestingSemanticsFragment> get interestingFragments sync* { + yield this; + } + + Set _tagsForChildren; + + /// Tag all children produced by [compileChildren] with `tags`. + void addTags(Iterable tags) { + if (tags == null || tags.isEmpty) + return; + _tagsForChildren ??= new Set(); + _tagsForChildren.addAll(tags); + } +} + +/// A [_InterestingSemanticsFragment] that produces the root [SemanticsNode] of +/// the semantics tree. +/// +/// The root node is available as only element in the Iterable returned by +/// [children]. +class _RootSemanticsFragment extends _InterestingSemanticsFragment { + _RootSemanticsFragment({ + @required RenderObject owner, + @required bool dropsSemanticsOfPreviousSiblings, + }) : super(owner: owner, dropsSemanticsOfPreviousSiblings: dropsSemanticsOfPreviousSiblings); + + @override + Iterable compileChildren() sync* { + assert(_tagsForChildren == null || _tagsForChildren.isEmpty); + owner._semantics ??= new SemanticsNode.root( + showOnScreen: owner.showOnScreen, + owner: owner.owner.semanticsOwner, + ); + final SemanticsNode node = owner._semantics; + assert(MatrixUtils.matrixEquals(node.transform, new Matrix4.identity())); + assert(!node.wasAffectedByClip); + node.rect = owner.semanticBounds; + assert(!node.isInvisible); + yield node; + } + + @override + SemanticsConfiguration get config => null; + + @override + void markAsExplicit() { + // nothing to do, we are always explicit. + } + + @override + void addAll(Iterable<_InterestingSemanticsFragment> fragments) { + final SemanticsNode root = compileChildren().first; + final List children = []; + for (_InterestingSemanticsFragment fragment in fragments) { + assert(fragment.config == null); + children.addAll(fragment.compileChildren()); + } + root.updateWith(config: null, childrenInInversePaintOrder: children); + } +} + +/// A [_InterstingSemanticsFragment] that can be told to only add explicit +/// [SemanticsNode]s to the parent. +/// +/// If [markAsExplicit] was not called before this fragment is added to +/// another fragment it will merge [config] into the parent's [SemanticsNode] +/// and add its [children] to it. +/// +/// If [markAsExplicit] was called before adding this fragment to another +/// fragment it will create a new [SemanticsNode]. The newly created node will +/// be annotated with the [SemanticsConfiguration] that - without the call to +/// [markAsExplicit] - would have been merged into the parent's [SemanticsNode]. +/// Similarity, the new node will also take over the children that otherwise +/// would have been added to the parent's [SemanticsNode]. +/// +/// After a call to [markAsExplicit] the only element returned by [children] +/// is the newly created node and [config] will return null as the fragment +/// no longer wants to merge any semantic information into the parent's +/// [SemanticsNode]. +class _SwitchableSemanticsFragment extends _InterestingSemanticsFragment { + _SwitchableSemanticsFragment({ + @required bool mergeIntoParent, + @required SemanticsConfiguration config, + @required _SemanticsGeometry geometry, + @required RenderObject owner, + @required bool dropsSemanticsOfPreviousSiblings, + }) : _mergeIntoParent = mergeIntoParent, + _config = config, + assert(mergeIntoParent != null), + assert(config != null), + super(geometry: geometry, owner: owner, dropsSemanticsOfPreviousSiblings: dropsSemanticsOfPreviousSiblings); + + final bool _mergeIntoParent; + SemanticsConfiguration _config; + bool _isConfigWritable = false; + final List _children = []; + + @override + Iterable compileChildren() sync* { + if (!_isExplicit) { + owner._semantics = null; + yield* _children; + return; + } + if (!_mergeIntoParent && geometry.rect.isEmpty) + return; // Drop the node, it's not going to be visible. + owner._semantics ??= new SemanticsNode(showOnScreen: owner.showOnScreen); + final SemanticsNode node = owner._semantics + ..isMergedIntoParent = _mergeIntoParent + ..tags = _tagsForChildren; + + geometry.updateNode(node); + + if (_config.isSemanticBoundary) { + owner.assembleSemanticsNode(node, _config, _children); + } else { + node.updateWith(config: _config, childrenInInversePaintOrder: _children); + } + + yield node; + } + + @override + SemanticsConfiguration get config { + return _isExplicit ? null : _config; + } + + @override + void addAll(Iterable<_InterestingSemanticsFragment> fragments) { + for (_InterestingSemanticsFragment fragment in fragments) { + _children.addAll(fragment.compileChildren()); + if (fragment.config == null) + continue; + if (!_isConfigWritable) { + _config = _config.copy(); + _isConfigWritable = true; + } + _config.absorb(fragment.config); + } + } + + bool _isExplicit = false; + + @override + void markAsExplicit() { + _isExplicit = true; + } +} + +/// Helper class that keeps track of the geometry of a [SemanticsNode]. +/// +/// It is used to annotate a [SemanticsNode] with the current information for +/// [SemanticsNode.rect] and [SemanticsNode.transform]. +class _SemanticsGeometry { + + /// `parentClippingRect` may be null if no clip is to be applied. + _SemanticsGeometry({ + @required RenderObject owner, + @required Rect parentClippingRect + }) : assert(owner != null), + _ancestorChain = [owner], + _parentClippingRect = parentClippingRect; + + final Rect _parentClippingRect; + final List _ancestorChain; + + RenderObject get _owner => _ancestorChain.first; + + /// The current clip [Rect] that would be applied to the [SemanticsNode] + /// owned by [owner]. + Rect get clipRect { + if (!_isClipRectValid) { + __clipRect = _computeClipRect(); + _isClipRectValid = true; + } + return __clipRect; + } + Rect __clipRect; + bool _isClipRectValid = false; + + Rect _computeClipRect() { + if (_owner.parent is! RenderObject) + return null; + final RenderObject parent = _owner.parent; + + // Clip rect in parent's coordinate system. + Rect clip = parent.describeApproximatePaintClip(_owner); + if (clip == null) { + if (_parentClippingRect == null) + return null; + clip = _parentClippingRect; + } else if (_parentClippingRect != null) { + clip = _parentClippingRect.intersect(clip); + } + + assert(clip != null); + if (clip.isEmpty) + return Rect.zero; + + // Translate clip into owner's coordinate system. + final Matrix4 clipTransform = new Matrix4.identity(); + parent.applyPaintTransform(_owner, clipTransform); + return MatrixUtils.inverseTransformRect(clipTransform, clip); + } + + /// The value for [SemanticsNode.rect]. + /// + /// This is essentially [RenderObject.semanticsBound] with [clipRect] applied. + Rect get rect { + if (__rect == null) + __rect = _computeRect(); + return __rect; + } + Rect __rect; + + Rect _computeRect() { + if (clipRect == null) + return _owner.semanticBounds; + return clipRect.intersect(_owner.semanticBounds); + } + + Matrix4 _computeTransformation() { + assert(_ancestorChain.length > 1); + final Matrix4 transform = new Matrix4.identity(); + for (int index = _ancestorChain.length-1; index > 0; index -= 1) { + final RenderObject parent = _ancestorChain[index]; + final RenderObject child = _ancestorChain[index - 1]; + parent.applyPaintTransform(child, transform); + + } + return transform; + } + + /// Annotates `node` with the latest geometry information. + /// + /// It sets [SemanticsNode.rect], [SemanticsNode.transform], and + /// [SemanticsNode.wasAffectedByClip] to the current values. + void updateNode(SemanticsNode node) { + if (_ancestorChain.length > 1) + node.transform = _computeTransformation(); + node.rect = rect; + node.wasAffectedByClip = rect != _owner.semanticBounds; + } + + /// Adds the geometric information of `ancestor` to this object. + /// + /// Those information are required to properly compute the value for + /// [SemanticsNode.transform]. + /// + /// Ancestors have to be added in order from [owner] up until the next + /// [RenderObject] that owns a [SemanticsNode] is reached. + void addAncestor(RenderObject ancestor) { + _ancestorChain.add(ancestor); + } + + /// Whether a [SemanticsNode] annotated with the geometric information tracked + /// by this object would be visible on screen. + bool get isInvisible { + return clipRect != null && clipRect.isEmpty || rect.isEmpty; + } +} diff --git a/packages/flutter/lib/src/rendering/paragraph.dart b/packages/flutter/lib/src/rendering/paragraph.dart index 85cd1c3c396..a0d70977a29 100644 --- a/packages/flutter/lib/src/rendering/paragraph.dart +++ b/packages/flutter/lib/src/rendering/paragraph.dart @@ -404,11 +404,11 @@ class RenderParagraph extends RenderBox { } @override - SemanticsAnnotator get semanticsAnnotator => _annotate; - - void _annotate(SemanticsNode node) { - node.label = text.toPlainText(); - node.textDirection = textDirection; + void describeSemanticsConfiguration(SemanticsConfiguration config) { + super.describeSemanticsConfiguration(config); + config + ..label = text.toPlainText() + ..textDirection = textDirection; } @override diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index e7836b822e1..8e65727a495 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -2860,7 +2860,7 @@ class RenderMetaData extends RenderProxyBoxWithHitTestBehavior { /// Listens for the specified gestures from the semantics server (e.g. /// an accessibility tool). -class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsActionHandler { +class RenderSemanticsGestureHandler extends RenderProxyBox { /// Creates a render object that listens for specific semantic gestures. /// /// The [scrollFactor] argument must not be null. @@ -2937,11 +2937,11 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA set onTap(GestureTapCallback value) { if (_onTap == value) return; - final bool wasSemanticBoundary = isSemanticBoundary; + final bool hadHandlers = _hasHandlers; final bool hadHandler = _onTap != null; _onTap = value; if ((value != null) != hadHandler) - markNeedsSemanticsUpdate(onlyLocalUpdates: isSemanticBoundary == wasSemanticBoundary); + markNeedsSemanticsUpdate(onlyLocalUpdates: _hasHandlers == hadHandlers); } /// Called when the user presses on the render object for a long period of time. @@ -2950,11 +2950,11 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA set onLongPress(GestureLongPressCallback value) { if (_onLongPress == value) return; - final bool wasSemanticBoundary = isSemanticBoundary; + final bool hadHandlers = _hasHandlers; final bool hadHandler = _onLongPress != null; _onLongPress = value; if ((value != null) != hadHandler) - markNeedsSemanticsUpdate(onlyLocalUpdates: isSemanticBoundary == wasSemanticBoundary); + markNeedsSemanticsUpdate(onlyLocalUpdates: _hasHandlers == hadHandlers); } /// Called when the user scrolls to the left or to the right. @@ -2963,11 +2963,11 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA set onHorizontalDragUpdate(GestureDragUpdateCallback value) { if (_onHorizontalDragUpdate == value) return; - final bool wasSemanticBoundary = isSemanticBoundary; + final bool hadHandlers = _hasHandlers; final bool hadHandler = _onHorizontalDragUpdate != null; _onHorizontalDragUpdate = value; if ((value != null) != hadHandler) - markNeedsSemanticsUpdate(onlyLocalUpdates: isSemanticBoundary == wasSemanticBoundary); + markNeedsSemanticsUpdate(onlyLocalUpdates: _hasHandlers == hadHandlers); } /// Called when the user scrolls up or down. @@ -2976,11 +2976,11 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA set onVerticalDragUpdate(GestureDragUpdateCallback value) { if (_onVerticalDragUpdate == value) return; - final bool wasSemanticBoundary = isSemanticBoundary; + final bool hadHandlers = _hasHandlers; final bool hadHandler = _onVerticalDragUpdate != null; _onVerticalDragUpdate = value; if ((value != null) != hadHandler) - markNeedsSemanticsUpdate(onlyLocalUpdates: isSemanticBoundary == wasSemanticBoundary); + markNeedsSemanticsUpdate(onlyLocalUpdates: _hasHandlers == hadHandlers); } /// The fraction of the dimension of this render box to use when @@ -2990,8 +2990,7 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA /// leftwards drag. double scrollFactor; - @override - bool get isSemanticBoundary { + bool get _hasHandlers { return onTap != null || onLongPress != null || onHorizontalDragUpdate != null @@ -2999,7 +2998,38 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA } @override - SemanticsAnnotator get semanticsAnnotator => isSemanticBoundary ? _annotate : null; + void describeSemanticsConfiguration(SemanticsConfiguration config) { + super.describeSemanticsConfiguration(config); + + config.isSemanticBoundary = _hasHandlers; + + // TODO(goderbauer): this needs to be set even when there is only potential + // for this to become a scroll view. + config.explicitChildNodes = onHorizontalDragUpdate != null + || onVerticalDragUpdate != null; + + final Map actions = {}; + if (onTap != null) + actions[SemanticsAction.tap] = onTap; + if (onLongPress != null) + actions[SemanticsAction.longPress] = onLongPress; + if (onHorizontalDragUpdate != null) { + actions[SemanticsAction.scrollRight] = _performSemanticScrollRight; + actions[SemanticsAction.scrollLeft] = _performSemanticScrollLeft; + } + if (onVerticalDragUpdate != null) { + actions[SemanticsAction.scrollUp] = _performSemanticScrollUp; + actions[SemanticsAction.scrollDown] = _performSemanticScrollDown; + } + + final Iterable actionsToAdd = validActions ?? actions.keys; + + for (SemanticsAction action in actionsToAdd) { + final VoidCallback handler = actions[action]; + if (handler != null) + config.addAction(action, handler); + } + } SemanticsNode _innerNode; SemanticsNode _annotatedNode; @@ -3011,114 +3041,70 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA } @override - void assembleSemanticsNode(SemanticsNode node, Iterable children) { - if (!node.hasTag(useTwoPaneSemantics)) { - super.assembleSemanticsNode(node, children); + void assembleSemanticsNode(SemanticsNode node, SemanticsConfiguration config, Iterable children) { + if (children.isEmpty || !children.first.isTagged(useTwoPaneSemantics)) { + _annotatedNode = node; + super.assembleSemanticsNode(node, config, children); return; } - _innerNode ??= new SemanticsNode(handler: this, showOnScreen: showOnScreen); + _innerNode ??= new SemanticsNode(showOnScreen: showOnScreen); _innerNode ..wasAffectedByClip = node.wasAffectedByClip ..isMergedIntoParent = node.isPartOfNodeMerging ..rect = Offset.zero & node.rect.size; - - semanticsAnnotator(_innerNode); + _annotatedNode = _innerNode; final List excluded = [_innerNode]; final List included = []; for (SemanticsNode child in children) { - if (child.hasTag(excludeFromScrolling)) + assert(child.isTagged(useTwoPaneSemantics)); + if (child.isTagged(excludeFromScrolling)) excluded.add(child); else included.add(child); } - node.addChildren(excluded); - _innerNode.addChildren(included); - _innerNode.finalizeChildren(); - node.finalizeChildren(); + node.updateWith(config: null, childrenInInversePaintOrder: excluded); + _innerNode.updateWith(config: config, childrenInInversePaintOrder: included); } - @override - void resetSemantics() { - _innerNode?.reset(); - super.resetSemantics(); - } - - void _annotate(SemanticsNode node) { - _annotatedNode = node; - List actions = []; - if (onTap != null) - actions.add(SemanticsAction.tap); - if (onLongPress != null) - actions.add(SemanticsAction.longPress); + void _performSemanticScrollLeft() { if (onHorizontalDragUpdate != null) { - actions.add(SemanticsAction.scrollRight); - actions.add(SemanticsAction.scrollLeft); + final double primaryDelta = size.width * -scrollFactor; + onHorizontalDragUpdate(new DragUpdateDetails( + delta: new Offset(primaryDelta, 0.0), primaryDelta: primaryDelta, + globalPosition: localToGlobal(size.center(Offset.zero)), + )); } - if (onVerticalDragUpdate != null) { - actions.add(SemanticsAction.scrollUp); - actions.add(SemanticsAction.scrollDown); - } - - // If a set of validActions has been provided only expose those. - if (validActions != null) - actions = actions.where((SemanticsAction action) => validActions.contains(action)).toList(); - - actions.forEach(node.addAction); } - @override - void performAction(SemanticsAction action) { - switch (action) { - case SemanticsAction.tap: - if (onTap != null) - onTap(); - break; - case SemanticsAction.longPress: - if (onLongPress != null) - onLongPress(); - break; - case SemanticsAction.scrollLeft: - if (onHorizontalDragUpdate != null) { - final double primaryDelta = size.width * -scrollFactor; - onHorizontalDragUpdate(new DragUpdateDetails( - delta: new Offset(primaryDelta, 0.0), primaryDelta: primaryDelta, - globalPosition: localToGlobal(size.center(Offset.zero)), - )); - } - break; - case SemanticsAction.scrollRight: - if (onHorizontalDragUpdate != null) { - final double primaryDelta = size.width * scrollFactor; - onHorizontalDragUpdate(new DragUpdateDetails( - delta: new Offset(primaryDelta, 0.0), primaryDelta: primaryDelta, - globalPosition: localToGlobal(size.center(Offset.zero)), - )); - } - break; - case SemanticsAction.scrollUp: - if (onVerticalDragUpdate != null) { - final double primaryDelta = size.height * -scrollFactor; - onVerticalDragUpdate(new DragUpdateDetails( - delta: new Offset(0.0, primaryDelta), primaryDelta: primaryDelta, - globalPosition: localToGlobal(size.center(Offset.zero)), - )); - } - break; - case SemanticsAction.scrollDown: - if (onVerticalDragUpdate != null) { - final double primaryDelta = size.height * scrollFactor; - onVerticalDragUpdate(new DragUpdateDetails( - delta: new Offset(0.0, primaryDelta), primaryDelta: primaryDelta, - globalPosition: localToGlobal(size.center(Offset.zero)), - )); - } - break; - case SemanticsAction.increase: - case SemanticsAction.decrease: - assert(false); - break; + void _performSemanticScrollRight() { + if (onHorizontalDragUpdate != null) { + final double primaryDelta = size.width * scrollFactor; + onHorizontalDragUpdate(new DragUpdateDetails( + delta: new Offset(primaryDelta, 0.0), primaryDelta: primaryDelta, + globalPosition: localToGlobal(size.center(Offset.zero)), + )); + } + } + + void _performSemanticScrollUp() { + if (onVerticalDragUpdate != null) { + final double primaryDelta = size.height * -scrollFactor; + onVerticalDragUpdate(new DragUpdateDetails( + delta: new Offset(0.0, primaryDelta), primaryDelta: primaryDelta, + globalPosition: localToGlobal(size.center(Offset.zero)), + )); + } + } + + void _performSemanticScrollDown() { + if (onVerticalDragUpdate != null) { + final double primaryDelta = size.height * scrollFactor; + onVerticalDragUpdate(new DragUpdateDetails( + delta: new Offset(0.0, primaryDelta), primaryDelta: primaryDelta, + globalPosition: localToGlobal(size.center(Offset.zero)), + )); } } @@ -3150,28 +3136,27 @@ class RenderSemanticsAnnotations extends RenderProxyBox { RenderSemanticsAnnotations({ RenderBox child, bool container: false, + bool explicitChildNodes, bool checked, bool selected, String label, TextDirection textDirection, }) : assert(container != null), _container = container, + _explicitChildNodes = explicitChildNodes, _checked = checked, _selected = selected, _label = label, _textDirection = textDirection, super(child); - /// If 'container' is true, this RenderObject will introduce a new + /// If 'container' is true, this [RenderObject] will introduce a new /// node in the semantics tree. Otherwise, the semantics will be /// merged with the semantics of any ancestors. /// - /// The 'container' flag is implicitly set to true on the immediate - /// semantics-providing descendants of a node where multiple - /// children have semantics or have descendants providing semantics. - /// In other words, the semantics of siblings are not merged. To - /// merge the semantics of an entire subtree, including siblings, - /// you can use a [RenderMergeSemantics]. + /// Whether descendants of this [RenderObject] can add their semantic information + /// to the [SemanticsNode] introduced by this configuration is controlled by + /// [explicitChildNodes]. bool get container => _container; bool _container; set container(bool value) { @@ -3182,6 +3167,28 @@ class RenderSemanticsAnnotations extends RenderProxyBox { markNeedsSemanticsUpdate(); } + /// Whether descendants of this [RenderObject] are allowed to add semantic + /// information to the [SemanticsNode] annotated by this widget. + /// + /// When set to false descendants are allowed to annotate [SemanticNode]s of + /// their parent with the semantic information they want to contribute to the + /// semantic tree. + /// When set to true the only way for descendants to contribute semantic + /// information to the semantic tree is to introduce new explicit + /// [SemanticNode]s to the tree. + /// + /// This setting is often used in combination with [isSemanticBoundary] to + /// create semantic boundaries that are either writable or not for children. + bool get explicitChildNodes => _explicitChildNodes; + bool _explicitChildNodes; + set explicitChildNodes(bool value) { + assert(value != null); + if (_explicitChildNodes == value) + return; + _explicitChildNodes = value; + markNeedsSemanticsUpdate(); + } + /// If non-null, sets the [SemanticsNode.hasCheckedState] semantic to true and /// the [SemanticsNode.isChecked] semantic to the given value. bool get checked => _checked; @@ -3231,23 +3238,18 @@ class RenderSemanticsAnnotations extends RenderProxyBox { } @override - bool get isSemanticBoundary => container; + void describeSemanticsConfiguration(SemanticsConfiguration config) { + config.isSemanticBoundary = container; + config.explicitChildNodes = explicitChildNodes; - @override - SemanticsAnnotator get semanticsAnnotator => checked != null || selected != null || label != null || textDirection != null ? _annotate : null; - - void _annotate(SemanticsNode node) { - if (checked != null) { - node - ..hasCheckedState = true - ..isChecked = checked; - } + if (checked != null) + config.isChecked = checked; if (selected != null) - node.isSelected = selected; + config.isSelected = selected; if (label != null) - node.label = label; + config.label = label; if (textDirection != null) - node.textDirection = textDirection; + config.textDirection = textDirection; } } @@ -3262,7 +3264,10 @@ class RenderBlockSemantics extends RenderProxyBox { RenderBlockSemantics({ RenderBox child }) : super(child); @override - bool get isBlockingSemanticsOfPreviouslyPaintedNodes => true; + void describeSemanticsConfiguration(SemanticsConfiguration config) { + super.describeSemanticsConfiguration(config); + config.isBlockingSemanticsOfPreviouslyPaintedNodes = true; + } } /// Causes the semantics of all descendants to be merged into this @@ -3277,8 +3282,12 @@ class RenderMergeSemantics extends RenderProxyBox { RenderMergeSemantics({ RenderBox child }) : super(child); @override - bool get isMergingSemanticsOfDescendants => true; - + void describeSemanticsConfiguration(SemanticsConfiguration config) { + super.describeSemanticsConfiguration(config); + config + ..isSemanticBoundary = true + ..isMergingSemanticsOfDescendants = true; + } } /// Excludes this subtree from the semantic tree. diff --git a/packages/flutter/lib/src/rendering/semantics.dart b/packages/flutter/lib/src/rendering/semantics.dart index e1208963f17..ca90d318971 100644 --- a/packages/flutter/lib/src/rendering/semantics.dart +++ b/packages/flutter/lib/src/rendering/semantics.dart @@ -18,31 +18,6 @@ import 'semantics_event.dart'; export 'dart:ui' show SemanticsAction; export 'semantics_event.dart'; -/// Interface for [RenderObject]s to implement when they want to support -/// being tapped, etc. -/// -/// The handler will only be called for a particular flag if that flag is set -/// (e.g. [performAction] will only be called with [SemanticsAction.tap] if -/// [SemanticsNode.addAction] was called for [SemanticsAction.tap].) -abstract class SemanticsActionHandler { // ignore: one_member_abstracts - /// Called when the object implementing this interface receives a - /// [SemanticsAction]. For example, if the user of an accessibility tool - /// instructs their device that they wish to tap a button, the [RenderObject] - /// behind that button would have its [performAction] method called with the - /// [SemanticsAction.tap] action. - void performAction(SemanticsAction action); -} - -/// Signature for functions returned by [RenderObject.semanticsAnnotator]. -/// -/// These callbacks are called with the [SemanticsNode] object that -/// corresponds to the [RenderObject]. (One [SemanticsNode] can -/// correspond to multiple [RenderObject] objects.) -/// -/// See [RenderObject.semanticsAnnotator] for details on the -/// contract that semantic annotators must follow. -typedef void SemanticsAnnotator(SemanticsNode semantics); - /// Signature for a function that is called for each [SemanticsNode]. /// /// Return false to stop visiting nodes. @@ -103,14 +78,13 @@ class SemanticsData extends Diagnosticable { @required this.label, @required this.textDirection, @required this.rect, - @required this.tags, + this.tags, this.transform, }) : assert(flags != null), assert(actions != null), assert(label != null), assert(label == '' || textDirection != null, 'A SemanticsData object with label "$label" had a null textDirection.'), - assert(rect != null), - assert(tags != null); + assert(rect != null); /// A bit field of [SemanticsFlags] that apply to this node. final int flags; @@ -223,22 +197,18 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { /// Each semantic node has a unique identifier that is assigned when the node /// is created. SemanticsNode({ - SemanticsActionHandler handler, VoidCallback showOnScreen, }) : id = _generateNewId(), - _showOnScreen = showOnScreen, - _actionHandler = handler; + _showOnScreen = showOnScreen; /// Creates a semantic node to represent the root of the semantics tree. /// /// The root node is assigned an identifier of zero. SemanticsNode.root({ - SemanticsActionHandler handler, VoidCallback showOnScreen, SemanticsOwner owner, }) : id = 0, - _showOnScreen = showOnScreen, - _actionHandler = handler { + _showOnScreen = showOnScreen { attach(owner); } @@ -254,11 +224,9 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { /// they are created. final int id; - final SemanticsActionHandler _actionHandler; final VoidCallback _showOnScreen; // GEOMETRY - // These are automatically handled by RenderObject's own logic /// The transform from this node's coordinate system to its parent's coordinate system. /// @@ -285,11 +253,11 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { } } - /// Whether [rect] was clipped by ancestors. + /// Whether [rect] was affected by a clip from an ancestors. /// - /// This is only true if the [rect] of this [SemanticsNode] has been altered - /// due to clipping by an ancestor. If ancestors have been clipped, but the - /// [rect] of this node was unaffected it will be false. + /// If this is true it means that an ancestor imposed a clip on this + /// [SemanticsNode]. However, it does not mean that the clip had any effect + /// on the [rect] whatsoever. bool wasAffectedByClip = false; /// Whether the node is invisible. @@ -304,57 +272,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { /// currently shown on screen. bool get isInvisible => !isMergedIntoParent && rect.isEmpty; - // FLAGS AND LABELS - // These are supposed to be set by SemanticsAnnotator obtained from getSemanticsAnnotators - - int _actions = 0; - - /// Adds the given action to the set of semantic actions. - /// - /// If the user chooses to perform an action, - /// [SemanticsActionHandler.performAction] will be called with the chosen - /// action. - void addAction(SemanticsAction action) { - assert(action != null); - final int index = action.index; - if ((_actions & index) == 0) { - _actions |= index; - _markDirty(); - } - } - - /// Adds the [SemanticsAction.scrollLeft] and [SemanticsAction.scrollRight] actions. - void addHorizontalScrollingActions() { - addAction(SemanticsAction.scrollLeft); - addAction(SemanticsAction.scrollRight); - } - - /// Adds the [SemanticsAction.scrollUp] and [SemanticsAction.scrollDown] actions. - void addVerticalScrollingActions() { - addAction(SemanticsAction.scrollUp); - addAction(SemanticsAction.scrollDown); - } - - /// Adds the [SemanticsAction.increase] and [SemanticsAction.decrease] actions. - void addAdjustmentActions() { - addAction(SemanticsAction.increase); - addAction(SemanticsAction.decrease); - } - - bool _canPerformAction(SemanticsAction action) { - return _actionHandler != null && (_actions & action.index) != 0; - } - - /// Whether this node and all of its descendants should be treated as one logical entity. - bool get mergeAllDescendantsIntoThisNode => _mergeAllDescendantsIntoThisNode; - bool _mergeAllDescendantsIntoThisNode = false; - set mergeAllDescendantsIntoThisNode(bool value) { - assert(value != null); - if (_mergeAllDescendantsIntoThisNode == value) - return; - _mergeAllDescendantsIntoThisNode = value; - _markDirty(); - } + // MERGING /// Whether this node merges its semantic information into an ancestor node. bool get isMergedIntoParent => _isMergedIntoParent; @@ -377,129 +295,91 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { /// * [mergeAllDescendantsIntoThisNode] bool get isPartOfNodeMerging => mergeAllDescendantsIntoThisNode || isMergedIntoParent; - int _flags = 0; - void _setFlag(SemanticsFlags flag, bool value) { - final int index = flag.index; - if (value) { - if ((_flags & index) == 0) { - _flags |= index; - _markDirty(); - } - } else { - if ((_flags & index) != 0) { - _flags &= ~index; - _markDirty(); - } - } - } + /// Whether this node and all of its descendants should be treated as one logical entity. + bool get mergeAllDescendantsIntoThisNode => _mergeAllDescendantsIntoThisNode; + bool _mergeAllDescendantsIntoThisNode = _kEmptyConfig.isMergingSemanticsOfDescendants; - /// Whether this node has Boolean state that can be controlled by the user. - bool get hasCheckedState => (_flags & SemanticsFlags.hasCheckedState.index) != 0; - set hasCheckedState(bool value) => _setFlag(SemanticsFlags.hasCheckedState, value); - /// If this node has Boolean state that can be controlled by the user, whether - /// that state is on or off, corresponding to true and false, respectively. - bool get isChecked => (_flags & SemanticsFlags.isChecked.index) != 0; - set isChecked(bool value) => _setFlag(SemanticsFlags.isChecked, value); + // CHILDREN - /// Whether the current node is selected (true) or not (false). - bool get isSelected => (_flags & SemanticsFlags.isSelected.index) != 0; - set isSelected(bool value) => _setFlag(SemanticsFlags.isSelected, value); + /// Contains the children in inverse hit test order (i.e. paint order). + List _children; - /// A textual description of this node. - /// - /// The text's reading direction is given by [textDirection]. - String get label => _label; - String _label = ''; - set label(String value) { - assert(value != null); - if (_label != value) { - _label = value; - _markDirty(); - } - } - - /// The reading direction for the text in [label]. - TextDirection get textDirection => _textDirection; - TextDirection _textDirection; - set textDirection(TextDirection value) { - assert(value != null); - if (_textDirection != value) { - _textDirection = value; - _markDirty(); - } - } - - final Set _tags = new Set(); - - /// Tags the [SemanticsNode] 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 addTag(SemanticsTag tag) { - assert(tag != null); - _tags.add(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() { - _actions = 0; - _flags = 0; - _label = ''; - _textDirection = null; - _mergeAllDescendantsIntoThisNode = false; - _tags.clear(); - _markDirty(); - } - - List _newChildren; - - /// Append the given children as children of this node. - /// - /// Children must be added in inverse hit test order (i.e. paint order). - /// - /// The [finalizeChildren] method must be called after all children have been - /// added. - void addChildren(Iterable childrenInInverseHitTestOrder) { - _newChildren ??= []; - _newChildren.addAll(childrenInInverseHitTestOrder); - // we do the asserts afterwards because children is an Iterable - // and doing the asserts before would mean the behavior is - // different in checked mode vs release mode (if you walk an - // iterator after having reached the end, it'll just start over; - // the values are not cached). - assert(!_newChildren.any((SemanticsNode child) => child == this)); + void _replaceChildren(List newChildren) { + assert(!newChildren.any((SemanticsNode child) => child == this)); assert(() { SemanticsNode ancestor = this; while (ancestor.parent is SemanticsNode) ancestor = ancestor.parent; - assert(!_newChildren.any((SemanticsNode child) => child == ancestor)); + assert(!newChildren.any((SemanticsNode child) => child == ancestor)); return true; }()); assert(() { final Set seenChildren = new Set(); - for (SemanticsNode child in _newChildren) + for (SemanticsNode child in newChildren) assert(seenChildren.add(child)); // check for duplicate adds return true; }()); - } - /// Contains the children in inverse hit test order (i.e. paint order). - List _children; + // The goal of this function is updating sawChange. + if (_children != null) { + for (SemanticsNode child in _children) + child._dead = true; + } + if (newChildren != null) { + for (SemanticsNode child in newChildren) { + assert(!child.isInvisible, 'Child with id ${child.id} is invisible and should not be added to tree.'); + child._dead = false; + } + } + bool sawChange = false; + if (_children != null) { + for (SemanticsNode child in _children) { + if (child._dead) { + if (child.parent == this) { + // we might have already had our child stolen from us by + // another node that is deeper in the tree. + dropChild(child); + } + sawChange = true; + } + } + } + if (newChildren != null) { + for (SemanticsNode child in newChildren) { + if (child.parent != this) { + if (child.parent != null) { + // we're rebuilding the tree from the bottom up, so it's possible + // that our child was, in the last pass, a child of one of our + // ancestors. In that case, we drop the child eagerly here. + // TODO(ianh): Find a way to assert that the same node didn't + // actually appear in the tree in two places. + child.parent?.dropChild(child); + } + assert(!child.attached); + adoptChild(child); + sawChange = true; + } + } + } + if (!sawChange && _children != null) { + assert(newChildren != null); + assert(newChildren.length == _children.length); + // Did the order change? + for (int i = 0; i < _children.length; i++) { + if (_children[i].id != newChildren[i].id) { + sawChange = true; + break; + } + } + } + final List oldChildren = _children; + _children = newChildren; + oldChildren?.clear(); + newChildren = oldChildren; + if (sawChange) + _markDirty(); + } /// Whether this node has a non-zero number of children. bool get hasChildren => _children?.isNotEmpty ?? false; @@ -522,86 +402,6 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { } } - /// Called during the compilation phase after all the children of this node have been compiled. - /// - /// This function lets the semantic node respond to all the changes to its - /// child list for the given frame at once instead of needing to process the - /// changes incrementally as new children are compiled. - void finalizeChildren() { - // The goal of this function is updating sawChange. - if (_children != null) { - for (SemanticsNode child in _children) - child._dead = true; - } - if (_newChildren != null) { - for (SemanticsNode child in _newChildren) { - assert(!child.isInvisible, 'Child with id ${child.id} is invisible and should not be added to tree.'); - child._dead = false; - } - } - bool sawChange = false; - if (_children != null) { - for (SemanticsNode child in _children) { - if (child._dead) { - if (child.parent == this) { - // we might have already had our child stolen from us by - // another node that is deeper in the tree. - dropChild(child); - } - sawChange = true; - } - } - } - if (_newChildren != null) { - for (SemanticsNode child in _newChildren) { - if (child.parent != this) { - if (child.parent != null) { - // we're rebuilding the tree from the bottom up, so it's possible - // that our child was, in the last pass, a child of one of our - // ancestors. In that case, we drop the child eagerly here. - // TODO(ianh): Find a way to assert that the same node didn't - // actually appear in the tree in two places. - child.parent?.dropChild(child); - } - assert(!child.attached); - adoptChild(child); - sawChange = true; - } - } - } - if (!sawChange && _children != null) { - assert(_newChildren != null); - assert(_newChildren.length == _children.length); - // Did the order change? - for (int i = 0; i < _children.length; i++) { - if (_children[i].id != _newChildren[i].id) { - sawChange = true; - break; - } - } - } - final List oldChildren = _children; - _children = _newChildren; - oldChildren?.clear(); - _newChildren = oldChildren; - if (sawChange) - _markDirty(); - } - - @override - SemanticsOwner get owner => super.owner; - - @override - SemanticsNode get parent => super.parent; - - @override - void redepthChildren() { - if (_children != null) { - for (SemanticsNode child in _children) - redepthChild(child); - } - } - /// Visit all the descendants of this node. /// /// This function calls visitor for each descendant in a pre-order travseral @@ -617,6 +417,22 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { return true; } + // AbstractNode OVERRIDES + + @override + SemanticsOwner get owner => super.owner; + + @override + SemanticsNode get parent => super.parent; + + @override + void redepthChildren() { + if (_children != null) { + for (SemanticsNode child in _children) + redepthChild(child); + } + } + @override void attach(SemanticsOwner owner) { super.attach(owner); @@ -627,7 +443,6 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { _dirty = false; _markDirty(); } - assert(isMergedIntoParent == (parent?.isPartOfNodeMerging ?? false)); if (_children != null) { for (SemanticsNode child in _children) child.attach(owner); @@ -656,6 +471,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { _markDirty(); } + // DIRTY MANAGEMENT + bool _dirty = false; void _markDirty() { if (_dirty) @@ -667,6 +484,66 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { } } + bool _isDifferentFromCurrentSemanticAnnotation(SemanticsConfiguration config) { + return _label != config.label || + _flags != config._flags || + _textDirection != config.textDirection || + _actionsAsBitMap(_actions) != _actionsAsBitMap(config._actions) || + _mergeAllDescendantsIntoThisNode != config.isMergingSemanticsOfDescendants; + } + + // TAGS, LABELS, ACTIONS + + Map _actions = _kEmptyConfig._actions; + + /// The [SemanticsTag]s this node is tagged with. + /// + /// Tags are used during the construction of the semantics tree. They are not + /// transfered to the engine. + Set tags; + + /// Whether this node is tagged with `tag`. + bool isTagged(SemanticsTag tag) => tags != null && tags.contains(tag); + + int _flags = _kEmptyConfig._flags; + + bool _hasFlag(SemanticsFlags flag) => _flags & flag.index != 0; + + /// A textual description of this node. + /// + /// The text's reading direction is given by [textDirection]. + String get label => _label; + String _label = _kEmptyConfig.label; + + /// The reading direction for [label]. + TextDirection get textDirection => _textDirection; + TextDirection _textDirection = _kEmptyConfig.textDirection; + + int _actionsAsBitMap(Map actions) { + return actions.keys.fold(0, (int prev, SemanticsAction action) => prev |= action.index); + } + + bool _canPerformAction(SemanticsAction action) => _actions.containsKey(action); + + static final SemanticsConfiguration _kEmptyConfig = new SemanticsConfiguration(); + + void updateWith({ + @required SemanticsConfiguration config, + @required List childrenInInversePaintOrder, + }) { + config ??= _kEmptyConfig; + if (_isDifferentFromCurrentSemanticAnnotation(config)) + _markDirty(); + + _label = config.label; + _flags = config._flags; + _textDirection = config.textDirection; + _actions = new Map.from(config._actions); + _mergeAllDescendantsIntoThisNode = config.isMergingSemanticsOfDescendants; + _replaceChildren(childrenInInversePaintOrder ?? const []); + } + + /// Returns a summary of the semantics for this node. /// /// If this node has [mergeAllDescendantsIntoThisNode], then the returned data @@ -674,21 +551,25 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { /// returned data matches the data on this node. SemanticsData getSemanticsData() { int flags = _flags; - int actions = _actions; + int actions = _actionsAsBitMap(_actions); String label = _label; TextDirection textDirection = _textDirection; - final Set tags = new Set.from(_tags); + Set mergedTags = tags == null ? null : new Set.from(tags); if (mergeAllDescendantsIntoThisNode) { _visitDescendants((SemanticsNode node) { + assert(node.isMergedIntoParent); flags |= node._flags; - actions |= node._actions; + actions |= _actionsAsBitMap(node._actions); textDirection ??= node._textDirection; - tags.addAll(node._tags); - if (node.label.isNotEmpty) { - String nestedLabel = node.label; - if (textDirection != node.textDirection && node.textDirection != null) { - switch (node.textDirection) { + if (node.tags != null) { + mergedTags ??= new Set(); + mergedTags.addAll(node.tags); + } + if (node._label.isNotEmpty) { + String nestedLabel = node._label; + if (textDirection != node._textDirection && node._textDirection != null) { + switch (node._textDirection) { case TextDirection.rtl: nestedLabel = '${Unicode.RLE}$nestedLabel${Unicode.PDF}'; break; @@ -713,7 +594,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { textDirection: textDirection, rect: rect, transform: transform, - tags: tags, + tags: mergedTags, ); } @@ -798,18 +679,13 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { properties.add(new DiagnosticsProperty('rect', rect, description: description, showName: false)); } properties.add(new FlagProperty('wasAffectedByClip', value: wasAffectedByClip, ifTrue: 'clipped')); - final List actions = []; - for (SemanticsAction action in SemanticsAction.values.values) { - if ((_actions & action.index) != 0) - actions.add(describeEnum(action)); - } + final List actions = _actions.keys.map((SemanticsAction action) => describeEnum(action)).toList()..sort(); properties.add(new IterableProperty('actions', actions, ifEmpty: null)); - properties.add(new IterableProperty('tags', _tags, ifEmpty: null)); - if (hasCheckedState) - properties.add(new FlagProperty('isChecked', value: isChecked, ifTrue: 'checked', ifFalse: 'unchecked')); - properties.add(new FlagProperty('isSelected', value: isSelected, ifTrue: 'selected')); - properties.add(new StringProperty('label', label, defaultValue: '')); - properties.add(new EnumProperty('textDirection', textDirection, defaultValue: null)); + if (_hasFlag(SemanticsFlags.hasCheckedState)) + properties.add(new FlagProperty('isChecked', value: _hasFlag(SemanticsFlags.isChecked), ifTrue: 'checked', ifFalse: 'unchecked')); + properties.add(new FlagProperty('isSelected', value: _hasFlag(SemanticsFlags.isSelected), ifTrue: 'selected')); + properties.add(new StringProperty('label', _label, defaultValue: '')); + properties.add(new EnumProperty('textDirection', _textDirection, defaultValue: null)); } /// Returns a string representation of this node and its descendants. @@ -939,7 +815,7 @@ class SemanticsOwner extends ChangeNotifier { notifyListeners(); } - SemanticsActionHandler _getSemanticsActionHandlerForId(int id, SemanticsAction action) { + VoidCallback _getSemanticsActionHandlerForId(int id, SemanticsAction action) { SemanticsNode result = _nodes[id]; if (result != null && result.isPartOfNodeMerging && !result._canPerformAction(action)) { result._visitDescendants((SemanticsNode node) { @@ -952,7 +828,7 @@ class SemanticsOwner extends ChangeNotifier { } if (result == null || !result._canPerformAction(action)) return null; - return result._actionHandler; + return result._actions[action]; } /// Asks the [SemanticsNode] with the given id to perform the given action. @@ -961,9 +837,9 @@ class SemanticsOwner extends ChangeNotifier { /// this function does nothing. void performAction(int id, SemanticsAction action) { assert(action != null); - final SemanticsActionHandler handler = _getSemanticsActionHandlerForId(id, action); + final VoidCallback handler = _getSemanticsActionHandlerForId(id, action); if (handler != null) { - handler.performAction(action); + handler(); return; } @@ -972,7 +848,7 @@ class SemanticsOwner extends ChangeNotifier { _nodes[id]._showOnScreen(); } - SemanticsActionHandler _getSemanticsActionHandlerForPosition(SemanticsNode node, Offset position, SemanticsAction action) { + VoidCallback _getSemanticsActionHandlerForPosition(SemanticsNode node, Offset position, SemanticsAction action) { if (node.transform != null) { final Matrix4 inverse = new Matrix4.identity(); if (inverse.copyInverse(node.transform) == 0.0) @@ -990,16 +866,16 @@ class SemanticsOwner extends ChangeNotifier { } return true; }); - return result?._actionHandler; + return result?._actions[action]; } if (node.hasChildren) { for (SemanticsNode child in node._children.reversed) { - final SemanticsActionHandler handler = _getSemanticsActionHandlerForPosition(child, position, action); + final VoidCallback handler = _getSemanticsActionHandlerForPosition(child, position, action); if (handler != null) return handler; } } - return node._canPerformAction(action) ? node._actionHandler : null; + return node._canPerformAction(action) ? node._actions[action] : null; } /// Asks the [SemanticsNode] at the given position to perform the given action. @@ -1011,10 +887,256 @@ class SemanticsOwner extends ChangeNotifier { final SemanticsNode node = rootSemanticsNode; if (node == null) return; - final SemanticsActionHandler handler = _getSemanticsActionHandlerForPosition(node, position, action); - handler?.performAction(action); + final VoidCallback handler = _getSemanticsActionHandlerForPosition(node, position, action); + if (handler != null) + handler(); } @override String toString() => describeIdentity(this); } + +/// Describes the semantic information associated with the owning +/// [RenderObject]. +/// +/// The information provided in the configuration is used to to generate the +/// semantics tree. +class SemanticsConfiguration { + + // SEMANTIC BOUNDARY BEHAVIOR + + /// Whether the [RenderObject] owner of this configuration wants to own its + /// own [SemanticsNode]. + /// + /// When set to true semantic information associated with the [RenderObject] + /// owner of this configuration or any of its defendants will not leak into + /// parents. The [SemanticsNode] generated out of this configuration will + /// act as a boundary. + /// + /// Whether descendants of the owning [RenderObject] can add their semantic + /// information to the [SemanticsNode] introduced by this configuration + /// is controlled by [explicitChildNodes]. + /// + /// This has to be true if [isMergingDescendantsIntoOneNode] is also true. + bool get isSemanticBoundary => _isSemanticBoundary; + bool _isSemanticBoundary = false; + set isSemanticBoundary(bool value) { + assert(!isMergingDescendantsIntoOneNode || value); + _isSemanticBoundary = value; + } + + /// Whether the configuration forces all children of the owning [RenderObject] + /// that want to contribute semantic information to the semantics tree to do + /// so in the form of explicit [SemanticsNode]s. + /// + /// When set to false children of the owning [RenderObject] are allowed to + /// annotate [SemanticNode]s of their parent with the semantic information + /// they want to contribute to the semantic tree. + /// When set to true the only way for children of the owning [RenderObject] + /// to contribute semantic information to the semantic tree is to introduce + /// new explicit [SemanticNode]s to the tree. + /// + /// This setting is often used in combination with [isSemanticBoundary] to + /// create semantic boundaries that are either writable or not for children. + bool explicitChildNodes = false; + + /// Whether the owning [RenderObject] makes other [RenderObjects] previously + /// painted within the same semantic boundary unreachable for accessibility + /// purposes. + /// + /// If set to true, the semantic information for all siblings and cousins of + /// this node, that are earlier in a depth-first pre-order traversal, are + /// dropped from the semantics tree up until a semantic boundary (as defined + /// by [isSemanticBoundary]) is reached. + /// + /// If [isSemanticBoundary] and [isBlockingSemanticsOfPreviouslyPaintedNodes] + /// is set on the same node, all previously painted siblings and cousins up + /// until the next ancestor that is a semantic boundary are dropped. + /// + /// Paint order as established by [visitChildrenForSemantics] is used to + /// determine if a node is previous to this one. + bool isBlockingSemanticsOfPreviouslyPaintedNodes = false; + + /// Whether the semantics information of all descendants should be merged + /// into the owning [RenderObject] semantics node. + /// + /// When this is set to true the [SemanticsNode] of the owning [RenderObject] + /// will not have any children. + /// + /// Setting this to true requires that [isSemanticBoundary] is also true. + bool get isMergingDescendantsIntoOneNode => _isMergingDescendantsIntoOneNode; + bool _isMergingDescendantsIntoOneNode = false; + set isMergingDescendantsIntoOneNode(bool value) { + assert(isSemanticBoundary); + _isMergingDescendantsIntoOneNode = isMergingDescendantsIntoOneNode; + } + + // SEMANTIC ANNOTATIONS + // These will end up on [SemanticNode]s generated from + // [SemanticsConfiguration]s. + + /// Whether this configuration is empty. + /// + /// An empty configuration doesn't contain any semantic information that it + /// wants to contribute to the semantics tree. + bool get hasBeenAnnotated => _hasBeenAnnotated; + bool _hasBeenAnnotated = false; + + /// The actions (with associated action handlers) that this configuration + /// would like to contribute to the semantics tree. + /// + /// See also: + /// * [addAction] to add an action. + final Map _actions = {}; + + /// Adds an `action` to the semantics tree. + /// + /// Whenever the user performs `action` the provided `handler` is called. + void addAction(SemanticsAction action, VoidCallback handler) { + _actions[action] = handler; + _hasBeenAnnotated = true; + } + + /// Returns the action handler registered for [action] or null if none was + /// registered. + /// + /// See also: + /// * [addAction] to add an action. + VoidCallback getActionHandler(SemanticsAction action) => _actions[action]; + + /// Whether the semantic information provided by the owning [RenderObject] and + /// all of its descendants should be treated as one logical entity. + /// + /// If set to true, the descendants of the owning [RenderObject]'s + /// [SemanticsNode] will merge their semantic information into the + /// [SemanticsNode] representing the owning [RenderObject]. + bool get isMergingSemanticsOfDescendants => _isMergingSemanticsOfDescendants; + bool _isMergingSemanticsOfDescendants = false; + set isMergingSemanticsOfDescendants(bool value) { + _isMergingSemanticsOfDescendants = value; + _hasBeenAnnotated = true; + } + + /// A textual description of the owning [RenderObject]. + /// + /// The text's reading direction is given by [textDirection]. + String get label => _label; + String _label = ''; + set label(String label) { + _label = label; + _hasBeenAnnotated = true; + } + + /// The reading direction for the text in [label]. + TextDirection get textDirection => _textDirection; + TextDirection _textDirection; + set textDirection(TextDirection textDirection) { + _textDirection = textDirection; + _hasBeenAnnotated = true; + } + + /// Whether the owning [RenderObject] is selected (true) or not (false). + set isSelected(bool value) { + _setFlag(SemanticsFlags.isSelected, value); + } + + /// If this node has Boolean state that can be controlled by the user, whether + /// that state is on or off, corresponding to true and false, respectively. + /// + /// Do not set this to any value if the owning [RenderObject] doesn't have + /// Booleans state that can be controlled by the user. + set isChecked(bool value) { + _setFlag(SemanticsFlags.hasCheckedState, true); + _setFlag(SemanticsFlags.isChecked, value); + } + + // TAGS + + Iterable get tagsForChildren => _tagsForChildren; + Set _tagsForChildren; + + void addTagForChildren(SemanticsTag tag) { + _tagsForChildren ??= new Set(); + _tagsForChildren.add(tag); + } + + // INTERNAL FLAG MANAGEMENT + + int _flags = 0; + void _setFlag(SemanticsFlags flag, bool value) { + if (value) { + _flags |= flag.index; + } else { + _flags &= ~flag.index; + } + _hasBeenAnnotated = true; + } + + // CONFIGURATION COMBINATION LOGIC + + /// Whether this configuration is compatible with the provided `other` + /// configuration. + /// + /// Two configurations are said to be compatible if they can be added to the + /// same [SemanticsNode] without losing any semantics information. + bool isCompatibleWith(SemanticsConfiguration other) { + if (other == null || !other.hasBeenAnnotated || !hasBeenAnnotated) + return true; + if (_actions.keys.toSet().intersection(other._actions.keys.toSet()).isNotEmpty) + return false; + if ((_flags & other._flags) != 0) + return false; + return true; + } + + /// Absorb the semantic information from `other` into this configuration. + /// + /// This adds the semantic information of both configurations and saves the + /// result in this configuration. + /// + /// Only configurations that have [explicitChildNodes] set to false can + /// absorb other configurations and its recommended to only absorb compatible + /// configurations as determined by [isCompatibleWith]. + void absorb(SemanticsConfiguration other) { + assert(!explicitChildNodes); + + if (!other.hasBeenAnnotated) + return; + + _actions.addAll(other._actions); + _flags |= other._flags; + + textDirection ??= other.textDirection; + if (other.label.isNotEmpty) { + String nestedLabel = other.label; + if (textDirection != other.textDirection && other.textDirection != null) { + switch (other.textDirection) { + case TextDirection.rtl: + nestedLabel = '${Unicode.RLE}$nestedLabel${Unicode.PDF}'; + break; + case TextDirection.ltr: + nestedLabel = '${Unicode.LRE}$nestedLabel${Unicode.PDF}'; + break; + } + } + if (label.isEmpty) + label = nestedLabel; + else + label = '$label\n$nestedLabel'; + } + + _hasBeenAnnotated = _hasBeenAnnotated || other._hasBeenAnnotated; + } + + /// Returns an exact copy of this configuration. + SemanticsConfiguration copy() { + return new SemanticsConfiguration() + ..isSemanticBoundary = isSemanticBoundary + ..explicitChildNodes = explicitChildNodes + .._hasBeenAnnotated = _hasBeenAnnotated + .._textDirection = _textDirection + .._label = _label + .._flags = _flags + .._actions.addAll(_actions); + } +} diff --git a/packages/flutter/lib/src/rendering/sliver_persistent_header.dart b/packages/flutter/lib/src/rendering/sliver_persistent_header.dart index 2ff0cb2c4c3..f846b6fdbf9 100644 --- a/packages/flutter/lib/src/rendering/sliver_persistent_header.dart +++ b/packages/flutter/lib/src/rendering/sliver_persistent_header.dart @@ -221,10 +221,11 @@ abstract class RenderSliverPersistentHeader extends RenderSliver with RenderObje } @override - SemanticsAnnotator get semanticsAnnotator => _excludeFromSemanticsScrolling ? _annotate : null; + void describeSemanticsConfiguration(SemanticsConfiguration config) { + super.describeSemanticsConfiguration(config); - void _annotate(SemanticsNode node) { - node.addTag(RenderSemanticsGestureHandler.excludeFromScrolling); + if (_excludeFromSemanticsScrolling) + config.addTagForChildren(RenderSemanticsGestureHandler.excludeFromScrolling); } @override diff --git a/packages/flutter/lib/src/rendering/viewport.dart b/packages/flutter/lib/src/rendering/viewport.dart index e336c116367..60f92ad88ca 100644 --- a/packages/flutter/lib/src/rendering/viewport.dart +++ b/packages/flutter/lib/src/rendering/viewport.dart @@ -91,11 +91,12 @@ abstract class RenderViewportBase _annotate; - void _annotate(SemanticsNode node) { - node.addTag(RenderSemanticsGestureHandler.useTwoPaneSemantics); + @override + void describeSemanticsConfiguration(SemanticsConfiguration config) { + super.describeSemanticsConfiguration(config); + + config.addTagForChildren(RenderSemanticsGestureHandler.useTwoPaneSemantics); } @override diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 83eedf5e7be..70ccea38250 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -4434,6 +4434,7 @@ class Semantics extends SingleChildRenderObjectWidget { Key key, Widget child, this.container: false, + this.explicitChildNodes: false, this.checked, this.selected, this.label, @@ -4441,18 +4442,29 @@ class Semantics extends SingleChildRenderObjectWidget { }) : assert(container != null), super(key: key, child: child); - /// If 'container' is true, this Widget will introduce a new node in - /// the semantics tree. Otherwise, the semantics will be merged with - /// the semantics of any ancestors. + /// If 'container' is true, this widget will introduce a new + /// node in the semantics tree. Otherwise, the semantics will be + /// merged with the semantics of any ancestors (if the ancestor allows that). /// - /// The 'container' flag is implicitly set to true on the immediate - /// semantics-providing descendants of a node where multiple - /// children have semantics or have descendants providing semantics. - /// In other words, the semantics of siblings are not merged. To - /// merge the semantics of an entire subtree, including siblings, - /// you can use a [MergeSemantics] widget. + /// Whether descendants of this widget can add their semantic information to the + /// [SemanticsNode] introduced by this configuration is controlled by + /// [explicitChildNodes]. final bool container; + /// Whether descendants of this widget are allowed to add semantic information + /// to the [SemanticsNode] annotated by this widget. + /// + /// When set to false descendants are allowed to annotate [SemanticNode]s of + /// their parent with the semantic information they want to contribute to the + /// semantic tree. + /// When set to true the only way for descendants to contribute semantic + /// information to the semantic tree is to introduce new explicit + /// [SemanticNode]s to the tree. + /// + /// This setting is often used in combination with [isSemanticBoundary] to + /// create semantic boundaries that are either writable or not for children. + final bool explicitChildNodes; + /// If non-null, indicates that this subtree represents a checkbox /// or similar widget with a "checked" state, and what its current /// state is. @@ -4484,6 +4496,7 @@ class Semantics extends SingleChildRenderObjectWidget { RenderSemanticsAnnotations createRenderObject(BuildContext context) { return new RenderSemanticsAnnotations( container: container, + explicitChildNodes: explicitChildNodes, checked: checked, selected: selected, label: label, @@ -4495,6 +4508,7 @@ class Semantics extends SingleChildRenderObjectWidget { void updateRenderObject(BuildContext context, RenderSemanticsAnnotations renderObject) { renderObject ..container = container + ..explicitChildNodes = explicitChildNodes ..checked = checked ..selected = selected ..label = label diff --git a/packages/flutter/lib/src/widgets/focus_scope.dart b/packages/flutter/lib/src/widgets/focus_scope.dart index 5e37a27a8d9..6e2ec84112d 100644 --- a/packages/flutter/lib/src/widgets/focus_scope.dart +++ b/packages/flutter/lib/src/widgets/focus_scope.dart @@ -104,7 +104,7 @@ class _FocusScopeState extends State { Widget build(BuildContext context) { FocusScope.of(context).reparentScopeIfNeeded(widget.node); return new Semantics( - container: true, + explicitChildNodes: true, child: new _FocusScopeMarker( node: widget.node, child: widget.child, diff --git a/packages/flutter/test/material/card_test.dart b/packages/flutter/test/material/card_test.dart new file mode 100644 index 00000000000..f182286c5be --- /dev/null +++ b/packages/flutter/test/material/card_test.dart @@ -0,0 +1,62 @@ +// 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:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../widgets/semantics_tester.dart'; + +void main() { + testWidgets('Card can take semantic text from multiple children', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Material( + child: new Center( + child: new Card( + child: new Column( + children: [ + const Text('I am text!'), + const Text('Moar text!!1'), + new MaterialButton( + child: const Text('Button'), + onPressed: () { }, + ) + ], + ) + ), + ), + ), + ), + ); + + expect(semantics, hasSemantics( + new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + id: 2, + label: 'I am text!\nMoar text!!1', + textDirection: TextDirection.ltr, + children: [ + new TestSemantics( + id: 1, + label: 'Button', + textDirection: TextDirection.ltr, + actions: SemanticsAction.tap.index, + ), + ], + ), + ], + ), + ignoreTransform: true, + ignoreRect: true, + )); + + semantics.dispose(); + }); + +} diff --git a/packages/flutter/test/material/control_list_tile_test.dart b/packages/flutter/test/material/control_list_tile_test.dart index 3550ea0bf67..3ee63d08c23 100644 --- a/packages/flutter/test/material/control_list_tile_test.dart +++ b/packages/flutter/test/material/control_list_tile_test.dart @@ -95,7 +95,7 @@ void main() { expect(semantics, hasSemantics(new TestSemantics.root( children: [ new TestSemantics.rootChild( - id: 1, + id: 7, rect: new Rect.fromLTWH(0.0, 0.0, 800.0, 56.0), transform: null, flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, @@ -103,7 +103,7 @@ void main() { label: 'aaa\nAAA', ), new TestSemantics.rootChild( - id: 6, + id: 8, rect: new Rect.fromLTWH(0.0, 0.0, 800.0, 56.0), transform: new Matrix4.translationValues(0.0, 56.0, 0.0), flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, @@ -111,7 +111,7 @@ void main() { label: 'bbb\nBBB', ), new TestSemantics.rootChild( - id: 11, + id: 9, rect: new Rect.fromLTWH(0.0, 0.0, 800.0, 56.0), transform: new Matrix4.translationValues(0.0, 112.0, 0.0), flags: SemanticsFlags.hasCheckedState.index, diff --git a/packages/flutter/test/material/tabs_test.dart b/packages/flutter/test/material/tabs_test.dart index ed8027f335a..70a03a16450 100644 --- a/packages/flutter/test/material/tabs_test.dart +++ b/packages/flutter/test/material/tabs_test.dart @@ -1011,11 +1011,11 @@ void main() { final TestSemantics expectedSemantics = new TestSemantics.root( children: [ new TestSemantics.rootChild( - id: 1, + id: 3, rect: TestSemantics.fullScreen, children: [ new TestSemantics( - id: 2, + id: 1, actions: SemanticsAction.tap.index, flags: SemanticsFlags.isSelected.index, label: 'TAB #0\nTab 1 of 2', @@ -1023,13 +1023,14 @@ void main() { transform: new Matrix4.translationValues(0.0, 276.0, 0.0), ), new TestSemantics( - id: 5, + id: 2, actions: SemanticsAction.tap.index, label: 'TAB #1\nTab 2 of 2', rect: new Rect.fromLTRB(0.0, 0.0, 108.0, kTextTabBarHeight), transform: new Matrix4.translationValues(108.0, 276.0, 0.0), ), - ]), + ], + ), ], ); @@ -1064,15 +1065,19 @@ void main() { ), ); + const String tab0title = 'This is a very wide tab #0\nTab 1 of 20'; + const String tab10title = 'This is a very wide tab #10\nTab 11 of 20'; + expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollLeft])); - expect(semantics, isNot(includesNodeWith(label: 'This is a very wide tab #10'))); + expect(semantics, includesNodeWith(label: tab0title)); + expect(semantics, isNot(includesNodeWith(label: tab10title))); controller.index = 10; await tester.pumpAndSettle(); - expect(semantics, isNot(includesNodeWith(label: 'This is a very wide tab #0'))); + expect(semantics, isNot(includesNodeWith(label: tab0title))); expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollLeft, SemanticsAction.scrollRight])); - expect(semantics, includesNodeWith(label: 'This is a very wide tab #10')); + expect(semantics, includesNodeWith(label: tab10title)); controller.index = 19; await tester.pumpAndSettle(); @@ -1083,7 +1088,8 @@ void main() { await tester.pumpAndSettle(); expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollLeft])); - expect(semantics, includesNodeWith(label: 'This is a very wide tab #0')); + expect(semantics, includesNodeWith(label: tab0title)); + expect(semantics, isNot(includesNodeWith(label: tab10title))); semantics.dispose(); }); diff --git a/packages/flutter/test/material/tooltip_test.dart b/packages/flutter/test/material/tooltip_test.dart index ad8add2ac3a..8b0842b9ed2 100644 --- a/packages/flutter/test/material/tooltip_test.dart +++ b/packages/flutter/test/material/tooltip_test.dart @@ -473,7 +473,7 @@ void main() { child: new Tooltip( key: key, message: tooltipText, - child: new Container(width: 0.0, height: 0.0), + child: new Container(width: 10.0, height: 10.0), ), ), ], @@ -485,14 +485,23 @@ void main() { ), ); - expect(semantics, hasSemantics(new TestSemantics.root(label: tooltipText))); + final TestSemantics expected = new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + id: 1, + label: tooltipText, + ), + ] + ); + + expect(semantics, hasSemantics(expected, ignoreTransform: true, ignoreRect: true)); // before using "as dynamic" in your code, see note top of file (key.currentState as dynamic).ensureTooltipVisible(); // this triggers a rebuild of the semantics because the tree changes await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0) - expect(semantics, hasSemantics(new TestSemantics.root(label: tooltipText))); + expect(semantics, hasSemantics(expected, ignoreTransform: true, ignoreRect: true)); semantics.dispose(); }); diff --git a/packages/flutter/test/rendering/object_test.dart b/packages/flutter/test/rendering/object_test.dart index b840a9336b5..15cba385452 100644 --- a/packages/flutter/test/rendering/object_test.dart +++ b/packages/flutter/test/rendering/object_test.dart @@ -55,6 +55,9 @@ class TestRenderObject extends RenderObject { Rect get semanticBounds => new Rect.fromLTWH(0.0, 0.0, 10.0, 20.0); @override - bool get isSemanticBoundary => true; + void describeSemanticsConfiguration(SemanticsConfiguration config) { + super.describeSemanticsConfiguration(config); + config.isSemanticBoundary = true; + } } diff --git a/packages/flutter/test/rendering/proxy_box_test.dart b/packages/flutter/test/rendering/proxy_box_test.dart index f3d637206d6..d08aa11dd58 100644 --- a/packages/flutter/test/rendering/proxy_box_test.dart +++ b/packages/flutter/test/rendering/proxy_box_test.dart @@ -72,23 +72,23 @@ void main() { }); test('RenderSemanticsGestureHandler adds/removes correct semantic actions', () { - SemanticsNode node = new SemanticsNode(); final RenderSemanticsGestureHandler renderObj = new RenderSemanticsGestureHandler( onTap: () {}, onHorizontalDragUpdate: (DragUpdateDetails details) {}, ); - renderObj.semanticsAnnotator(node); - expect(node.getSemanticsData().hasAction(SemanticsAction.tap), isTrue); - expect(node.getSemanticsData().hasAction(SemanticsAction.scrollLeft), isTrue); - expect(node.getSemanticsData().hasAction(SemanticsAction.scrollRight), isTrue); + SemanticsConfiguration config = new SemanticsConfiguration(); + renderObj.describeSemanticsConfiguration(config); + expect(config.getActionHandler(SemanticsAction.tap), isNotNull); + expect(config.getActionHandler(SemanticsAction.scrollLeft), isNotNull); + expect(config.getActionHandler(SemanticsAction.scrollRight), isNotNull); - node = new SemanticsNode(); + config = new SemanticsConfiguration(); renderObj.validActions = [SemanticsAction.tap, SemanticsAction.scrollLeft].toSet(); - renderObj.semanticsAnnotator(node); - expect(node.getSemanticsData().hasAction(SemanticsAction.tap), isTrue); - expect(node.getSemanticsData().hasAction(SemanticsAction.scrollLeft), isTrue); - expect(node.getSemanticsData().hasAction(SemanticsAction.scrollRight), isFalse); + renderObj.describeSemanticsConfiguration(config); + expect(config.getActionHandler(SemanticsAction.tap), isNotNull); + expect(config.getActionHandler(SemanticsAction.scrollLeft), isNotNull); + expect(config.getActionHandler(SemanticsAction.scrollRight), isNull); }); } diff --git a/packages/flutter/test/rendering/reattach_test.dart b/packages/flutter/test/rendering/reattach_test.dart index f7e15f89d3b..802085638bb 100644 --- a/packages/flutter/test/rendering/reattach_test.dart +++ b/packages/flutter/test/rendering/reattach_test.dart @@ -29,6 +29,7 @@ class TestTree { child: new RenderPositionedBox( child: child = new RenderConstrainedBox( additionalConstraints: const BoxConstraints.tightFor(height: 20.0, width: 20.0), + child: new RenderSemanticsAnnotations(label: 'Hello there foo', textDirection: TextDirection.ltr) ), ), ), @@ -127,7 +128,7 @@ void main() { layout(testTree.root, phase: EnginePhase.paint); expect(testTree.painted, isTrue); }); - test('objects can be detached and re-attached: semantics', () { + test('objects can be detached and re-attached: semantics (no change)', () { final TestTree testTree = new TestTree(); int semanticsUpdateCount = 0; final SemanticsHandle semanticsHandle = renderer.pipelineOwner.ensureSemantics( @@ -147,7 +148,31 @@ void main() { expect(semanticsUpdateCount, 0); // Lay out, composite, paint, and update semantics again layout(testTree.root, phase: EnginePhase.flushSemantics); + expect(semanticsUpdateCount, 0); // no semantics have changed. + semanticsHandle.dispose(); + }); + test('objects can be detached and re-attached: semantics (with change)', () { + final TestTree testTree = new TestTree(); + int semanticsUpdateCount = 0; + final SemanticsHandle semanticsHandle = renderer.pipelineOwner.ensureSemantics( + listener: () { + ++semanticsUpdateCount; + } + ); + // Lay out, composite, paint, and update semantics + layout(testTree.root, phase: EnginePhase.flushSemantics); expect(semanticsUpdateCount, 1); + // Remove testTree from the custom render view + renderer.renderView.child = null; + expect(testTree.child.owner, isNull); + // Dirty one of the elements + semanticsUpdateCount = 0; + testTree.child.additionalConstraints = const BoxConstraints.tightFor(height: 20.0, width: 30.0); + testTree.child.markNeedsSemanticsUpdate(); + expect(semanticsUpdateCount, 0); + // Lay out, composite, paint, and update semantics again + layout(testTree.root, phase: EnginePhase.flushSemantics); + expect(semanticsUpdateCount, 1); // semantics have changed. semanticsHandle.dispose(); }); } diff --git a/packages/flutter/test/rendering/semantics_test.dart b/packages/flutter/test/rendering/semantics_test.dart index 6052249dfdb..86559d866dc 100644 --- a/packages/flutter/test/rendering/semantics_test.dart +++ b/packages/flutter/test/rendering/semantics_test.dart @@ -18,41 +18,45 @@ void main() { test('tagging', () { final SemanticsNode node = new SemanticsNode(); - expect(node.hasTag(tag1), isFalse); - expect(node.hasTag(tag2), isFalse); + expect(node.isTagged(tag1), isFalse); + expect(node.isTagged(tag2), isFalse); - node.addTag(tag1); - expect(node.hasTag(tag1), isTrue); - expect(node.hasTag(tag2), isFalse); + node.tags = new Set()..add(tag1); + expect(node.isTagged(tag1), isTrue); + expect(node.isTagged(tag2), isFalse); - node.addTag(tag2); - expect(node.hasTag(tag1), isTrue); - expect(node.hasTag(tag2), isTrue); + node.tags.add(tag2); + expect(node.isTagged(tag1), isTrue); + expect(node.isTagged(tag2), isTrue); }); test('getSemanticsData includes tags', () { - final SemanticsNode node = new SemanticsNode() - ..rect = new Rect.fromLTRB(0.0, 0.0, 10.0, 10.0) - ..addTag(tag1) - ..addTag(tag2); - - final Set expected = new Set() + final Set tags = new Set() ..add(tag1) ..add(tag2); - expect(node.getSemanticsData().tags, expected); + final SemanticsNode node = new SemanticsNode() + ..rect = new Rect.fromLTRB(0.0, 0.0, 10.0, 10.0) + ..tags = tags; - node.mergeAllDescendantsIntoThisNode = true; - node.addChildren([ - new SemanticsNode() - ..rect = new Rect.fromLTRB(5.0, 5.0, 10.0, 10.0) - ..addTag(tag3), - ]); - node.finalizeChildren(); + expect(node.getSemanticsData().tags, tags); - expected.add(tag3); + tags.add(tag3); - expect(node.getSemanticsData().tags, expected); + final SemanticsConfiguration config = new SemanticsConfiguration() + ..isMergingSemanticsOfDescendants = true; + + node.updateWith( + config: config, + childrenInInversePaintOrder: [ + new SemanticsNode() + ..isMergedIntoParent = true + ..rect = new Rect.fromLTRB(5.0, 5.0, 10.0, 10.0) + ..tags = tags, + ], + ); + + expect(node.getSemanticsData().tags, tags); }); test('after markNeedsSemanticsUpdate(onlyLocalUpdates: true) all render objects between two semantic boundaries are asked for annotations', () { @@ -88,7 +92,6 @@ void main() { middle.action = SemanticsAction.scrollDown; middle.markNeedsSemanticsUpdate(onlyLocalUpdates: true); - expect(root.debugSemantics.getSemanticsData().actions, 0); // SemanticsNode is reset pumpFrame(phase: EnginePhase.flushSemantics); @@ -104,8 +107,10 @@ void main() { ..rect = new Rect.fromLTRB(5.0, 0.0, 10.0, 5.0); final SemanticsNode root = new SemanticsNode() ..rect = new Rect.fromLTRB(0.0, 0.0, 10.0, 5.0); - root.addChildren([child1, child2]); - root.finalizeChildren(); + root.updateWith( + config: null, + childrenInInversePaintOrder: [child1, child2], + ); expect(root.transform, isNull); expect(child1.transform, isNull); @@ -126,8 +131,10 @@ void main() { ..rect = new Rect.fromLTRB(10.0, 0.0, 15.0, 5.0); final SemanticsNode root = new SemanticsNode() ..rect = new Rect.fromLTRB(0.0, 0.0, 20.0, 5.0); - root.addChildren([child1, child2]); - root.finalizeChildren(); + root.updateWith( + config: null, + childrenInInversePaintOrder: [child1, child2], + ); expect( root.toStringDeep(childOrder: DebugSemanticsDumpOrder.traversal), 'SemanticsNode#11(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 20.0, 5.0))\n' @@ -144,18 +151,22 @@ void main() { final SemanticsNode child3 = new SemanticsNode() ..rect = new Rect.fromLTRB(0.0, 0.0, 10.0, 5.0); - child3.addChildren([ - new SemanticsNode() - ..rect = new Rect.fromLTRB(5.0, 0.0, 10.0, 5.0), - new SemanticsNode() - ..rect = new Rect.fromLTRB(0.0, 0.0, 5.0, 5.0), - ]); - child3.finalizeChildren(); + child3.updateWith( + config: null, + childrenInInversePaintOrder: [ + new SemanticsNode() + ..rect = new Rect.fromLTRB(5.0, 0.0, 10.0, 5.0), + new SemanticsNode() + ..rect = new Rect.fromLTRB(0.0, 0.0, 5.0, 5.0), + ], + ); final SemanticsNode rootComplex = new SemanticsNode() ..rect = new Rect.fromLTRB(0.0, 0.0, 25.0, 5.0); - rootComplex.addChildren([child1, child2, child3]); - rootComplex.finalizeChildren(); + rootComplex.updateWith( + config: null, + childrenInInversePaintOrder: [child1, child2, child3] + ); expect( rootComplex.toStringDeep(childOrder: DebugSemanticsDumpOrder.traversal), @@ -187,28 +198,30 @@ void main() { expect( minimalProperties.toStringDeep(minLevel: DiagnosticLevel.hidden), - 'SemanticsNode#16(owner: null, isPartOfNodeMerging: false, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), wasAffectedByClip: false, actions: [], tags: [], isSelected: false, label: "", textDirection: null)\n', + 'SemanticsNode#16(owner: null, isPartOfNodeMerging: false, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), wasAffectedByClip: false, actions: [], isSelected: false, label: "", textDirection: null)\n', ); - final SemanticsNode allProperties = new SemanticsNode() - ..rect = new Rect.fromLTWH(50.0, 10.0, 20.0, 30.0) - ..mergeAllDescendantsIntoThisNode = true - ..transform = new Matrix4.translation(new Vector3(10.0, 10.0, 0.0)) - ..wasAffectedByClip = true - ..addAction(SemanticsAction.scrollUp) - ..addAction(SemanticsAction.longPress) - ..addAction(SemanticsAction.showOnScreen) + final SemanticsConfiguration config = new SemanticsConfiguration() + ..isMergingSemanticsOfDescendants = true + ..addAction(SemanticsAction.scrollUp, () { }) + ..addAction(SemanticsAction.longPress, () { }) + ..addAction(SemanticsAction.showOnScreen, () { }) ..isChecked = false ..isSelected = true ..label = "Use all the properties" ..textDirection = TextDirection.rtl; + final SemanticsNode allProperties = new SemanticsNode() + ..rect = new Rect.fromLTWH(50.0, 10.0, 20.0, 30.0) + ..transform = new Matrix4.translation(new Vector3(10.0, 10.0, 0.0)) + ..wasAffectedByClip = true + ..updateWith(config: config, childrenInInversePaintOrder: null); expect( allProperties.toStringDeep(), - 'SemanticsNode#17(STALE, owner: null, leaf merge, Rect.fromLTRB(60.0, 20.0, 80.0, 50.0), clipped, actions: [longPress, scrollUp, showOnScreen], selected, label: "Use all the properties", textDirection: rtl)\n', + 'SemanticsNode#17(STALE, owner: null, leaf merge, Rect.fromLTRB(60.0, 20.0, 80.0, 50.0), clipped, actions: [longPress, scrollUp, showOnScreen], unchecked, selected, label: "Use all the properties", textDirection: rtl)\n', ); expect( allProperties.getSemanticsData().toString(), - 'SemanticsData(Rect.fromLTRB(50.0, 10.0, 70.0, 40.0), [1.0,0.0,0.0,10.0; 0.0,1.0,0.0,10.0; 0.0,0.0,1.0,0.0; 0.0,0.0,0.0,1.0], actions: [longPress, scrollUp, showOnScreen], flags: [isSelected], label: "Use all the properties", textDirection: rtl)', + 'SemanticsData(Rect.fromLTRB(50.0, 10.0, 70.0, 40.0), [1.0,0.0,0.0,10.0; 0.0,1.0,0.0,10.0; 0.0,0.0,1.0,0.0; 0.0,0.0,0.0,1.0], actions: [longPress, scrollUp, showOnScreen], flags: [hasCheckedState, isSelected], label: "Use all the properties", textDirection: rtl)', ); final SemanticsNode scaled = new SemanticsNode() @@ -223,37 +236,22 @@ void main() { 'SemanticsData(Rect.fromLTRB(50.0, 10.0, 70.0, 40.0), [10.0,0.0,0.0,0.0; 0.0,10.0,0.0,0.0; 0.0,0.0,1.0,0.0; 0.0,0.0,0.0,1.0])', ); }); - - test('reset clears tags', () { - const SemanticsTag tag = const SemanticsTag('tag for testing'); - final SemanticsNode node = new SemanticsNode(); - - expect(node.hasTag(tag), isFalse); - - node.addTag(tag); - - expect(node.hasTag(tag), isTrue); - - node.reset(); - - expect(node.hasTag(tag), isFalse); - }); } class TestRender extends RenderProxyBox { TestRender({ this.action, this.isSemanticBoundary, RenderObject child }) : super(child); - @override final bool isSemanticBoundary; SemanticsAction action; @override - SemanticsAnnotator get semanticsAnnotator => _annotate; + void describeSemanticsConfiguration(SemanticsConfiguration config) { + super.describeSemanticsConfiguration(config); - void _annotate(SemanticsNode node) { - node.addAction(action); + config + ..isSemanticBoundary = isSemanticBoundary + ..addAction(action, () { }); } - } diff --git a/packages/flutter/test/rendering/simple_semantics_test.dart b/packages/flutter/test/rendering/simple_semantics_test.dart new file mode 100644 index 00000000000..7b61183bee7 --- /dev/null +++ b/packages/flutter/test/rendering/simple_semantics_test.dart @@ -0,0 +1,70 @@ +// 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/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:test/test.dart'; + +import 'rendering_tester.dart'; + + +void main() { + test('only send semantics update if semantics have changed', () { + final TestRender testRender = new TestRender() + ..label = 'hello' + ..textDirection = TextDirection.ltr; + + final RenderObject tree = new RenderConstrainedBox( + additionalConstraints: const BoxConstraints.tightFor(height: 20.0, width: 20.0), + child: testRender, + ); + int semanticsUpdateCount = 0; + final SemanticsHandle semanticsHandle = renderer.pipelineOwner.ensureSemantics( + listener: () { + ++semanticsUpdateCount; + } + ); + + layout(tree, phase: EnginePhase.flushSemantics); + + // Initial render does semantics. + expect(semanticsUpdateCount, 1); + expect(testRender.describeSemanticsConfigurationCallCount, isNot(0)); + + testRender.describeSemanticsConfigurationCallCount = 0; + semanticsUpdateCount = 0; + + // Request semantics update even though nothing changed. + testRender.markNeedsSemanticsUpdate(); + pumpFrame(phase: EnginePhase.flushSemantics); + + // Object is asked for semantics, but no update is sent. + expect(semanticsUpdateCount, 0); + expect(testRender.describeSemanticsConfigurationCallCount, 1); + + testRender.describeSemanticsConfigurationCallCount = 0; + semanticsUpdateCount = 0; + + // Change semantics and request update. + testRender.label = 'bye'; + testRender.markNeedsSemanticsUpdate(); + pumpFrame(phase: EnginePhase.flushSemantics); + + // Object is asked for semantics, and update is sent. + expect(semanticsUpdateCount, 1); + expect(testRender.describeSemanticsConfigurationCallCount, 1); + + semanticsHandle.dispose(); + }); +} + +class TestRender extends RenderSemanticsAnnotations { + int describeSemanticsConfigurationCallCount = 0; + + @override + void describeSemanticsConfiguration(SemanticsConfiguration config) { + super.describeSemanticsConfiguration(config); + describeSemanticsConfigurationCallCount += 1; + } +} diff --git a/packages/flutter/test/widgets/icon_test.dart b/packages/flutter/test/widgets/icon_test.dart index e2ebd543f4d..e5d8ecf971a 100644 --- a/packages/flutter/test/widgets/icon_test.dart +++ b/packages/flutter/test/widgets/icon_test.dart @@ -141,7 +141,7 @@ void main() { ), ); - expect(semantics, hasSemantics(new TestSemantics.root(label: 'a label'))); + expect(semantics, includesNodeWith(label: 'a label')); }); testWidgets('Null icon with semantic label', (WidgetTester tester) async { @@ -159,7 +159,7 @@ void main() { ), ); - expect(semantics, hasSemantics(new TestSemantics.root(label: 'a label'))); + expect(semantics, includesNodeWith(label: 'a label')); }); testWidgets('Changing semantic label from null doesn\'t rebuild tree ', (WidgetTester tester) async { diff --git a/packages/flutter/test/widgets/implicit_semantics_test.dart b/packages/flutter/test/widgets/implicit_semantics_test.dart new file mode 100644 index 00000000000..cb2bdfa5bf2 --- /dev/null +++ b/packages/flutter/test/widgets/implicit_semantics_test.dart @@ -0,0 +1,260 @@ +// 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 'dart:ui' show SemanticsFlags; + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'semantics_tester.dart'; + +void main() { + testWidgets('Implicit Semantics merge behavior', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Semantics( + container: true, + explicitChildNodes: false, + child: new Column( + children: [ + const Text('Michael Goderbauer'), + const Text('goderbauer@google.com'), + ], + ), + ), + ), + ); + + // SemanticsNode#0() + // └SemanticsNode#1(label: "Michael Goderbauer\ngoderbauer@google.com", textDirection: ltr) + expect( + semantics, + hasSemantics( + new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + id: 1, + label: 'Michael Goderbauer\ngoderbauer@google.com', + ), + ], + ), + ignoreRect: true, + ignoreTransform: true, + ), + ); + + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Semantics( + container: true, + explicitChildNodes: true, + child: new Column( + children: [ + const Text('Michael Goderbauer'), + const Text('goderbauer@google.com'), + ], + ), + ), + ), + ); + + // SemanticsNode#0() + // └SemanticsNode#1() + // ├SemanticsNode#2(label: "Michael Goderbauer", textDirection: ltr) + // └SemanticsNode#3(label: "goderbauer@google.com", textDirection: ltr) + expect( + semantics, + hasSemantics( + new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + id: 1, + children: [ + new TestSemantics( + id: 2, + label: 'Michael Goderbauer', + ), + new TestSemantics( + id: 3, + label: 'goderbauer@google.com', + ), + ], + ), + ], + ), + ignoreRect: true, + ignoreTransform: true, + ), + ); + + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Semantics( + container: true, + explicitChildNodes: true, + child: new Semantics( + label: 'Signed in as', + child: new Column( + children: [ + const Text('Michael Goderbauer'), + const Text('goderbauer@google.com'), + ], + ), + ), + ), + ), + ); + + // SemanticsNode#0() + // └SemanticsNode#1() + // └SemanticsNode#4(label: "Signed in as\nMichael Goderbauer\ngoderbauer@google.com", textDirection: ltr) + expect( + semantics, + hasSemantics( + new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + id: 1, + children: [ + new TestSemantics( + id: 4, + label: 'Signed in as\nMichael Goderbauer\ngoderbauer@google.com', + ), + ], + ), + ], + ), + ignoreRect: true, + ignoreTransform: true, + ), + ); + + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Semantics( + container: true, + explicitChildNodes: false, + child: new Semantics( + label: 'Signed in as', + child: new Column( + children: [ + const Text('Michael Goderbauer'), + const Text('goderbauer@google.com'), + ], + ), + ), + ), + ), + ); + + // SemanticsNode#0() + // └SemanticsNode#1(label: "Signed in as\nMichael Goderbauer\ngoderbauer@google.com", textDirection: ltr) + expect( + semantics, + hasSemantics( + new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + id: 1, + label: 'Signed in as\nMichael Goderbauer\ngoderbauer@google.com', + ), + ], + ), + ignoreRect: true, + ignoreTransform: true, + ), + ); + + semantics.dispose(); + }); + + testWidgets('Do not merge with conflicts', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Semantics( + container: true, + explicitChildNodes: false, + child: new Column( + children: [ + new Semantics( + label: 'node 1', + selected: true, + child: new Container( + width: 10.0, + height: 10.0, + ), + ), + new Semantics( + label: 'node 2', + selected: true, + child: new Container( + width: 10.0, + height: 10.0, + ), + ), + new Semantics( + label: 'node 3', + selected: true, + child: new Container( + width: 10.0, + height: 10.0, + ), + ), + ], + ), + ), + ), + ); + + // SemanticsNode#0() + // └SemanticsNode#8() + // ├SemanticsNode#5(selected, label: "node 1", textDirection: ltr) + // ├SemanticsNode#6(selected, label: "node 2", textDirection: ltr) + // └SemanticsNode#7(selected, label: "node 3", textDirection: ltr) + expect( + semantics, + hasSemantics( + new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + id: 8, + children: [ + new TestSemantics( + id: 5, + flags: SemanticsFlags.isSelected.index, + label: 'node 1', + ), + new TestSemantics( + id: 6, + flags: SemanticsFlags.isSelected.index, + label: 'node 2', + ), + new TestSemantics( + id: 7, + flags: SemanticsFlags.isSelected.index, + label: 'node 3', + ), + ], + ), + ], + ), + ignoreRect: true, + ignoreTransform: true, + ), + ); + + semantics.dispose(); + }); +} diff --git a/packages/flutter/test/widgets/modal_barrier_test.dart b/packages/flutter/test/widgets/modal_barrier_test.dart index c58702fc897..b8c697b07cb 100644 --- a/packages/flutter/test/widgets/modal_barrier_test.dart +++ b/packages/flutter/test/widgets/modal_barrier_test.dart @@ -104,11 +104,11 @@ void main() { final TestSemantics expectedSemantics = new TestSemantics.root( children: [ new TestSemantics.rootChild( - id: 1, + id: 2, rect: TestSemantics.fullScreen, children: [ new TestSemantics( - id: 2, + id: 1, rect: TestSemantics.fullScreen, actions: SemanticsAction.tap.index, ), diff --git a/packages/flutter/test/widgets/semantics_10_test.dart b/packages/flutter/test/widgets/semantics_10_test.dart index 5ed047d29b8..d7603f83f89 100644 --- a/packages/flutter/test/widgets/semantics_10_test.dart +++ b/packages/flutter/test/widgets/semantics_10_test.dart @@ -94,12 +94,19 @@ class TestWidget extends SingleChildRenderObjectWidget { } class RenderTest extends RenderProxyBox { - @override - SemanticsAnnotator get semanticsAnnotator => isSemanticBoundary ? _annotate : null; - void _annotate(SemanticsNode node) { - node.label = _label; - node.textDirection = TextDirection.ltr; + @override + void describeSemanticsConfiguration(SemanticsConfiguration config) { + super.describeSemanticsConfiguration(config); + + if (!_isSemanticBoundary) + return; + + config + ..isSemanticBoundary = _isSemanticBoundary + ..label = _label + ..textDirection = TextDirection.ltr; + } String _label; @@ -111,8 +118,7 @@ class RenderTest extends RenderProxyBox { callLog.add('markNeedsSemanticsUpdate(onlyChanges: true)'); } - @override - bool get isSemanticBoundary => _isSemanticBoundary; + bool _isSemanticBoundary; set isSemanticBoundary(bool value) { if (_isSemanticBoundary == value) diff --git a/packages/flutter/test/widgets/semantics_11_test.dart b/packages/flutter/test/widgets/semantics_11_test.dart deleted file mode 100644 index 91c52bd21c4..00000000000 --- a/packages/flutter/test/widgets/semantics_11_test.dart +++ /dev/null @@ -1,77 +0,0 @@ -// 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/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'semantics_tester.dart'; - -void main() { - testWidgets('markNeedsSemanticsUpdate allways resets node', (WidgetTester tester) async { - final SemanticsTester semantics = new SemanticsTester(tester); - - await tester.pumpWidget(const TestWidget()); - final RenderTest renderObj = tester.renderObject(find.byType(TestWidget)); - expect(renderObj.labelWasReset, hasLength(1)); - expect(renderObj.labelWasReset.last, true); - expect(semantics, includesNodeWith(label: 'Label 1')); - - renderObj.markNeedsSemanticsUpdate(onlyLocalUpdates: false, noGeometry: false); - await tester.pumpAndSettle(); - - expect(renderObj.labelWasReset, hasLength(2)); - expect(renderObj.labelWasReset.last, true); - expect(semantics, includesNodeWith(label: 'Label 2')); - - renderObj.markNeedsSemanticsUpdate(onlyLocalUpdates: true, noGeometry: false); - await tester.pumpAndSettle(); - - expect(renderObj.labelWasReset, hasLength(3)); - expect(renderObj.labelWasReset.last, true); - expect(semantics, includesNodeWith(label: 'Label 3')); - - renderObj.markNeedsSemanticsUpdate(onlyLocalUpdates: true, noGeometry: true); - await tester.pumpAndSettle(); - - expect(renderObj.labelWasReset, hasLength(4)); - expect(renderObj.labelWasReset.last, true); - expect(semantics, includesNodeWith(label: 'Label 4')); - - renderObj.markNeedsSemanticsUpdate(onlyLocalUpdates: false, noGeometry: true); - await tester.pumpAndSettle(); - - expect(renderObj.labelWasReset, hasLength(5)); - expect(renderObj.labelWasReset.last, true); - expect(semantics, includesNodeWith(label: 'Label 5')); - - semantics.dispose(); - }); -} - -class TestWidget extends SingleChildRenderObjectWidget { - const TestWidget({ - Key key, - Widget child, - }) : super(key: key, child: child); - - @override - RenderTest createRenderObject(BuildContext context) { - return new RenderTest(); - } -} - -class RenderTest extends RenderProxyBox { - List labelWasReset = []; - - @override - SemanticsAnnotator get semanticsAnnotator => _annotate; - - void _annotate(SemanticsNode node) { - labelWasReset.add(node.label == ''); - node.label = 'Label ${labelWasReset.length}'; - node.textDirection = TextDirection.ltr; - } -} diff --git a/packages/flutter/test/widgets/semantics_1_test.dart b/packages/flutter/test/widgets/semantics_1_test.dart index 11e7f9a5d1f..2263cd612e3 100644 --- a/packages/flutter/test/widgets/semantics_1_test.dart +++ b/packages/flutter/test/widgets/semantics_1_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui' show SemanticsFlags; + import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; @@ -15,136 +17,221 @@ void main() { // smoketest await tester.pumpWidget( - new Container( - child: new Semantics( - label: 'test1', - textDirection: TextDirection.ltr, - child: new Container() - ) - ) + new Semantics( + container: true, + child: new Container( + child: new Semantics( + label: 'test1', + textDirection: TextDirection.ltr, + child: new Container(), + selected: true, + ), + ), + ), ); - expect(semantics, hasSemantics(new TestSemantics.root(label: 'test1'))); + expect(semantics, hasSemantics(new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + id: 1, + label: 'test1', + rect: TestSemantics.fullScreen, + flags: SemanticsFlags.isSelected.index, + ) + ] + ))); // control for forking await tester.pumpWidget( - new Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - new Container( - height: 10.0, - child: const Semantics(label: 'child1', textDirection: TextDirection.ltr), - ), - new Container( - height: 10.0, - child: const IgnorePointer( - ignoring: true, - child: const Semantics(label: 'child2', textDirection: TextDirection.ltr), - ) - ), - ], - ) + new Semantics( + container: true, + child: new Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + new Container( + height: 10.0, + child: const Semantics( + label: 'child1', + textDirection: TextDirection.ltr, + selected: true, + ), + ), + new Container( + height: 10.0, + child: const IgnorePointer( + ignoring: true, + child: const Semantics( + label: 'child1', + textDirection: TextDirection.ltr, + selected: true, + ), + ), + ), + ], + ), + ), ); - expect(semantics, hasSemantics(new TestSemantics.root(label: 'child1'))); + expect(semantics, hasSemantics(new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + id: 1, + label: 'child1', + rect: TestSemantics.fullScreen, + flags: SemanticsFlags.isSelected.index, + ) + ], + ))); // forking semantics await tester.pumpWidget( - new Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - new Container( - height: 10.0, - child: const Semantics(label: 'child1', textDirection: TextDirection.ltr), - ), - new Container( - height: 10.0, - child: const IgnorePointer( - ignoring: false, - child: const Semantics(label: 'child2', textDirection: TextDirection.ltr), - ) - ), - ], - ) + new Semantics( + container: true, + child: new Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + new Container( + height: 10.0, + child: const Semantics( + label: 'child1', + textDirection: TextDirection.ltr, + selected: true, + ), + ), + new Container( + height: 10.0, + child: const IgnorePointer( + ignoring: false, + child: const Semantics( + label: 'child2', + textDirection: TextDirection.ltr, + selected: true, + ), + ), + ), + ], + ), + ), ); - expect(semantics, hasSemantics( - new TestSemantics.root( - children: [ - new TestSemantics.rootChild( - id: 1, - label: 'child1', - rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), - ), - new TestSemantics.rootChild( - id: 2, - label: 'child2', - rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), - transform: new Matrix4.translationValues(0.0, 10.0, 0.0), - ), - ], - ) - )); + expect(semantics, hasSemantics(new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + id: 1, + rect: TestSemantics.fullScreen, + children: [ + new TestSemantics( + id: 2, + label: 'child1', + rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), + flags: SemanticsFlags.isSelected.index, + ), + new TestSemantics( + id: 3, + label: 'child2', + rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), + flags: SemanticsFlags.isSelected.index, + ), + ], + ), + ], + ), ignoreTransform: true)); // toggle a branch off await tester.pumpWidget( - new Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - new Container( - height: 10.0, - child: const Semantics(label: 'child1', textDirection: TextDirection.ltr) - ), - new Container( - height: 10.0, - child: const IgnorePointer( - ignoring: true, - child: const Semantics(label: 'child2', textDirection: TextDirection.ltr) - ) - ), - ], - ) + new Semantics( + container: true, + child: new Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + new Container( + height: 10.0, + child: const Semantics( + label: 'child1', + textDirection: TextDirection.ltr, + selected: true, + ), + ), + new Container( + height: 10.0, + child: const IgnorePointer( + ignoring: true, + child: const Semantics( + label: 'child2', + textDirection: TextDirection.ltr, + selected: true, + ), + ), + ), + ], + ), + ), ); - expect(semantics, hasSemantics(new TestSemantics.root(label: 'child1'))); + expect(semantics, hasSemantics(new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + id: 1, + label: 'child1', + rect: TestSemantics.fullScreen, + flags: SemanticsFlags.isSelected.index, + ) + ], + ))); // toggle a branch back on await tester.pumpWidget( - new Column( - children: [ - new Container( - height: 10.0, - child: const Semantics(label: 'child1', textDirection: TextDirection.ltr) - ), - new Container( - height: 10.0, - child: const IgnorePointer( - ignoring: false, - child: const Semantics(label: 'child2', textDirection: TextDirection.ltr) - ) - ), - ], - crossAxisAlignment: CrossAxisAlignment.stretch - ) + new Semantics( + container: true, + child: new Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + new Container( + height: 10.0, + child: const Semantics( + label: 'child1', + textDirection: TextDirection.ltr, + selected: true, + ), + ), + new Container( + height: 10.0, + child: const IgnorePointer( + ignoring: false, + child: const Semantics( + label: 'child2', + textDirection: TextDirection.ltr, + selected: true, + ), + ), + ), + ], + ), + ), ); - expect(semantics, hasSemantics( - new TestSemantics.root( - children: [ - new TestSemantics.rootChild( - id: 3, - label: 'child1', - rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), - ), - new TestSemantics.rootChild( - id: 2, - label: 'child2', - rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), - transform: new Matrix4.translationValues(0.0, 10.0, 0.0), - ), - ], - ) - )); + expect(semantics, hasSemantics(new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + id: 1, + rect: TestSemantics.fullScreen, + children: [ + new TestSemantics( + id: 4, + label: 'child1', + rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), + flags: SemanticsFlags.isSelected.index, + ), + new TestSemantics( + id: 3, + label: 'child2', + rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), + flags: SemanticsFlags.isSelected.index, + ), + ], + ), + ], + ), ignoreTransform: true)); semantics.dispose(); }); diff --git a/packages/flutter/test/widgets/semantics_2_test.dart b/packages/flutter/test/widgets/semantics_2_test.dart index 063d8b735e4..574d72821fa 100644 --- a/packages/flutter/test/widgets/semantics_2_test.dart +++ b/packages/flutter/test/widgets/semantics_2_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui' show SemanticsFlags; + import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; @@ -19,105 +21,153 @@ void main() { // forking semantics await tester.pumpWidget( - new Column( - children: [ - new Container( - height: 10.0, - child: const Semantics(label: 'child1', textDirection: TextDirection.ltr) - ), - new Container( - height: 10.0, - child: const IgnorePointer( - ignoring: false, - child: const Semantics(label: 'child2', textDirection: TextDirection.ltr) - ) - ), - ], - crossAxisAlignment: CrossAxisAlignment.stretch - ) + new Semantics( + container: true, + child: new Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + new Container( + height: 10.0, + child: const Semantics( + label: 'child1', + textDirection: TextDirection.ltr, + selected: true, + ), + ), + new Container( + height: 10.0, + child: const IgnorePointer( + ignoring: false, + child: const Semantics( + label: 'child2', + textDirection: TextDirection.ltr, + selected: true, + ), + ), + ), + ], + ), + ), ); - expect(semantics, hasSemantics( - new TestSemantics.root( - children: [ - new TestSemantics.rootChild( - id: 1, - label: 'child1', - textDirection: TextDirection.ltr, - rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), - ), - new TestSemantics.rootChild( - id: 2, - label: 'child2', - textDirection: TextDirection.ltr, - rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), - transform: new Matrix4.translationValues(0.0, 10.0, 0.0), - ), - ], - ) - )); + expect(semantics, hasSemantics(new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + id: 3, + rect: TestSemantics.fullScreen, + children: [ + new TestSemantics( + id: 1, + label: 'child1', + rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), + flags: SemanticsFlags.isSelected.index, + ), + new TestSemantics( + id: 2, + label: 'child2', + rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), + flags: SemanticsFlags.isSelected.index, + ), + ], + ), + ], + ), ignoreTransform: true)); // toggle a branch off await tester.pumpWidget( - new Column( - children: [ - new Container( - height: 10.0, - child: const Semantics(label: 'child1', textDirection: TextDirection.ltr) - ), - new Container( - height: 10.0, - child: const IgnorePointer( - ignoring: true, - child: const Semantics(label: 'child2', textDirection: TextDirection.ltr) - ) - ), - ], - crossAxisAlignment: CrossAxisAlignment.stretch - ) + new Semantics( + container: true, + child: new Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + new Container( + height: 10.0, + child: const Semantics( + label: 'child1', + textDirection: TextDirection.ltr, + selected: true, + ), + ), + new Container( + height: 10.0, + child: const IgnorePointer( + ignoring: true, + child: const Semantics( + label: 'child2', + textDirection: TextDirection.ltr, + selected: true, + ), + ), + ), + ], + ), + ), ); - expect(semantics, hasSemantics(new TestSemantics.root(label: 'child1'))); + expect(semantics, hasSemantics(new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + id: 3, + label: 'child1', + rect: TestSemantics.fullScreen, + flags: SemanticsFlags.isSelected.index, + ) + ], + ))); // toggle a branch back on await tester.pumpWidget( - new Column( - children: [ - new Container( - height: 10.0, - child: const Semantics(label: 'child1', textDirection: TextDirection.ltr) - ), - new Container( - height: 10.0, - child: const IgnorePointer( - ignoring: false, - child: const Semantics(label: 'child2', textDirection: TextDirection.ltr) - ) - ), - ], - crossAxisAlignment: CrossAxisAlignment.stretch - ) + new Semantics( + container: true, + child: new Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + new Container( + height: 10.0, + child: const Semantics( + label: 'child1', + textDirection: TextDirection.ltr, + selected: true, + ), + ), + new Container( + height: 10.0, + child: const IgnorePointer( + ignoring: false, + child: const Semantics( + label: 'child2', + textDirection: TextDirection.ltr, + selected: true, + ), + ), + ), + ], + ), + ), ); - expect(semantics, hasSemantics( - new TestSemantics.root( - children: [ - new TestSemantics.rootChild( - id: 3, - label: 'child1', - textDirection: TextDirection.ltr, - rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), - ), - new TestSemantics.rootChild( - id: 2, - label: 'child2', - textDirection: TextDirection.ltr, - rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), - transform: new Matrix4.translationValues(0.0, 10.0, 0.0), - ), - ], - ) - )); + expect(semantics, hasSemantics(new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + id: 3, + rect: TestSemantics.fullScreen, + children: [ + new TestSemantics( + id: 4, + label: 'child1', + rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), + flags: SemanticsFlags.isSelected.index, + ), + new TestSemantics( + id: 2, + label: 'child2', + rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), + flags: SemanticsFlags.isSelected.index, + ), + ], + ), + ], + ), ignoreTransform: true)); semantics.dispose(); }); diff --git a/packages/flutter/test/widgets/semantics_3_test.dart b/packages/flutter/test/widgets/semantics_3_test.dart index 7c24528eaea..c899eae58ba 100644 --- a/packages/flutter/test/widgets/semantics_3_test.dart +++ b/packages/flutter/test/widgets/semantics_3_test.dart @@ -16,81 +16,112 @@ void main() { // implicit annotators await tester.pumpWidget( - new Container( - child: new Semantics( - label: 'test', - textDirection: TextDirection.ltr, - child: new Container( - child: const Semantics( - checked: true - ) - ) - ) - ) + new Semantics( + container: true, + child: new Container( + child: new Semantics( + label: 'test', + textDirection: TextDirection.ltr, + child: new Container( + child: const Semantics( + checked: true + ), + ), + ), + ), + ), ); expect(semantics, hasSemantics( new TestSemantics.root( - flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, - label: 'test', + children: [ + new TestSemantics.rootChild( + id: 1, + flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, + label: 'test', + rect: TestSemantics.fullScreen, + ) + ] ) )); // remove one await tester.pumpWidget( - new Container( + new Semantics( + container: true, child: new Container( child: const Semantics( - checked: true - ) - ) - ) + checked: true, + ), + ), + ), ); expect(semantics, hasSemantics( new TestSemantics.root( - flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, + children: [ + new TestSemantics.rootChild( + id: 1, + flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, + rect: TestSemantics.fullScreen, + ), + ] ) )); // change what it says await tester.pumpWidget( - new Container( + new Semantics( + container: true, child: new Container( child: const Semantics( label: 'test', textDirection: TextDirection.ltr, - ) - ) - ) + ), + ), + ), ); expect(semantics, hasSemantics( new TestSemantics.root( - label: 'test', + children: [ + new TestSemantics.rootChild( + id: 1, + label: 'test', + textDirection: TextDirection.ltr, + rect: TestSemantics.fullScreen, + ), + ] ) )); // add a node await tester.pumpWidget( - new Container( - child: new Semantics( - checked: true, - child: new Container( + new Semantics( + container: true, + child: new Container( + child: const Semantics( + checked: true, child: const Semantics( label: 'test', textDirection: TextDirection.ltr, - ) - ) - ) - ) + ), + ), + ), + ), ); expect(semantics, hasSemantics( new TestSemantics.root( - flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, - label: 'test', - ) + children: [ + new TestSemantics.rootChild( + id: 1, + flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, + label: 'test', + rect: TestSemantics.fullScreen, + ) + ], + ), )); int changeCount = 0; @@ -100,17 +131,18 @@ void main() { // make no changes await tester.pumpWidget( - new Container( - child: new Semantics( - checked: true, - child: new Container( + new Semantics( + container: true, + child: new Container( + child: const Semantics( + checked: true, child: const Semantics( label: 'test', textDirection: TextDirection.ltr, - ) - ) - ) - ) + ), + ), + ), + ), ); expect(changeCount, 0); diff --git a/packages/flutter/test/widgets/semantics_4_test.dart b/packages/flutter/test/widgets/semantics_4_test.dart index 5902698c69a..3e1d89f7e4d 100644 --- a/packages/flutter/test/widgets/semantics_4_test.dart +++ b/packages/flutter/test/widgets/semantics_4_test.dart @@ -6,6 +6,7 @@ import 'dart:ui' show SemanticsFlags; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'semantics_tester.dart'; @@ -26,10 +27,12 @@ void main() { fit: StackFit.expand, children: [ const Semantics( + container: true, label: 'L1', ), new Semantics( label: 'L2', + container: true, child: new Stack( fit: StackFit.expand, children: [ @@ -50,22 +53,22 @@ void main() { new TestSemantics.root( children: [ new TestSemantics.rootChild( - id: 1, + id: 3, label: 'L1', rect: TestSemantics.fullScreen, ), new TestSemantics.rootChild( - id: 2, + id: 4, label: 'L2', rect: TestSemantics.fullScreen, children: [ new TestSemantics( - id: 3, + id: 1, flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, rect: TestSemantics.fullScreen, ), new TestSemantics( - id: 4, + id: 2, flags: SemanticsFlags.hasCheckedState.index, rect: TestSemantics.fullScreen, ), @@ -87,9 +90,11 @@ void main() { children: [ const Semantics( label: 'L1', + container: true, ), new Semantics( label: 'L2', + container: true, child: new Stack( fit: StackFit.expand, children: [ @@ -108,12 +113,12 @@ void main() { new TestSemantics.root( children: [ new TestSemantics.rootChild( - id: 1, + id: 3, label: 'L1', rect: TestSemantics.fullScreen, ), new TestSemantics.rootChild( - id: 2, + id: 4, label: 'L2', flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, rect: TestSemantics.fullScreen, @@ -134,6 +139,7 @@ void main() { const Semantics(), new Semantics( label: 'L2', + container: true, child: new Stack( fit: StackFit.expand, children: [ @@ -150,8 +156,14 @@ void main() { expect(semantics, hasSemantics( new TestSemantics.root( - label: 'L2', - flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, + children: [ + new TestSemantics.rootChild( + id: 4, + label: 'L2', + flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, + rect: TestSemantics.fullScreen, + ), + ], ) )); diff --git a/packages/flutter/test/widgets/semantics_7_test.dart b/packages/flutter/test/widgets/semantics_7_test.dart index e5b8377e5ab..96e7f8e9be0 100644 --- a/packages/flutter/test/widgets/semantics_7_test.dart +++ b/packages/flutter/test/widgets/semantics_7_test.dart @@ -55,7 +55,7 @@ void main() { new TestSemantics.root( children: [ new TestSemantics.rootChild( - id: 1, + id: 3, flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, label: label, rect: TestSemantics.fullScreen, @@ -111,7 +111,7 @@ void main() { new TestSemantics.root( children: [ new TestSemantics.rootChild( - id: 1, + id: 3, flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, label: label, rect: TestSemantics.fullScreen, diff --git a/packages/flutter/test/widgets/semantics_8_test.dart b/packages/flutter/test/widgets/semantics_8_test.dart index 58f1c8107a4..c12d82f14e8 100644 --- a/packages/flutter/test/widgets/semantics_8_test.dart +++ b/packages/flutter/test/widgets/semantics_8_test.dart @@ -39,9 +39,15 @@ void main() { expect(semantics, hasSemantics( new TestSemantics.root( - flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, - label: 'label', - textDirection: TextDirection.ltr, + children: [ + new TestSemantics.rootChild( + id: 3, + flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, + label: 'label', + textDirection: TextDirection.ltr, + rect: TestSemantics.fullScreen, + ) + ] ) )); @@ -71,10 +77,16 @@ void main() { expect(semantics, hasSemantics( new TestSemantics.root( - flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, - label: 'label', - textDirection: TextDirection.ltr, - ) + children: [ + new TestSemantics.rootChild( + id: 3, + flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, + label: 'label', + textDirection: TextDirection.ltr, + rect: TestSemantics.fullScreen, + ) + ], + ), )); semantics.dispose(); diff --git a/packages/flutter/test/widgets/semantics_9_test.dart b/packages/flutter/test/widgets/semantics_9_test.dart index 4480f2ffdfb..faff0f337ea 100644 --- a/packages/flutter/test/widgets/semantics_9_test.dart +++ b/packages/flutter/test/widgets/semantics_9_test.dart @@ -61,6 +61,7 @@ void main() { new Semantics( label: '#2', container: true, + explicitChildNodes: true, child: new Stack( children: [ new Semantics( @@ -146,9 +147,12 @@ class RenderBoundaryBlockSemantics extends RenderProxyBox { RenderBoundaryBlockSemantics({ RenderBox child }) : super(child); @override - bool get isBlockingSemanticsOfPreviouslyPaintedNodes => true; + void describeSemanticsConfiguration(SemanticsConfiguration config) { + super.describeSemanticsConfiguration(config); - @override - bool get isSemanticBoundary => true; + config + ..isBlockingSemanticsOfPreviouslyPaintedNodes = true + ..isSemanticBoundary = true; + } } diff --git a/packages/flutter/test/widgets/semantics_clipping_test.dart b/packages/flutter/test/widgets/semantics_clipping_test.dart index aef3c49cbad..76e5364d952 100644 --- a/packages/flutter/test/widgets/semantics_clipping_test.dart +++ b/packages/flutter/test/widgets/semantics_clipping_test.dart @@ -62,15 +62,9 @@ void main() { final SemanticsNode node1 = tester.renderObject(find.byWidget(const Text('1'))).debugSemantics; final SemanticsNode node2 = tester.renderObject(find.byWidget(const Text('2'))).debugSemantics; - final SemanticsNode node3 = tester.renderObject(find.byWidget(const Text('3'))).debugSemantics; expect(node1.wasAffectedByClip, false); expect(node2.wasAffectedByClip, true); - expect(node3.wasAffectedByClip, true); - - expect(node1.isInvisible, isFalse); - expect(node2.isInvisible, isFalse); - expect(node3.isInvisible, isTrue); semantics.dispose(); }); @@ -117,12 +111,12 @@ void main() { new TestSemantics.root( children: [ new TestSemantics( - id: 4, + id: 3, label: '1', rect: new Rect.fromLTRB(0.0, 0.0, 75.0, 14.0), ), new TestSemantics( - id: 5, + id: 4, label: '2\n3', rect: new Rect.fromLTRB(0.0, 0.0, 25.0, 14.0), // clipped form original 75.0 to 25.0 ), diff --git a/packages/flutter/test/widgets/semantics_merge_test.dart b/packages/flutter/test/widgets/semantics_merge_test.dart index 4cd35c2c534..26cd124f127 100644 --- a/packages/flutter/test/widgets/semantics_merge_test.dart +++ b/packages/flutter/test/widgets/semantics_merge_test.dart @@ -53,7 +53,14 @@ void main() { ); expect(semantics, hasSemantics( - new TestSemantics.root(label: 'test1\ntest2'), + new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + id: 3, + label: 'test1\ntest2', + ), + ] + ), ignoreRect: true, ignoreTransform: true, )); @@ -74,8 +81,8 @@ void main() { expect(semantics, hasSemantics( new TestSemantics.root( children: [ - new TestSemantics.rootChild(id: 5, label: 'test1'), - new TestSemantics.rootChild(id: 6, label: 'test2'), + new TestSemantics.rootChild(id: 4, label: 'test1'), + new TestSemantics.rootChild(id: 5, label: 'test2'), ], ), ignoreRect: true, diff --git a/packages/flutter/test/widgets/semantics_test.dart b/packages/flutter/test/widgets/semantics_test.dart index fcadf8af06e..573239ecd4f 100644 --- a/packages/flutter/test/widgets/semantics_test.dart +++ b/packages/flutter/test/widgets/semantics_test.dart @@ -13,8 +13,12 @@ void main() { SemanticsTester semantics = new SemanticsTester(tester); final TestSemantics expectedSemantics = new TestSemantics.root( - label: 'test1', - textDirection: TextDirection.ltr, + children: [ + new TestSemantics.rootChild( + label: 'test1', + textDirection: TextDirection.ltr, + ) + ], ); await tester.pumpWidget( @@ -27,7 +31,12 @@ void main() { ) ); - expect(semantics, hasSemantics(expectedSemantics)); + expect(semantics, hasSemantics( + expectedSemantics, + ignoreTransform: true, + ignoreRect: true, + ignoreId: true, + )); semantics.dispose(); semantics = null; @@ -37,7 +46,12 @@ void main() { expect(tester.binding.hasScheduledFrame, isTrue); await tester.pump(); - expect(semantics, hasSemantics(expectedSemantics)); + expect(semantics, hasSemantics( + expectedSemantics, + ignoreTransform: true, + ignoreRect: true, + ignoreId: true, + )); semantics.dispose(); }); @@ -62,15 +76,20 @@ void main() { expect(semantics, hasSemantics( new TestSemantics.root( - label: 'test1', children: [ new TestSemantics.rootChild( - id: 1, - label: 'test2a', - rect: TestSemantics.fullScreen, + label: 'test1', + children: [ + new TestSemantics( + label: 'test2a', + ) + ] ) ] - ) + ), + ignoreId: true, + ignoreRect: true, + ignoreTransform: true, )); await tester.pumpWidget(new Directionality( @@ -94,22 +113,25 @@ void main() { expect(semantics, hasSemantics( new TestSemantics.root( - label: 'test1', - children: [ - new TestSemantics.rootChild( - id: 2, - label: 'middle', - rect: TestSemantics.fullScreen, - children: [ - new TestSemantics( - id: 1, - label: 'test2b', - rect: TestSemantics.fullScreen, - ) - ] - ) - ] - ) + children: [ + new TestSemantics.rootChild( + label: 'test1', + children: [ + new TestSemantics( + label: 'middle', + children: [ + new TestSemantics( + label: 'test2b', + ), + ], + ) + ] + ) + ] + ), + ignoreId: true, + ignoreRect: true, + ignoreTransform: true, )); semantics.dispose(); @@ -118,11 +140,6 @@ void main() { testWidgets('Semantics and Directionality - RTL', (WidgetTester tester) async { final SemanticsTester semantics = new SemanticsTester(tester); - final TestSemantics expectedSemantics = new TestSemantics.root( - label: 'test1', - textDirection: TextDirection.rtl, - ); - await tester.pumpWidget( new Directionality( textDirection: TextDirection.rtl, @@ -133,17 +150,12 @@ void main() { ), ); - expect(semantics, hasSemantics(expectedSemantics)); + expect(semantics, includesNodeWith(label: 'test1', textDirection: TextDirection.rtl)); }); testWidgets('Semantics and Directionality - LTR', (WidgetTester tester) async { final SemanticsTester semantics = new SemanticsTester(tester); - final TestSemantics expectedSemantics = new TestSemantics.root( - label: 'test1', - textDirection: TextDirection.ltr, - ); - await tester.pumpWidget( new Directionality( textDirection: TextDirection.ltr, @@ -154,15 +166,19 @@ void main() { ), ); - expect(semantics, hasSemantics(expectedSemantics)); + expect(semantics, includesNodeWith(label: 'test1', textDirection: TextDirection.ltr)); }); - testWidgets('Semantics and Directionality - overriding RTL with LTR', (WidgetTester tester) async { + testWidgets('Semantics and Directionality - cannot override RTL with LTR', (WidgetTester tester) async { final SemanticsTester semantics = new SemanticsTester(tester); final TestSemantics expectedSemantics = new TestSemantics.root( - label: 'test1', - textDirection: TextDirection.ltr, + children: [ + new TestSemantics.rootChild( + label: 'test1', + textDirection: TextDirection.ltr, + ) + ] ); await tester.pumpWidget( @@ -176,15 +192,19 @@ void main() { ), ); - expect(semantics, hasSemantics(expectedSemantics)); + expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true)); }); - testWidgets('Semantics and Directionality - overriding LTR with RTL', (WidgetTester tester) async { + testWidgets('Semantics and Directionality - cannot override LTR with RTL', (WidgetTester tester) async { final SemanticsTester semantics = new SemanticsTester(tester); final TestSemantics expectedSemantics = new TestSemantics.root( - label: 'test1', - textDirection: TextDirection.rtl, + children: [ + new TestSemantics.rootChild( + label: 'test1', + textDirection: TextDirection.rtl, + ) + ] ); await tester.pumpWidget( @@ -198,6 +218,6 @@ void main() { ), ); - expect(semantics, hasSemantics(expectedSemantics)); + expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true)); }); } diff --git a/packages/flutter/test/widgets/semantics_tester.dart b/packages/flutter/test/widgets/semantics_tester.dart index b7c09a573a4..4bdb3f81598 100644 --- a/packages/flutter/test/widgets/semantics_tester.dart +++ b/packages/flutter/test/widgets/semantics_tester.dart @@ -31,7 +31,7 @@ class TestSemantics { /// * [TestSemantics.fullScreen] 800x600, the test screen's size in logical /// pixels, useful for other full-screen widgets. TestSemantics({ - @required this.id, + this.id, this.flags: 0, this.actions: 0, this.label: '', @@ -40,8 +40,7 @@ class TestSemantics { this.transform, this.children: const [], Iterable tags, - }) : assert(id != null), - assert(flags != null), + }) : assert(flags != null), assert(label != null), assert(children != null), tags = tags?.toSet() ?? new Set(); @@ -73,7 +72,7 @@ class TestSemantics { /// [TestSemantics.fullScreen] property may be useful as a value; it describes /// an 800x600 rectangle, which is the test screen's size in logical pixels. TestSemantics.rootChild({ - @required this.id, + this.id, this.flags: 0, this.actions: 0, this.label: '', @@ -152,7 +151,7 @@ class TestSemantics { /// The tags of this node. final Set tags; - bool _matches(SemanticsNode node, Map matchState, { bool ignoreRect: false, bool ignoreTransform: false }) { + bool _matches(SemanticsNode node, Map matchState, { bool ignoreRect: false, bool ignoreTransform: false, bool ignoreId: false }) { final SemanticsData nodeData = node.getSemanticsData(); bool fail(String message) { @@ -162,7 +161,7 @@ class TestSemantics { if (node == null) return fail('could not find node with id $id.'); - if (id != node.id) + if (!ignoreId && id != node.id) return fail('expected node id $id but found id ${node.id}.'); if (flags != nodeData.flags) return fail('expected node id $id to have flags $flags but found flags ${nodeData.flags}.'); @@ -188,7 +187,7 @@ class TestSemantics { final Iterator it = children.iterator; node.visitChildren((SemanticsNode node) { it.moveNext(); - if (!it.current._matches(node, matchState, ignoreRect: ignoreRect, ignoreTransform: ignoreTransform)) { + if (!it.current._matches(node, matchState, ignoreRect: ignoreRect, ignoreTransform: ignoreTransform, ignoreId: ignoreId)) { result = false; return false; } @@ -233,15 +232,16 @@ class SemanticsTester { } class _HasSemantics extends Matcher { - const _HasSemantics(this._semantics, { this.ignoreRect: false, this.ignoreTransform: false }) : assert(_semantics != null), assert(ignoreRect != null), assert(ignoreTransform != null); + const _HasSemantics(this._semantics, { this.ignoreRect: false, this.ignoreTransform: false, this.ignoreId: false }) : assert(_semantics != null), assert(ignoreRect != null), assert(ignoreId != null), assert(ignoreTransform != null); final TestSemantics _semantics; final bool ignoreRect; final bool ignoreTransform; + final bool ignoreId; @override bool matches(covariant SemanticsTester item, Map matchState) { - return _semantics._matches(item.tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode, matchState, ignoreTransform: ignoreTransform, ignoreRect: ignoreRect); + return _semantics._matches(item.tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode, matchState, ignoreTransform: ignoreTransform, ignoreRect: ignoreRect, ignoreId: ignoreId); } @override @@ -259,7 +259,8 @@ class _HasSemantics extends Matcher { Matcher hasSemantics(TestSemantics semantics, { bool ignoreRect: false, bool ignoreTransform: false, -}) => new _HasSemantics(semantics, ignoreRect: ignoreRect, ignoreTransform: ignoreTransform); + bool ignoreId: false, +}) => new _HasSemantics(semantics, ignoreRect: ignoreRect, ignoreTransform: ignoreTransform, ignoreId: ignoreId); class _IncludesNodeWith extends Matcher { const _IncludesNodeWith({ diff --git a/packages/flutter/test/widgets/simple_semantics_test.dart b/packages/flutter/test/widgets/simple_semantics_test.dart new file mode 100644 index 00000000000..9f4afe7723e --- /dev/null +++ b/packages/flutter/test/widgets/simple_semantics_test.dart @@ -0,0 +1,67 @@ +// 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/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'semantics_tester.dart'; + +void main() { + testWidgets('Simple tree is simple', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + + await tester.pumpWidget( + const Center( + child: const Text('Hello!', textDirection: TextDirection.ltr) + ), + ); + + expect(semantics, hasSemantics(new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + id: 1, + label: 'Hello!', + textDirection: TextDirection.ltr, + rect: new Rect.fromLTRB(0.0, 0.0, 84.0, 14.0), + transform: new Matrix4.translationValues(358.0, 293.0, 0.0), + ) + ], + ))); + + semantics.dispose(); + }); + + testWidgets('Simple tree is simple - material', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + + // Not using Text widget because of https://github.com/flutter/flutter/issues/12357. + await tester.pumpWidget(new MaterialApp( + home: new Center( + child: new Semantics( + label: 'Hello!', + child: new Container( + width: 10.0, + height: 10.0, + ), + ), + ), + )); + + expect(semantics, hasSemantics(new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + id: 2, + label: 'Hello!', + textDirection: TextDirection.ltr, + rect: new Rect.fromLTRB(0.0, 0.0, 10.0, 10.0), + transform: new Matrix4.translationValues(395.0, 295.0, 0.0), + ) + ], + ))); + + semantics.dispose(); + }); +} diff --git a/packages/flutter/test/widgets/sliver_semantics_test.dart b/packages/flutter/test/widgets/sliver_semantics_test.dart index 846e53cfa96..68235d94960 100644 --- a/packages/flutter/test/widgets/sliver_semantics_test.dart +++ b/packages/flutter/test/widgets/sliver_semantics_test.dart @@ -50,7 +50,7 @@ void main() { new TestSemantics.root( children: [ new TestSemantics.rootChild( - id: 1, + id: 4, tags: [RenderSemanticsGestureHandler.useTwoPaneSemantics], children: [ new TestSemantics( @@ -58,15 +58,15 @@ void main() { actions: SemanticsAction.scrollUp.index, children: [ new TestSemantics( - id: 2, + id: 1, label: 'Item 0', ), new TestSemantics( - id: 3, + id: 2, label: 'Item 1', ), new TestSemantics( - id: 4, + id: 3, label: 'Semantics Test with Slivers', ), ], @@ -88,7 +88,7 @@ void main() { new TestSemantics.root( children: [ new TestSemantics.rootChild( - id: 1, + id: 4, tags: [RenderSemanticsGestureHandler.useTwoPaneSemantics], children: [ new TestSemantics( @@ -96,11 +96,11 @@ void main() { actions: SemanticsAction.scrollUp.index | SemanticsAction.scrollDown.index, children: [ new TestSemantics( - id: 2, + id: 1, label: 'Item 0', ), new TestSemantics( - id: 3, + id: 2, label: 'Item 1', ), new TestSemantics( @@ -110,7 +110,7 @@ void main() { ], ), new TestSemantics( - id: 7, + id: 3, label: 'Semantics Test with Slivers', tags: [RenderSemanticsGestureHandler.excludeFromScrolling], ), @@ -131,7 +131,7 @@ void main() { new TestSemantics.root( children: [ new TestSemantics.rootChild( - id: 1, + id: 4, tags: [RenderSemanticsGestureHandler.useTwoPaneSemantics], children: [ new TestSemantics( @@ -139,11 +139,11 @@ void main() { actions: SemanticsAction.scrollUp.index | SemanticsAction.scrollDown.index, children: [ new TestSemantics( - id: 2, + id: 1, label: 'Item 0', ), new TestSemantics( - id: 3, + id: 2, label: 'Item 1', ), new TestSemantics( @@ -151,7 +151,7 @@ void main() { label: 'Item 2', ), new TestSemantics( - id: 8, + id: 3, label: 'Semantics Test with Slivers', ), ], @@ -206,16 +206,16 @@ void main() { tags: [RenderSemanticsGestureHandler.useTwoPaneSemantics], children: [ new TestSemantics( - id: 12, + id: 10, actions: SemanticsAction.scrollUp.index | SemanticsAction.scrollDown.index, children: [ new TestSemantics( - id: 10, + id: 7, label: 'Item 2', textDirection: TextDirection.ltr, ), new TestSemantics( - id: 11, + id: 8, label: 'Item 1', textDirection: TextDirection.ltr, ), @@ -256,34 +256,34 @@ void main() { new TestSemantics.root( children: [ new TestSemantics.rootChild( - id: 13, + id: 16, tags: [RenderSemanticsGestureHandler.useTwoPaneSemantics], children: [ new TestSemantics( - id: 19, + id: 17, children: [ new TestSemantics( - id: 14, + id: 11, label: 'Item 4', textDirection: TextDirection.ltr, ), new TestSemantics( - id: 15, + id: 12, label: 'Item 3', textDirection: TextDirection.ltr, ), new TestSemantics( - id: 16, + id: 13, label: 'Item 2', textDirection: TextDirection.ltr, ), new TestSemantics( - id: 17, + id: 14, label: 'Item 1', textDirection: TextDirection.ltr, ), new TestSemantics( - id: 18, + id: 15, label: 'Item 0', textDirection: TextDirection.ltr, ), @@ -319,6 +319,7 @@ void main() { const SliverAppBar( pinned: true, expandedHeight: 100.0, + title: const Text('AppBar'), ), new SliverList( delegate: new SliverChildListDelegate(listChildren), @@ -336,31 +337,31 @@ void main() { new TestSemantics.root( children: [ new TestSemantics.rootChild( - id: 20, + id: 22, rect: TestSemantics.fullScreen, tags: [RenderSemanticsGestureHandler.useTwoPaneSemantics], children: [ new TestSemantics( - id: 25, + id: 23, actions: SemanticsAction.scrollUp.index | SemanticsAction.scrollDown.index, rect: TestSemantics.fullScreen, children: [ // Item 0 is missing because its covered by the app bar. new TestSemantics( - id: 21, + id: 18, rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), // Item 1 starts 20.0dp below edge, so there would be room for Item 0. transform: new Matrix4.translation(new Vector3(0.0, 20.0, 0.0)), label: 'Item 1', ), new TestSemantics( - id: 22, + id: 19, rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), transform: new Matrix4.translation(new Vector3(0.0, 220.0, 0.0)), label: 'Item 2', ), new TestSemantics( - id: 23, + id: 20, rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), transform: new Matrix4.translation(new Vector3(0.0, 420.0, 0.0)), label: 'Item 3', @@ -368,14 +369,16 @@ void main() { ], ), new TestSemantics( - id: 24, - rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 56.0), + id: 21, + rect: new Rect.fromLTRB(0.0, 0.0, 120.0, 20.0), tags: [RenderSemanticsGestureHandler.excludeFromScrolling], + label: 'AppBar', ), ], ) ], ), + ignoreTransform: true, )); semantics.dispose(); @@ -403,6 +406,7 @@ void main() { const SliverAppBar( pinned: true, expandedHeight: 100.0, + title: const Text('AppBar'), ), ]..addAll(slivers), ), @@ -416,29 +420,29 @@ void main() { new TestSemantics.root( children: [ new TestSemantics.rootChild( - id: 26, + id: 28, rect: TestSemantics.fullScreen, tags: [RenderSemanticsGestureHandler.useTwoPaneSemantics], children: [ new TestSemantics( - id: 31, + id: 29, actions: SemanticsAction.scrollUp.index | SemanticsAction.scrollDown.index, rect: TestSemantics.fullScreen, children: [ new TestSemantics( - id: 27, + id: 24, rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), transform: new Matrix4.translation(new Vector3(0.0, 420.0, 0.0)), label: 'Item 3', ), new TestSemantics( - id: 28, + id: 25, rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), transform: new Matrix4.translation(new Vector3(0.0, 220.0, 0.0)), label: 'Item 2', ), new TestSemantics( - id: 29, + id: 26, rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), // Item 1 starts 20.0dp below edge, so there would be room for Item 0. transform: new Matrix4.translation(new Vector3(0.0, 20.0, 0.0)), @@ -448,14 +452,16 @@ void main() { ], ), new TestSemantics( - id: 30, - rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 56.0), + id: 27, + rect: new Rect.fromLTRB(0.0, 0.0, 120.0, 20.0), tags: [RenderSemanticsGestureHandler.excludeFromScrolling], + label: 'AppBar' ), ], ) ], ), + ignoreTransform: true, )); semantics.dispose(); @@ -481,6 +487,7 @@ void main() { const SliverAppBar( pinned: true, expandedHeight: 100.0, + title: const Text('AppBar'), ), new SliverList( delegate: new SliverChildListDelegate(listChildren), @@ -498,31 +505,31 @@ void main() { new TestSemantics.root( children: [ new TestSemantics.rootChild( - id: 32, + id: 34, rect: TestSemantics.fullScreen, tags: [RenderSemanticsGestureHandler.useTwoPaneSemantics], children: [ new TestSemantics( - id: 37, + id: 35, actions: SemanticsAction.scrollUp.index | SemanticsAction.scrollDown.index, rect: TestSemantics.fullScreen, children: [ // Item 0 is missing because its covered by the app bar. new TestSemantics( - id: 33, + id: 30, // Item 1 ends at 580dp, so there would be 20dp space for Item 0. rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), transform: new Matrix4.translation(new Vector3(0.0, 380.0, 0.0)), label: 'Item 1', ), new TestSemantics( - id: 34, + id: 31, rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), transform: new Matrix4.translation(new Vector3(0.0, 180.0, 0.0)), label: 'Item 2', ), new TestSemantics( - id: 35, + id: 32, rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), transform: new Matrix4.translation(new Vector3(0.0, -20.0, 0.0)), label: 'Item 3', @@ -530,15 +537,17 @@ void main() { ], ), new TestSemantics( - id: 36, - rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 56.0), + id: 33, + rect: new Rect.fromLTRB(0.0, 0.0, 120.0, 20.0), transform: new Matrix4.translation(new Vector3(0.0, 544.0, 0.0)), tags: [RenderSemanticsGestureHandler.excludeFromScrolling], + label: 'AppBar' ), ], ) ], ), + ignoreTransform: true, )); semantics.dispose(); @@ -567,6 +576,7 @@ void main() { const SliverAppBar( pinned: true, expandedHeight: 100.0, + title: const Text('AppBar'), ), ]..addAll(slivers), ), @@ -580,29 +590,29 @@ void main() { new TestSemantics.root( children: [ new TestSemantics.rootChild( - id: 38, + id: 40, rect: TestSemantics.fullScreen, tags: [RenderSemanticsGestureHandler.useTwoPaneSemantics], children: [ new TestSemantics( - id: 43, + id: 41, actions: SemanticsAction.scrollUp.index | SemanticsAction.scrollDown.index, rect: TestSemantics.fullScreen, children: [ new TestSemantics( - id: 39, + id: 36, rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), transform: new Matrix4.translation(new Vector3(0.0, -20.0, 0.0)), label: 'Item 3', ), new TestSemantics( - id: 40, + id: 37, rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), transform: new Matrix4.translation(new Vector3(0.0, 180.0, 0.0)), label: 'Item 2', ), new TestSemantics( - id: 41, + id: 38, rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), // Item 1 ends at 580dp, so there would be 20dp space for Item 0. transform: new Matrix4.translation(new Vector3(0.0, 380.0, 0.0)), @@ -612,15 +622,17 @@ void main() { ], ), new TestSemantics( - id: 42, - rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 56.0), + id: 39, + rect: new Rect.fromLTRB(0.0, 0.0, 120.0, 20.0), transform: new Matrix4.translation(new Vector3(0.0, 544.0, 0.0)), tags: [RenderSemanticsGestureHandler.excludeFromScrolling], + label: 'AppBar' ), ], ) ], ), + ignoreTransform: true, )); semantics.dispose();