diff --git a/packages/flutter/lib/src/widgets/implicit_animations.dart b/packages/flutter/lib/src/widgets/implicit_animations.dart index cf5d3ee1f6..62c156f0d3 100644 --- a/packages/flutter/lib/src/widgets/implicit_animations.dart +++ b/packages/flutter/lib/src/widgets/implicit_animations.dart @@ -1293,6 +1293,8 @@ class _AnimatedPositionedDirectionalState extends AnimatedWidgetBaseState with SingleTickerProviderStateMixin { +/// bool _visible = true; +/// +/// Widget build(BuildContext context) { +/// return CustomScrollView( +/// slivers: [ +/// SliverAnimatedOpacity( +/// opacity: _visible ? 1.0 : 0.0, +/// duration: Duration(milliseconds: 500), +/// sliver: SliverFixedExtentList( +/// itemExtent: 100.0, +/// delegate: SliverChildBuilderDelegate( +/// (BuildContext context, int index) { +/// return Container( +/// color: index % 2 == 0 +/// ? Colors.indigo[200] +/// : Colors.orange[200], +/// ); +/// }, +/// childCount: 5, +/// ), +/// ), +/// ), +/// SliverToBoxAdapter( +/// child: FloatingActionButton( +/// onPressed: () { +/// setState(() { +/// _visible = !_visible; +/// }); +/// }, +/// tooltip: 'Toggle opacity', +/// child: Icon(Icons.flip), +/// ) +/// ), +/// ] +/// ); +/// } +/// } +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [SliverFadeTransition], an explicitly animated version of this widget, where +/// an [Animation] is provided by the caller instead of being built in. +/// * [AnimatedOpacity], for automatically transitioning a box child's +/// opacity over a given duration whenever the given opacity changes. +class SliverAnimatedOpacity extends ImplicitlyAnimatedWidget { + /// Creates a widget that animates its opacity implicitly. + /// + /// The [opacity] argument must not be null and must be between 0.0 and 1.0, + /// inclusive. The [curve] and [duration] arguments must not be null. + const SliverAnimatedOpacity({ + Key key, + this.sliver, + @required this.opacity, + Curve curve = Curves.linear, + @required Duration duration, + VoidCallback onEnd, + this.alwaysIncludeSemantics = false, + }) : assert(opacity != null && opacity >= 0.0 && opacity <= 1.0), + super(key: key, curve: curve, duration: duration, onEnd: onEnd); + + /// The sliver below this widget in the tree. + final Widget sliver; + + /// The target opacity. + /// + /// An opacity of 1.0 is fully opaque. An opacity of 0.0 is fully transparent + /// (i.e., invisible). + /// + /// The opacity must not be null. + final double opacity; + + /// Whether the semantic information of the children is always included. + /// + /// Defaults to false. + /// + /// When true, regardless of the opacity settings the sliver child's semantic + /// information is exposed as if the widget were fully visible. This is + /// useful in cases where labels may be hidden during animations that + /// would otherwise contribute relevant semantics. + final bool alwaysIncludeSemantics; + + @override + _SliverAnimatedOpacityState createState() => _SliverAnimatedOpacityState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DoubleProperty('opacity', opacity)); + } +} + +class _SliverAnimatedOpacityState extends ImplicitlyAnimatedWidgetState { + Tween _opacity; + Animation _opacityAnimation; + + @override + void forEachTween(TweenVisitor visitor) { + _opacity = visitor(_opacity, widget.opacity, (dynamic value) => Tween(begin: value as double)) as Tween; + } + + @override + void didUpdateTweens() { + _opacityAnimation = animation.drive(_opacity); + } + + @override + Widget build(BuildContext context) { + return SliverFadeTransition( + opacity: _opacityAnimation, + sliver: widget.sliver, + alwaysIncludeSemantics: widget.alwaysIncludeSemantics, + ); + } +} + /// Animated version of [DefaultTextStyle] which automatically transitions the /// default text style (the text style to apply to descendant [Text] widgets /// without explicit style) over a given duration whenever the given style diff --git a/packages/flutter/test/widgets/implicit_animations_test.dart b/packages/flutter/test/widgets/implicit_animations_test.dart index 7310eccf56..133dd3fc1d 100644 --- a/packages/flutter/test/widgets/implicit_animations_test.dart +++ b/packages/flutter/test/widgets/implicit_animations_test.dart @@ -69,7 +69,11 @@ void main() { testWidgets('AnimatedContainer onEnd callback test', (WidgetTester tester) async { await tester.pumpWidget(wrap( - child: TestAnimatedWidget(callback: mockOnEndFunction, switchKey: switchKey, state: _TestAnimatedContainerWidgetState(),) + child: TestAnimatedWidget( + callback: mockOnEndFunction, + switchKey: switchKey, + state: _TestAnimatedContainerWidgetState(), + ) )); final Finder widgetFinder = find.byKey(switchKey); @@ -86,7 +90,11 @@ void main() { testWidgets('AnimatedPadding onEnd callback test', (WidgetTester tester) async { await tester.pumpWidget(wrap( - child: TestAnimatedWidget(callback: mockOnEndFunction, switchKey: switchKey, state: _TestAnimatedPaddingWidgetState(),) + child: TestAnimatedWidget( + callback: mockOnEndFunction, + switchKey: switchKey, + state: _TestAnimatedPaddingWidgetState(), + ) )); final Finder widgetFinder = find.byKey(switchKey); @@ -103,7 +111,11 @@ void main() { testWidgets('AnimatedAlign onEnd callback test', (WidgetTester tester) async { await tester.pumpWidget(wrap( - child: TestAnimatedWidget(callback: mockOnEndFunction, switchKey: switchKey, state: _TestAnimatedAlignWidgetState(),) + child: TestAnimatedWidget( + callback: mockOnEndFunction, + switchKey: switchKey, + state: _TestAnimatedAlignWidgetState(), + ) )); final Finder widgetFinder = find.byKey(switchKey); @@ -120,7 +132,11 @@ void main() { testWidgets('AnimatedPositioned onEnd callback test', (WidgetTester tester) async { await tester.pumpWidget(wrap( - child: TestAnimatedWidget(callback: mockOnEndFunction, switchKey: switchKey, state: _TestAnimatedPositionedWidgetState(),) + child: TestAnimatedWidget( + callback: mockOnEndFunction, + switchKey: switchKey, + state: _TestAnimatedPositionedWidgetState(), + ) )); final Finder widgetFinder = find.byKey(switchKey); @@ -137,7 +153,11 @@ void main() { testWidgets('AnimatedPositionedDirectional onEnd callback test', (WidgetTester tester) async { await tester.pumpWidget(wrap( - child: TestAnimatedWidget(callback: mockOnEndFunction, switchKey: switchKey, state: _TestAnimatedPositionedDirectionalWidgetState(),) + child: TestAnimatedWidget( + callback: mockOnEndFunction, + switchKey: switchKey, + state: _TestAnimatedPositionedDirectionalWidgetState(), + ) )); final Finder widgetFinder = find.byKey(switchKey); @@ -154,7 +174,58 @@ void main() { testWidgets('AnimatedOpacity onEnd callback test', (WidgetTester tester) async { await tester.pumpWidget(wrap( - child: TestAnimatedWidget(callback: mockOnEndFunction, switchKey: switchKey, state: _TestAnimatedOpacityWidgetState(),) + child: TestAnimatedWidget( + callback: mockOnEndFunction, + switchKey: switchKey, + state: _TestAnimatedOpacityWidgetState(), + ) + )); + + final Finder widgetFinder = find.byKey(switchKey); + + await tester.tap(widgetFinder); + await tester.pump(); + expect(mockOnEndFunction.called, 0); + await tester.pump(animationDuration); + expect(mockOnEndFunction.called, 0); + await tester.pump(additionalDelay); + expect(mockOnEndFunction.called, 1); + }); + + testWidgets('AnimatedOpacity transition test', (WidgetTester tester) async { + await tester.pumpWidget(wrap( + child: TestAnimatedWidget( + switchKey: switchKey, + state: _TestAnimatedOpacityWidgetState(), + ) + )); + + final Finder switchFinder = find.byKey(switchKey); + final FadeTransition opacityWidget = tester.widget( + find.ancestor( + of: find.byType(Placeholder), + matching: find.byType(FadeTransition), + ).first, + ); + + await tester.tap(switchFinder); + await tester.pump(); + expect(opacityWidget.opacity.value, equals(0.0)); + + await tester.pump(const Duration(milliseconds: 500)); + expect(opacityWidget.opacity.value, equals(0.5)); + await tester.pump(const Duration(milliseconds: 250)); + expect(opacityWidget.opacity.value, equals(0.75)); + await tester.pump(const Duration(milliseconds: 250)); + expect(opacityWidget.opacity.value, equals(1.0)); + }); + + + testWidgets('SliverAnimatedOpacity onEnd callback test', (WidgetTester tester) async { + await tester.pumpWidget(TestAnimatedWidget( + callback: mockOnEndFunction, + switchKey: switchKey, + state: _TestSliverAnimatedOpacityWidgetState(), )); final Finder widgetFinder = find.byKey(switchKey); @@ -169,9 +240,41 @@ void main() { expect(mockOnEndFunction.called, 1); }); + testWidgets('SliverAnimatedOpacity transition test', (WidgetTester tester) async { + await tester.pumpWidget(wrap( + child: TestAnimatedWidget( + switchKey: switchKey, + state: _TestSliverAnimatedOpacityWidgetState(), + ) + )); + + final Finder switchFinder = find.byKey(switchKey); + final SliverFadeTransition opacityWidget = tester.widget( + find.ancestor( + of: find.byType(Placeholder), + matching: find.byType(SliverFadeTransition), + ).first, + ); + + await tester.tap(switchFinder); + await tester.pump(); + expect(opacityWidget.opacity.value, equals(0.0)); + + await tester.pump(const Duration(milliseconds: 500)); + expect(opacityWidget.opacity.value, equals(0.5)); + await tester.pump(const Duration(milliseconds: 250)); + expect(opacityWidget.opacity.value, equals(0.75)); + await tester.pump(const Duration(milliseconds: 250)); + expect(opacityWidget.opacity.value, equals(1.0)); + }); + testWidgets('AnimatedDefaultTextStyle onEnd callback test', (WidgetTester tester) async { await tester.pumpWidget(wrap( - child: TestAnimatedWidget(callback: mockOnEndFunction, switchKey: switchKey, state: _TestAnimatedDefaultTextStyleWidgetState(),) + child: TestAnimatedWidget( + callback: mockOnEndFunction, + switchKey: switchKey, + state: _TestAnimatedDefaultTextStyleWidgetState(), + ) )); final Finder widgetFinder = find.byKey(switchKey); @@ -188,7 +291,11 @@ void main() { testWidgets('AnimatedPhysicalModel onEnd callback test', (WidgetTester tester) async { await tester.pumpWidget(wrap( - child: TestAnimatedWidget(callback: mockOnEndFunction, switchKey: switchKey, state: _TestAnimatedPhysicalModelWidgetState(),) + child: TestAnimatedWidget( + callback: mockOnEndFunction, + switchKey: switchKey, + state: _TestAnimatedPhysicalModelWidgetState(), + ) )); final Finder widgetFinder = find.byKey(switchKey); @@ -205,7 +312,11 @@ void main() { testWidgets('TweenAnimationBuilder onEnd callback test', (WidgetTester tester) async { await tester.pumpWidget(wrap( - child: TestAnimatedWidget(callback: mockOnEndFunction, switchKey: switchKey, state: _TestTweenAnimationBuilderWidgetState(),) + child: TestAnimatedWidget( + callback: mockOnEndFunction, + switchKey: switchKey, + state: _TestTweenAnimationBuilderWidgetState(), + ) )); final Finder widgetFinder = find.byKey(switchKey); @@ -222,7 +333,11 @@ void main() { testWidgets('AnimatedTheme onEnd callback test', (WidgetTester tester) async { await tester.pumpWidget(wrap( - child: TestAnimatedWidget(callback: mockOnEndFunction, switchKey: switchKey, state: _TestAnimatedThemeWidgetState(),) + child: TestAnimatedWidget( + callback: mockOnEndFunction, + switchKey: switchKey, + state: _TestAnimatedThemeWidgetState(), + ) )); final Finder widgetFinder = find.byKey(switchKey); @@ -360,7 +475,42 @@ class _TestAnimatedOpacityWidgetState extends _TestAnimatedWidgetState { child: child, duration: duration, onEnd: widget.callback, - opacity: toggle ? 0.1 : 0.9, + opacity: toggle ? 1.0 : 0.0, + ); + } +} + +class _TestSliverAnimatedOpacityWidgetState extends _TestAnimatedWidgetState { + @override + Widget getAnimatedWidget() { + return SliverAnimatedOpacity( + sliver: SliverToBoxAdapter(child: child), + duration: duration, + onEnd: widget.callback, + opacity: toggle ? 1.0 : 0.0, + ); + } + + @override + Widget build(BuildContext context) { + final Widget animatedWidget = getAnimatedWidget(); + + return Material( + child: Directionality( + textDirection: TextDirection.ltr, + child: CustomScrollView( + slivers: [ + animatedWidget, + SliverToBoxAdapter( + child: Switch( + key: widget.switchKey, + value: toggle, + onChanged: onChanged, + ), + ), + ], + ), + ), ); } } @@ -369,12 +519,12 @@ class _TestAnimatedDefaultTextStyleWidgetState extends _TestAnimatedWidgetState @override Widget getAnimatedWidget() { return AnimatedDefaultTextStyle( - child: child, - duration: duration, - onEnd: widget.callback, - style: toggle - ? const TextStyle(fontStyle: FontStyle.italic) - : const TextStyle(fontStyle: FontStyle.normal)); + child: child, + duration: duration, + onEnd: widget.callback, + style: toggle + ? const TextStyle(fontStyle: FontStyle.italic) + : const TextStyle(fontStyle: FontStyle.normal)); } } @@ -397,17 +547,17 @@ class _TestTweenAnimationBuilderWidgetState extends _TestAnimatedWidgetState { @override Widget getAnimatedWidget() { return TweenAnimationBuilder( - child: child, - tween: Tween(begin: 1, end: 2), - duration: duration, - onEnd: widget.callback, - builder: (BuildContext context, double size, Widget child) { - return Container( - child: child, - width: size, - height: size, - ); - }, + child: child, + tween: Tween(begin: 1, end: 2), + duration: duration, + onEnd: widget.callback, + builder: (BuildContext context, double size, Widget child) { + return Container( + child: child, + width: size, + height: size, + ); + }, ); } }