Skip to content

Commit fe15d1e

Browse files
authored
[PageTransitionsBuilder] Fix 'ZoomPageTransition' built more than once (flutter#58686)
1 parent e0ed12c commit fe15d1e

File tree

5 files changed

+743
-116
lines changed

5 files changed

+743
-116
lines changed

packages/flutter/lib/src/material/page_transitions_theme.dart

Lines changed: 191 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -150,19 +150,19 @@ class _OpenUpwardsPageTransition extends StatelessWidget {
150150

151151
// Zooms and fades a new page in, zooming out the previous page. This transition
152152
// is designed to match the Android 10 activity transition.
153-
class _ZoomPageTransition extends StatefulWidget {
153+
class _ZoomPageTransition extends StatelessWidget {
154+
/// Creates a [_ZoomPageTransition].
155+
///
156+
/// The [animation] and [secondaryAnimation] argument are required and must
157+
/// not be null.
154158
const _ZoomPageTransition({
155159
Key key,
156-
this.animation,
157-
this.secondaryAnimation,
160+
@required this.animation,
161+
@required this.secondaryAnimation,
158162
this.child,
159-
}) : super(key: key);
160-
161-
// The scrim obscures the old page by becoming increasingly opaque.
162-
static final Tween<double> _scrimOpacityTween = Tween<double>(
163-
begin: 0.0,
164-
end: 0.60,
165-
);
163+
}) : assert(animation != null),
164+
assert(secondaryAnimation != null),
165+
super(key: key);
166166

167167
// A curve sequence that is similar to the 'fastOutExtraSlowIn' curve used in
168168
// the native transition.
@@ -179,132 +179,207 @@ class _ZoomPageTransition extends StatefulWidget {
179179
),
180180
];
181181
static final TweenSequence<double> _scaleCurveSequence = TweenSequence<double>(fastOutExtraSlowInTweenSequenceItems);
182-
static final FlippedTweenSequence _flippedScaleCurveSequence = FlippedTweenSequence(fastOutExtraSlowInTweenSequenceItems);
183182

183+
/// The animation that drives the [child]'s entrance and exit.
184+
///
185+
/// See also:
186+
///
187+
/// * [TransitionRoute.animation], which is the value given to this property
188+
/// when the [_ZoomPageTransition] is used as a page transition.
184189
final Animation<double> animation;
190+
191+
/// The animation that transitions [child] when new content is pushed on top
192+
/// of it.
193+
///
194+
/// See also:
195+
///
196+
/// * [TransitionRoute.secondaryAnimation], which is the value given to this
197+
// property when the [_ZoomPageTransition] is used as a page transition.
185198
final Animation<double> secondaryAnimation;
199+
200+
/// The widget below this widget in the tree.
201+
///
202+
/// This widget will transition in and out as driven by [animation] and
203+
/// [secondaryAnimation].
186204
final Widget child;
187205

188206
@override
189-
__ZoomPageTransitionState createState() => __ZoomPageTransitionState();
207+
Widget build(BuildContext context) {
208+
return DualTransitionBuilder(
209+
animation: animation,
210+
forwardBuilder: (
211+
BuildContext context,
212+
Animation<double> animation,
213+
Widget child,
214+
) {
215+
return _ZoomEnterTransition(
216+
animation: animation,
217+
child: child,
218+
);
219+
},
220+
reverseBuilder: (
221+
BuildContext context,
222+
Animation<double> animation,
223+
Widget child,
224+
) {
225+
return _ZoomExitTransition(
226+
animation: animation,
227+
reverse: true,
228+
child: child,
229+
);
230+
},
231+
child: DualTransitionBuilder(
232+
animation: ReverseAnimation(secondaryAnimation),
233+
forwardBuilder: (
234+
BuildContext context,
235+
Animation<double> animation,
236+
Widget child,
237+
) {
238+
return _ZoomEnterTransition(
239+
animation: animation,
240+
reverse: true,
241+
child: child,
242+
);
243+
},
244+
reverseBuilder: (
245+
BuildContext context,
246+
Animation<double> animation,
247+
Widget child,
248+
) {
249+
return _ZoomExitTransition(
250+
animation: animation,
251+
child: child,
252+
);
253+
},
254+
child: child,
255+
),
256+
);
257+
}
190258
}
191259

192-
class __ZoomPageTransitionState extends State<_ZoomPageTransition> {
193-
AnimationStatus _currentAnimationStatus;
194-
AnimationStatus _lastAnimationStatus;
195-
196-
@override
197-
void initState() {
198-
super.initState();
199-
widget.animation.addStatusListener((AnimationStatus animationStatus) {
200-
_lastAnimationStatus = _currentAnimationStatus;
201-
_currentAnimationStatus = animationStatus;
202-
});
203-
}
260+
class _ZoomEnterTransition extends StatelessWidget {
261+
const _ZoomEnterTransition({
262+
Key key,
263+
@required this.animation,
264+
this.reverse = false,
265+
this.child,
266+
}) : assert(animation != null),
267+
assert(reverse != null),
268+
super(key: key);
204269

205-
// This check ensures that the animation reverses the original animation if
206-
// the transition were interrupted midway. This prevents a disjointed
207-
// experience since the reverse animation uses different fade and scaling
208-
// curves.
209-
bool get _transitionWasInterrupted {
210-
bool wasInProgress = false;
211-
bool isInProgress = false;
212-
213-
switch (_currentAnimationStatus) {
214-
case AnimationStatus.completed:
215-
case AnimationStatus.dismissed:
216-
isInProgress = false;
217-
break;
218-
case AnimationStatus.forward:
219-
case AnimationStatus.reverse:
220-
isInProgress = true;
221-
break;
222-
}
223-
switch (_lastAnimationStatus) {
224-
case AnimationStatus.completed:
225-
case AnimationStatus.dismissed:
226-
wasInProgress = false;
227-
break;
228-
case AnimationStatus.forward:
229-
case AnimationStatus.reverse:
230-
wasInProgress = true;
231-
break;
232-
}
233-
return wasInProgress && isInProgress;
234-
}
270+
final Animation<double> animation;
271+
final Widget child;
272+
final bool reverse;
235273

236-
@override
237-
Widget build(BuildContext context) {
238-
final Animation<double> _forwardScrimOpacityAnimation = widget.animation.drive(
239-
_ZoomPageTransition._scrimOpacityTween
240-
.chain(CurveTween(curve: const Interval(0.2075, 0.4175))));
274+
static final Animatable<double> _fadeInTransition = Tween<double>(
275+
begin: 0.0,
276+
end: 1.00,
277+
).chain(CurveTween(curve: const Interval(0.125, 0.250)));
241278

242-
final Animation<double> _forwardEndScreenScaleTransition = widget.animation.drive(
243-
Tween<double>(begin: 0.85, end: 1.00)
244-
.chain(_ZoomPageTransition._scaleCurveSequence));
279+
static final Animatable<double> _scaleDownTransition = Tween<double>(
280+
begin: 1.10,
281+
end: 1.00,
282+
).chain(_ZoomPageTransition._scaleCurveSequence);
245283

246-
final Animation<double> _forwardStartScreenScaleTransition = widget.secondaryAnimation.drive(
247-
Tween<double>(begin: 1.00, end: 1.05)
248-
.chain(_ZoomPageTransition._scaleCurveSequence));
284+
static final Animatable<double> _scaleUpTransition = Tween<double>(
285+
begin: 0.85,
286+
end: 1.00,
287+
).chain(_ZoomPageTransition._scaleCurveSequence);
249288

250-
final Animation<double> _forwardEndScreenFadeTransition = widget.animation.drive(
251-
Tween<double>(begin: 0.0, end: 1.00)
252-
.chain(CurveTween(curve: const Interval(0.125, 0.250))));
289+
static final Animatable<double> _scrimOpacityTween = Tween<double>(
290+
begin: 0.0,
291+
end: 0.60,
292+
).chain(CurveTween(curve: const Interval(0.2075, 0.4175)));
253293

254-
final Animation<double> _reverseEndScreenScaleTransition = widget.secondaryAnimation.drive(
255-
Tween<double>(begin: 1.00, end: 1.10)
256-
.chain(_ZoomPageTransition._flippedScaleCurveSequence));
294+
@override
295+
Widget build(BuildContext context) {
296+
double opacity = 0;
297+
// The transition's scrim opacity only increases on the forward transition. In the reverse
298+
// transition, the opacity should always be 0.0.
299+
//
300+
// Therefore, we need to only apply the scrim opacity animation when the transition
301+
// is running forwards.
302+
//
303+
// The reason that we check that the animation's status is not `completed` instead
304+
// of checking that it is `forward` is that this allows the interrupted reversal of the
305+
// forward transition to smoothly fade the scrim away. This prevents a disjointed
306+
// removal of the scrim.
307+
if (!reverse && animation.status != AnimationStatus.completed) {
308+
opacity = _scrimOpacityTween.evaluate(animation);
309+
}
257310

258-
final Animation<double> _reverseStartScreenScaleTransition = widget.animation.drive(
259-
Tween<double>(begin: 0.9, end: 1.0)
260-
.chain(_ZoomPageTransition._flippedScaleCurveSequence));
311+
final Animation<double> fadeTransition = reverse
312+
? kAlwaysCompleteAnimation
313+
: _fadeInTransition.animate(animation);
261314

262-
final Animation<double> _reverseStartScreenFadeTransition = widget.animation.drive(
263-
Tween<double>(begin: 0.0, end: 1.00)
264-
.chain(CurveTween(curve: const Interval(1 - 0.2075, 1 - 0.0825))));
315+
final Animation<double> scaleTransition = (reverse
316+
? _scaleDownTransition
317+
: _scaleUpTransition
318+
).animate(animation);
265319

266320
return AnimatedBuilder(
267-
animation: widget.animation,
321+
animation: animation,
268322
builder: (BuildContext context, Widget child) {
269-
if (widget.animation.status == AnimationStatus.forward || _transitionWasInterrupted) {
270-
return Container(
271-
color: Colors.black.withOpacity(_forwardScrimOpacityAnimation.value),
272-
child: FadeTransition(
273-
opacity: _forwardEndScreenFadeTransition,
274-
child: ScaleTransition(
275-
scale: _forwardEndScreenScaleTransition,
276-
child: child,
277-
),
278-
),
279-
);
280-
} else if (widget.animation.status == AnimationStatus.reverse) {
281-
return ScaleTransition(
282-
scale: _reverseStartScreenScaleTransition,
283-
child: FadeTransition(
284-
opacity: _reverseStartScreenFadeTransition,
285-
child: child,
286-
),
287-
);
288-
}
289-
return child;
323+
return Container(
324+
color: Colors.black.withOpacity(opacity),
325+
child: child,
326+
);
290327
},
291-
child: AnimatedBuilder(
292-
animation: widget.secondaryAnimation,
293-
builder: (BuildContext context, Widget child) {
294-
if (widget.secondaryAnimation.status == AnimationStatus.forward || _transitionWasInterrupted) {
295-
return ScaleTransition(
296-
scale: _forwardStartScreenScaleTransition,
297-
child: child,
298-
);
299-
} else if (widget.secondaryAnimation.status == AnimationStatus.reverse) {
300-
return ScaleTransition(
301-
scale: _reverseEndScreenScaleTransition,
302-
child: child,
303-
);
304-
}
305-
return child;
306-
},
307-
child: widget.child,
328+
child: FadeTransition(
329+
opacity: fadeTransition,
330+
child: ScaleTransition(
331+
scale: scaleTransition,
332+
child: child,
333+
),
334+
),
335+
);
336+
}
337+
}
338+
339+
class _ZoomExitTransition extends StatelessWidget {
340+
const _ZoomExitTransition({
341+
Key key,
342+
@required this.animation,
343+
this.reverse = false,
344+
this.child,
345+
}) : assert(animation != null),
346+
assert(reverse != null),
347+
super(key: key);
348+
349+
final Animation<double> animation;
350+
final bool reverse;
351+
final Widget child;
352+
353+
static final Animatable<double> _fadeOutTransition = Tween<double>(
354+
begin: 1.0,
355+
end: 0.0,
356+
).chain(CurveTween(curve: const Interval(0.0825, 0.2075)));
357+
358+
static final Animatable<double> _scaleUpTransition = Tween<double>(
359+
begin: 1.00,
360+
end: 1.05,
361+
).chain(_ZoomPageTransition._scaleCurveSequence);
362+
363+
static final Animatable<double> _scaleDownTransition = Tween<double>(
364+
begin: 1.00,
365+
end: 0.90,
366+
).chain(_ZoomPageTransition._scaleCurveSequence);
367+
368+
@override
369+
Widget build(BuildContext context) {
370+
final Animation<double> fadeTransition = reverse
371+
? _fadeOutTransition.animate(animation)
372+
: kAlwaysCompleteAnimation;
373+
final Animation<double> scaleTransition = (reverse
374+
? _scaleDownTransition
375+
: _scaleUpTransition
376+
).animate(animation);
377+
378+
return FadeTransition(
379+
opacity: fadeTransition,
380+
child: ScaleTransition(
381+
scale: scaleTransition,
382+
child: child,
308383
),
309384
);
310385
}

0 commit comments

Comments
 (0)