Introduce and migrate to DualTransitionBuilder (#160)

This commit is contained in:
Michael Goderbauer
2020-05-26 10:32:20 -07:00
committed by GitHub
parent 5ca63a2879
commit 3d34e64da4
11 changed files with 1017 additions and 488 deletions

View File

@ -22,52 +22,23 @@
# Flutter/Dart/Pub related # Flutter/Dart/Pub related
**/doc/api/ **/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/ .dart_tool/
.flutter-plugins .flutter-plugins
.flutter-plugins-dependencies
.packages .packages
.pub-cache/ .pub-cache/
.pub/ .pub/
build/ /build/
# Android related # Web related
**/android/**/gradle-wrapper.jar lib/generated_plugin_registrant.dart
**/android/.gradle
**/android/captures/
**/android/gradlew
**/android/gradlew.bat
**/android/local.properties
**/android/**/GeneratedPluginRegistrant.java
# iOS/XCode related # Symbolication related
**/ios/**/*.mode1v3 app.*.symbols
**/ios/**/*.mode2v3
**/ios/**/*.moved-aside # Obfuscation related
**/ios/**/*.pbxuser app.*.map.json
**/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.*
# Exceptions to above rules. # 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 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages

View File

@ -9,11 +9,7 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 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 */; }; 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 */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
@ -26,8 +22,6 @@
dstPath = ""; dstPath = "";
dstSubfolderSpec = 10; dstSubfolderSpec = 10;
files = ( files = (
3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */,
9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */,
); );
name = "Embed Frameworks"; name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@ -38,13 +32,11 @@
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = "<group>"; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@ -57,8 +49,6 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */,
3B80C3941E831B6300D905FE /* App.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -68,9 +58,7 @@
9740EEB11CF90186004384FC /* Flutter */ = { 9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
3B80C3931E831B6300D905FE /* App.framework */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEBA1CF902C7004384FC /* Flutter.framework */,
9740EEB21CF90195004384FC /* Debug.xcconfig */, 9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */,
@ -201,7 +189,7 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; 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 */ = { 9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
@ -253,7 +241,6 @@
/* Begin XCBuildConfiguration section */ /* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = { 249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
@ -309,6 +296,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = "";
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -330,7 +318,6 @@
}; };
97C147031CF9000F007C117D /* Debug */ = { 97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
@ -386,7 +373,6 @@
}; };
97C147041CF9000F007C117D /* Release */ = { 97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
@ -443,6 +429,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = "";
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -470,6 +457,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = "";
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -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<double> 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<double> 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<DualTransitionBuilder> createState() => _DualTransitionBuilderState();
}
class _DualTransitionBuilderState extends State<DualTransitionBuilder> {
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,
),
);
}
}

View File

@ -4,6 +4,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'dual_transition_builder.dart';
import 'modal.dart'; import 'modal.dart';
import 'utils/curves.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, /// This widget is not to be confused with Flutter's [FadeTransition] widget,
/// which animates only the opacity of its child 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. /// Creates a widget that implements the Material fade transition.
/// ///
/// The fade pattern is used for UI elements that enter or exit from within /// The fade pattern is used for UI elements that enter or exit from within
@ -136,143 +137,46 @@ class FadeScaleTransition extends StatefulWidget {
/// [secondaryAnimation]. /// [secondaryAnimation].
final Widget child; final Widget child;
@override static final Animatable<double> _fadeInTransition = CurveTween(
_FadeScaleTransitionState createState() => _FadeScaleTransitionState();
}
class _FadeScaleTransitionState extends State<FadeScaleTransition> {
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<double> oldAnimation,
Animation<double> 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<double> animation;
final Widget child;
static Animatable<double> fadeInTransition = CurveTween(
curve: const Interval(0.0, 0.3), curve: const Interval(0.0, 0.3),
); );
static Animatable<double> scaleInTransition = Tween<double>( static final Animatable<double> _scaleInTransition = Tween<double>(
begin: 0.80, begin: 0.80,
end: 1.00, end: 1.00,
).chain(CurveTween(curve: decelerateEasing)); ).chain(CurveTween(curve: decelerateEasing));
static final Animatable<double> _fadeOutTransition = Tween<double>(
begin: 1.0,
end: 0.0,
);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DualTransitionBuilder(
animation: animation,
forwardBuilder: (
BuildContext context,
Animation<double> animation,
Widget child,
) {
return FadeTransition( return FadeTransition(
opacity: fadeInTransition.animate(animation), opacity: _fadeInTransition.animate(animation),
child: ScaleTransition( child: ScaleTransition(
scale: scaleInTransition.animate(animation), scale: _scaleInTransition.animate(animation),
child: child, child: child,
), ),
); );
},
reverseBuilder: (
BuildContext context,
Animation<double> animation,
Widget child,
) {
return FadeTransition(
opacity: _fadeOutTransition.animate(animation),
child: child,
);
},
child: child,
);
} }
} }

View File

@ -4,6 +4,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'dual_transition_builder.dart';
/// Used by [PageTransitionsTheme] to define a page route transition animation /// Used by [PageTransitionsTheme] to define a page route transition animation
/// in which the outgoing page fades out, then the incoming page fades in and /// in which the outgoing page fades out, then the incoming page fades in and
/// scale up. /// scale up.
@ -60,7 +62,12 @@ import 'package:flutter/material.dart';
/// ``` /// ```
class FadeThroughPageTransitionsBuilder extends PageTransitionsBuilder { class FadeThroughPageTransitionsBuilder extends PageTransitionsBuilder {
/// Creates a [FadeThroughPageTransitionsBuilder]. /// 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 @override
Widget buildTransitions<T>( Widget buildTransitions<T>(
@ -73,6 +80,7 @@ class FadeThroughPageTransitionsBuilder extends PageTransitionsBuilder {
return FadeThroughTransition( return FadeThroughTransition(
animation: animation, animation: animation,
secondaryAnimation: secondaryAnimation, secondaryAnimation: secondaryAnimation,
fillColor: fillColor,
child: child, child: child,
); );
} }
@ -150,7 +158,7 @@ class FadeThroughPageTransitionsBuilder extends PageTransitionsBuilder {
/// ); /// );
/// } /// }
/// ``` /// ```
class FadeThroughTransition extends StatefulWidget { class FadeThroughTransition extends StatelessWidget {
/// Creates a [FadeThroughTransition]. /// Creates a [FadeThroughTransition].
/// ///
/// The [animation] and [secondaryAnimation] argument are required and must /// The [animation] and [secondaryAnimation] argument are required and must
@ -158,6 +166,7 @@ class FadeThroughTransition extends StatefulWidget {
const FadeThroughTransition({ const FadeThroughTransition({
@required this.animation, @required this.animation,
@required this.secondaryAnimation, @required this.secondaryAnimation,
this.fillColor,
this.child, this.child,
}) : assert(animation != null), }) : assert(animation != null),
assert(secondaryAnimation != null); assert(secondaryAnimation != null);
@ -179,6 +188,11 @@ class FadeThroughTransition extends StatefulWidget {
// property when the [FadeThroughTransition] is used as a page transition. // property when the [FadeThroughTransition] is used as a page transition.
final Animation<double> secondaryAnimation; final Animation<double> 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. /// The widget below this widget in the tree.
/// ///
/// This widget will transition in and out as driven by [animation] and /// This widget will transition in and out as driven by [animation] and
@ -186,152 +200,52 @@ class FadeThroughTransition extends StatefulWidget {
final Widget child; final Widget child;
@override @override
State<FadeThroughTransition> 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<FadeThroughTransition> { class _ZoomedFadeInFadeOut extends StatelessWidget {
AnimationStatus _effectiveAnimationStatus; const _ZoomedFadeInFadeOut({Key key, this.animation, this.child})
AnimationStatus _effectiveSecondaryAnimationStatus; : super(key: key);
@override final Animation<double> animation;
void initState() { final Widget child;
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<double> _flippedTween = Tween<double>(
begin: 1.0,
end: 0.0,
);
static Animation<double> _flip(Animation<double> animation) {
return _flippedTween.animate(animation);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AnimatedBuilder( return DualTransitionBuilder(
animation: widget.animation, animation: animation,
builder: (BuildContext context, Widget child) { forwardBuilder: (
assert(_effectiveAnimationStatus != null); BuildContext context,
switch (_effectiveAnimationStatus) { Animation<double> animation,
case AnimationStatus.forward: Widget child,
) {
return _ZoomedFadeIn( return _ZoomedFadeIn(
animation: widget.animation, animation: animation,
child: child, child: child,
); );
case AnimationStatus.dismissed:
case AnimationStatus.reverse:
case AnimationStatus.completed:
return _FadeOut(
animation: _flip(widget.animation),
child: child,
);
}
return null; // unreachable
}, },
child: Container( reverseBuilder: (
color: Theme.of(context).canvasColor, BuildContext context,
child: AnimatedBuilder( Animation<double> animation,
animation: widget.secondaryAnimation, Widget child,
builder: (BuildContext context, Widget child) { ) {
assert(_effectiveSecondaryAnimationStatus != null);
switch (_effectiveSecondaryAnimationStatus) {
case AnimationStatus.forward:
return _FadeOut( return _FadeOut(
child: child, child: child,
animation: widget.secondaryAnimation, animation: animation,
); );
case AnimationStatus.dismissed:
case AnimationStatus.reverse:
case AnimationStatus.completed:
return _ZoomedFadeIn(
animation: _flip(widget.secondaryAnimation),
child: child,
);
}
return null; // unreachable
}, },
child: widget.child, child: child,
),
),
); );
} }
} }

View File

@ -7,6 +7,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'dual_transition_builder.dart';
import 'utils/curves.dart'; import 'utils/curves.dart';
/// Determines which type of shared axis transition is used. /// Determines which type of shared axis transition is used.
@ -79,9 +80,9 @@ enum SharedAxisTransitionType {
class SharedAxisPageTransitionsBuilder extends PageTransitionsBuilder { class SharedAxisPageTransitionsBuilder extends PageTransitionsBuilder {
/// Construct a [SharedAxisPageTransitionsBuilder]. /// Construct a [SharedAxisPageTransitionsBuilder].
const SharedAxisPageTransitionsBuilder({ const SharedAxisPageTransitionsBuilder({
this.transitionType, @required this.transitionType,
this.fillColor, this.fillColor,
}); }) : assert(transitionType != null);
/// Determines which [SharedAxisTransitionType] to build. /// Determines which [SharedAxisTransitionType] to build.
final SharedAxisTransitionType transitionType; final SharedAxisTransitionType transitionType;
@ -183,7 +184,7 @@ class SharedAxisPageTransitionsBuilder extends PageTransitionsBuilder {
/// ); /// );
/// } /// }
/// ``` /// ```
class SharedAxisTransition extends StatefulWidget { class SharedAxisTransition extends StatelessWidget {
/// Creates a [SharedAxisTransition]. /// Creates a [SharedAxisTransition].
/// ///
/// The [animation] and [secondaryAnimation] argument are required and must /// The [animation] and [secondaryAnimation] argument are required and must
@ -234,157 +235,62 @@ class SharedAxisTransition extends StatefulWidget {
/// [secondaryAnimation]. /// [secondaryAnimation].
final Widget child; final Widget child;
@override
_SharedAxisTransitionState createState() => _SharedAxisTransitionState();
}
class _SharedAxisTransitionState extends State<SharedAxisTransition> {
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<double> _flippedTween = Tween<double>(
begin: 1.0,
end: 0.0,
);
static Animation<double> _flip(Animation<double> animation) {
return _flippedTween.animate(animation);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AnimatedBuilder( final Color color = fillColor ?? Theme.of(context).canvasColor;
animation: widget.animation, return DualTransitionBuilder(
builder: (BuildContext context, Widget child) { animation: animation,
assert(_effectiveAnimationStatus != null); forwardBuilder: (
switch (_effectiveAnimationStatus) { BuildContext context,
case AnimationStatus.forward: Animation<double> animation,
Widget child,
) {
return _EnterTransition( return _EnterTransition(
animation: widget.animation, animation: animation,
transitionType: widget.transitionType, transitionType: transitionType,
child: child, 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
}, },
child: AnimatedBuilder( reverseBuilder: (
animation: widget.secondaryAnimation, BuildContext context,
builder: (BuildContext context, Widget child) { Animation<double> animation,
assert(_effectiveSecondaryAnimationStatus != null); Widget child,
switch (_effectiveSecondaryAnimationStatus) { ) {
case AnimationStatus.forward:
return _ExitTransition( return _ExitTransition(
animation: widget.secondaryAnimation, animation: animation,
transitionType: widget.transitionType, transitionType: transitionType,
fillColor: widget.fillColor, reverse: true,
fillColor: color,
child: child, child: child,
); );
case AnimationStatus.dismissed: },
case AnimationStatus.reverse: child: DualTransitionBuilder(
case AnimationStatus.completed: animation: ReverseAnimation(secondaryAnimation),
forwardBuilder: (
BuildContext context,
Animation<double> animation,
Widget child,
) {
return _EnterTransition( return _EnterTransition(
animation: _flip(widget.secondaryAnimation), animation: animation,
transitionType: widget.transitionType, transitionType: transitionType,
reverse: true, reverse: true,
child: child, child: child,
); );
}
return null; // unreachable
}, },
child: widget.child, reverseBuilder: (
BuildContext context,
Animation<double> animation,
Widget child,
) {
return _ExitTransition(
animation: animation,
transitionType: transitionType,
fillColor: color,
child: child,
);
},
child: child,
), ),
); );
} }
@ -428,9 +334,15 @@ class _EnterTransition extends StatelessWidget {
return FadeTransition( return FadeTransition(
opacity: _fadeInTransition.animate(animation), opacity: _fadeInTransition.animate(animation),
child: Transform.translate( child: AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget child) {
return Transform.translate(
offset: slideInTransition.evaluate(animation), offset: slideInTransition.evaluate(animation),
child: child, child: child,
);
},
child: child,
), ),
); );
break; break;
@ -442,9 +354,15 @@ class _EnterTransition extends StatelessWidget {
return FadeTransition( return FadeTransition(
opacity: _fadeInTransition.animate(animation), opacity: _fadeInTransition.animate(animation),
child: Transform.translate( child: AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget child) {
return Transform.translate(
offset: slideInTransition.evaluate(animation), offset: slideInTransition.evaluate(animation),
child: child, child: child,
);
},
child: child,
), ),
); );
break; break;
@ -468,7 +386,7 @@ class _ExitTransition extends StatelessWidget {
this.animation, this.animation,
this.transitionType, this.transitionType,
this.reverse = false, this.reverse = false,
this.fillColor, @required this.fillColor,
this.child, this.child,
}); });
@ -504,10 +422,16 @@ class _ExitTransition extends StatelessWidget {
return FadeTransition( return FadeTransition(
opacity: _fadeOutTransition.animate(animation), opacity: _fadeOutTransition.animate(animation),
child: Container( child: Container(
color: fillColor ?? Theme.of(context).canvasColor, color: fillColor,
child: Transform.translate( child: AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget child) {
return Transform.translate(
offset: slideOutTransition.evaluate(animation), offset: slideOutTransition.evaluate(animation),
child: child, child: child,
);
},
child: child,
), ),
), ),
); );
@ -521,10 +445,16 @@ class _ExitTransition extends StatelessWidget {
return FadeTransition( return FadeTransition(
opacity: _fadeOutTransition.animate(animation), opacity: _fadeOutTransition.animate(animation),
child: Container( child: Container(
color: fillColor ?? Theme.of(context).canvasColor, color: fillColor,
child: Transform.translate( child: AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget child) {
return Transform.translate(
offset: slideOutTransition.evaluate(animation), offset: slideOutTransition.evaluate(animation),
child: child, child: child,
);
},
child: child,
), ),
), ),
); );
@ -533,7 +463,7 @@ class _ExitTransition extends StatelessWidget {
return FadeTransition( return FadeTransition(
opacity: _fadeOutTransition.animate(animation), opacity: _fadeOutTransition.animate(animation),
child: Container( child: Container(
color: fillColor ?? Theme.of(context).canvasColor, color: fillColor,
child: ScaleTransition( child: ScaleTransition(
scale: (!reverse ? _scaleUpTransition : _scaleDownTransition) scale: (!reverse ? _scaleUpTransition : _scaleDownTransition)
.animate(animation), .animate(animation),

View File

@ -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<double> animation,
Widget child,
) {
return ScaleTransition(
scale: animation,
child: child,
);
},
reverseBuilder: (
BuildContext context,
Animation<double> animation,
Widget child,
) {
return FadeTransition(
opacity: Tween<double>(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<double> animation,
Widget child,
) {
return ScaleTransition(
scale: animation,
child: child,
);
},
reverseBuilder: (
BuildContext context,
Animation<double> animation,
Widget child,
) {
return FadeTransition(
opacity: Tween<double>(begin: 1.0, end: 0.0).animate(animation),
child: child,
);
},
child: const _StatefulTestWidget(name: 'Foo'),
),
),
));
final State<StatefulWidget> 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<double> animation,
Widget child,
) {
return ScaleTransition(
scale: animation,
child: child,
);
},
reverseBuilder: (
BuildContext context,
Animation<double> animation,
Widget child,
) {
return FadeTransition(
opacity: Tween<double>(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<double> animation,
Widget child,
) {
return ScaleTransition(
scale: animation,
child: child,
);
},
reverseBuilder: (
BuildContext context,
Animation<double> animation,
Widget child,
) {
return FadeTransition(
opacity: Tween<double>(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);
}
}

View File

@ -395,6 +395,58 @@ void main() {
expect(find.byKey(topKey), findsNothing); 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<StatefulWidget> 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) { double _getOpacity(GlobalKey key, WidgetTester tester) {

View File

@ -364,6 +364,71 @@ void main() {
); );
expect(find.byKey(const ValueKey<String>(topRoute)), findsNothing); expect(find.byKey(const ValueKey<String>(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<StatefulWidget> 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) { double _getOpacity(String key, WidgetTester tester) {

View File

@ -583,6 +583,72 @@ void main() {
expect(fillContainerFinder, findsOneWidget); expect(fillContainerFinder, findsOneWidget);
expect(tester.widget<Container>(fillContainerFinder).color, Colors.green); expect(tester.widget<Container>(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<StatefulWidget> 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', () { group('SharedAxisTransitionType.vertical', () {
@ -1160,6 +1226,72 @@ void main() {
expect(fillContainerFinder, findsOneWidget); expect(fillContainerFinder, findsOneWidget);
expect(tester.widget<Container>(fillContainerFinder).color, Colors.green); expect(tester.widget<Container>(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<StatefulWidget> 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', () { group('SharedAxisTransitionType.scaled', () {
@ -1630,6 +1762,72 @@ void main() {
expect(fillContainerFinder, findsOneWidget); expect(fillContainerFinder, findsOneWidget);
expect(tester.widget<Container>(fillContainerFinder).color, Colors.green); expect(tester.widget<Container>(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<StatefulWidget> 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))));
});
}); });
} }