From 3d34e64da4f0c72e2f90612cc1b4cd03bd31c00f Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Tue, 26 May 2020 10:32:20 -0700 Subject: [PATCH] Introduce and migrate to DualTransitionBuilder (#160) --- packages/animations/.gitignore | 49 +-- .../ios/Runner.xcodeproj/project.pbxproj | 20 +- .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../lib/src/dual_transition_builder.dart | 200 ++++++++++++ .../lib/src/fade_scale_transition.dart | 164 ++-------- .../lib/src/fade_through_transition.dart | 196 ++++-------- .../lib/src/shared_axis_transition.dart | 254 ++++++--------- .../test/dual_transition_builder_test.dart | 299 ++++++++++++++++++ .../test/fade_scale_transition_test.dart | 52 +++ .../test/fade_through_transition_test.dart | 65 ++++ .../test/shared_axis_transition_test.dart | 198 ++++++++++++ 11 files changed, 1017 insertions(+), 488 deletions(-) create mode 100644 packages/animations/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/animations/lib/src/dual_transition_builder.dart create mode 100644 packages/animations/test/dual_transition_builder_test.dart diff --git a/packages/animations/.gitignore b/packages/animations/.gitignore index 3132dc5ff7..f3c205341e 100644 --- a/packages/animations/.gitignore +++ b/packages/animations/.gitignore @@ -22,52 +22,23 @@ # Flutter/Dart/Pub related **/doc/api/ +**/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins +.flutter-plugins-dependencies .packages .pub-cache/ .pub/ -build/ +/build/ -# Android related -**/android/**/gradle-wrapper.jar -**/android/.gradle -**/android/captures/ -**/android/gradlew -**/android/gradlew.bat -**/android/local.properties -**/android/**/GeneratedPluginRegistrant.java +# Web related +lib/generated_plugin_registrant.dart -# iOS/XCode related -**/ios/**/*.mode1v3 -**/ios/**/*.mode2v3 -**/ios/**/*.moved-aside -**/ios/**/*.pbxuser -**/ios/**/*.perspectivev3 -**/ios/**/*sync/ -**/ios/**/.sconsign.dblite -**/ios/**/.tags* -**/ios/**/.vagrant/ -**/ios/**/DerivedData/ -**/ios/**/Icon? -**/ios/**/Pods/ -**/ios/**/.symlinks/ -**/ios/**/profile -**/ios/**/xcuserdata -**/ios/.generated/ -**/ios/Flutter/App.framework -**/ios/Flutter/Flutter.framework -**/ios/Flutter/Generated.xcconfig -**/ios/Flutter/app.flx -**/ios/Flutter/app.zip -**/ios/Flutter/flutter_assets/ -**/ios/Flutter/flutter_export_environment.sh -**/ios/ServiceDefinitions.json -**/ios/Runner/GeneratedPluginRegistrant.* +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json # Exceptions to above rules. -!**/ios/**/default.mode1v3 -!**/ios/**/default.mode2v3 -!**/ios/**/default.pbxuser -!**/ios/**/default.perspectivev3 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/packages/animations/example/ios/Runner.xcodeproj/project.pbxproj b/packages/animations/example/ios/Runner.xcodeproj/project.pbxproj index 30f054abca..0ab5cd85b1 100644 --- a/packages/animations/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/animations/example/ios/Runner.xcodeproj/project.pbxproj @@ -9,11 +9,7 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -26,8 +22,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -38,13 +32,11 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -57,8 +49,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -68,9 +58,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( - 3B80C3931E831B6300D905FE /* App.framework */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, @@ -201,7 +189,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; @@ -253,7 +241,6 @@ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -309,6 +296,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -330,7 +318,6 @@ }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -386,7 +373,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -443,6 +429,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -470,6 +457,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", diff --git a/packages/animations/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/animations/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/packages/animations/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/animations/lib/src/dual_transition_builder.dart b/packages/animations/lib/src/dual_transition_builder.dart new file mode 100644 index 0000000000..5534a6d416 --- /dev/null +++ b/packages/animations/lib/src/dual_transition_builder.dart @@ -0,0 +1,200 @@ +// Copyright 2020 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/widgets.dart'; + +/// Builder callback used by [DualTransitionBuilder]. +/// +/// The builder is expected to return a transition powered by the provided +/// `animation` and wrapping the provided `child`. +/// +/// The `animation` provided to the builder always runs forward from 0.0 to 1.0. +typedef TransitionBuilder = Widget Function( + BuildContext context, + Animation animation, + Widget child, +); + +/// A transition builder that animates its [child] based on the +/// [AnimationStatus] of the provided [animation]. +/// +/// This widget can be used to specify different enter and exit transitions for +/// a [child]. +/// +/// While the [animation] runs forward, the [child] is animated according to +/// [forwardBuilder] and while the [animation] is running in reverse, it is +/// animated according to [reverseBuilder]. +/// +/// Using this builder allows the widget tree to maintain its shape by nesting +/// the enter and exit transitions. This ensures that no state information of +/// any descendant widget is lost when the transition starts or completes. +class DualTransitionBuilder extends StatefulWidget { + /// Creates a [DualTransitionBuilder]. + /// + /// The [animation], [forwardBuilder], and [reverseBuilder] arguments are + /// required and must not be null. + const DualTransitionBuilder({ + Key key, + @required this.animation, + @required this.forwardBuilder, + @required this.reverseBuilder, + this.child, + }) : assert(animation != null), + assert(forwardBuilder != null), + assert(reverseBuilder != null), + super(key: key); + + /// The animation that drives the [child]'s transition. + /// + /// When this animation runs forward, the [child] transitions as specified by + /// [forwardBuilder]. When it runs in reverse, the child transitions according + /// to [reverseBuilder]. + final Animation animation; + + /// A builder for the transition that makes [child] appear on screen. + /// + /// The [child] should be fully visible when the provided `animation` reaches + /// 1.0. + /// + /// The `animation` provided to this builder is running forward from 0.0 to + /// 1.0 when [animation] runs _forward_. When [animation] runs in reverse, + /// the given `animation` is set to [kAlwaysCompleteAnimation]. + /// + /// See also: + /// + /// * [reverseBuilder], which builds the transition for making the [child] + /// disappear from the screen. + final TransitionBuilder forwardBuilder; + + /// A builder for a transition that makes [child] disappear from the screen. + /// + /// The [child] should be fully invisible when the provided `animation` + /// reaches 1.0. + /// + /// The `animation` provided to this builder is running forward from 0.0 to + /// 1.0 when [animation] runs in _reverse_. When [animation] runs forward, + /// the given `animation` is set to [kAlwaysDismissedAnimation]. + /// + /// See also: + /// + /// * [forwardBuilder], which builds the transition for making the [child] + /// appear on screen. + final TransitionBuilder reverseBuilder; + + /// The widget below this [DualTransitionBuilder] in the tree. + /// + /// This child widget will be wrapped by the transitions built by + /// [forwardBuilder] and [reverseBuilder]. + final Widget child; + + @override + State createState() => _DualTransitionBuilderState(); +} + +class _DualTransitionBuilderState extends State { + AnimationStatus _effectiveAnimationStatus; + final ProxyAnimation _forwardAnimation = ProxyAnimation(); + final ProxyAnimation _reverseAnimation = ProxyAnimation(); + + @override + void initState() { + super.initState(); + _effectiveAnimationStatus = widget.animation.status; + widget.animation.addStatusListener(_animationListener); + _updateAnimations(); + } + + void _animationListener(AnimationStatus animationStatus) { + final AnimationStatus oldEffective = _effectiveAnimationStatus; + _effectiveAnimationStatus = _calculateEffectiveAnimationStatus( + lastEffective: _effectiveAnimationStatus, + current: animationStatus, + ); + if (oldEffective != _effectiveAnimationStatus) { + _updateAnimations(); + } + } + + @override + void didUpdateWidget(DualTransitionBuilder oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.animation != widget.animation) { + oldWidget.animation.removeStatusListener(_animationListener); + widget.animation.addStatusListener(_animationListener); + _animationListener(widget.animation.status); + } + } + + // When a transition is interrupted midway we just want to play the ongoing + // animation in reverse. Switching to the actual reverse transition would + // yield a disjoint experience since the forward and reverse transitions are + // very different. + AnimationStatus _calculateEffectiveAnimationStatus({ + @required AnimationStatus lastEffective, + @required AnimationStatus current, + }) { + assert(current != null); + assert(lastEffective != null); + switch (current) { + case AnimationStatus.dismissed: + case AnimationStatus.completed: + return current; + case AnimationStatus.forward: + switch (lastEffective) { + case AnimationStatus.dismissed: + case AnimationStatus.completed: + case AnimationStatus.forward: + return current; + case AnimationStatus.reverse: + return lastEffective; + } + break; + case AnimationStatus.reverse: + switch (lastEffective) { + case AnimationStatus.dismissed: + case AnimationStatus.completed: + case AnimationStatus.reverse: + return current; + case AnimationStatus.forward: + return lastEffective; + } + break; + } + return null; // unreachable + } + + void _updateAnimations() { + switch (_effectiveAnimationStatus) { + case AnimationStatus.dismissed: + case AnimationStatus.forward: + _forwardAnimation.parent = widget.animation; + _reverseAnimation.parent = kAlwaysDismissedAnimation; + break; + case AnimationStatus.reverse: + case AnimationStatus.completed: + _forwardAnimation.parent = kAlwaysCompleteAnimation; + _reverseAnimation.parent = ReverseAnimation(widget.animation); + break; + } + } + + @override + void dispose() { + widget.animation.removeStatusListener(_animationListener); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return widget.forwardBuilder( + context, + _forwardAnimation, + widget.reverseBuilder( + context, + _reverseAnimation, + widget.child, + ), + ); + } +} diff --git a/packages/animations/lib/src/fade_scale_transition.dart b/packages/animations/lib/src/fade_scale_transition.dart index 89bd42d788..b01fa1e6b0 100644 --- a/packages/animations/lib/src/fade_scale_transition.dart +++ b/packages/animations/lib/src/fade_scale_transition.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; +import 'dual_transition_builder.dart'; import 'modal.dart'; import 'utils/curves.dart'; @@ -102,7 +103,7 @@ class FadeScaleTransitionConfiguration extends ModalConfiguration { /// /// This widget is not to be confused with Flutter's [FadeTransition] widget, /// which animates only the opacity of its child widget. -class FadeScaleTransition extends StatefulWidget { +class FadeScaleTransition extends StatelessWidget { /// Creates a widget that implements the Material fade transition. /// /// The fade pattern is used for UI elements that enter or exit from within @@ -136,143 +137,46 @@ class FadeScaleTransition extends StatefulWidget { /// [secondaryAnimation]. final Widget child; - @override - _FadeScaleTransitionState createState() => _FadeScaleTransitionState(); -} - -class _FadeScaleTransitionState extends State { - AnimationStatus _effectiveAnimationStatus; - - @override - void initState() { - super.initState(); - _effectiveAnimationStatus = widget.animation.status; - widget.animation.addStatusListener(_animationListener); - } - - void _animationListener(AnimationStatus animationStatus) { - _effectiveAnimationStatus = _calculateEffectiveAnimationStatus( - lastEffective: _effectiveAnimationStatus, - current: animationStatus, - ); - } - - // When a transition is interrupted midway we just want to play the ongoing - // animation in reverse. Switching to the actual reverse transition would - // yield a disjoint experience since the forward and reverse transitions are - // very different. - AnimationStatus _calculateEffectiveAnimationStatus({ - @required AnimationStatus lastEffective, - @required AnimationStatus current, - }) { - assert(current != null); - assert(lastEffective != null); - switch (current) { - case AnimationStatus.dismissed: - case AnimationStatus.completed: - return current; - case AnimationStatus.forward: - switch (lastEffective) { - case AnimationStatus.dismissed: - case AnimationStatus.completed: - case AnimationStatus.forward: - return current; - case AnimationStatus.reverse: - return lastEffective; - } - break; - case AnimationStatus.reverse: - switch (lastEffective) { - case AnimationStatus.dismissed: - case AnimationStatus.completed: - case AnimationStatus.reverse: - return current; - case AnimationStatus.forward: - return lastEffective; - } - break; - } - return null; // unreachable - } - - void _updateAnimationListener( - Animation oldAnimation, - Animation animation, - ) { - if (oldAnimation != animation) { - oldAnimation.removeStatusListener(_animationListener); - animation.addStatusListener(_animationListener); - _animationListener(animation.status); - } - } - - @override - void didUpdateWidget(FadeScaleTransition oldWidget) { - super.didUpdateWidget(oldWidget); - _updateAnimationListener( - oldWidget.animation, - widget.animation, - ); - } - - @override - void dispose() { - widget.animation.removeStatusListener(_animationListener); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: widget.animation, - builder: (BuildContext context, Widget child) { - assert(_effectiveAnimationStatus != null); - switch (_effectiveAnimationStatus) { - case AnimationStatus.forward: - return _EnterTransition( - animation: widget.animation, - child: child, - ); - case AnimationStatus.dismissed: - case AnimationStatus.reverse: - case AnimationStatus.completed: - return FadeTransition( - opacity: widget.animation, - child: child, - ); - } - return null; // unreachable - }, - child: widget.child, - ); - } -} - -class _EnterTransition extends StatelessWidget { - const _EnterTransition({ - this.animation, - this.child, - }); - - final Animation animation; - final Widget child; - - static Animatable fadeInTransition = CurveTween( + static final Animatable _fadeInTransition = CurveTween( curve: const Interval(0.0, 0.3), ); - static Animatable scaleInTransition = Tween( + static final Animatable _scaleInTransition = Tween( begin: 0.80, end: 1.00, ).chain(CurveTween(curve: decelerateEasing)); + static final Animatable _fadeOutTransition = Tween( + begin: 1.0, + end: 0.0, + ); @override Widget build(BuildContext context) { - return FadeTransition( - opacity: fadeInTransition.animate(animation), - child: ScaleTransition( - scale: scaleInTransition.animate(animation), - child: child, - ), + return DualTransitionBuilder( + animation: animation, + forwardBuilder: ( + BuildContext context, + Animation animation, + Widget child, + ) { + return FadeTransition( + opacity: _fadeInTransition.animate(animation), + child: ScaleTransition( + scale: _scaleInTransition.animate(animation), + child: child, + ), + ); + }, + reverseBuilder: ( + BuildContext context, + Animation animation, + Widget child, + ) { + return FadeTransition( + opacity: _fadeOutTransition.animate(animation), + child: child, + ); + }, + child: child, ); } } diff --git a/packages/animations/lib/src/fade_through_transition.dart b/packages/animations/lib/src/fade_through_transition.dart index de17ff0a45..f6978e4bbb 100644 --- a/packages/animations/lib/src/fade_through_transition.dart +++ b/packages/animations/lib/src/fade_through_transition.dart @@ -4,6 +4,8 @@ import 'package:flutter/material.dart'; +import 'dual_transition_builder.dart'; + /// Used by [PageTransitionsTheme] to define a page route transition animation /// in which the outgoing page fades out, then the incoming page fades in and /// scale up. @@ -60,7 +62,12 @@ import 'package:flutter/material.dart'; /// ``` class FadeThroughPageTransitionsBuilder extends PageTransitionsBuilder { /// Creates a [FadeThroughPageTransitionsBuilder]. - const FadeThroughPageTransitionsBuilder(); + const FadeThroughPageTransitionsBuilder({this.fillColor}); + + /// The color to use for the background color during the transition. + /// + /// This defaults to the [Theme]'s [ThemeData.canvasColor]. + final Color fillColor; @override Widget buildTransitions( @@ -73,6 +80,7 @@ class FadeThroughPageTransitionsBuilder extends PageTransitionsBuilder { return FadeThroughTransition( animation: animation, secondaryAnimation: secondaryAnimation, + fillColor: fillColor, child: child, ); } @@ -150,7 +158,7 @@ class FadeThroughPageTransitionsBuilder extends PageTransitionsBuilder { /// ); /// } /// ``` -class FadeThroughTransition extends StatefulWidget { +class FadeThroughTransition extends StatelessWidget { /// Creates a [FadeThroughTransition]. /// /// The [animation] and [secondaryAnimation] argument are required and must @@ -158,6 +166,7 @@ class FadeThroughTransition extends StatefulWidget { const FadeThroughTransition({ @required this.animation, @required this.secondaryAnimation, + this.fillColor, this.child, }) : assert(animation != null), assert(secondaryAnimation != null); @@ -179,6 +188,11 @@ class FadeThroughTransition extends StatefulWidget { // property when the [FadeThroughTransition] is used as a page transition. final Animation secondaryAnimation; + /// The color to use for the background color during the transition. + /// + /// This defaults to the [Theme]'s [ThemeData.canvasColor]. + final Color fillColor; + /// The widget below this widget in the tree. /// /// This widget will transition in and out as driven by [animation] and @@ -186,152 +200,52 @@ class FadeThroughTransition extends StatefulWidget { final Widget child; @override - State createState() => _FadeThroughTransitionState(); + Widget build(BuildContext context) { + return _ZoomedFadeInFadeOut( + animation: animation, + child: Container( + color: fillColor ?? Theme.of(context).canvasColor, + child: _ZoomedFadeInFadeOut( + animation: ReverseAnimation(secondaryAnimation), + child: child, + ), + ), + ); + } } -class _FadeThroughTransitionState extends State { - AnimationStatus _effectiveAnimationStatus; - AnimationStatus _effectiveSecondaryAnimationStatus; +class _ZoomedFadeInFadeOut extends StatelessWidget { + const _ZoomedFadeInFadeOut({Key key, this.animation, this.child}) + : super(key: key); - @override - void initState() { - super.initState(); - _effectiveAnimationStatus = widget.animation.status; - _effectiveSecondaryAnimationStatus = widget.secondaryAnimation.status; - widget.animation.addStatusListener(_animationListener); - widget.secondaryAnimation.addStatusListener(_secondaryAnimationListener); - } - - void _animationListener(AnimationStatus animationStatus) { - _effectiveAnimationStatus = _calculateEffectiveAnimationStatus( - lastEffective: _effectiveAnimationStatus, - current: animationStatus, - ); - } - - void _secondaryAnimationListener(AnimationStatus animationStatus) { - _effectiveSecondaryAnimationStatus = _calculateEffectiveAnimationStatus( - lastEffective: _effectiveSecondaryAnimationStatus, - current: animationStatus, - ); - } - - // When a transition is interrupted midway we just want to play the ongoing - // animation in reverse. Switching to the actual reverse transition would - // yield a disjoint experience since the forward and reverse transitions are - // very different. - AnimationStatus _calculateEffectiveAnimationStatus({ - @required AnimationStatus lastEffective, - @required AnimationStatus current, - }) { - assert(current != null); - assert(lastEffective != null); - switch (current) { - case AnimationStatus.dismissed: - case AnimationStatus.completed: - return current; - case AnimationStatus.forward: - switch (lastEffective) { - case AnimationStatus.dismissed: - case AnimationStatus.completed: - case AnimationStatus.forward: - return current; - case AnimationStatus.reverse: - return lastEffective; - } - break; - case AnimationStatus.reverse: - switch (lastEffective) { - case AnimationStatus.dismissed: - case AnimationStatus.completed: - case AnimationStatus.reverse: - return current; - case AnimationStatus.forward: - return lastEffective; - } - break; - } - return null; // unreachable - } - - @override - void didUpdateWidget(FadeThroughTransition oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.animation != widget.animation) { - oldWidget.animation.removeStatusListener(_animationListener); - widget.animation.addStatusListener(_animationListener); - _animationListener(widget.animation.status); - } - if (oldWidget.secondaryAnimation != widget.secondaryAnimation) { - oldWidget.secondaryAnimation - .removeStatusListener(_secondaryAnimationListener); - widget.secondaryAnimation.addStatusListener(_secondaryAnimationListener); - _secondaryAnimationListener(widget.secondaryAnimation.status); - } - } - - @override - void dispose() { - widget.animation.removeStatusListener(_animationListener); - widget.secondaryAnimation.removeStatusListener(_secondaryAnimationListener); - super.dispose(); - } - - static final Tween _flippedTween = Tween( - begin: 1.0, - end: 0.0, - ); - static Animation _flip(Animation animation) { - return _flippedTween.animate(animation); - } + final Animation animation; + final Widget child; @override Widget build(BuildContext context) { - return AnimatedBuilder( - animation: widget.animation, - builder: (BuildContext context, Widget child) { - assert(_effectiveAnimationStatus != null); - switch (_effectiveAnimationStatus) { - case AnimationStatus.forward: - return _ZoomedFadeIn( - animation: widget.animation, - child: child, - ); - case AnimationStatus.dismissed: - case AnimationStatus.reverse: - case AnimationStatus.completed: - return _FadeOut( - animation: _flip(widget.animation), - child: child, - ); - } - return null; // unreachable + return DualTransitionBuilder( + animation: animation, + forwardBuilder: ( + BuildContext context, + Animation animation, + Widget child, + ) { + return _ZoomedFadeIn( + animation: animation, + child: child, + ); }, - child: Container( - color: Theme.of(context).canvasColor, - child: AnimatedBuilder( - animation: widget.secondaryAnimation, - builder: (BuildContext context, Widget child) { - assert(_effectiveSecondaryAnimationStatus != null); - switch (_effectiveSecondaryAnimationStatus) { - case AnimationStatus.forward: - return _FadeOut( - child: child, - animation: widget.secondaryAnimation, - ); - case AnimationStatus.dismissed: - case AnimationStatus.reverse: - case AnimationStatus.completed: - return _ZoomedFadeIn( - animation: _flip(widget.secondaryAnimation), - child: child, - ); - } - return null; // unreachable - }, - child: widget.child, - ), - ), + reverseBuilder: ( + BuildContext context, + Animation animation, + Widget child, + ) { + return _FadeOut( + child: child, + animation: animation, + ); + }, + child: child, ); } } diff --git a/packages/animations/lib/src/shared_axis_transition.dart b/packages/animations/lib/src/shared_axis_transition.dart index 52344ce918..3439bd49a2 100644 --- a/packages/animations/lib/src/shared_axis_transition.dart +++ b/packages/animations/lib/src/shared_axis_transition.dart @@ -7,6 +7,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; +import 'dual_transition_builder.dart'; import 'utils/curves.dart'; /// Determines which type of shared axis transition is used. @@ -79,9 +80,9 @@ enum SharedAxisTransitionType { class SharedAxisPageTransitionsBuilder extends PageTransitionsBuilder { /// Construct a [SharedAxisPageTransitionsBuilder]. const SharedAxisPageTransitionsBuilder({ - this.transitionType, + @required this.transitionType, this.fillColor, - }); + }) : assert(transitionType != null); /// Determines which [SharedAxisTransitionType] to build. final SharedAxisTransitionType transitionType; @@ -183,7 +184,7 @@ class SharedAxisPageTransitionsBuilder extends PageTransitionsBuilder { /// ); /// } /// ``` -class SharedAxisTransition extends StatefulWidget { +class SharedAxisTransition extends StatelessWidget { /// Creates a [SharedAxisTransition]. /// /// The [animation] and [secondaryAnimation] argument are required and must @@ -234,157 +235,62 @@ class SharedAxisTransition extends StatefulWidget { /// [secondaryAnimation]. final Widget child; - @override - _SharedAxisTransitionState createState() => _SharedAxisTransitionState(); -} - -class _SharedAxisTransitionState extends State { - AnimationStatus _effectiveAnimationStatus; - AnimationStatus _effectiveSecondaryAnimationStatus; - - @override - void initState() { - super.initState(); - _effectiveAnimationStatus = widget.animation.status; - _effectiveSecondaryAnimationStatus = widget.secondaryAnimation.status; - widget.animation.addStatusListener(_animationListener); - widget.secondaryAnimation.addStatusListener(_secondaryAnimationListener); - } - - void _animationListener(AnimationStatus animationStatus) { - _effectiveAnimationStatus = _calculateEffectiveAnimationStatus( - lastEffective: _effectiveAnimationStatus, - current: animationStatus, - ); - } - - void _secondaryAnimationListener(AnimationStatus animationStatus) { - _effectiveSecondaryAnimationStatus = _calculateEffectiveAnimationStatus( - lastEffective: _effectiveSecondaryAnimationStatus, - current: animationStatus, - ); - } - - // When a transition is interrupted midway we just want to play the ongoing - // animation in reverse. Switching to the actual reverse transition would - // yield a disjoint experience since the forward and reverse transitions are - // very different. - AnimationStatus _calculateEffectiveAnimationStatus({ - @required AnimationStatus lastEffective, - @required AnimationStatus current, - }) { - assert(current != null); - assert(lastEffective != null); - switch (current) { - case AnimationStatus.dismissed: - case AnimationStatus.completed: - return current; - case AnimationStatus.forward: - switch (lastEffective) { - case AnimationStatus.dismissed: - case AnimationStatus.completed: - case AnimationStatus.forward: - return current; - case AnimationStatus.reverse: - return lastEffective; - } - break; - case AnimationStatus.reverse: - switch (lastEffective) { - case AnimationStatus.dismissed: - case AnimationStatus.completed: - case AnimationStatus.reverse: - return current; - case AnimationStatus.forward: - return lastEffective; - } - break; - } - return null; // unreachable - } - - @override - void didUpdateWidget(SharedAxisTransition oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.animation != widget.animation) { - oldWidget.animation.removeStatusListener(_animationListener); - widget.animation.addStatusListener(_animationListener); - _animationListener(widget.animation.status); - } - if (oldWidget.secondaryAnimation != widget.secondaryAnimation) { - oldWidget.secondaryAnimation - .removeStatusListener(_secondaryAnimationListener); - widget.secondaryAnimation.addStatusListener(_secondaryAnimationListener); - _secondaryAnimationListener(widget.secondaryAnimation.status); - } - } - - @override - void dispose() { - widget.animation.removeStatusListener(_animationListener); - widget.secondaryAnimation.removeStatusListener(_secondaryAnimationListener); - super.dispose(); - } - - static final Tween _flippedTween = Tween( - begin: 1.0, - end: 0.0, - ); - static Animation _flip(Animation animation) { - return _flippedTween.animate(animation); - } - @override Widget build(BuildContext context) { - return AnimatedBuilder( - animation: widget.animation, - builder: (BuildContext context, Widget child) { - assert(_effectiveAnimationStatus != null); - switch (_effectiveAnimationStatus) { - case AnimationStatus.forward: - return _EnterTransition( - animation: widget.animation, - transitionType: widget.transitionType, - child: child, - ); - case AnimationStatus.dismissed: - case AnimationStatus.reverse: - case AnimationStatus.completed: - return _ExitTransition( - animation: _flip(widget.animation), - transitionType: widget.transitionType, - reverse: true, - fillColor: widget.fillColor, - child: child, - ); - } - return null; // unreachable + final Color color = fillColor ?? Theme.of(context).canvasColor; + return DualTransitionBuilder( + animation: animation, + forwardBuilder: ( + BuildContext context, + Animation animation, + Widget child, + ) { + return _EnterTransition( + animation: animation, + transitionType: transitionType, + child: child, + ); }, - child: AnimatedBuilder( - animation: widget.secondaryAnimation, - builder: (BuildContext context, Widget child) { - assert(_effectiveSecondaryAnimationStatus != null); - switch (_effectiveSecondaryAnimationStatus) { - case AnimationStatus.forward: - return _ExitTransition( - animation: widget.secondaryAnimation, - transitionType: widget.transitionType, - fillColor: widget.fillColor, - child: child, - ); - case AnimationStatus.dismissed: - case AnimationStatus.reverse: - case AnimationStatus.completed: - return _EnterTransition( - animation: _flip(widget.secondaryAnimation), - transitionType: widget.transitionType, - reverse: true, - child: child, - ); - } - return null; // unreachable + reverseBuilder: ( + BuildContext context, + Animation animation, + Widget child, + ) { + return _ExitTransition( + animation: animation, + transitionType: transitionType, + reverse: true, + fillColor: color, + child: child, + ); + }, + child: DualTransitionBuilder( + animation: ReverseAnimation(secondaryAnimation), + forwardBuilder: ( + BuildContext context, + Animation animation, + Widget child, + ) { + return _EnterTransition( + animation: animation, + transitionType: transitionType, + reverse: true, + child: child, + ); }, - child: widget.child, + reverseBuilder: ( + BuildContext context, + Animation animation, + Widget child, + ) { + return _ExitTransition( + animation: animation, + transitionType: transitionType, + fillColor: color, + child: child, + ); + }, + child: child, ), ); } @@ -428,8 +334,14 @@ class _EnterTransition extends StatelessWidget { return FadeTransition( opacity: _fadeInTransition.animate(animation), - child: Transform.translate( - offset: slideInTransition.evaluate(animation), + child: AnimatedBuilder( + animation: animation, + builder: (BuildContext context, Widget child) { + return Transform.translate( + offset: slideInTransition.evaluate(animation), + child: child, + ); + }, child: child, ), ); @@ -442,8 +354,14 @@ class _EnterTransition extends StatelessWidget { return FadeTransition( opacity: _fadeInTransition.animate(animation), - child: Transform.translate( - offset: slideInTransition.evaluate(animation), + child: AnimatedBuilder( + animation: animation, + builder: (BuildContext context, Widget child) { + return Transform.translate( + offset: slideInTransition.evaluate(animation), + child: child, + ); + }, child: child, ), ); @@ -468,7 +386,7 @@ class _ExitTransition extends StatelessWidget { this.animation, this.transitionType, this.reverse = false, - this.fillColor, + @required this.fillColor, this.child, }); @@ -504,9 +422,15 @@ class _ExitTransition extends StatelessWidget { return FadeTransition( opacity: _fadeOutTransition.animate(animation), child: Container( - color: fillColor ?? Theme.of(context).canvasColor, - child: Transform.translate( - offset: slideOutTransition.evaluate(animation), + color: fillColor, + child: AnimatedBuilder( + animation: animation, + builder: (BuildContext context, Widget child) { + return Transform.translate( + offset: slideOutTransition.evaluate(animation), + child: child, + ); + }, child: child, ), ), @@ -521,9 +445,15 @@ class _ExitTransition extends StatelessWidget { return FadeTransition( opacity: _fadeOutTransition.animate(animation), child: Container( - color: fillColor ?? Theme.of(context).canvasColor, - child: Transform.translate( - offset: slideOutTransition.evaluate(animation), + color: fillColor, + child: AnimatedBuilder( + animation: animation, + builder: (BuildContext context, Widget child) { + return Transform.translate( + offset: slideOutTransition.evaluate(animation), + child: child, + ); + }, child: child, ), ), @@ -533,7 +463,7 @@ class _ExitTransition extends StatelessWidget { return FadeTransition( opacity: _fadeOutTransition.animate(animation), child: Container( - color: fillColor ?? Theme.of(context).canvasColor, + color: fillColor, child: ScaleTransition( scale: (!reverse ? _scaleUpTransition : _scaleDownTransition) .animate(animation), diff --git a/packages/animations/test/dual_transition_builder_test.dart b/packages/animations/test/dual_transition_builder_test.dart new file mode 100644 index 0000000000..4a1d65e4da --- /dev/null +++ b/packages/animations/test/dual_transition_builder_test.dart @@ -0,0 +1,299 @@ +// Copyright 2020 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:animations/src/dual_transition_builder.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/widgets.dart'; + +void main() { + testWidgets('runs animations', (WidgetTester tester) async { + final AnimationController controller = AnimationController( + vsync: const TestVSync(), + duration: const Duration(milliseconds: 300), + ); + + await tester.pumpWidget(Center( + child: DualTransitionBuilder( + animation: controller, + forwardBuilder: ( + BuildContext context, + Animation animation, + Widget child, + ) { + return ScaleTransition( + scale: animation, + child: child, + ); + }, + reverseBuilder: ( + BuildContext context, + Animation animation, + Widget child, + ) { + return FadeTransition( + opacity: Tween(begin: 1.0, end: 0.0).animate(animation), + child: child, + ); + }, + child: Container( + color: Colors.green, + height: 100, + width: 100, + ), + ), + )); + expect(_getScale(tester), 0.0); + expect(_getOpacity(tester), 1.0); + + controller.forward(); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 150)); + expect(_getScale(tester), 0.5); + expect(_getOpacity(tester), 1.0); + + await tester.pump(const Duration(milliseconds: 150)); + expect(_getScale(tester), 1.0); + expect(_getOpacity(tester), 1.0); + + await tester.pumpAndSettle(); + expect(_getScale(tester), 1.0); + expect(_getOpacity(tester), 1.0); + + controller.reverse(); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 150)); + expect(_getScale(tester), 1.0); + expect(_getOpacity(tester), 0.5); + + await tester.pump(const Duration(milliseconds: 150)); + expect(_getScale(tester), 1.0); + expect(_getOpacity(tester), 0.0); + + await tester.pumpAndSettle(); + expect(_getScale(tester), 0.0); + expect(_getOpacity(tester), 1.0); + }); + + testWidgets('keeps state', (WidgetTester tester) async { + final AnimationController controller = AnimationController( + vsync: const TestVSync(), + duration: const Duration(milliseconds: 300), + ); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: DualTransitionBuilder( + animation: controller, + forwardBuilder: ( + BuildContext context, + Animation animation, + Widget child, + ) { + return ScaleTransition( + scale: animation, + child: child, + ); + }, + reverseBuilder: ( + BuildContext context, + Animation animation, + Widget child, + ) { + return FadeTransition( + opacity: Tween(begin: 1.0, end: 0.0).animate(animation), + child: child, + ); + }, + child: const _StatefulTestWidget(name: 'Foo'), + ), + ), + )); + final State state = + tester.state(find.byType(_StatefulTestWidget)); + expect(state, isNotNull); + + controller.forward(); + await tester.pump(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + + await tester.pumpAndSettle(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + + controller.reverse(); + await tester.pump(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + + await tester.pumpAndSettle(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + }); + + testWidgets('does not jump when interrupted - forward', + (WidgetTester tester) async { + final AnimationController controller = AnimationController( + vsync: const TestVSync(), + duration: const Duration(milliseconds: 300), + ); + await tester.pumpWidget(Center( + child: DualTransitionBuilder( + animation: controller, + forwardBuilder: ( + BuildContext context, + Animation animation, + Widget child, + ) { + return ScaleTransition( + scale: animation, + child: child, + ); + }, + reverseBuilder: ( + BuildContext context, + Animation animation, + Widget child, + ) { + return FadeTransition( + opacity: Tween(begin: 1.0, end: 0.0).animate(animation), + child: child, + ); + }, + child: Container( + color: Colors.green, + height: 100, + width: 100, + ), + ), + )); + expect(_getScale(tester), 0.0); + expect(_getOpacity(tester), 1.0); + + controller.forward(); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 150)); + expect(_getScale(tester), 0.5); + expect(_getOpacity(tester), 1.0); + + controller.reverse(); + expect(_getScale(tester), 0.5); + expect(_getOpacity(tester), 1.0); + await tester.pump(); + expect(_getScale(tester), 0.5); + expect(_getOpacity(tester), 1.0); + + await tester.pump(const Duration(milliseconds: 75)); + expect(_getScale(tester), 0.25); + expect(_getOpacity(tester), 1.0); + + await tester.pump(const Duration(milliseconds: 75)); + expect(_getScale(tester), 0.0); + expect(_getOpacity(tester), 1.0); + + await tester.pumpAndSettle(); + expect(_getScale(tester), 0.0); + expect(_getOpacity(tester), 1.0); + }); + + testWidgets('does not jump when interrupted - reverse', + (WidgetTester tester) async { + final AnimationController controller = AnimationController( + value: 1.0, + vsync: const TestVSync(), + duration: const Duration(milliseconds: 300), + ); + await tester.pumpWidget(Center( + child: DualTransitionBuilder( + animation: controller, + forwardBuilder: ( + BuildContext context, + Animation animation, + Widget child, + ) { + return ScaleTransition( + scale: animation, + child: child, + ); + }, + reverseBuilder: ( + BuildContext context, + Animation animation, + Widget child, + ) { + return FadeTransition( + opacity: Tween(begin: 1.0, end: 0.0).animate(animation), + child: child, + ); + }, + child: Container( + color: Colors.green, + height: 100, + width: 100, + ), + ), + )); + expect(_getScale(tester), 1.0); + expect(_getOpacity(tester), 1.0); + + controller.reverse(); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 150)); + expect(_getScale(tester), 1.0); + expect(_getOpacity(tester), 0.5); + + controller.forward(); + expect(_getScale(tester), 1.0); + expect(_getOpacity(tester), 0.5); + await tester.pump(); + expect(_getScale(tester), 1.0); + expect(_getOpacity(tester), 0.5); + + await tester.pump(const Duration(milliseconds: 75)); + expect(_getScale(tester), 1.0); + expect(_getOpacity(tester), 0.75); + + await tester.pump(const Duration(milliseconds: 75)); + expect(_getScale(tester), 1.0); + expect(_getOpacity(tester), 1.0); + + await tester.pumpAndSettle(); + expect(_getScale(tester), 1.0); + expect(_getOpacity(tester), 1.0); + }); +} + +double _getScale(WidgetTester tester) { + final ScaleTransition scale = tester.widget(find.byType(ScaleTransition)); + return scale.scale.value; +} + +double _getOpacity(WidgetTester tester) { + final FadeTransition scale = tester.widget(find.byType(FadeTransition)); + return scale.opacity.value; +} + +class _StatefulTestWidget extends StatefulWidget { + const _StatefulTestWidget({Key key, this.name}) : super(key: key); + + final String name; + + @override + State<_StatefulTestWidget> createState() => _StatefulTestWidgetState(); +} + +class _StatefulTestWidgetState extends State<_StatefulTestWidget> { + @override + Widget build(BuildContext context) { + return Text(widget.name); + } +} diff --git a/packages/animations/test/fade_scale_transition_test.dart b/packages/animations/test/fade_scale_transition_test.dart index dc45396ec3..6577be9d49 100644 --- a/packages/animations/test/fade_scale_transition_test.dart +++ b/packages/animations/test/fade_scale_transition_test.dart @@ -395,6 +395,58 @@ void main() { expect(find.byKey(topKey), findsNothing); }, ); + + testWidgets( + 'should preserve state', + (WidgetTester tester) async { + final AnimationController controller = AnimationController( + vsync: const TestVSync(), + duration: const Duration(milliseconds: 300), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: FadeScaleTransition( + animation: controller, + child: const _FlutterLogoModal(), + ), + ), + ), + ), + ); + + final State state = tester.state( + find.byType(_FlutterLogoModal), + ); + expect(state, isNotNull); + + controller.forward(); + await tester.pump(); + expect(state, same(tester.state(find.byType(_FlutterLogoModal)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_FlutterLogoModal)))); + await tester.pumpAndSettle(); + expect(state, same(tester.state(find.byType(_FlutterLogoModal)))); + + controller.reverse(); + await tester.pump(); + expect(state, same(tester.state(find.byType(_FlutterLogoModal)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_FlutterLogoModal)))); + await tester.pumpAndSettle(); + expect(state, same(tester.state(find.byType(_FlutterLogoModal)))); + + controller.forward(); + await tester.pump(); + expect(state, same(tester.state(find.byType(_FlutterLogoModal)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_FlutterLogoModal)))); + await tester.pumpAndSettle(); + expect(state, same(tester.state(find.byType(_FlutterLogoModal)))); + }, + ); } double _getOpacity(GlobalKey key, WidgetTester tester) { diff --git a/packages/animations/test/fade_through_transition_test.dart b/packages/animations/test/fade_through_transition_test.dart index 3406da1cdc..551d403b58 100644 --- a/packages/animations/test/fade_through_transition_test.dart +++ b/packages/animations/test/fade_through_transition_test.dart @@ -364,6 +364,71 @@ void main() { ); expect(find.byKey(const ValueKey(topRoute)), findsNothing); }); + + testWidgets('should keep state', (WidgetTester tester) async { + final AnimationController animation = AnimationController( + vsync: const TestVSync(), + duration: const Duration(milliseconds: 300), + ); + final AnimationController secondaryAnimation = AnimationController( + vsync: const TestVSync(), + duration: const Duration(milliseconds: 300), + ); + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: FadeThroughTransition( + child: const _StatefulTestWidget(name: 'Foo'), + animation: animation, + secondaryAnimation: secondaryAnimation, + ), + ), + )); + final State state = tester.state( + find.byType(_StatefulTestWidget), + ); + expect(state, isNotNull); + + animation.forward(); + await tester.pump(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pumpAndSettle(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + + secondaryAnimation.forward(); + await tester.pump(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pumpAndSettle(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + + secondaryAnimation.reverse(); + await tester.pump(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pumpAndSettle(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + + animation.reverse(); + await tester.pump(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pumpAndSettle(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + }); } double _getOpacity(String key, WidgetTester tester) { diff --git a/packages/animations/test/shared_axis_transition_test.dart b/packages/animations/test/shared_axis_transition_test.dart index 443b7bd086..687d942035 100644 --- a/packages/animations/test/shared_axis_transition_test.dart +++ b/packages/animations/test/shared_axis_transition_test.dart @@ -583,6 +583,72 @@ void main() { expect(fillContainerFinder, findsOneWidget); expect(tester.widget(fillContainerFinder).color, Colors.green); }); + + testWidgets('should keep state', (WidgetTester tester) async { + final AnimationController animation = AnimationController( + vsync: const TestVSync(), + duration: const Duration(milliseconds: 300), + ); + final AnimationController secondaryAnimation = AnimationController( + vsync: const TestVSync(), + duration: const Duration(milliseconds: 300), + ); + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: SharedAxisTransition( + transitionType: SharedAxisTransitionType.horizontal, + child: const _StatefulTestWidget(name: 'Foo'), + animation: animation, + secondaryAnimation: secondaryAnimation, + ), + ), + )); + final State state = tester.state( + find.byType(_StatefulTestWidget), + ); + expect(state, isNotNull); + + animation.forward(); + await tester.pump(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pumpAndSettle(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + + secondaryAnimation.forward(); + await tester.pump(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pumpAndSettle(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + + secondaryAnimation.reverse(); + await tester.pump(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pumpAndSettle(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + + animation.reverse(); + await tester.pump(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pumpAndSettle(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + }); }); group('SharedAxisTransitionType.vertical', () { @@ -1160,6 +1226,72 @@ void main() { expect(fillContainerFinder, findsOneWidget); expect(tester.widget(fillContainerFinder).color, Colors.green); }); + + testWidgets('should keep state', (WidgetTester tester) async { + final AnimationController animation = AnimationController( + vsync: const TestVSync(), + duration: const Duration(milliseconds: 300), + ); + final AnimationController secondaryAnimation = AnimationController( + vsync: const TestVSync(), + duration: const Duration(milliseconds: 300), + ); + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: SharedAxisTransition( + transitionType: SharedAxisTransitionType.vertical, + child: const _StatefulTestWidget(name: 'Foo'), + animation: animation, + secondaryAnimation: secondaryAnimation, + ), + ), + )); + final State state = tester.state( + find.byType(_StatefulTestWidget), + ); + expect(state, isNotNull); + + animation.forward(); + await tester.pump(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pumpAndSettle(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + + secondaryAnimation.forward(); + await tester.pump(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pumpAndSettle(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + + secondaryAnimation.reverse(); + await tester.pump(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pumpAndSettle(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + + animation.reverse(); + await tester.pump(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pumpAndSettle(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + }); }); group('SharedAxisTransitionType.scaled', () { @@ -1630,6 +1762,72 @@ void main() { expect(fillContainerFinder, findsOneWidget); expect(tester.widget(fillContainerFinder).color, Colors.green); }); + + testWidgets('should keep state', (WidgetTester tester) async { + final AnimationController animation = AnimationController( + vsync: const TestVSync(), + duration: const Duration(milliseconds: 300), + ); + final AnimationController secondaryAnimation = AnimationController( + vsync: const TestVSync(), + duration: const Duration(milliseconds: 300), + ); + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: SharedAxisTransition( + transitionType: SharedAxisTransitionType.scaled, + child: const _StatefulTestWidget(name: 'Foo'), + animation: animation, + secondaryAnimation: secondaryAnimation, + ), + ), + )); + final State state = tester.state( + find.byType(_StatefulTestWidget), + ); + expect(state, isNotNull); + + animation.forward(); + await tester.pump(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pumpAndSettle(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + + secondaryAnimation.forward(); + await tester.pump(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pumpAndSettle(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + + secondaryAnimation.reverse(); + await tester.pump(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pumpAndSettle(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + + animation.reverse(); + await tester.pump(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pump(const Duration(milliseconds: 150)); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + await tester.pumpAndSettle(); + expect(state, same(tester.state(find.byType(_StatefulTestWidget)))); + }); }); }