Unverified Commit 17cb865e authored by Shi-Hao Hong's avatar Shi-Hao Hong Committed by GitHub

Revert "Revert "[PageTransitionsBuilder] Fix 'ZoomPageTransition' built more...

Revert "Revert "[PageTransitionsBuilder] Fix 'ZoomPageTransition' built more than once (#58686)" (#59992)" (#60245)
parent 0486cebd
// 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,
),
);
}
}
......@@ -38,6 +38,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));
}
// 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);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment