diff --git a/packages/flutter/lib/src/rendering/sliver.dart b/packages/flutter/lib/src/rendering/sliver.dart index 754ebbea37..aaa5e32c57 100644 --- a/packages/flutter/lib/src/rendering/sliver.dart +++ b/packages/flutter/lib/src/rendering/sliver.dart @@ -1965,7 +1965,7 @@ class RenderSliverOpacity extends RenderSliver with RenderObjectWithChildMixin('ignoring', ignoring)); - properties.add(DiagnosticsProperty('ignoringSemantics', _effectiveIgnoringSemantics, description: ignoringSemantics == null ? 'implicitly $_effectiveIgnoringSemantics' : null,),); + properties.add(DiagnosticsProperty('ignoringSemantics', _effectiveIgnoringSemantics, description: ignoringSemantics == null ? 'implicitly $_effectiveIgnoringSemantics' : null)); + } +} + +/// Lays the sliver child out as if it was in the tree, but without painting +/// anything, without making the sliver child available for hit testing, and +/// without taking any room in the parent. +class RenderSliverOffstage extends RenderSliver with RenderObjectWithChildMixin { + /// Creates an offstage render object. + RenderSliverOffstage({ + bool offstage = true, + RenderSliver sliver, + }) : assert(offstage != null), + _offstage = offstage { + child = sliver; + } + + /// Whether the sliver child is hidden from the rest of the tree. + /// + /// If true, the sliver child is laid out as if it was in the tree, but + /// without painting anything, without making the sliver child available for + /// hit testing, and without taking any room in the parent. + /// + /// If false, the sliver child is included in the tree as normal. + bool get offstage => _offstage; + bool _offstage; + + set offstage(bool value) { + assert(value != null); + if (value == _offstage) + return; + _offstage = value; + markNeedsLayoutForSizedByParentChange(); + } + + @override + void setupParentData(RenderObject child) { + if (child.parentData is! SliverPhysicalParentData) + child.parentData = SliverPhysicalParentData(); + } + + @override + void performLayout() { + assert(child != null); + child.layout(constraints, parentUsesSize: true); + geometry = child.geometry; + } + + @override + bool hitTest(SliverHitTestResult result, {double mainAxisPosition, double crossAxisPosition}) { + return !offstage && super.hitTest( + result, + mainAxisPosition: mainAxisPosition, + crossAxisPosition: crossAxisPosition, + ); + } + + @override + bool hitTestChildren(SliverHitTestResult result, {double mainAxisPosition, double crossAxisPosition}) { + return !offstage + && child != null + && child.geometry.hitTestExtent > 0 + && child.hitTest( + result, + mainAxisPosition: mainAxisPosition, + crossAxisPosition: crossAxisPosition, + ); + } + + @override + double childMainAxisPosition(RenderSliver child) { + assert(child != null); + assert(child == this.child); + return 0.0; + } + + @override + void paint(PaintingContext context, Offset offset) { + if (offstage) + return; + super.paint(context, offset); + } + + @override + void applyPaintTransform(RenderObject child, Matrix4 transform) { + assert(child != null); + final SliverPhysicalParentData childParentData = child.parentData; + childParentData.applyPaintTransform(transform); + } + + @override + void visitChildrenForSemantics(RenderObjectVisitor visitor) { + if (offstage) + return; + super.visitChildrenForSemantics(visitor); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('offstage', offstage)); + } + + @override + List debugDescribeChildren() { + if (child == null) + return []; + return [ + child.toDiagnosticsNode( + name: 'child', + style: offstage ? DiagnosticsTreeStyle.offstage : DiagnosticsTreeStyle.sparse, + ), + ]; } } diff --git a/packages/flutter/lib/src/widgets/sliver.dart b/packages/flutter/lib/src/widgets/sliver.dart index 0cab703e0b..48e314785b 100644 --- a/packages/flutter/lib/src/widgets/sliver.dart +++ b/packages/flutter/lib/src/widgets/sliver.dart @@ -1712,8 +1712,7 @@ class SliverOpacity extends SingleChildRenderObjectWidget { } @override - void updateRenderObject(BuildContext context, - RenderSliverOpacity renderObject) { + void updateRenderObject(BuildContext context, RenderSliverOpacity renderObject) { renderObject ..opacity = opacity ..alwaysIncludeSemantics = alwaysIncludeSemantics; @@ -1787,6 +1786,66 @@ class SliverIgnorePointer extends SingleChildRenderObjectWidget { } } +/// A sliver that lays its sliver child out as if it was in the tree, but +/// without painting anything, without making the sliver child available for hit +/// testing, and without taking any room in the parent. +/// +/// Animations continue to run in offstage sliver children, and therefore use +/// battery and CPU time, regardless of whether the animations end up being +/// visible. +/// +/// To hide a sliver widget from view while it is +/// not needed, prefer removing the widget from the tree entirely rather than +/// keeping it alive in an [Offstage] subtree. +class SliverOffstage extends SingleChildRenderObjectWidget { + /// Creates a sliver that visually hides its sliver child. + const SliverOffstage({ + Key key, + this.offstage = true, + Widget sliver, + }) : assert(offstage != null), + super(key: key, child: sliver); + + /// Whether the sliver child is hidden from the rest of the tree. + /// + /// If true, the sliver child is laid out as if it was in the tree, but + /// without painting anything, without making the child available for hit + /// testing, and without taking any room in the parent. + /// + /// If false, the sliver child is included in the tree as normal. + final bool offstage; + + @override + RenderSliverOffstage createRenderObject(BuildContext context) => RenderSliverOffstage(offstage: offstage); + + @override + void updateRenderObject(BuildContext context, RenderSliverOffstage renderObject) { + renderObject.offstage = offstage; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('offstage', offstage)); + } + + @override + _SliverOffstageElement createElement() => _SliverOffstageElement(this); +} + +class _SliverOffstageElement extends SingleChildRenderObjectElement { + _SliverOffstageElement(SliverOffstage widget) : super(widget); + + @override + SliverOffstage get widget => super.widget; + + @override + void debugVisitOnstageChildren(ElementVisitor visitor) { + if (!widget.offstage) + super.debugVisitOnstageChildren(visitor); + } +} + /// Mark a child as needing to stay alive even when it's in a lazy list that /// would otherwise remove it. /// diff --git a/packages/flutter/test/widgets/slivers_test.dart b/packages/flutter/test/widgets/slivers_test.dart index eccb1af741..8fe8c4acef 100644 --- a/packages/flutter/test/widgets/slivers_test.dart +++ b/packages/flutter/test/widgets/slivers_test.dart @@ -440,6 +440,38 @@ void main() { ); } + group('SliverOffstage - ', () { + testWidgets('offstage true', (WidgetTester tester) async { + final SemanticsTester semantics = SemanticsTester(tester); + await tester.pumpWidget(_boilerPlate( + const SliverOffstage( + offstage: true, + sliver: SliverToBoxAdapter( + child: Text('a'), + ) + ) + )); + + expect(semantics.nodesWith(label: 'a'), hasLength(0)); + expect(find.byType(Text), findsNothing); + }); + + testWidgets('offstage false', (WidgetTester tester) async { + final SemanticsTester semantics = SemanticsTester(tester); + await tester.pumpWidget(_boilerPlate( + const SliverOffstage( + offstage: false, + sliver: SliverToBoxAdapter( + child: Text('a'), + ) + ) + )); + + expect(semantics.nodesWith(label: 'a'), hasLength(1)); + expect(find.byType(Text), findsOneWidget); + }); + }); + group('SliverOpacity - ', () { testWidgets('painting & semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester);