[PageTransitionsBuilder] Fix 'ZoomPageTransition' built more than once (#58686)
This commit is contained in:
@@ -150,19 +150,19 @@ class _OpenUpwardsPageTransition extends StatelessWidget {
|
||||
|
||||
// Zooms and fades a new page in, zooming out the previous page. This transition
|
||||
// is designed to match the Android 10 activity transition.
|
||||
class _ZoomPageTransition extends StatefulWidget {
|
||||
class _ZoomPageTransition extends StatelessWidget {
|
||||
/// Creates a [_ZoomPageTransition].
|
||||
///
|
||||
/// The [animation] and [secondaryAnimation] argument are required and must
|
||||
/// not be null.
|
||||
const _ZoomPageTransition({
|
||||
Key key,
|
||||
this.animation,
|
||||
this.secondaryAnimation,
|
||||
@required this.animation,
|
||||
@required this.secondaryAnimation,
|
||||
this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
// The scrim obscures the old page by becoming increasingly opaque.
|
||||
static final Tween<double> _scrimOpacityTween = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 0.60,
|
||||
);
|
||||
}) : assert(animation != null),
|
||||
assert(secondaryAnimation != null),
|
||||
super(key: key);
|
||||
|
||||
// A curve sequence that is similar to the 'fastOutExtraSlowIn' curve used in
|
||||
// the native transition.
|
||||
@@ -179,132 +179,207 @@ class _ZoomPageTransition extends StatefulWidget {
|
||||
),
|
||||
];
|
||||
static final TweenSequence<double> _scaleCurveSequence = TweenSequence<double>(fastOutExtraSlowInTweenSequenceItems);
|
||||
static final FlippedTweenSequence _flippedScaleCurveSequence = FlippedTweenSequence(fastOutExtraSlowInTweenSequenceItems);
|
||||
|
||||
/// The animation that drives the [child]'s entrance and exit.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TransitionRoute.animation], which is the value given to this property
|
||||
/// when the [_ZoomPageTransition] is used as a page transition.
|
||||
final Animation<double> animation;
|
||||
|
||||
/// The animation that transitions [child] when new content is pushed on top
|
||||
/// of it.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TransitionRoute.secondaryAnimation], which is the value given to this
|
||||
// property when the [_ZoomPageTransition] is used as a page transition.
|
||||
final Animation<double> secondaryAnimation;
|
||||
|
||||
/// The widget below this widget in the tree.
|
||||
///
|
||||
/// This widget will transition in and out as driven by [animation] and
|
||||
/// [secondaryAnimation].
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
__ZoomPageTransitionState createState() => __ZoomPageTransitionState();
|
||||
Widget build(BuildContext context) {
|
||||
return DualTransitionBuilder(
|
||||
animation: animation,
|
||||
forwardBuilder: (
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Widget child,
|
||||
) {
|
||||
return _ZoomEnterTransition(
|
||||
animation: animation,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
reverseBuilder: (
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Widget child,
|
||||
) {
|
||||
return _ZoomExitTransition(
|
||||
animation: animation,
|
||||
reverse: true,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: DualTransitionBuilder(
|
||||
animation: ReverseAnimation(secondaryAnimation),
|
||||
forwardBuilder: (
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Widget child,
|
||||
) {
|
||||
return _ZoomEnterTransition(
|
||||
animation: animation,
|
||||
reverse: true,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
reverseBuilder: (
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Widget child,
|
||||
) {
|
||||
return _ZoomExitTransition(
|
||||
animation: animation,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class __ZoomPageTransitionState extends State<_ZoomPageTransition> {
|
||||
AnimationStatus _currentAnimationStatus;
|
||||
AnimationStatus _lastAnimationStatus;
|
||||
class _ZoomEnterTransition extends StatelessWidget {
|
||||
const _ZoomEnterTransition({
|
||||
Key key,
|
||||
@required this.animation,
|
||||
this.reverse = false,
|
||||
this.child,
|
||||
}) : assert(animation != null),
|
||||
assert(reverse != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.animation.addStatusListener((AnimationStatus animationStatus) {
|
||||
_lastAnimationStatus = _currentAnimationStatus;
|
||||
_currentAnimationStatus = animationStatus;
|
||||
});
|
||||
}
|
||||
final Animation<double> animation;
|
||||
final Widget child;
|
||||
final bool reverse;
|
||||
|
||||
// This check ensures that the animation reverses the original animation if
|
||||
// the transition were interrupted midway. This prevents a disjointed
|
||||
// experience since the reverse animation uses different fade and scaling
|
||||
// curves.
|
||||
bool get _transitionWasInterrupted {
|
||||
bool wasInProgress = false;
|
||||
bool isInProgress = false;
|
||||
static final Animatable<double> _fadeInTransition = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1.00,
|
||||
).chain(CurveTween(curve: const Interval(0.125, 0.250)));
|
||||
|
||||
switch (_currentAnimationStatus) {
|
||||
case AnimationStatus.completed:
|
||||
case AnimationStatus.dismissed:
|
||||
isInProgress = false;
|
||||
break;
|
||||
case AnimationStatus.forward:
|
||||
case AnimationStatus.reverse:
|
||||
isInProgress = true;
|
||||
break;
|
||||
}
|
||||
switch (_lastAnimationStatus) {
|
||||
case AnimationStatus.completed:
|
||||
case AnimationStatus.dismissed:
|
||||
wasInProgress = false;
|
||||
break;
|
||||
case AnimationStatus.forward:
|
||||
case AnimationStatus.reverse:
|
||||
wasInProgress = true;
|
||||
break;
|
||||
}
|
||||
return wasInProgress && isInProgress;
|
||||
}
|
||||
static final Animatable<double> _scaleDownTransition = Tween<double>(
|
||||
begin: 1.10,
|
||||
end: 1.00,
|
||||
).chain(_ZoomPageTransition._scaleCurveSequence);
|
||||
|
||||
static final Animatable<double> _scaleUpTransition = Tween<double>(
|
||||
begin: 0.85,
|
||||
end: 1.00,
|
||||
).chain(_ZoomPageTransition._scaleCurveSequence);
|
||||
|
||||
static final Animatable<double> _scrimOpacityTween = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 0.60,
|
||||
).chain(CurveTween(curve: const Interval(0.2075, 0.4175)));
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Animation<double> _forwardScrimOpacityAnimation = widget.animation.drive(
|
||||
_ZoomPageTransition._scrimOpacityTween
|
||||
.chain(CurveTween(curve: const Interval(0.2075, 0.4175))));
|
||||
double opacity = 0;
|
||||
// The transition's scrim opacity only increases on the forward transition. In the reverse
|
||||
// transition, the opacity should always be 0.0.
|
||||
//
|
||||
// Therefore, we need to only apply the scrim opacity animation when the transition
|
||||
// is running forwards.
|
||||
//
|
||||
// The reason that we check that the animation's status is not `completed` instead
|
||||
// of checking that it is `forward` is that this allows the interrupted reversal of the
|
||||
// forward transition to smoothly fade the scrim away. This prevents a disjointed
|
||||
// removal of the scrim.
|
||||
if (!reverse && animation.status != AnimationStatus.completed) {
|
||||
opacity = _scrimOpacityTween.evaluate(animation);
|
||||
}
|
||||
|
||||
final Animation<double> _forwardEndScreenScaleTransition = widget.animation.drive(
|
||||
Tween<double>(begin: 0.85, end: 1.00)
|
||||
.chain(_ZoomPageTransition._scaleCurveSequence));
|
||||
final Animation<double> fadeTransition = reverse
|
||||
? kAlwaysCompleteAnimation
|
||||
: _fadeInTransition.animate(animation);
|
||||
|
||||
final Animation<double> _forwardStartScreenScaleTransition = widget.secondaryAnimation.drive(
|
||||
Tween<double>(begin: 1.00, end: 1.05)
|
||||
.chain(_ZoomPageTransition._scaleCurveSequence));
|
||||
|
||||
final Animation<double> _forwardEndScreenFadeTransition = widget.animation.drive(
|
||||
Tween<double>(begin: 0.0, end: 1.00)
|
||||
.chain(CurveTween(curve: const Interval(0.125, 0.250))));
|
||||
|
||||
final Animation<double> _reverseEndScreenScaleTransition = widget.secondaryAnimation.drive(
|
||||
Tween<double>(begin: 1.00, end: 1.10)
|
||||
.chain(_ZoomPageTransition._flippedScaleCurveSequence));
|
||||
|
||||
final Animation<double> _reverseStartScreenScaleTransition = widget.animation.drive(
|
||||
Tween<double>(begin: 0.9, end: 1.0)
|
||||
.chain(_ZoomPageTransition._flippedScaleCurveSequence));
|
||||
|
||||
final Animation<double> _reverseStartScreenFadeTransition = widget.animation.drive(
|
||||
Tween<double>(begin: 0.0, end: 1.00)
|
||||
.chain(CurveTween(curve: const Interval(1 - 0.2075, 1 - 0.0825))));
|
||||
final Animation<double> scaleTransition = (reverse
|
||||
? _scaleDownTransition
|
||||
: _scaleUpTransition
|
||||
).animate(animation);
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: widget.animation,
|
||||
animation: animation,
|
||||
builder: (BuildContext context, Widget child) {
|
||||
if (widget.animation.status == AnimationStatus.forward || _transitionWasInterrupted) {
|
||||
return Container(
|
||||
color: Colors.black.withOpacity(_forwardScrimOpacityAnimation.value),
|
||||
child: FadeTransition(
|
||||
opacity: _forwardEndScreenFadeTransition,
|
||||
child: ScaleTransition(
|
||||
scale: _forwardEndScreenScaleTransition,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (widget.animation.status == AnimationStatus.reverse) {
|
||||
return ScaleTransition(
|
||||
scale: _reverseStartScreenScaleTransition,
|
||||
child: FadeTransition(
|
||||
opacity: _reverseStartScreenFadeTransition,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
return child;
|
||||
return Container(
|
||||
color: Colors.black.withOpacity(opacity),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: AnimatedBuilder(
|
||||
animation: widget.secondaryAnimation,
|
||||
builder: (BuildContext context, Widget child) {
|
||||
if (widget.secondaryAnimation.status == AnimationStatus.forward || _transitionWasInterrupted) {
|
||||
return ScaleTransition(
|
||||
scale: _forwardStartScreenScaleTransition,
|
||||
child: child,
|
||||
);
|
||||
} else if (widget.secondaryAnimation.status == AnimationStatus.reverse) {
|
||||
return ScaleTransition(
|
||||
scale: _reverseEndScreenScaleTransition,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return child;
|
||||
},
|
||||
child: widget.child,
|
||||
child: FadeTransition(
|
||||
opacity: fadeTransition,
|
||||
child: ScaleTransition(
|
||||
scale: scaleTransition,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ZoomExitTransition extends StatelessWidget {
|
||||
const _ZoomExitTransition({
|
||||
Key key,
|
||||
@required this.animation,
|
||||
this.reverse = false,
|
||||
this.child,
|
||||
}) : assert(animation != null),
|
||||
assert(reverse != null),
|
||||
super(key: key);
|
||||
|
||||
final Animation<double> animation;
|
||||
final bool reverse;
|
||||
final Widget child;
|
||||
|
||||
static final Animatable<double> _fadeOutTransition = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 0.0,
|
||||
).chain(CurveTween(curve: const Interval(0.0825, 0.2075)));
|
||||
|
||||
static final Animatable<double> _scaleUpTransition = Tween<double>(
|
||||
begin: 1.00,
|
||||
end: 1.05,
|
||||
).chain(_ZoomPageTransition._scaleCurveSequence);
|
||||
|
||||
static final Animatable<double> _scaleDownTransition = Tween<double>(
|
||||
begin: 1.00,
|
||||
end: 0.90,
|
||||
).chain(_ZoomPageTransition._scaleCurveSequence);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Animation<double> fadeTransition = reverse
|
||||
? _fadeOutTransition.animate(animation)
|
||||
: kAlwaysCompleteAnimation;
|
||||
final Animation<double> scaleTransition = (reverse
|
||||
? _scaleDownTransition
|
||||
: _scaleUpTransition
|
||||
).animate(animation);
|
||||
|
||||
return FadeTransition(
|
||||
opacity: fadeTransition,
|
||||
child: ScaleTransition(
|
||||
scale: scaleTransition,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
203
packages/flutter/lib/src/widgets/dual_transition_builder.dart
Normal file
203
packages/flutter/lib/src/widgets/dual_transition_builder.dart
Normal file
@@ -0,0 +1,203 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// @dart = 2.8
|
||||
|
||||
import 'basic.dart';
|
||||
import 'framework.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 AnimatedTransitionBuilder = 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 AnimatedTransitionBuilder 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 AnimatedTransitionBuilder 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ export 'src/widgets/dismissible.dart';
|
||||
export 'src/widgets/disposable_build_context.dart';
|
||||
export 'src/widgets/drag_target.dart';
|
||||
export 'src/widgets/draggable_scrollable_sheet.dart';
|
||||
export 'src/widgets/dual_transition_builder.dart';
|
||||
export 'src/widgets/editable_text.dart';
|
||||
export 'src/widgets/fade_in_image.dart';
|
||||
export 'src/widgets/focus_manager.dart';
|
||||
|
||||
@@ -161,4 +161,50 @@ void main() {
|
||||
expect(find.text('page b'), findsOneWidget);
|
||||
expect(findZoomPageTransition(), findsOneWidget);
|
||||
}, variant: TargetPlatformVariant.only(TargetPlatform.android));
|
||||
|
||||
testWidgets('_ZoomPageTransition only cause child widget built once', (WidgetTester tester) async {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/58345
|
||||
|
||||
int builtCount = 0;
|
||||
|
||||
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
|
||||
'/': (BuildContext context) => Material(
|
||||
child: FlatButton(
|
||||
child: const Text('push'),
|
||||
onPressed: () { Navigator.of(context).pushNamed('/b'); },
|
||||
),
|
||||
),
|
||||
'/b': (BuildContext context) => StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
builtCount++; // Increase [builtCount] each time the widget build
|
||||
return FlatButton(
|
||||
child: const Text('pop'),
|
||||
onPressed: () { Navigator.pop(context); },
|
||||
);
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(
|
||||
pageTransitionsTheme: const PageTransitionsTheme(
|
||||
builders: <TargetPlatform, PageTransitionsBuilder>{
|
||||
TargetPlatform.android: ZoomPageTransitionsBuilder(), // creates a _ZoomPageTransition
|
||||
},
|
||||
),
|
||||
),
|
||||
routes: routes,
|
||||
),
|
||||
);
|
||||
|
||||
// No matter push or pop was called, the child widget should built only once.
|
||||
await tester.tap(find.text('push'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(builtCount, 1);
|
||||
|
||||
await tester.tap(find.text('pop'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(builtCount, 1);
|
||||
}, variant: TargetPlatformVariant.only(TargetPlatform.android));
|
||||
}
|
||||
|
||||
302
packages/flutter/test/widgets/dual_transition_builder_test.dart
Normal file
302
packages/flutter/test/widgets/dual_transition_builder_test.dart
Normal file
@@ -0,0 +1,302 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// @dart = 2.8
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:flutter/src/widgets/dual_transition_builder.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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user