Flutter Hero Animation transitioning Opacity and/or Color

1.1k Views Asked by At

The Hero Widget effectively transitions size and position between two screens. How would you extend it to include opacity so that it effectively fades out the child of one screen whilst fading in the child of the second screen during the animation.

I have tried to achieve this with FadeTransitions connected to the ModalRoute.of(context).animation and secondaryAnimation but it does not achieve this result at all. Instead only one child of each occurrence of the Hero is visible.

Page 1##

Hero(
 tag: 'test',
 transitionOnUserGestures: true,
 child: FadeTransition(
   opacity: Tween<double>(begin: 0, end: 1)
     .animate(ModalRoute.of(context)?.animation ?? const AlwaysStoppedAnimation(1)),
   child: FadeTransition(
     opacity: Tween<double>(begin: 1, end: 0)
       .animate(ModalRoute.of(context)?.secondaryAnimation ?? const AlwaysStoppedAnimation(1)),
      child: Container(color: UIColor.blue, height: 100, width: 110),
            ),
          ),
        ),

Page 2##

Hero(
 tag: 'test',
 transitionOnUserGestures: true,
 child: FadeTransition(
   opacity: Tween<double>(begin: 0, end: 1)
     .animate(ModalRoute.of(context)?.animation ?? const AlwaysStoppedAnimation(1)),
   child: FadeTransition(
     opacity: Tween<double>(begin: 1, end: 0)
       .animate(ModalRoute.of(context)?.secondaryAnimation ?? const AlwaysStoppedAnimation(1)),
      child: Container(color: UIColor.green, height: 60, width: 120),
            ),
          ),
        ),

I would further like to be able to transition the Color instead of opacity in an AnimatedBuilder using the ModalRoute.of(context).animation as the animation attribute of the builder, but have encountered the same problem.

This is contained in a standard CupertinoPageRoute but the result is identical when using a Material Route as well.

Clearly this is not the correct approach. I presume it is because I am falsely making an assumption that both widgets are present during the transition, but my attempt with the flightRouteBuilder lead to almost identical results.

How would you go about this.

Update ## This works perfectly when Transitioning with UserGestures, but does not when transitioning via Navigator.push() or Navigator.pop():

Hero(
  tag: 'test',
  transitionOnUserGestures: true,
  flightShuttleBuilder: (flightContext, animation, flightDirection, fromHeroContext, toHeroContext) {
    return Stack(children: [
      Positioned.fill(child: FadeTransition(opacity: animation, child: fromHeroContext.widget)),
      Positioned.fill(child: FadeTransition(opacity: ReverseAnimation(animation), child: toHeroContext.widget)),
            ]);
          },
     child: Container(color: UIColor.green, width: 120, height: 60),

),
1

There are 1 best solutions below

0
On

I cannot be certain that this is a solution however replacing the hero on each page with this has worked and is operating correctly for both push, pop and user gestures. From what I can see, Hero's default flightShuttleBuilder will only render one Widget but hold the Size and Positional parameters of both.

To add additional transition's involving the Widget on each screen, it is necessary to put both Widgets in the flightShuttleBuilder. It is then necessary to access the secondaryAnimation of the route transition via ModalRoute.of(context).secondaryAnimation.

To create the FadeTransition that I wanted, both the from context and to context are placed in a stack. They fill their container as their own Widget will override the size constraint. And then they each invoke a different FadeTransition, one using the flightShuttleBuilder's animation and the second using the ModalRoute.of(context).secondaryAnimation.

#Page 1

 Hero(
   tag: 'test',
   transitionOnUserGestures: true,
   flightShuttleBuilder: (flightContext, animation, flightDirection, fromHeroContext, toHeroContext) {

     return Stack(children: [
       Positioned.fill(child: FadeTransition(
         opacity: animation, child: fromHeroContext.widget),
       ),
       Positioned.fill(
         child: FadeTransition(
           opacity: ReverseAnimation(ModalRoute.of(context)?.secondaryAnimation ?? animation),
             child: toHeroContext.widget)),
      ]);
    },
    child: Container(color: UIColor.blue, width: 100, height: 100),
  );

#Page 2

Identical accept the child of the Hero Widget is:

Container(color: UIColor.green, width: 120, height: 60),

Edit Therefore

The objective is to override the FlightShuttleBuilder to include both Widgets. Those Widgets need to be in a stack and each wrapped with position.fill. Then one can animated as one normally would from using ModalRoute.of(context).animation and ModalRoute.of(context).secondaryAnimation.

For example:

Hero(
  tag: 'test',
  transitionOnUserGestures: true,
  flightShuttleBuilder: (flightContext, animation, flightDirection, fromHeroContext, toHeroContext) {
    return Stack(children: [
      Positioned.fill(child: fromHeroContext.widget),
      Positioned.fill(child: toHeroContext.widget),
    ]);
   },
   child: FadeTransition(
     opacity: ModalRoute.of(context)?.animation ?? const AlwaysStoppedAnimation(1),
      child: FadeTransition(
        opacity: ReverseAnimation(ModalRoute.of(context)?.secondaryAnimation ?? const AlwaysStoppedAnimation(1)),
        child: Container(color: UIColor.blue, width: 100, height: 100),
      ),
    ),
  );