From 22d1a25f1ff69811c29cb99c5235dfe0d1ddb659 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Mon, 2 Dec 2019 15:43:32 -0800 Subject: [PATCH 01/11] initial --- .../lib/src/fade_through_transition.dart | 298 ++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 packages/animations/lib/src/fade_through_transition.dart diff --git a/packages/animations/lib/src/fade_through_transition.dart b/packages/animations/lib/src/fade_through_transition.dart new file mode 100644 index 000000000000..dbb0122ba998 --- /dev/null +++ b/packages/animations/lib/src/fade_through_transition.dart @@ -0,0 +1,298 @@ +// Copyright 2019 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/material.dart'; + +/// Used by [PageTransitionsTheme] to define a page route transition animation +/// in which the outgoing page fade out, then the incoming page fade in and +/// scale up. +/// +/// This pattern is recommended for transition animation between UI elements +/// that do not have a strong relationship to one another. +/// +/// Scale is only applied to incoming elements to emphasize new content over +/// old. +/// +/// See also: +/// +/// * [Fade-through](https://spec.googleplex.com/draft/mio-design/motion-new/the-motion-system.html#fade-through) +/// in the Material Design spec. +class FadeThroughPageTransitionsBuilder extends PageTransitionsBuilder { + /// Creates a [FadeThroughPageTransitionsBuilder]. + const FadeThroughPageTransitionsBuilder(); + + @override + Widget buildTransitions( + PageRoute route, + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { + return FadeThroughTransition( + animation: animation, + secondaryAnimation: secondaryAnimation, + child: child, + ); + } +} + +/// Defines a transition in which outgoing elements fade out, then incoming +/// elements fade in and scale up. +/// +/// The Fade through pattern provides a transition animation between UI elements +/// that do not have a strong relationship to one another. As an example, the +/// [BottomNavigationBar] may use this animation to transition the currently +/// displayed content when a new [BottomNavigationBarItem] is selected. +/// +/// Scale is only applied to incoming elements to emphasize new content over +/// old. +/// +/// See also: +/// +/// * [Fade-through](https://spec.googleplex.com/draft/mio-design/motion-new/the-motion-system.html#fade-through) +/// in the Material Design spec. +class FadeThroughTransition extends StatefulWidget { + /// Creates a [FadeThroughTransition]. + /// + /// The [animation] and [secondaryAnimation] argument are required and must + /// not be null. + const FadeThroughTransition({ + @required this.animation, + @required this.secondaryAnimation, + this.child, + }) : assert(animation != null), + assert(secondaryAnimation != null); + + /// The animation that drives the [child]'s entrance and exit. + /// + /// See also: + /// + /// * [TransitionRoute.animate], which is the value given to this property + /// when the [FadeThroughTransition] is used as a page transition. + final Animation 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 [FadeThroughTransition] is used as a page transition. + final Animation 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 + State createState() => _FadeThroughTransitionState(); +} + +class _FadeThroughTransitionState extends State { + AnimationStatus _effectiveAnimationStatus; + AnimationStatus _effectiveSecondaryAnimationStatus; + + @override + void initState() { + super.initState(); + _effectiveAnimationStatus = widget.animation.status; + _effectiveSecondaryAnimationStatus = widget.secondaryAnimation.status; + widget.animation.addStatusListener(_animationListener); + widget.secondaryAnimation.addStatusListener(_secondaryAnimationListener); + } + + void _animationListener(AnimationStatus animationStatus) { + _effectiveAnimationStatus = _calculateEffectiveAnimationStatus( + lastEffective: _effectiveAnimationStatus, + current: animationStatus, + ); + } + + void _secondaryAnimationListener(AnimationStatus animationStatus) { + _effectiveSecondaryAnimationStatus = _calculateEffectiveAnimationStatus( + lastEffective: _effectiveSecondaryAnimationStatus, + current: animationStatus, + ); + } + + // When a transition is interrupted midway we just want to play the ongoing + // animation in reverse. Switching to the actual reverse transition would + // yield a disjoint experience since the forward and reverse transitions are + // very different. + AnimationStatus _calculateEffectiveAnimationStatus({ + @required AnimationStatus lastEffective, + @required AnimationStatus current, + }) { + assert(current != null); + assert(lastEffective != null); + switch (current) { + case AnimationStatus.dismissed: + case AnimationStatus.completed: + return current; + case AnimationStatus.forward: + switch (lastEffective) { + case AnimationStatus.dismissed: + case AnimationStatus.completed: + case AnimationStatus.forward: + return current; + case AnimationStatus.reverse: + return lastEffective; + } + break; + case AnimationStatus.reverse: + switch (lastEffective) { + case AnimationStatus.dismissed: + case AnimationStatus.completed: + case AnimationStatus.reverse: + return current; + case AnimationStatus.forward: + return lastEffective; + } + break; + } + return null; // unreachable + } + + @override + void didUpdateWidget(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() { + super.dispose(); + widget.animation.removeStatusListener(_animationListener); + widget.secondaryAnimation.removeStatusListener(_secondaryAnimationListener); + } + + static final Tween _flippedTween = Tween(begin: 1.0, end: 0.0); + static Animation _flip(Animation animation) { + return _flippedTween.animate(animation); + } + + // Since widget.child gets moved around in the tree a bit we associate a + // global key with it to ensure that it's state doesn't get lost. + final GlobalKey _childKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: widget.animation, + builder: (BuildContext context, Widget child) { + assert(_effectiveAnimationStatus != null); + switch (_effectiveAnimationStatus) { + case AnimationStatus.forward: + return _ZoomedFadeIn( + animation: widget.animation, + child: child, + ); + case AnimationStatus.dismissed: + case AnimationStatus.reverse: + case AnimationStatus.completed: + return _FadeOut( + animation: _flip(widget.animation), + child: child, + ); + } + return null; // unreachable + }, + child: Container( + color: Theme.of(context).canvasColor, + child: AnimatedBuilder( + animation: widget.secondaryAnimation, + builder: (BuildContext context, Widget child) { + assert(_effectiveSecondaryAnimationStatus != null); + switch (_effectiveSecondaryAnimationStatus) { + case AnimationStatus.forward: + return _FadeOut( + child: child, + animation: widget.secondaryAnimation, + ); + case AnimationStatus.dismissed: + case AnimationStatus.reverse: + case AnimationStatus.completed: + return _ZoomedFadeIn( + animation: _flip(widget.secondaryAnimation), + child: child, + ); + } + return null; // unreachable + }, + child: KeyedSubtree( + key: _childKey, + child: widget.child, + ), + ), + ), + ); + } +} + +class _ZoomedFadeIn extends StatelessWidget { + const _ZoomedFadeIn({ + this.child, + this.animation, + }); + + final Widget child; + final Animation animation; + + static final CurveTween _inCurve = CurveTween(curve: const Cubic(0.0, 0.0, 0.2, 1.0)); + static final TweenSequence _scaleIn = TweenSequence(>[ + TweenSequenceItem(tween: ConstantTween(0.0), weight: 6 / 20), + TweenSequenceItem(tween: Tween(begin: 0.95, end: 1.0).chain(_inCurve), weight: 14 / 20), + ]); + static final TweenSequence _fadeInOpacity = TweenSequence(>[ + TweenSequenceItem(tween: ConstantTween(0.0), weight: 6 / 20), + TweenSequenceItem(tween: Tween(begin: 0.0, end: 1.0).chain(_inCurve), weight: 14 / 20), + ]); + + @override + Widget build(BuildContext context) { + return FadeTransition( + opacity: _fadeInOpacity.animate(animation), + child: ScaleTransition( + scale: _scaleIn.animate(animation), + child: child, + ), + ); + } +} + +class _FadeOut extends StatelessWidget { + const _FadeOut({ + this.child, + this.animation, + }); + + final Widget child; + final Animation animation; + + static final CurveTween _outCurve = CurveTween(curve: const Cubic(0.4, 0.0, 1.0, 1.0)); + static final TweenSequence _fadeOutOpacity = TweenSequence(>[ + TweenSequenceItem(tween: Tween(begin: 1.0, end: 0.0).chain(_outCurve), weight: 6 / 20), + TweenSequenceItem(tween: ConstantTween(0.0), weight: 14 / 20), + ]); + + @override + Widget build(BuildContext context) { + return FadeTransition( + opacity: _fadeOutOpacity.animate(animation), + child: child, + ); + } +} From ce393f3d7c308e6c8dc4948e4e5cce727eba6cf4 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Mon, 2 Dec 2019 15:46:59 -0800 Subject: [PATCH 02/11] format --- .../lib/src/fade_through_transition.dart | 62 ++++++++++++++----- 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/packages/animations/lib/src/fade_through_transition.dart b/packages/animations/lib/src/fade_through_transition.dart index dbb0122ba998..d090f7ed3ff8 100644 --- a/packages/animations/lib/src/fade_through_transition.dart +++ b/packages/animations/lib/src/fade_through_transition.dart @@ -166,7 +166,8 @@ class _FadeThroughTransitionState extends State { _animationListener(widget.animation.status); } if (oldWidget.secondaryAnimation != widget.secondaryAnimation) { - oldWidget.secondaryAnimation.removeStatusListener(_secondaryAnimationListener); + oldWidget.secondaryAnimation + .removeStatusListener(_secondaryAnimationListener); widget.secondaryAnimation.addStatusListener(_secondaryAnimationListener); _secondaryAnimationListener(widget.secondaryAnimation.status); } @@ -179,7 +180,8 @@ class _FadeThroughTransitionState extends State { widget.secondaryAnimation.removeStatusListener(_secondaryAnimationListener); } - static final Tween _flippedTween = Tween(begin: 1.0, end: 0.0); + static final Tween _flippedTween = + Tween(begin: 1.0, end: 0.0); static Animation _flip(Animation animation) { return _flippedTween.animate(animation); } @@ -251,15 +253,33 @@ class _ZoomedFadeIn extends StatelessWidget { final Widget child; final Animation animation; - static final CurveTween _inCurve = CurveTween(curve: const Cubic(0.0, 0.0, 0.2, 1.0)); - static final TweenSequence _scaleIn = TweenSequence(>[ - TweenSequenceItem(tween: ConstantTween(0.0), weight: 6 / 20), - TweenSequenceItem(tween: Tween(begin: 0.95, end: 1.0).chain(_inCurve), weight: 14 / 20), - ]); - static final TweenSequence _fadeInOpacity = TweenSequence(>[ - TweenSequenceItem(tween: ConstantTween(0.0), weight: 6 / 20), - TweenSequenceItem(tween: Tween(begin: 0.0, end: 1.0).chain(_inCurve), weight: 14 / 20), - ]); + static final CurveTween _inCurve = CurveTween( + curve: const Cubic(0.0, 0.0, 0.2, 1.0), + ); + static final TweenSequence _scaleIn = TweenSequence( + >[ + TweenSequenceItem( + tween: ConstantTween(0.0), + weight: 6 / 20, + ), + TweenSequenceItem( + tween: Tween(begin: 0.95, end: 1.0).chain(_inCurve), + weight: 14 / 20, + ), + ], + ); + static final TweenSequence _fadeInOpacity = TweenSequence( + >[ + TweenSequenceItem( + tween: ConstantTween(0.0), + weight: 6 / 20, + ), + TweenSequenceItem( + tween: Tween(begin: 0.0, end: 1.0).chain(_inCurve), + weight: 14 / 20, + ), + ], + ); @override Widget build(BuildContext context) { @@ -282,11 +302,21 @@ class _FadeOut extends StatelessWidget { final Widget child; final Animation animation; - static final CurveTween _outCurve = CurveTween(curve: const Cubic(0.4, 0.0, 1.0, 1.0)); - static final TweenSequence _fadeOutOpacity = TweenSequence(>[ - TweenSequenceItem(tween: Tween(begin: 1.0, end: 0.0).chain(_outCurve), weight: 6 / 20), - TweenSequenceItem(tween: ConstantTween(0.0), weight: 14 / 20), - ]); + static final CurveTween _outCurve = CurveTween( + curve: const Cubic(0.4, 0.0, 1.0, 1.0), + ); + static final TweenSequence _fadeOutOpacity = TweenSequence( + >[ + TweenSequenceItem( + tween: Tween(begin: 1.0, end: 0.0).chain(_outCurve), + weight: 6 / 20, + ), + TweenSequenceItem( + tween: ConstantTween(0.0), + weight: 14 / 20, + ), + ], + ); @override Widget build(BuildContext context) { From 492ff8f58b2d1f479fee670edb7af50555fa8b46 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Mon, 2 Dec 2019 15:48:00 -0800 Subject: [PATCH 03/11] add export --- packages/animations/lib/animations.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/animations/lib/animations.dart b/packages/animations/lib/animations.dart index f58279ddaa58..d34f54e408fa 100644 --- a/packages/animations/lib/animations.dart +++ b/packages/animations/lib/animations.dart @@ -2,4 +2,5 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +export 'src/fade_through_transition.dart'; export 'src/open_container.dart'; From 285838387ac1ba0795a1330bafe2a816707c7988 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Tue, 3 Dec 2019 12:22:42 -0800 Subject: [PATCH 04/11] tests --- .../lib/src/fade_through_transition.dart | 12 +- .../test/fade_through_transition_test.dart | 319 ++++++++++++++++++ 2 files changed, 328 insertions(+), 3 deletions(-) create mode 100644 packages/animations/test/fade_through_transition_test.dart diff --git a/packages/animations/lib/src/fade_through_transition.dart b/packages/animations/lib/src/fade_through_transition.dart index d090f7ed3ff8..a1279e8ed908 100644 --- a/packages/animations/lib/src/fade_through_transition.dart +++ b/packages/animations/lib/src/fade_through_transition.dart @@ -49,6 +49,12 @@ class FadeThroughPageTransitionsBuilder extends PageTransitionsBuilder { /// Scale is only applied to incoming elements to emphasize new content over /// old. /// +/// Consider using [FadeThroughPageTransitionsBuilder] within a +/// [PageTransitionsTheme] if you want to apply this kind of transition to +/// [MaterialPageRoute] transitions within a Navigator. Or use this transition +/// directly in a [PageTransitionSwitcher.transitionBuilder] to transition +/// from one widget to another. +/// /// See also: /// /// * [Fade-through](https://spec.googleplex.com/draft/mio-design/motion-new/the-motion-system.html#fade-through) @@ -205,7 +211,7 @@ class _FadeThroughTransitionState extends State { case AnimationStatus.dismissed: case AnimationStatus.reverse: case AnimationStatus.completed: - return _FadeOut( + return _FadeOut( animation: _flip(widget.animation), child: child, ); @@ -227,7 +233,7 @@ class _FadeThroughTransitionState extends State { case AnimationStatus.dismissed: case AnimationStatus.reverse: case AnimationStatus.completed: - return _ZoomedFadeIn( + return _ZoomedFadeIn( animation: _flip(widget.secondaryAnimation), child: child, ); @@ -259,7 +265,7 @@ class _ZoomedFadeIn extends StatelessWidget { static final TweenSequence _scaleIn = TweenSequence( >[ TweenSequenceItem( - tween: ConstantTween(0.0), + tween: ConstantTween(0.95), weight: 6 / 20, ), TweenSequenceItem( diff --git a/packages/animations/test/fade_through_transition_test.dart b/packages/animations/test/fade_through_transition_test.dart new file mode 100644 index 000000000000..e194084d79db --- /dev/null +++ b/packages/animations/test/fade_through_transition_test.dart @@ -0,0 +1,319 @@ +// Copyright 2019 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/fade_through_transition.dart'; +import 'package:flutter/animation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/widgets.dart'; + +void main() { + testWidgets('FadeThroughPageTransitionsBuilder builds a FadeThroughTransition', (WidgetTester tester) async { + final AnimationController animation = AnimationController(vsync: const TestVSync()); + final AnimationController secondaryAnimation = AnimationController(vsync: const TestVSync()); + + await tester.pumpWidget(const FadeThroughPageTransitionsBuilder().buildTransitions( + null, + null, + animation, + secondaryAnimation, + const Placeholder(), + )); + + expect(find.byType(FadeThroughTransition), findsOneWidget); + }); + + testWidgets('FadeThroughTransition runs forward', (WidgetTester tester) async { + final GlobalKey navigator = GlobalKey(); + const String bottomRoute = '/'; + const String topRoute = '/a'; + + await tester.pumpWidget( + _TestWidget( + navigatorKey: navigator, + ), + ); + expect(find.text(bottomRoute), findsOneWidget); + expect(_getScale(bottomRoute, tester), 1.0); + expect(_getOpacity(bottomRoute, tester), 1.0); + expect(find.text(topRoute), findsNothing); + + navigator.currentState.pushNamed(topRoute); + await tester.pump(); + await tester.pump(); + + // Bottom route is full size and fully visible. + expect(find.text(bottomRoute), findsOneWidget); + expect(_getScale(bottomRoute, tester), 1.0); + expect(_getOpacity(bottomRoute, tester), 1.0); + // top route is at 95% of full size and not visible yet. + expect(find.text(topRoute), findsOneWidget); + expect(_getScale(topRoute, tester), 0.95); + expect(_getOpacity(topRoute, tester), 0.0); + + // Jump to half-way through the fade out (total duration is 300ms, 6/12th of + // that are fade out, so half-way is 300 * 6/12 / 2 = 45ms. + await tester.pump(const Duration(milliseconds: 45)); + // Bottom route is fading out. + expect(find.text(bottomRoute), findsOneWidget); + expect(_getScale(bottomRoute, tester), 1.0); + final double bottomOpacity = _getOpacity(bottomRoute, tester); + expect(bottomOpacity, lessThan(1.0)); + expect(bottomOpacity, greaterThan(0.0)); + // Top route is still invisible. + expect(find.text(topRoute), findsOneWidget); + expect(_getScale(topRoute, tester), 0.95); + expect(_getOpacity(topRoute, tester), 0.0); + + // Let's jump to the end of the fade-out. + await tester.pump(const Duration(milliseconds: 45)); + // Bottom route is completely faded out. + expect(find.text(bottomRoute), findsOneWidget); + expect(_getScale(bottomRoute, tester), 1.0); + expect(_getOpacity(bottomRoute, tester), 0.0); + // Top route is still invisible. + expect(find.text(topRoute), findsOneWidget); + expect(_getScale(topRoute, tester), 0.95); + expect(_getOpacity(topRoute, tester), 0.0); + + // Let's jump to the middle of the fade-in. + await tester.pump(const Duration(milliseconds: 105)); + // Bottom route is not visible. + expect(find.text(bottomRoute), findsOneWidget); + expect(_getScale(bottomRoute, tester), 1.0); + expect(_getOpacity(bottomRoute, tester), 0.0); + // Top route is fading/scaling in. + expect(find.text(topRoute), findsOneWidget); + final double topScale = _getScale(topRoute, tester); + final double topOpacity = _getOpacity(topRoute, tester); + expect(topScale, greaterThan(0.95)); + expect(topScale, lessThan(1.0)); + expect(topOpacity, greaterThan(0.0)); + expect(topOpacity, lessThan(1.0)); + + // Let's jump to the end of the animation. + await tester.pump(const Duration(milliseconds: 105)); + // Bottom route is not visible. + expect(find.text(bottomRoute), findsOneWidget); + expect(_getScale(bottomRoute, tester), 1.0); + expect(_getOpacity(bottomRoute, tester), 0.0); + // Top route fully scaled in and visible. + expect(find.text(topRoute), findsOneWidget); + expect(_getScale(topRoute, tester), 1.0); + expect(_getOpacity(topRoute, tester), 1.0); + + await tester.pump(const Duration(milliseconds: 1)); + expect(find.text(bottomRoute), findsNothing); + expect(find.text(topRoute), findsOneWidget); + }); + + testWidgets('FadeThroughTransition runs backwards', (WidgetTester tester) async { + final GlobalKey navigator = GlobalKey(); + const String bottomRoute = '/'; + const String topRoute = '/a'; + + await tester.pumpWidget( + _TestWidget( + navigatorKey: navigator, + ), + ); + navigator.currentState.pushNamed('/a'); + await tester.pumpAndSettle(); + + expect(find.text(topRoute), findsOneWidget); + expect(_getScale(topRoute, tester), 1.0); + expect(_getOpacity(topRoute, tester), 1.0); + expect(find.text(bottomRoute), findsNothing); + + navigator.currentState.pop(); + await tester.pump(); + + // Top route is full size and fully visible. + expect(find.text(topRoute), findsOneWidget); + expect(_getScale(topRoute, tester), 1.0); + expect(_getOpacity(topRoute, tester), 1.0); + // Bottom route is at 95% of full size and not visible yet. + expect(find.text(bottomRoute), findsOneWidget); + expect(_getScale(bottomRoute, tester), 0.95); + expect(_getOpacity(bottomRoute, tester), 0.0); + + // Jump to half-way through the fade out (total duration is 300ms, 6/12th of + // that are fade out, so half-way is 300 * 6/12 / 2 = 45ms. + await tester.pump(const Duration(milliseconds: 45)); + // Bottom route is fading out. + expect(find.text(topRoute), findsOneWidget); + expect(_getScale(topRoute, tester), 1.0); + final double topOpacity = _getOpacity(topRoute, tester); + expect(topOpacity, lessThan(1.0)); + expect(topOpacity, greaterThan(0.0)); + // Top route is still invisible. + expect(find.text(bottomRoute), findsOneWidget); + expect(_getScale(bottomRoute, tester), 0.95); + expect(_getOpacity(bottomRoute, tester), 0.0); + + // Let's jump to the end of the fade-out. + await tester.pump(const Duration(milliseconds: 45)); + // Bottom route is completely faded out. + expect(find.text(topRoute), findsOneWidget); + expect(_getScale(topRoute, tester), 1.0); + expect(_getOpacity(topRoute, tester), 0.0); + // Top route is still invisible. + expect(find.text(bottomRoute), findsOneWidget); + expect(_getScale(bottomRoute, tester), moreOrLessEquals(0.95, epsilon: 0.005)); + expect(_getOpacity(bottomRoute, tester), moreOrLessEquals(0.0, epsilon: 0.005)); + + // Let's jump to the middle of the fade-in. + await tester.pump(const Duration(milliseconds: 105)); + // Bottom route is not visible. + expect(find.text(topRoute), findsOneWidget); + expect(_getScale(topRoute, tester), 1.0); + expect(_getOpacity(topRoute, tester), 0.0); + // Top route is fading/scaling in. + expect(find.text(bottomRoute), findsOneWidget); + final double bottomScale = _getScale(bottomRoute, tester); + final double bottomOpacity = _getOpacity(bottomRoute, tester); + expect(bottomScale, greaterThan(0.96)); + expect(bottomScale, lessThan(1.0)); + expect(bottomOpacity, greaterThan(0.1)); + expect(bottomOpacity, lessThan(1.0)); + + // Let's jump to the end of the animation. + await tester.pump(const Duration(milliseconds: 105)); + // Bottom route is not visible. + expect(find.text(topRoute), findsOneWidget); + expect(_getScale(topRoute, tester), 1.0); + expect(_getOpacity(topRoute, tester), 0.0); + // Top route fully scaled in and visible. + expect(find.text(bottomRoute), findsOneWidget); + expect(_getScale(bottomRoute, tester), 1.0); + expect(_getOpacity(bottomRoute, tester), 1.0); + + await tester.pump(const Duration(milliseconds: 1)); + expect(find.text(topRoute), findsNothing); + expect(find.text(bottomRoute), findsOneWidget); + }); + + testWidgets('FadeThroughTransition does not jump when interrupted', (WidgetTester tester) async { + final GlobalKey navigator = GlobalKey(); + const String bottomRoute = '/'; + const String topRoute = '/a'; + + await tester.pumpWidget( + _TestWidget( + navigatorKey: navigator, + ), + ); + expect(find.text(bottomRoute), findsOneWidget); + expect(find.text(topRoute), findsNothing); + + navigator.currentState.pushNamed(topRoute); + await tester.pump(); + + // Jump to halfway point of transition. + await tester.pump(const Duration(milliseconds: 150)); + // Bottom route is fully faded out. + expect(find.text(bottomRoute), findsOneWidget); + expect(_getScale(bottomRoute, tester), 1.0); + expect(_getOpacity(bottomRoute, tester), 0.0); + // Top route is fading/scaling in. + expect(find.text(topRoute), findsOneWidget); + final double topScale = _getScale(topRoute, tester); + final double topOpacity = _getOpacity(topRoute, tester); + expect(topScale, greaterThan(0.95)); + expect(topScale, lessThan(1.0)); + expect(topOpacity, greaterThan(0.0)); + expect(topOpacity, lessThan(1.0)); + + // Interrupt the transition with a pop. + navigator.currentState.pop(); + await tester.pump(); + // Noting changed. + expect(find.text(bottomRoute), findsOneWidget); + expect(_getScale(bottomRoute, tester), 1.0); + expect(_getOpacity(bottomRoute, tester), 0.0); + expect(find.text(topRoute), findsOneWidget); + expect(_getScale(topRoute, tester), topScale); + expect(_getOpacity(topRoute, tester), topOpacity); + + // Jump to the halfway point. + await tester.pump(const Duration(milliseconds: 75)); + expect(find.text(bottomRoute), findsOneWidget); + expect(_getScale(bottomRoute, tester), 1.0); + final double bottomOpacity = _getOpacity(bottomRoute, tester); + expect(bottomOpacity, greaterThan(0.0)); + expect(bottomOpacity, lessThan(1.0)); + expect(find.text(topRoute), findsOneWidget); + expect(_getScale(topRoute, tester), lessThan(topScale)); + expect(_getOpacity(topRoute, tester), lessThan(topOpacity)); + + // Jump to the end. + await tester.pump(const Duration(milliseconds: 75)); + expect(find.text(bottomRoute), findsOneWidget); + expect(_getScale(bottomRoute, tester), 1.0); + expect(_getOpacity(bottomRoute, tester), 1.0); + expect(find.text(topRoute), findsOneWidget); + expect(_getScale(topRoute, tester), 0.95); + expect(_getOpacity(topRoute, tester), 0.0); + + await tester.pump(const Duration(milliseconds: 1)); + expect(find.text(topRoute), findsNothing); + expect(find.text(bottomRoute), findsOneWidget); + }); +} + +double _getOpacity(String key, WidgetTester tester) { + final Finder finder = find.ancestor( + of: find.byKey(ValueKey(key)), + matching: find.byType(FadeTransition), + ); + return tester.widgetList(finder).fold(1.0, (double a, Widget widget) { + final FadeTransition transition = widget; + return a * transition.opacity.value; + }); +} + +double _getScale(String key, WidgetTester tester) { + final Finder finder = find.ancestor( + of: find.byKey(ValueKey(key)), + matching: find.byType(ScaleTransition), + ); + return tester.widgetList(finder).fold(1.0, (double a, Widget widget) { + final ScaleTransition transition = widget; + return a * transition.scale.value; + }); +} + +class _TestWidget extends StatelessWidget { + const _TestWidget({this.navigatorKey}); + + final Key navigatorKey; + + @override + Widget build(BuildContext context) { + return MaterialApp( + navigatorKey: navigatorKey, + theme: ThemeData( + platform: TargetPlatform.android, + pageTransitionsTheme: const PageTransitionsTheme( + builders: { + TargetPlatform.android: FadeThroughPageTransitionsBuilder(), + }, + ), + ), + onGenerateRoute: (RouteSettings settings) { + return MaterialPageRoute( + settings: settings, + builder: (BuildContext context) { + return Container( + child: Center( + key: ValueKey(settings.name), + child: Text(settings.name), + ), + ); + }, + ); + }, + ); + } +} From be99a7e24a7e3bb6581c9755ae61a3c5a485e814 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Tue, 3 Dec 2019 12:24:15 -0800 Subject: [PATCH 05/11] format --- .../lib/src/fade_through_transition.dart | 4 +-- .../test/fade_through_transition_test.dart | 34 ++++++++++++++----- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/packages/animations/lib/src/fade_through_transition.dart b/packages/animations/lib/src/fade_through_transition.dart index a1279e8ed908..a50bc2b2bcd9 100644 --- a/packages/animations/lib/src/fade_through_transition.dart +++ b/packages/animations/lib/src/fade_through_transition.dart @@ -211,7 +211,7 @@ class _FadeThroughTransitionState extends State { case AnimationStatus.dismissed: case AnimationStatus.reverse: case AnimationStatus.completed: - return _FadeOut( + return _FadeOut( animation: _flip(widget.animation), child: child, ); @@ -233,7 +233,7 @@ class _FadeThroughTransitionState extends State { case AnimationStatus.dismissed: case AnimationStatus.reverse: case AnimationStatus.completed: - return _ZoomedFadeIn( + return _ZoomedFadeIn( animation: _flip(widget.secondaryAnimation), child: child, ); diff --git a/packages/animations/test/fade_through_transition_test.dart b/packages/animations/test/fade_through_transition_test.dart index e194084d79db..9aee306dc370 100644 --- a/packages/animations/test/fade_through_transition_test.dart +++ b/packages/animations/test/fade_through_transition_test.dart @@ -9,11 +9,18 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/widgets.dart'; void main() { - testWidgets('FadeThroughPageTransitionsBuilder builds a FadeThroughTransition', (WidgetTester tester) async { - final AnimationController animation = AnimationController(vsync: const TestVSync()); - final AnimationController secondaryAnimation = AnimationController(vsync: const TestVSync()); + testWidgets( + 'FadeThroughPageTransitionsBuilder builds a FadeThroughTransition', + (WidgetTester tester) async { + final AnimationController animation = AnimationController( + vsync: const TestVSync(), + ); + final AnimationController secondaryAnimation = AnimationController( + vsync: const TestVSync(), + ); - await tester.pumpWidget(const FadeThroughPageTransitionsBuilder().buildTransitions( + await tester.pumpWidget( + const FadeThroughPageTransitionsBuilder().buildTransitions( null, null, animation, @@ -24,7 +31,8 @@ void main() { expect(find.byType(FadeThroughTransition), findsOneWidget); }); - testWidgets('FadeThroughTransition runs forward', (WidgetTester tester) async { + testWidgets('FadeThroughTransition runs forward', + (WidgetTester tester) async { final GlobalKey navigator = GlobalKey(); const String bottomRoute = '/'; const String topRoute = '/a'; @@ -108,7 +116,8 @@ void main() { expect(find.text(topRoute), findsOneWidget); }); - testWidgets('FadeThroughTransition runs backwards', (WidgetTester tester) async { + testWidgets('FadeThroughTransition runs backwards', + (WidgetTester tester) async { final GlobalKey navigator = GlobalKey(); const String bottomRoute = '/'; const String topRoute = '/a'; @@ -160,8 +169,14 @@ void main() { expect(_getOpacity(topRoute, tester), 0.0); // Top route is still invisible. expect(find.text(bottomRoute), findsOneWidget); - expect(_getScale(bottomRoute, tester), moreOrLessEquals(0.95, epsilon: 0.005)); - expect(_getOpacity(bottomRoute, tester), moreOrLessEquals(0.0, epsilon: 0.005)); + expect( + _getScale(bottomRoute, tester), + moreOrLessEquals(0.95, epsilon: 0.005), + ); + expect( + _getOpacity(bottomRoute, tester), + moreOrLessEquals(0.0, epsilon: 0.005), + ); // Let's jump to the middle of the fade-in. await tester.pump(const Duration(milliseconds: 105)); @@ -194,7 +209,8 @@ void main() { expect(find.text(bottomRoute), findsOneWidget); }); - testWidgets('FadeThroughTransition does not jump when interrupted', (WidgetTester tester) async { + testWidgets('FadeThroughTransition does not jump when interrupted', + (WidgetTester tester) async { final GlobalKey navigator = GlobalKey(); const String bottomRoute = '/'; const String topRoute = '/a'; From 184e050bda20c92399b7a7f33a26550a2ec08efb Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Tue, 3 Dec 2019 12:25:29 -0800 Subject: [PATCH 06/11] more format --- packages/animations/lib/src/fade_through_transition.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/animations/lib/src/fade_through_transition.dart b/packages/animations/lib/src/fade_through_transition.dart index a50bc2b2bcd9..6d54a95bfa33 100644 --- a/packages/animations/lib/src/fade_through_transition.dart +++ b/packages/animations/lib/src/fade_through_transition.dart @@ -186,8 +186,10 @@ class _FadeThroughTransitionState extends State { widget.secondaryAnimation.removeStatusListener(_secondaryAnimationListener); } - static final Tween _flippedTween = - Tween(begin: 1.0, end: 0.0); + static final Tween _flippedTween = Tween( + begin: 1.0, + end: 0.0, + ); static Animation _flip(Animation animation) { return _flippedTween.animate(animation); } From 35823fd071083737ce3e23ae98206980ad609b9e Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Tue, 3 Dec 2019 14:09:25 -0800 Subject: [PATCH 07/11] more tests --- .../lib/src/fade_through_transition.dart | 9 +- .../test/fade_through_transition_test.dart | 87 +++++++++++++++++-- 2 files changed, 81 insertions(+), 15 deletions(-) diff --git a/packages/animations/lib/src/fade_through_transition.dart b/packages/animations/lib/src/fade_through_transition.dart index 6d54a95bfa33..963357f3cd6e 100644 --- a/packages/animations/lib/src/fade_through_transition.dart +++ b/packages/animations/lib/src/fade_through_transition.dart @@ -194,10 +194,6 @@ class _FadeThroughTransitionState extends State { return _flippedTween.animate(animation); } - // Since widget.child gets moved around in the tree a bit we associate a - // global key with it to ensure that it's state doesn't get lost. - final GlobalKey _childKey = GlobalKey(); - @override Widget build(BuildContext context) { return AnimatedBuilder( @@ -242,10 +238,7 @@ class _FadeThroughTransitionState extends State { } return null; // unreachable }, - child: KeyedSubtree( - key: _childKey, - child: widget.child, - ), + child: widget.child, ), ), ); diff --git a/packages/animations/test/fade_through_transition_test.dart b/packages/animations/test/fade_through_transition_test.dart index 9aee306dc370..a6d6aae8c7f9 100644 --- a/packages/animations/test/fade_through_transition_test.dart +++ b/packages/animations/test/fade_through_transition_test.dart @@ -276,6 +276,58 @@ void main() { expect(find.text(topRoute), findsNothing); expect(find.text(bottomRoute), findsOneWidget); }); + + testWidgets('State is not lost when transitioning', + (WidgetTester tester) async { + final GlobalKey navigator = GlobalKey(); + const String bottomRoute = '/'; + const String topRoute = '/a'; + + await tester.pumpWidget( + _TestWidget( + navigatorKey: navigator, + contentBuilder: (RouteSettings settings) { + return _StatefulTestWidget( + key: ValueKey(settings.name), + name: settings.name, + ); + }, + ), + ); + + final _StatefulTestWidgetState bottomState = tester.state(find.byKey(const ValueKey(bottomRoute))); + expect(bottomState.widget.name, bottomRoute); + + navigator.currentState.pushNamed(topRoute); + await tester.pump(); + await tester.pump(); + + expect(tester.state(find.byKey(const ValueKey(bottomRoute))), bottomState); + final _StatefulTestWidgetState topState = tester.state(find.byKey(const ValueKey(topRoute))); + expect(topState.widget.name, topRoute); + + await tester.pump(const Duration(milliseconds: 150)); + expect(tester.state(find.byKey(const ValueKey(bottomRoute))), bottomState); + expect(tester.state(find.byKey(const ValueKey(topRoute))), topState); + + await tester.pumpAndSettle(); + expect(tester.state(find.byKey(const ValueKey(bottomRoute), skipOffstage: false)), bottomState); + expect(tester.state(find.byKey(const ValueKey(topRoute))), topState); + + navigator.currentState.pop(); + await tester.pump(); + + expect(tester.state(find.byKey(const ValueKey(bottomRoute))), bottomState); + expect(tester.state(find.byKey(const ValueKey(topRoute))), topState); + + await tester.pump(const Duration(milliseconds: 150)); + expect(tester.state(find.byKey(const ValueKey(bottomRoute))), bottomState); + expect(tester.state(find.byKey(const ValueKey(topRoute))), topState); + + await tester.pumpAndSettle(); + expect(tester.state(find.byKey(const ValueKey(bottomRoute))), bottomState); + expect(find.byKey(const ValueKey(topRoute)), findsNothing); + }); } double _getOpacity(String key, WidgetTester tester) { @@ -301,9 +353,10 @@ double _getScale(String key, WidgetTester tester) { } class _TestWidget extends StatelessWidget { - const _TestWidget({this.navigatorKey}); + const _TestWidget({this.navigatorKey, this.contentBuilder}); final Key navigatorKey; + final _ContentBuilder contentBuilder; @override Widget build(BuildContext context) { @@ -321,15 +374,35 @@ class _TestWidget extends StatelessWidget { return MaterialPageRoute( settings: settings, builder: (BuildContext context) { - return Container( - child: Center( - key: ValueKey(settings.name), - child: Text(settings.name), - ), - ); + return contentBuilder != null + ? contentBuilder(settings) + : Container( + child: Center( + key: ValueKey(settings.name), + child: Text(settings.name), + ), + ); }, ); }, ); } } + +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); + } +} + +typedef _ContentBuilder = Widget Function(RouteSettings settings); From 7c3ffae2aff29d16e4e556b82c5bd836334b1e07 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Tue, 3 Dec 2019 14:15:50 -0800 Subject: [PATCH 08/11] format --- .../test/fade_through_transition_test.dart | 62 +++++++++++++++---- 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/packages/animations/test/fade_through_transition_test.dart b/packages/animations/test/fade_through_transition_test.dart index a6d6aae8c7f9..ba2408ad7302 100644 --- a/packages/animations/test/fade_through_transition_test.dart +++ b/packages/animations/test/fade_through_transition_test.dart @@ -295,39 +295,75 @@ void main() { ), ); - final _StatefulTestWidgetState bottomState = tester.state(find.byKey(const ValueKey(bottomRoute))); + final _StatefulTestWidgetState bottomState = + tester.state(find.byKey(const ValueKey(bottomRoute))); expect(bottomState.widget.name, bottomRoute); navigator.currentState.pushNamed(topRoute); await tester.pump(); await tester.pump(); - expect(tester.state(find.byKey(const ValueKey(bottomRoute))), bottomState); - final _StatefulTestWidgetState topState = tester.state(find.byKey(const ValueKey(topRoute))); + expect( + tester.state(find.byKey(const ValueKey(bottomRoute))), + bottomState, + ); + final _StatefulTestWidgetState topState = tester.state( + find.byKey(const ValueKey(topRoute)), + ); expect(topState.widget.name, topRoute); await tester.pump(const Duration(milliseconds: 150)); - expect(tester.state(find.byKey(const ValueKey(bottomRoute))), bottomState); - expect(tester.state(find.byKey(const ValueKey(topRoute))), topState); + expect( + tester.state(find.byKey(const ValueKey(bottomRoute))), + bottomState, + ); + expect( + tester.state(find.byKey(const ValueKey(topRoute))), + topState, + ); await tester.pumpAndSettle(); - expect(tester.state(find.byKey(const ValueKey(bottomRoute), skipOffstage: false)), bottomState); - expect(tester.state(find.byKey(const ValueKey(topRoute))), topState); + expect( + tester.state(find.byKey( + const ValueKey(bottomRoute), + skipOffstage: false, + )), + bottomState, + ); + expect( + tester.state(find.byKey(const ValueKey(topRoute))), + topState, + ); navigator.currentState.pop(); await tester.pump(); - expect(tester.state(find.byKey(const ValueKey(bottomRoute))), bottomState); - expect(tester.state(find.byKey(const ValueKey(topRoute))), topState); + expect( + tester.state(find.byKey(const ValueKey(bottomRoute))), + bottomState, + ); + expect( + tester.state(find.byKey(const ValueKey(topRoute))), + topState, + ); await tester.pump(const Duration(milliseconds: 150)); - expect(tester.state(find.byKey(const ValueKey(bottomRoute))), bottomState); - expect(tester.state(find.byKey(const ValueKey(topRoute))), topState); + expect( + tester.state(find.byKey(const ValueKey(bottomRoute))), + bottomState, + ); + expect( + tester.state(find.byKey(const ValueKey(topRoute))), + topState, + ); await tester.pumpAndSettle(); - expect(tester.state(find.byKey(const ValueKey(bottomRoute))), bottomState); + expect( + tester.state(find.byKey(const ValueKey(bottomRoute))), + bottomState, + ); expect(find.byKey(const ValueKey(topRoute)), findsNothing); - }); + }); } double _getOpacity(String key, WidgetTester tester) { From 2ab2229bc19af2d781a83a936e55073b6b613dd1 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Wed, 4 Dec 2019 13:40:00 -0800 Subject: [PATCH 09/11] reviews --- .../lib/src/fade_through_transition.dart | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/packages/animations/lib/src/fade_through_transition.dart b/packages/animations/lib/src/fade_through_transition.dart index 963357f3cd6e..431b9c397105 100644 --- a/packages/animations/lib/src/fade_through_transition.dart +++ b/packages/animations/lib/src/fade_through_transition.dart @@ -5,19 +5,14 @@ import 'package:flutter/material.dart'; /// Used by [PageTransitionsTheme] to define a page route transition animation -/// in which the outgoing page fade out, then the incoming page fade in and +/// in which the outgoing page fades out, then the incoming page fades in and /// scale up. /// -/// This pattern is recommended for transition animation between UI elements -/// that do not have a strong relationship to one another. +/// This pattern is recommended for a transition between UI elements that do not +/// have a strong relationship to one another. /// /// Scale is only applied to incoming elements to emphasize new content over /// old. -/// -/// See also: -/// -/// * [Fade-through](https://spec.googleplex.com/draft/mio-design/motion-new/the-motion-system.html#fade-through) -/// in the Material Design spec. class FadeThroughPageTransitionsBuilder extends PageTransitionsBuilder { /// Creates a [FadeThroughPageTransitionsBuilder]. const FadeThroughPageTransitionsBuilder(); @@ -41,7 +36,7 @@ class FadeThroughPageTransitionsBuilder extends PageTransitionsBuilder { /// Defines a transition in which outgoing elements fade out, then incoming /// elements fade in and scale up. /// -/// The Fade through pattern provides a transition animation between UI elements +/// The fade through pattern provides a transition animation between UI elements /// that do not have a strong relationship to one another. As an example, the /// [BottomNavigationBar] may use this animation to transition the currently /// displayed content when a new [BottomNavigationBarItem] is selected. @@ -54,11 +49,6 @@ class FadeThroughPageTransitionsBuilder extends PageTransitionsBuilder { /// [MaterialPageRoute] transitions within a Navigator. Or use this transition /// directly in a [PageTransitionSwitcher.transitionBuilder] to transition /// from one widget to another. -/// -/// See also: -/// -/// * [Fade-through](https://spec.googleplex.com/draft/mio-design/motion-new/the-motion-system.html#fade-through) -/// in the Material Design spec. class FadeThroughTransition extends StatefulWidget { /// Creates a [FadeThroughTransition]. /// @@ -181,9 +171,9 @@ class _FadeThroughTransitionState extends State { @override void dispose() { - super.dispose(); widget.animation.removeStatusListener(_animationListener); widget.secondaryAnimation.removeStatusListener(_secondaryAnimationListener); + super.dispose(); } static final Tween _flippedTween = Tween( From a681e6410ddb9942cdeab9b4756e584447b08326 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Wed, 4 Dec 2019 14:09:52 -0800 Subject: [PATCH 10/11] examples --- .../lib/src/fade_through_transition.dart | 105 +++++++++++++++++- 1 file changed, 103 insertions(+), 2 deletions(-) diff --git a/packages/animations/lib/src/fade_through_transition.dart b/packages/animations/lib/src/fade_through_transition.dart index 431b9c397105..59c1076c62cb 100644 --- a/packages/animations/lib/src/fade_through_transition.dart +++ b/packages/animations/lib/src/fade_through_transition.dart @@ -13,6 +13,51 @@ import 'package:flutter/material.dart'; /// /// Scale is only applied to incoming elements to emphasize new content over /// old. +/// +/// The following example shows how the FadeThroughPageTransitionsBuilder can +/// be used in a [PageTransitionsTheme] to change the default transitions +/// of [MaterialPageRoute]s. +/// +/// ```dart +/// MaterialApp( +/// theme: ThemeData( +/// pageTransitionsTheme: PageTransitionsTheme( +/// builders: { +/// TargetPlatform.android: FadeThroughPageTransitionsBuilder(), +/// TargetPlatform.iOS: FadeThroughPageTransitionsBuilder(), +/// }, +/// ), +/// ), +/// routes: { +/// '/': (BuildContext context) { +/// return Container( +/// color: Colors.red, +/// child: Center( +/// child: MaterialButton( +/// child: Text('Push route'), +/// onPressed: () { +/// Navigator.of(context).pushNamed('/a'); +/// }, +/// ), +/// ), +/// ); +/// }, +/// '/a' : (BuildContext context) { +/// return Container( +/// color: Colors.blue, +/// child: Center( +/// child: MaterialButton( +/// child: Text('Pop route'), +/// onPressed: () { +/// Navigator.of(context).pop(); +/// }, +/// ), +/// ), +/// ); +/// }, +/// }, +/// ); +/// ``` class FadeThroughPageTransitionsBuilder extends PageTransitionsBuilder { /// Creates a [FadeThroughPageTransitionsBuilder]. const FadeThroughPageTransitionsBuilder(); @@ -46,9 +91,65 @@ class FadeThroughPageTransitionsBuilder extends PageTransitionsBuilder { /// /// Consider using [FadeThroughPageTransitionsBuilder] within a /// [PageTransitionsTheme] if you want to apply this kind of transition to -/// [MaterialPageRoute] transitions within a Navigator. Or use this transition +/// [MaterialPageRoute] transitions within a Navigator (see +/// [FadeThroughPageTransitionsBuilder] for some example code_. Or use this transition /// directly in a [PageTransitionSwitcher.transitionBuilder] to transition -/// from one widget to another. +/// from one widget to another as seen in the following example: +/// +/// ```dart +/// int _selectedIndex = 0; +/// +/// final List _colors = [Colors.blue, Colors.red, Colors.yellow]; +/// +/// @override +/// Widget build(BuildContext context) { +/// return Scaffold( +/// appBar: AppBar( +/// title: const Text('Switcher Sample'), +/// ), +/// body: PageTransitionSwitcher( +/// transitionBuilder: ( +/// Widget child, +/// Animation primaryAnimation, +/// Animation secondaryAnimation, +/// ) { +/// return FadeThroughTransition( +/// child: child, +/// animation: primaryAnimation, +/// secondaryAnimation: secondaryAnimation, +/// ); +/// }, +/// child: Container( +/// key: ValueKey(_selectedIndex), +/// color: _colors[_selectedIndex], +/// ), +/// ), +/// bottomNavigationBar: BottomNavigationBar( +/// items: const [ +/// BottomNavigationBarItem( +/// icon: Icon(Icons.home), +/// title: Text('Blue'), +/// ), +/// BottomNavigationBarItem( +/// icon: Icon(Icons.business), +/// title: Text('Red'), +/// ), +/// BottomNavigationBarItem( +/// icon: Icon(Icons.school), +/// title: Text('Yellow'), +/// ), +/// ], +/// currentIndex: _selectedIndex, +/// selectedItemColor: Colors.amber[800], +/// onTap: (int index) { +/// setState(() { +/// _selectedIndex = index; +/// }); +/// }, +/// ), +/// ); +/// } +/// ``` class FadeThroughTransition extends StatefulWidget { /// Creates a [FadeThroughTransition]. /// From 5fd8564ee0f8ed6122c06888e37377342f76e5a3 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Wed, 4 Dec 2019 15:52:03 -0800 Subject: [PATCH 11/11] Update packages/animations/lib/src/fade_through_transition.dart Co-Authored-By: Shi-Hao Hong --- packages/animations/lib/src/fade_through_transition.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/animations/lib/src/fade_through_transition.dart b/packages/animations/lib/src/fade_through_transition.dart index 59c1076c62cb..51c17a7fcf86 100644 --- a/packages/animations/lib/src/fade_through_transition.dart +++ b/packages/animations/lib/src/fade_through_transition.dart @@ -92,7 +92,7 @@ class FadeThroughPageTransitionsBuilder extends PageTransitionsBuilder { /// Consider using [FadeThroughPageTransitionsBuilder] within a /// [PageTransitionsTheme] if you want to apply this kind of transition to /// [MaterialPageRoute] transitions within a Navigator (see -/// [FadeThroughPageTransitionsBuilder] for some example code_. Or use this transition +/// [FadeThroughPageTransitionsBuilder] for some example code). Or use this transition /// directly in a [PageTransitionSwitcher.transitionBuilder] to transition /// from one widget to another as seen in the following example: ///