diff --git a/packages/flutter/lib/src/widgets/visibility.dart b/packages/flutter/lib/src/widgets/visibility.dart index 5bd499125f..d039873c80 100644 --- a/packages/flutter/lib/src/widgets/visibility.dart +++ b/packages/flutter/lib/src/widgets/visibility.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'basic.dart'; import 'framework.dart'; +import 'sliver.dart'; import 'ticker_provider.dart'; /// Whether to show or hide a child. @@ -49,7 +50,8 @@ class Visibility extends StatelessWidget { /// /// The [maintainSize] argument can only be set if [maintainAnimation] is set. /// - /// The [maintainAnimation] argument can only be set if [maintainState] is set. + /// The [maintainAnimation] argument can only be set if [maintainState] is + /// set. const Visibility({ Key key, @required this.child, @@ -66,10 +68,22 @@ class Visibility extends StatelessWidget { assert(maintainState != null), assert(maintainAnimation != null), assert(maintainSize != null), - assert(maintainState == true || maintainAnimation == false, 'Cannot maintain animations if the state is not also maintained.'), - assert(maintainAnimation == true || maintainSize == false, 'Cannot maintain size if animations are not maintained.'), - assert(maintainSize == true || maintainSemantics == false, 'Cannot maintain semantics if size is not maintained.'), - assert(maintainSize == true || maintainInteractivity == false, 'Cannot maintain interactivity if size is not maintained.'), + assert( + maintainState == true || maintainAnimation == false, + 'Cannot maintain animations if the state is not also maintained.' + ), + assert( + maintainAnimation == true || maintainSize == false, + 'Cannot maintain size if animations are not maintained.', + ), + assert( + maintainSize == true || maintainSemantics == false, + 'Cannot maintain semantics if size is not maintained.', + ), + assert( + maintainSize == true || maintainInteractivity == false, + 'Cannot maintain interactivity if size is not maintained.', + ), super(key: key); /// The widget to show or hide, as controlled by [visible]. @@ -243,3 +257,241 @@ class Visibility extends StatelessWidget { properties.add(FlagProperty('maintainInteractivity', value: maintainInteractivity, ifFalse: 'maintainInteractivity')); } } + +/// Whether to show or hide a sliver child. +/// +/// By default, the [visible] property controls whether the [sliver] is included +/// in the subtree or not; when it is not [visible], the [replacementSliver] is +/// included instead. +/// +/// A variety of flags can be used to tweak exactly how the sliver is hidden. +/// (Changing the flags dynamically is discouraged, as it can cause the [sliver] +/// subtree to be rebuilt, with any state in the subtree being discarded. +/// Typically, only the [visible] flag is changed dynamically.) +/// +/// These widgets provide some of the facets of this one: +/// +/// * [SliverOpacity], which can stop its sliver child from being painted. +/// * [SliverOffstage], which can stop its sliver child from being laid out or +/// painted. +/// * [TickerMode], which can stop its child from being animated. +/// * [ExcludeSemantics], which can hide the child from accessibility tools. +/// * [SliverIgnorePointer], which can disable touch interactions with the +/// sliver child. +/// +/// Using this widget is not necessary to hide children. The simplest way to +/// hide a child is just to not include it, or, if a child _must_ be given (e.g. +/// because the parent is a [StatelessWidget]) then to use a childless +/// [SliverToBoxAdapter] instead of the child that would otherwise be included. +class SliverVisibility extends StatelessWidget { + /// Control whether the given [sliver] is [visible]. + /// + /// The [sliver] and [replacementSliver] arguments must not be null. + /// + /// The boolean arguments must not be null. + /// + /// The [maintainSemantics] and [maintainInteractivity] arguments can only be + /// set if [maintainSize] is set. + /// + /// The [maintainSize] argument can only be set if [maintainAnimation] is set. + /// + /// The [maintainAnimation] argument can only be set if [maintainState] is + /// set. + const SliverVisibility({ + Key key, + @required this.sliver, + this.replacementSliver = const SliverToBoxAdapter(), + this.visible = true, + this.maintainState = false, + this.maintainAnimation = false, + this.maintainSize = false, + this.maintainSemantics = false, + this.maintainInteractivity = false, + }) : assert(sliver != null), + assert(replacementSliver != null), + assert(visible != null), + assert(maintainState != null), + assert(maintainAnimation != null), + assert(maintainSize != null), + assert(maintainSemantics != null), + assert(maintainInteractivity != null), + assert( + maintainState == true || maintainAnimation == false, + 'Cannot maintain animations if the state is not also maintained.', + ), + assert( + maintainAnimation == true || maintainSize == false, + 'Cannot maintain size if animations are not maintained.', + ), + assert( + maintainSize == true || maintainSemantics == false, + 'Cannot maintain semantics if size is not maintained.', + ), + assert( + maintainSize == true || maintainInteractivity == false, + 'Cannot maintain interactivity if size is not maintained.', + ), + super(key: key); + + /// The sliver to show or hide, as controlled by [visible]. + final Widget sliver; + + /// The widget to use when the sliver child is not [visible], assuming that + /// none of the `maintain` flags (in particular, [maintainState]) are set. + /// + /// The normal behavior is to replace the widget with a childless + /// [SliverToBoxAdapter], which by default has a geometry of + /// [SliverGeometry.zero]. + final Widget replacementSliver; + + /// Switches between showing the [sliver] or hiding it. + /// + /// The `maintain` flags should be set to the same values regardless of the + /// state of the [visible] property, otherwise they will not operate correctly + /// (specifically, the state will be lost regardless of the state of + /// [maintainState] whenever any of the `maintain` flags are changed, since + /// doing so will result in a subtree shape change). + /// + /// Unless [maintainState] is set, the [sliver] subtree will be disposed + /// (removed from the tree) while hidden. + final bool visible; + + /// Whether to maintain the [State] objects of the [sliver] subtree when it is + /// not [visible]. + /// + /// Keeping the state of the subtree is potentially expensive (because it + /// means all the objects are still in memory; their resources are not + /// released). It should only be maintained if it cannot be recreated on + /// demand. One example of when the state would be maintained is if the sliver + /// subtree contains a [Navigator], since that widget maintains elaborate + /// state that cannot be recreated on the fly. + /// + /// If this property is true, a [SliverOffstage] widget is used to hide the + /// sliver instead of replacing it with [replacementSliver]. + /// + /// If this property is false, then [maintainAnimation] must also be false. + /// + /// Dynamically changing this value may cause the current state of the + /// subtree to be lost (and a new instance of the subtree, with new [State] + /// objects, to be immediately created if [visible] is true). + final bool maintainState; + + /// Whether to maintain animations within the [sliver] subtree when it is + /// not [visible]. + /// + /// To set this, [maintainState] must also be set. + /// + /// Keeping animations active when the widget is not visible is even more + /// expensive than only maintaining the state. + /// + /// One example when this might be useful is if the subtree is animating its + /// layout in time with an [AnimationController], and the result of that + /// layout is being used to influence some other logic. If this flag is false, + /// then any [AnimationController]s hosted inside the [sliver] subtree will be + /// muted while the [visible] flag is false. + /// + /// If this property is true, no [TickerMode] widget is used. + /// + /// If this property is false, then [maintainSize] must also be false. + /// + /// Dynamically changing this value may cause the current state of the + /// subtree to be lost (and a new instance of the subtree, with new [State] + /// objects, to be immediately created if [visible] is true). + final bool maintainAnimation; + + /// Whether to maintain space for where the sliver would have been. + /// + /// To set this, [maintainAnimation] must also be set. + /// + /// Maintaining the size when the sliver is not [visible] is not notably more + /// expensive than just keeping animations running without maintaining the + /// size, and may in some circumstances be slightly cheaper if the subtree is + /// simple and the [visible] property is frequently toggled, since it avoids + /// triggering a layout change when the [visible] property is toggled. If the + /// [sliver] subtree is not trivial then it is significantly cheaper to not + /// even keep the state (see [maintainState]). + /// + /// If this property is true, [SliverOpacity] is used instead of + /// [SliverOffstage]. + /// + /// If this property is false, then [maintainSemantics] and + /// [maintainInteractivity] must also be false. + /// + /// Dynamically changing this value may cause the current state of the + /// subtree to be lost (and a new instance of the subtree, with new [State] + /// objects, to be immediately created if [visible] is true). + final bool maintainSize; + + /// Whether to maintain the semantics for the sliver when it is hidden (e.g. + /// for accessibility). + /// + /// To set this, [maintainSize] must also be set. + /// + /// By default, with [maintainSemantics] set to false, the [sliver] is not + /// visible to accessibility tools when it is hidden from the user. If this + /// flag is set to true, then accessibility tools will report the widget as if + /// it was present. + /// + /// Dynamically changing this value may cause the current state of the + /// subtree to be lost (and a new instance of the subtree, with new [State] + /// objects, to be immediately created if [visible] is true). + final bool maintainSemantics; + + /// Whether to allow the sliver to be interactive when hidden. + /// + /// To set this, [maintainSize] must also be set. + /// + /// By default, with [maintainInteractivity] set to false, touch events cannot + /// reach the [sliver] when it is hidden from the user. If this flag is set to + /// true, then touch events will nonetheless be passed through. + /// + /// Dynamically changing this value may cause the current state of the + /// subtree to be lost (and a new instance of the subtree, with new [State] + /// objects, to be immediately created if [visible] is true). + final bool maintainInteractivity; + + @override + Widget build(BuildContext context) { + if (maintainSize) { + Widget result = sliver; + if (!maintainInteractivity) { + result = SliverIgnorePointer( + sliver: sliver, + ignoring: !visible, + ignoringSemantics: !visible && !maintainSemantics, + ); + } + return SliverOpacity( + opacity: visible ? 1.0 : 0.0, + alwaysIncludeSemantics: maintainSemantics, + sliver: result, + ); + } + assert(!maintainInteractivity); + assert(!maintainSemantics); + assert(!maintainSize); + if (maintainState) { + Widget result = sliver; + if (!maintainAnimation) + result = TickerMode(child: sliver, enabled: visible); + return SliverOffstage( + sliver: result, + offstage: !visible, + ); + } + assert(!maintainAnimation); + assert(!maintainState); + return visible ? sliver : replacementSliver; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(FlagProperty('visible', value: visible, ifFalse: 'hidden', ifTrue: 'visible')); + properties.add(FlagProperty('maintainState', value: maintainState, ifFalse: 'maintainState')); + properties.add(FlagProperty('maintainAnimation', value: maintainAnimation, ifFalse: 'maintainAnimation')); + properties.add(FlagProperty('maintainSize', value: maintainSize, ifFalse: 'maintainSize')); + properties.add(FlagProperty('maintainSemantics', value: maintainSemantics, ifFalse: 'maintainSemantics')); + properties.add(FlagProperty('maintainInteractivity', value: maintainInteractivity, ifFalse: 'maintainInteractivity')); + } +} diff --git a/packages/flutter/test/widgets/sliver_visibility_test.dart b/packages/flutter/test/widgets/sliver_visibility_test.dart new file mode 100644 index 0000000000..7409fd2fdb --- /dev/null +++ b/packages/flutter/test/widgets/sliver_visibility_test.dart @@ -0,0 +1,490 @@ +// Copyright 2014 The Flutter 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 '../rendering/mock_canvas.dart'; +import 'semantics_tester.dart'; + +class TestState extends StatefulWidget { + const TestState({ Key key, this.child, this.log }) : super(key: key); + final Widget child; + final List log; + @override + State createState() => _TestStateState(); +} + +class _TestStateState extends State { + @override + void initState() { + super.initState(); + widget.log.add('created new state'); + } + @override + Widget build(BuildContext context) { + return widget.child; + } +} + +void main() { + testWidgets('SliverVisibility', (WidgetTester tester) async { + final SemanticsTester semantics = SemanticsTester(tester); + final List log = []; + const Key anchor = Key('drag'); + + Widget _boilerPlate(Widget sliver) { + return Localizations( + locale: const Locale('en', 'us'), + delegates: const >[ + DefaultWidgetsLocalizations.delegate, + DefaultMaterialLocalizations.delegate, + ], + child: Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: const MediaQueryData(), + child: CustomScrollView(slivers: [sliver]) + ) + ) + ); + } + + final Widget testChild = SliverToBoxAdapter( + child: GestureDetector( + onTap: () { + log.add('tap'); + }, + child: Builder( + builder: (BuildContext context) { + final bool animating = TickerMode.of(context); + return TestState( + key: anchor, + log: log, + child: Text('a $animating', textDirection: TextDirection.rtl), + ); + }, + ), + ) + ); + + // We now run a sequence of pumpWidget calls one after the other. In + // addition to verifying that the right behavior is seen in each case, this + // also verifies that the widget can dynamically change from state to state. + + // Default + await tester.pumpWidget(_boilerPlate(SliverVisibility(sliver: testChild))); + expect(find.byType(Text, skipOffstage: false), findsOneWidget); + expect(find.text('a true', skipOffstage: false), findsOneWidget); + expect(find.byType(SliverVisibility), findsOneWidget); + expect(find.byType(SliverVisibility), paints..paragraph()); + RenderViewport renderViewport = tester.renderObject(find.byType(Viewport)); + RenderSliver renderSliver = renderViewport.lastChild; + expect(renderSliver.geometry.scrollExtent, 14.0); + expect(renderSliver.constraints.crossAxisExtent, 800.0); + expect(semantics.nodesWith(label: 'a true'), hasLength(1)); + expect(log, ['created new state']); + await tester.tap(find.byKey(anchor)); + expect(log, ['created new state', 'tap']); + log.clear(); + + // visible: false + await tester.pumpWidget(_boilerPlate(SliverVisibility( + sliver: testChild, + visible: false, + ))); + expect(find.byType(Text), findsNothing); + expect(find.byType(Text, skipOffstage: false), findsNothing); + expect(find.byType(SliverVisibility, skipOffstage: false), findsOneWidget); + expect(find.byType(SliverVisibility, skipOffstage: false), paintsNothing); + renderViewport = tester.renderObject(find.byType(Viewport)); + renderSliver = renderViewport.lastChild; + expect(renderSliver.geometry.scrollExtent, 0.0); + expect(semantics.nodesWith(label: 'a true'), hasLength(0)); + expect(log, []); + expect(find.byKey(anchor), findsNothing); + log.clear(); + + // visible: false, with replacementSliver + await tester.pumpWidget(_boilerPlate(SliverVisibility( + sliver: testChild, + replacementSliver: const SliverToBoxAdapter(child: Placeholder()), + visible: false, + ))); + expect(find.byType(Text, skipOffstage: false), findsNothing); + expect(find.byType(Placeholder), findsOneWidget); + expect(find.byType(SliverVisibility), findsOneWidget); + expect(find.byType(SliverVisibility), paints..path()); + renderViewport = tester.renderObject(find.byType(Viewport)); + renderSliver = renderViewport.lastChild; + expect(renderSliver.geometry.scrollExtent, 400.0); + expect(semantics.nodesWith(label: 'a true'), hasLength(0)); + expect(log, []); + expect(find.byKey(anchor), findsNothing); + log.clear(); + + // visible: true, with replacementSliver + await tester.pumpWidget(_boilerPlate(SliverVisibility( + sliver: testChild, + replacementSliver: const SliverToBoxAdapter(child: Placeholder()), + visible: true, + ))); + expect(find.byType(Text, skipOffstage: false), findsOneWidget); + expect(find.text('a true', skipOffstage: false), findsOneWidget); + expect(find.byType(Placeholder), findsNothing); + expect(find.byType(SliverVisibility), findsOneWidget); + expect(find.byType(SliverVisibility), paints..paragraph()); + renderViewport = tester.renderObject(find.byType(Viewport)); + renderSliver = renderViewport.lastChild; + expect(renderSliver.geometry.scrollExtent, 14.0); + expect(semantics.nodesWith(label: 'a true'), hasLength(1)); + expect(log, ['created new state']); + await tester.tap(find.byKey(anchor)); + expect(log, ['created new state', 'tap']); + log.clear(); + + // visible: true, maintain all + await tester.pumpWidget(_boilerPlate(SliverVisibility( + sliver: testChild, + visible: true, + maintainState: true, + maintainAnimation: true, + maintainSize: true, + maintainInteractivity: true, + maintainSemantics: true, + ))); + expect(find.byType(Text, skipOffstage: false), findsOneWidget); + expect(find.text('a true', skipOffstage: false), findsOneWidget); + expect(find.byType(SliverVisibility), findsOneWidget); + expect(find.byType(SliverVisibility), paints..paragraph()); + renderViewport = tester.renderObject(find.byType(Viewport)); + renderSliver = renderViewport.lastChild; + expect(renderSliver.geometry.scrollExtent, 14.0); + expect(semantics.nodesWith(label: 'a true'), hasLength(1)); + expect(log, ['created new state']); + await tester.tap(find.byKey(anchor)); + expect(log, ['created new state', 'tap']); + log.clear(); + + // visible: false, maintain all + await tester.pumpWidget(_boilerPlate(SliverVisibility( + sliver: testChild, + visible: false, + maintainState: true, + maintainAnimation: true, + maintainSize: true, + maintainInteractivity: true, + maintainSemantics: true, + ))); + expect(find.byType(Text, skipOffstage: false), findsOneWidget); + expect(find.text('a true', skipOffstage: false), findsOneWidget); + expect(find.byType(SliverVisibility), findsOneWidget); + expect(find.byType(SliverVisibility), paintsNothing); + renderViewport = tester.renderObject(find.byType(Viewport)); + renderSliver = renderViewport.lastChild; + expect(renderSliver.geometry.scrollExtent, 14.0); + expect(semantics.nodesWith(label: 'a true'), hasLength(1)); + expect(log, []); + await tester.tap(find.byKey(anchor)); + expect(log, ['tap']); + log.clear(); + + // visible: false, maintain all, replacementSliver + await tester.pumpWidget(_boilerPlate(SliverVisibility( + sliver: testChild, + replacementSliver: const SliverToBoxAdapter(child: Placeholder()), + visible: false, + maintainState: true, + maintainAnimation: true, + maintainSize: true, + maintainInteractivity: true, + maintainSemantics: true, + ))); + expect(find.byType(Text, skipOffstage: false), findsOneWidget); + expect(find.text('a true', skipOffstage: false), findsOneWidget); + expect(find.byType(Placeholder), findsNothing); + expect(find.byType(SliverVisibility), findsOneWidget); + expect(find.byType(SliverVisibility), paintsNothing); + renderViewport = tester.renderObject(find.byType(Viewport)); + renderSliver = renderViewport.lastChild; + expect(renderSliver.geometry.scrollExtent, 14.0); + expect(renderSliver.constraints.crossAxisExtent, 800.0); + expect(semantics.nodesWith(label: 'a true'), hasLength(1)); + expect(log, []); + await tester.tap(find.byKey(anchor)); + expect(log, ['tap']); + log.clear(); + + // visible: false, maintain all but semantics + await tester.pumpWidget(_boilerPlate(SliverVisibility( + sliver: testChild, + visible: false, + maintainState: true, + maintainAnimation: true, + maintainSize: true, + maintainInteractivity: true, + ))); + expect(find.byType(Text, skipOffstage: false), findsOneWidget); + expect(find.text('a true', skipOffstage: false), findsOneWidget); + expect(find.byType(SliverVisibility), findsOneWidget); + expect(find.byType(SliverVisibility), paintsNothing); + renderViewport = tester.renderObject(find.byType(Viewport)); + renderSliver = renderViewport.lastChild; + expect(renderSliver.geometry.scrollExtent, 14.0); + expect(renderSliver.constraints.crossAxisExtent, 800.0); + expect(semantics.nodesWith(label: 'a true'), hasLength(0)); + expect(log, []); + await tester.tap(find.byKey(anchor)); + expect(log, ['tap']); + log.clear(); + + // visible: false, maintain all but interactivity + await tester.pumpWidget(_boilerPlate(SliverVisibility( + sliver: testChild, + visible: false, + maintainState: true, + maintainAnimation: true, + maintainSize: true, + maintainSemantics: true, + ))); + expect(find.byType(Text, skipOffstage: false), findsOneWidget); + expect(find.text('a true', skipOffstage: false), findsOneWidget); + expect(find.byType(SliverVisibility), findsOneWidget); + expect(find.byType(SliverVisibility), paintsNothing); + renderViewport = tester.renderObject(find.byType(Viewport)); + renderSliver = renderViewport.lastChild; + expect(renderSliver.geometry.scrollExtent, 14.0); + expect(renderSliver.constraints.crossAxisExtent, 800.0); + expect(semantics.nodesWith(label: 'a true'), hasLength(1)); + expect(log, ['created new state']); + await tester.tap(find.byKey(anchor)); + expect(log, ['created new state']); + log.clear(); + + // visible: false, maintain state, animation, size. + await tester.pumpWidget(_boilerPlate(SliverVisibility( + sliver: testChild, + visible: false, + maintainState: true, + maintainAnimation: true, + maintainSize: true, + ))); + expect(find.byType(Text, skipOffstage: false), findsOneWidget); + expect(find.text('a true', skipOffstage: false), findsOneWidget); + expect(find.byType(SliverVisibility), findsOneWidget); + expect(find.byType(SliverVisibility), paintsNothing); + renderViewport = tester.renderObject(find.byType(Viewport)); + renderSliver = renderViewport.lastChild; + expect(renderSliver.geometry.scrollExtent, 14.0); + expect(renderSliver.constraints.crossAxisExtent, 800.0); + expect(semantics.nodesWith(label: 'a true'), hasLength(0)); + expect(log, []); + await tester.tap(find.byKey(anchor)); + expect(log, []); + log.clear(); + + // visible: false, maintain state and animation. + await tester.pumpWidget(_boilerPlate(SliverVisibility( + sliver: testChild, + visible: false, + maintainState: true, + maintainAnimation: true, + ))); + expect(find.byType(Text, skipOffstage: false), findsOneWidget); + expect(find.byType(Text, skipOffstage: true), findsNothing); + expect(find.text('a true', skipOffstage: false), findsOneWidget); + expect(find.byType(SliverVisibility, skipOffstage: false), findsOneWidget); + expect(find.byType(SliverVisibility, skipOffstage: false), paintsNothing); + renderViewport = tester.renderObject(find.byType(Viewport)); + renderSliver = renderViewport.lastChild; + expect(renderSliver.geometry.scrollExtent, 0.0); + expect(renderSliver.constraints.crossAxisExtent, 800.0); + expect(semantics.nodesWith(label: 'a true'), hasLength(0)); + expect(log, ['created new state']); + expect(find.byKey(anchor), findsNothing); + log.clear(); + + // visible: false, maintain state. + await tester.pumpWidget(_boilerPlate(SliverVisibility( + sliver: testChild, + visible: false, + maintainState: true, + ))); + expect(find.byType(Text, skipOffstage: false), findsOneWidget); + expect(find.byType(Text, skipOffstage: true), findsNothing); + expect(find.text('a false', skipOffstage: false), findsOneWidget); + expect(find.byType(SliverVisibility, skipOffstage: false), findsOneWidget); + expect(find.byType(SliverVisibility, skipOffstage: false), paintsNothing); + renderViewport = tester.renderObject(find.byType(Viewport)); + renderSliver = renderViewport.lastChild; + expect(renderSliver.geometry.scrollExtent, 0.0); + expect(renderSliver.constraints.crossAxisExtent, 800.0); + expect(semantics.nodesWith(label: 'a true'), hasLength(0)); + expect(log, ['created new state']); + expect(find.byKey(anchor), findsNothing); + log.clear(); + + // Now we toggle the visibility off and on a few times to make sure that + // works. + + // visible: true, maintain state + await tester.pumpWidget(_boilerPlate(SliverVisibility( + sliver: testChild, + visible: true, + maintainState: true, + ))); + expect(find.byType(Text), findsOneWidget); + expect(find.text('a true'), findsOneWidget); + expect(find.byType(SliverVisibility), findsOneWidget); + expect(find.byType(SliverVisibility), paints..paragraph()); + renderViewport = tester.renderObject(find.byType(Viewport)); + renderSliver = renderViewport.lastChild; + expect(renderSliver.geometry.scrollExtent, 14.0); + expect(renderSliver.constraints.crossAxisExtent, 800.0); + expect(semantics.nodesWith(label: 'a true'), hasLength(1)); + expect(log, []); + await tester.tap(find.byKey(anchor)); + expect(log, ['tap']); + log.clear(); + + // visible: false, maintain state. + await tester.pumpWidget(_boilerPlate(SliverVisibility( + sliver: testChild, + visible: false, + maintainState: true, + ))); + expect(find.byType(Text, skipOffstage: false), findsOneWidget); + expect(find.byType(Text, skipOffstage: true), findsNothing); + expect(find.text('a false', skipOffstage: false), findsOneWidget); + expect(find.byType(SliverVisibility, skipOffstage: false), findsOneWidget); + expect(find.byType(SliverVisibility, skipOffstage: false), paintsNothing); + renderViewport = tester.renderObject(find.byType(Viewport)); + renderSliver = renderViewport.lastChild; + expect(renderSliver.geometry.scrollExtent, 0.0); + expect(renderSliver.constraints.crossAxisExtent, 800.0); + expect(semantics.nodesWith(label: 'a false'), hasLength(0)); + expect(log, []); + expect(find.byKey(anchor), findsNothing); + log.clear(); + + // visible: true, maintain state. + await tester.pumpWidget(_boilerPlate(SliverVisibility( + sliver: testChild, + visible: true, + maintainState: true, + ))); + expect(find.byType(Text), findsOneWidget); + expect(find.text('a true', skipOffstage: false), findsOneWidget); + expect(find.byType(SliverVisibility, skipOffstage: false), findsOneWidget); + expect( + find.byType(SliverVisibility, skipOffstage: false), paints..paragraph()); + renderViewport = tester.renderObject(find.byType(Viewport)); + renderSliver = renderViewport.lastChild; + expect(renderSliver.geometry.scrollExtent, 14.0); + expect(renderSliver.constraints.crossAxisExtent, 800.0); + expect(semantics.nodesWith(label: 'a true'), hasLength(1)); + expect(log, []); + await tester.tap(find.byKey(anchor)); + expect(log, ['tap']); + log.clear(); + + // visible: false, maintain state. + await tester.pumpWidget(_boilerPlate(SliverVisibility( + sliver: testChild, + visible: false, + maintainState: true, + ))); + expect(find.byType(Text, skipOffstage: false), findsOneWidget); + expect(find.byType(Text, skipOffstage: true), findsNothing); + expect(find.text('a false', skipOffstage: false), findsOneWidget); + expect(find.byType(SliverVisibility, skipOffstage: false), findsOneWidget); + expect(find.byType(SliverVisibility, skipOffstage: false), paintsNothing); + renderViewport = tester.renderObject(find.byType(Viewport)); + renderSliver = renderViewport.lastChild; + expect(renderSliver.geometry.scrollExtent, 0.0); + expect(renderSliver.constraints.crossAxisExtent, 800.0); + expect(semantics.nodesWith(label: 'a false'), hasLength(0)); + expect(log, []); + expect(find.byKey(anchor), findsNothing); + log.clear(); + + // Same but without maintainState. + + // visible: false. + await tester.pumpWidget(_boilerPlate(SliverVisibility( + sliver: testChild, + visible: false, + ))); + expect(find.byType(Text, skipOffstage: false), findsNothing); + expect(find.byType(SliverVisibility, skipOffstage: false), findsOneWidget); + expect(find.byType(SliverVisibility, skipOffstage: false), paintsNothing); + renderViewport = tester.renderObject(find.byType(Viewport)); + renderSliver = renderViewport.lastChild; + expect(renderSliver.geometry.scrollExtent, 0.0); + expect(renderSliver.constraints.crossAxisExtent, 800.0); + expect(semantics.nodesWith(label: 'a true'), hasLength(0)); + expect(log, []); + expect(find.byKey(anchor), findsNothing); + log.clear(); + + // visible: true. + await tester.pumpWidget(_boilerPlate(SliverVisibility( + sliver: testChild, + visible: true, + ))); + expect(find.byType(Text), findsOneWidget); + expect(find.text('a true', skipOffstage: false), findsOneWidget); + expect(find.byType(SliverVisibility), findsOneWidget); + expect(find.byType(SliverVisibility), paints..paragraph()); + renderViewport = tester.renderObject(find.byType(Viewport)); + renderSliver = renderViewport.lastChild; + expect(renderSliver.geometry.scrollExtent, 14.0); + expect(renderSliver.constraints.crossAxisExtent, 800.0); + expect(semantics.nodesWith(label: 'a true'), hasLength(1)); + expect(log, ['created new state']); + await tester.tap(find.byKey(anchor)); + expect(log, ['created new state', 'tap']); + log.clear(); + + //visible: false. + await tester.pumpWidget(_boilerPlate(SliverVisibility( + sliver: testChild, + visible: false, + ))); + expect(find.byType(Text, skipOffstage: false), findsNothing); + expect(find.byType(SliverVisibility, skipOffstage: false), findsOneWidget); + expect(find.byType(SliverVisibility, skipOffstage: false), paintsNothing); + renderViewport = tester.renderObject(find.byType(Viewport)); + renderSliver = renderViewport.lastChild; + expect(renderSliver.geometry.scrollExtent, 0.0); + expect(renderSliver.constraints.crossAxisExtent, 800.0); + expect(semantics.nodesWith(label: 'a true'), hasLength(0)); + expect(log, []); + expect(find.byKey(anchor), findsNothing); + log.clear(); + + // visible: true. + await tester.pumpWidget(_boilerPlate(SliverVisibility( + sliver: testChild, + visible: true, + ))); + expect(find.byType(Text), findsOneWidget); + expect(find.text('a true', skipOffstage: false), findsOneWidget); + expect(find.byType(SliverVisibility), findsOneWidget); + expect(find.byType(SliverVisibility), paints..paragraph()); + renderViewport = tester.renderObject(find.byType(Viewport)); + renderSliver = renderViewport.lastChild; + expect(renderSliver.geometry.scrollExtent, 14.0); + expect(renderSliver.constraints.crossAxisExtent, 800.0); + expect(semantics.nodesWith(label: 'a true'), hasLength(1)); + expect(log, ['created new state']); + await tester.tap(find.byKey(anchor)); + expect(log, ['created new state', 'tap']); + log.clear(); + + semantics.dispose(); + }, skip: isBrowser); +} diff --git a/packages/flutter/test/widgets/visibility_test.dart b/packages/flutter/test/widgets/visibility_test.dart index 2a471aea0e..3edd8c3558 100644 --- a/packages/flutter/test/widgets/visibility_test.dart +++ b/packages/flutter/test/widgets/visibility_test.dart @@ -88,7 +88,12 @@ void main() { expect(log, []); log.clear(); - await tester.pumpWidget(Center(child: Visibility(child: testChild, visible: false))); + await tester.pumpWidget(Center( + child: Visibility( + child: testChild, + visible: false, + ) + )); expect(find.byType(Text, skipOffstage: false), findsNothing); expect(find.byType(Placeholder), findsNothing); expect(find.byType(Visibility), paintsNothing); @@ -99,7 +104,13 @@ void main() { expect(log, []); log.clear(); - await tester.pumpWidget(Center(child: Visibility(child: testChild, replacement: const Placeholder(), visible: false))); + await tester.pumpWidget(Center( + child: Visibility( + child: testChild, + replacement: const Placeholder(), + visible: false, + ) + )); expect(find.byType(Text, skipOffstage: false), findsNothing); expect(find.byType(Placeholder), findsOneWidget); expect(find.byType(Visibility), paints..path()); @@ -110,7 +121,13 @@ void main() { expect(log, []); log.clear(); - await tester.pumpWidget(Center(child: Visibility(child: testChild, replacement: const Placeholder(), visible: true))); + await tester.pumpWidget(Center( + child: Visibility( + child: testChild, + replacement: const Placeholder(), + visible: true, + ) + )); expect(find.byType(Text, skipOffstage: false), findsOneWidget); expect(find.text('a true', skipOffstage: false), findsOneWidget); expect(find.byType(Placeholder), findsNothing); @@ -122,7 +139,17 @@ void main() { expect(log, ['created new state', 'tap']); log.clear(); - await tester.pumpWidget(Center(child: Visibility(child: testChild, visible: true, maintainState: true, maintainAnimation: true, maintainSize: true, maintainInteractivity: true, maintainSemantics: true))); + await tester.pumpWidget(Center( + child: Visibility( + child: testChild, + visible: true, + maintainState: true, + maintainAnimation: true, + maintainSize: true, + maintainInteractivity: true, + maintainSemantics: true, + ) + )); expect(find.byType(Text, skipOffstage: false), findsOneWidget); expect(find.text('a true', skipOffstage: false), findsOneWidget); expect(find.byType(Placeholder), findsNothing); @@ -134,7 +161,17 @@ void main() { expect(log, ['created new state', 'tap']); log.clear(); - await tester.pumpWidget(Center(child: Visibility(child: testChild, visible: false, maintainState: true, maintainAnimation: true, maintainSize: true, maintainInteractivity: true, maintainSemantics: true))); + await tester.pumpWidget(Center( + child: Visibility( + child: testChild, + visible: false, + maintainState: true, + maintainAnimation: true, + maintainSize: true, + maintainInteractivity: true, + maintainSemantics: true, + ) + )); expect(find.byType(Text, skipOffstage: false), findsOneWidget); expect(find.text('a true', skipOffstage: false), findsOneWidget); expect(find.byType(Placeholder), findsNothing); @@ -146,7 +183,16 @@ void main() { expect(log, ['tap']); log.clear(); - await tester.pumpWidget(Center(child: Visibility(child: testChild, visible: false, maintainState: true, maintainAnimation: true, maintainSize: true, maintainInteractivity: true))); + await tester.pumpWidget(Center( + child: Visibility( + child: testChild, + visible: false, + maintainState: true, + maintainAnimation: true, + maintainSize: true, + maintainInteractivity: true, + ) + )); expect(find.byType(Text, skipOffstage: false), findsOneWidget); expect(find.text('a true', skipOffstage: false), findsOneWidget); expect(find.byType(Placeholder), findsNothing); @@ -158,7 +204,16 @@ void main() { expect(log, ['tap']); log.clear(); - await tester.pumpWidget(Center(child: Visibility(child: testChild, visible: false, maintainState: true, maintainAnimation: true, maintainSize: true, maintainSemantics: true))); + await tester.pumpWidget(Center( + child: Visibility( + child: testChild, + visible: false, + maintainState: true, + maintainAnimation: true, + maintainSize: true, + maintainSemantics: true, + ) + )); expect(find.byType(Text, skipOffstage: false), findsOneWidget); expect(find.text('a true', skipOffstage: false), findsOneWidget); expect(find.byType(Placeholder), findsNothing); @@ -170,7 +225,15 @@ void main() { expect(log, ['created new state']); log.clear(); - await tester.pumpWidget(Center(child: Visibility(child: testChild, visible: false, maintainState: true, maintainAnimation: true, maintainSize: true))); + await tester.pumpWidget(Center( + child: Visibility( + child: testChild, + visible: false, + maintainState: true, + maintainAnimation: true, + maintainSize: true, + ) + )); expect(find.byType(Text, skipOffstage: false), findsOneWidget); expect(find.text('a true', skipOffstage: false), findsOneWidget); expect(find.byType(Placeholder), findsNothing); @@ -182,7 +245,14 @@ void main() { expect(log, []); log.clear(); - await tester.pumpWidget(Center(child: Visibility(child: testChild, visible: false, maintainState: true, maintainAnimation: true))); + await tester.pumpWidget(Center( + child: Visibility( + child: testChild, + visible: false, + maintainState: true, + maintainAnimation: true, + ) + )); expect(find.byType(Text, skipOffstage: false), findsOneWidget); expect(find.byType(Text, skipOffstage: true), findsNothing); expect(find.text('a true', skipOffstage: false), findsOneWidget); @@ -195,7 +265,13 @@ void main() { expect(log, ['created new state']); log.clear(); - await tester.pumpWidget(Center(child: Visibility(child: testChild, visible: false, maintainState: true))); + await tester.pumpWidget(Center( + child: Visibility( + child: testChild, + visible: false, + maintainState: true, + ) + )); expect(find.byType(Text, skipOffstage: false), findsOneWidget); expect(find.byType(Text, skipOffstage: true), findsNothing); expect(find.text('a false', skipOffstage: false), findsOneWidget); @@ -210,7 +286,13 @@ void main() { // Now we toggle the visibility off and on a few times to make sure that works. - await tester.pumpWidget(Center(child: Visibility(child: testChild, visible: true, maintainState: true))); + await tester.pumpWidget(Center( + child: Visibility( + child: testChild, + visible: true, + maintainState: true, + ) + )); expect(find.byType(Text), findsOneWidget); expect(find.text('a true', skipOffstage: false), findsOneWidget); expect(find.byType(Placeholder), findsNothing); @@ -222,7 +304,13 @@ void main() { expect(log, ['tap']); log.clear(); - await tester.pumpWidget(Center(child: Visibility(child: testChild, visible: false, maintainState: true))); + await tester.pumpWidget(Center( + child: Visibility( + child: testChild, + visible: false, + maintainState: true, + ) + )); expect(find.byType(Text, skipOffstage: false), findsOneWidget); expect(find.byType(Text, skipOffstage: true), findsNothing); expect(find.text('a false', skipOffstage: false), findsOneWidget); @@ -235,7 +323,13 @@ void main() { expect(log, []); log.clear(); - await tester.pumpWidget(Center(child: Visibility(child: testChild, visible: true, maintainState: true))); + await tester.pumpWidget(Center( + child: Visibility( + child: testChild, + visible: true, + maintainState: true, + ) + )); expect(find.byType(Text), findsOneWidget); expect(find.text('a true', skipOffstage: false), findsOneWidget); expect(find.byType(Placeholder), findsNothing); @@ -247,7 +341,13 @@ void main() { expect(log, ['tap']); log.clear(); - await tester.pumpWidget(Center(child: Visibility(child: testChild, visible: false, maintainState: true))); + await tester.pumpWidget(Center( + child: Visibility( + child: testChild, + visible: false, + maintainState: true, + ) + )); expect(find.byType(Text, skipOffstage: false), findsOneWidget); expect(find.byType(Text, skipOffstage: true), findsNothing); expect(find.text('a false', skipOffstage: false), findsOneWidget); @@ -262,7 +362,12 @@ void main() { // Same but without maintainState. - await tester.pumpWidget(Center(child: Visibility(child: testChild, visible: false))); + await tester.pumpWidget(Center( + child: Visibility( + child: testChild, + visible: false, + ) + )); expect(find.byType(Text, skipOffstage: false), findsNothing); expect(find.byType(Placeholder), findsNothing); expect(find.byType(Visibility), paintsNothing); @@ -273,7 +378,12 @@ void main() { expect(log, []); log.clear(); - await tester.pumpWidget(Center(child: Visibility(child: testChild, visible: true))); + await tester.pumpWidget(Center( + child: Visibility( + child: testChild, + visible: true, + ) + )); expect(find.byType(Text), findsOneWidget); expect(find.text('a true', skipOffstage: false), findsOneWidget); expect(find.byType(Placeholder), findsNothing); @@ -285,7 +395,12 @@ void main() { expect(log, ['created new state', 'tap']); log.clear(); - await tester.pumpWidget(Center(child: Visibility(child: testChild, visible: false))); + await tester.pumpWidget(Center( + child: Visibility( + child: testChild, + visible: false, + ) + )); expect(find.byType(Text, skipOffstage: false), findsNothing); expect(find.byType(Placeholder), findsNothing); expect(find.byType(Visibility), paintsNothing); @@ -296,7 +411,12 @@ void main() { expect(log, []); log.clear(); - await tester.pumpWidget(Center(child: Visibility(child: testChild, visible: true))); + await tester.pumpWidget(Center( + child: Visibility( + child: testChild, + visible: true, + ) + )); expect(find.byType(Text), findsOneWidget); expect(find.text('a true', skipOffstage: false), findsOneWidget); expect(find.byType(Placeholder), findsNothing);