diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index 345ed92f1f..a009613175 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -2233,6 +2233,12 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im node = node.parent; node._cachedSemanticsConfiguration = null; isEffectiveSemanticsBoundary = node._semanticsConfiguration.isSemanticBoundary; + if (isEffectiveSemanticsBoundary && node._semantics == null) { + // We have reached a semantics boundary that doesn't own a semantics node. + // That means the semantics of this branch are currently blocked and will + // not appear in the semantics tree. We can abort the walk here. + return; + } } if (node != this && _semantics != null && _needsSemanticsUpdate) { // If `this` node has already been added to [owner._nodesNeedingSemantics] diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index 5271c16a39..7de397d9e2 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -3315,12 +3315,30 @@ class RenderSemanticsAnnotations extends RenderProxyBox { class RenderBlockSemantics extends RenderProxyBox { /// Create a render object that blocks semantics for nodes below it in paint /// order. - RenderBlockSemantics({ RenderBox child }) : super(child); + RenderBlockSemantics({ RenderBox child, bool blocking: true, }) : _blocking = blocking, super(child); + + /// Whether this render object is blocking semantics of previously painted + /// [RenderObject]s below a common semantics boundary from the semantic tree. + bool get blocking => _blocking; + bool _blocking; + set blocking(bool value) { + assert(value != null); + if (value == _blocking) + return; + _blocking = value; + markNeedsSemanticsUpdate(); + } @override void describeSemanticsConfiguration(SemanticsConfiguration config) { super.describeSemanticsConfiguration(config); - config.isBlockingSemanticsOfPreviouslyPaintedNodes = true; + config.isBlockingSemanticsOfPreviouslyPaintedNodes = blocking; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder description) { + super.debugFillProperties(description); + description.add(new DiagnosticsProperty('blocking', blocking)); } } diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index fec89ee1c9..4e56fc9b80 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -4966,10 +4966,25 @@ class MergeSemantics extends SingleChildRenderObjectWidget { class BlockSemantics extends SingleChildRenderObjectWidget { /// Creates a widget that excludes the semantics of all widgets painted before /// it in the same semantic container. - const BlockSemantics({ Key key, Widget child }) : super(key: key, child: child); + const BlockSemantics({ Key key, this.blocking: true, Widget child }) : super(key: key, child: child); + + /// Whether this widget is blocking semantics of all widget that were painted + /// before it in the same semantic container. + final bool blocking; @override - RenderBlockSemantics createRenderObject(BuildContext context) => new RenderBlockSemantics(); + RenderBlockSemantics createRenderObject(BuildContext context) => new RenderBlockSemantics(blocking: blocking); + + @override + void updateRenderObject(BuildContext context, RenderBlockSemantics renderObject) { + renderObject.blocking = blocking; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder description) { + super.debugFillProperties(description); + description.add(new DiagnosticsProperty('blocking', blocking)); + } } /// A widget that drops all the semantics of its descendants. diff --git a/packages/flutter/test/widgets/semantics_6_test.dart b/packages/flutter/test/widgets/semantics_6_test.dart new file mode 100644 index 0000000000..123d926ef1 --- /dev/null +++ b/packages/flutter/test/widgets/semantics_6_test.dart @@ -0,0 +1,77 @@ +// 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 'package:meta/meta.dart'; + +import 'semantics_tester.dart'; + +void main() { + testWidgets('can change semantics in a branch blocked by BlockSemantics', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + + final TestSemantics expectedSemantics = new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + id: 1, + label: 'hello', + textDirection: TextDirection.ltr, + rect: TestSemantics.fullScreen, + ) + ], + ); + + await tester.pumpWidget(buildWidget( + blockedText: 'one', + )); + + expect(semantics, hasSemantics(expectedSemantics)); + + // The purpose of the test is to ensure that this change does not throw. + await tester.pumpWidget(buildWidget( + blockedText: 'two', + )); + + expect(semantics, hasSemantics(expectedSemantics)); + + // Ensure that the previously blocked semantics end up in the tree correctly when unblocked. + await tester.pumpWidget(buildWidget( + blockedText: 'two', + blocking: false, + )); + expect(semantics, includesNodeWith(label: 'two', textDirection: TextDirection.ltr)); + + semantics.dispose(); + }); +} + +Widget buildWidget({ @required String blockedText, bool blocking: true }) { + assert(blockedText != null); + return new Directionality( + textDirection: TextDirection.ltr, + child: new Stack( + fit: StackFit.expand, + children: [ + new Semantics( + container: true, + child: new ListView( + children: [ + new Text(blockedText), + ], + ), + ), + new BlockSemantics( + blocking: blocking, + child: new Semantics( + label: 'hello', + container: true, + ), + ), + ] + ), + ); +}