Flutter animating two routes with inner animations

951

I've just come to a solution. I can access the animation via

ModalRoute.of(context).animation;

Here's the working example

import 'package:flutter/material.dart';

class Page1 extends StatefulWidget {
  @override
  _Page1State createState() => _Page1State();
}

class _Page1State extends State<Page1> with SingleTickerProviderStateMixin {

  @override
  Widget build(BuildContext context) {
    var modalRoute = ModalRoute.of(context);

    return Container(
      color: Colors.red,
      width: double.infinity,
      height: double.infinity,
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Container(
            width: double.infinity,
            height: 60.0,
            color: Colors.amber,
            child: Stack(
              children: <Widget>[
                AnimatedBuilder(
                  animation: modalRoute.animation,
                  builder: (c, w) {
                    var isCompleted = modalRoute.animation.status == AnimationStatus.completed;
                    var posX = 150.0 * modalRoute.animation.value;
                    if (isCompleted) {
                      posX = 0;
                    }
                    return Transform(
                      transform: Matrix4.translationValues(posX, 0, 0),
                      child: Container(
                        height: 60.0,
                        width: 60.0,
                        color: Colors.blue,
                      ),
                    );
                  },
                ),
              ],
            ),
          ),
          SizedBox(height: 100.0,),
          RaisedButton(
            child: Text('Add new route'),
            onPressed: () {
              Navigator.of(context).push(
                FromMenuRoute(prevPage: widget, nextPage: Page2())
              );
            },
          ),
        ],
      ),
    );
  }
}


class Page2 extends StatefulWidget {
  @override
  _Page2State createState() => _Page2State();
}

class _Page2State extends State<Page2> {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.green,
      width: double.infinity,
      height: double.infinity,
    );
  }
}


class FromMenuRoute extends PageRouteBuilder {

  final Widget nextPage;
  final Widget prevPage;

  FromMenuRoute({this.prevPage, this.nextPage}) : super(
    transitionDuration: Duration(milliseconds: 3000),
    pageBuilder: (
      BuildContext context,
      Animation<double> animation,
      Animation<double> secondaryAnimation,
    ) {
      return nextPage;
    },
    maintainState: true,
    transitionsBuilder: (
      BuildContext context,
      Animation<double> animation,
      Animation<double> secondaryAnimation,
      Widget child,
    ) {
      return Material(
        child: Stack(
          overflow: Overflow.visible,
          children: <Widget>[
            SlideTransition(
              position: Tween<Offset>(
                begin: const Offset(0.0, 0.0),
                end: const Offset(-0.3, 0.0),
              ).animate(animation),
              child: prevPage,
            ),

            SlideTransition(
              position: Tween<Offset>(
                begin: const Offset(1.0, 0.0),
                end: Offset.zero,
              ).animate(animation),
              child: AnimatedBuilder(
                animation: animation,
                builder: (c, w) {
                  return Material(
                    shadowColor: Colors.black,
                    elevation: 30.0 * animation.value,
                    child: nextPage
                  );
                },
              ),
            )
          ],
        ),
      );
    }
  );
}
Share:
951
Konstantin
Author by

Konstantin

Updated on December 01, 2022

Comments

  • Konstantin
    Konstantin over 1 year

    I'm completely stuck with it. I want to create a custom page route that will animate both the IN and the OUT pages. The animation of the routes themselves is a simple task, I'm doing it like this:

        import 'package:flutter/material.dart';
    
        class FromMenuRoute extends PageRouteBuilder {
    
          final Widget nextPage;
          final Widget prevPage;
    
          FromMenuRoute({this.prevPage, this.nextPage}) : super(
            transitionDuration: Duration(milliseconds: 500),
            pageBuilder: (
              BuildContext context,
              Animation<double> animation,
              Animation<double> secondaryAnimation,
            ) {
              return nextPage;
            },
            maintainState: false,
            transitionsBuilder: (
              BuildContext context,
              Animation<double> animation,
              Animation<double> secondaryAnimation,
              Widget child,
            ) {
              var colorTheme = CustomThemeData.of(context).colorTheme;
              return Material(
                child: Stack(
                  overflow: Overflow.visible,
                  children: <Widget>[
                    SlideTransition(
                      position: Tween<Offset>(
                        begin: const Offset(0.0, 0.0),
                        end: const Offset(-0.3, 0.0),
                      ).animate(animation),
                      child: prevPage,
                    ),
                    SlideTransition(
                      position: Tween<Offset>(
                        begin: const Offset(1.0, 0.0),
                        end: Offset.zero,
                      ).animate(animation),
                      child: AnimatedBuilder(
                        animation: animation,
                        builder: (c, w) {
                          return Material(
                            shadowColor: colorTheme.textColor,
                            elevation: 30.0 * animation.value,
                            child: nextPage
                          );
                        },
                      ),
                    )
                  ],
                ),
              );
            }
          );
        }
    

    But the problem is that I also want to run some animations inside those pages, not only animating the page widgets themselves. And I have no idea of how can I do that.

    I'm using the route like this

    Navigator.of(context).push(
        FromMenuRoute(prevPage: widget, nextPage: nextPageWidget)
    );
    

    My first idea was to launch an animation controller in the OUT page before pushing the route like this:

    _animationController.forward();
    Navigator.of(context).push(
        FromMenuRoute(prevPage: widget, nextPage: nextPageWidget)
    );
    

    And it worked. The page animation ran along with the page transition. But I couldn't manage to reverse the controller when I needed to pop the route. I mean, I could, of course, e.g. in a didWidgetUpdate method, but it had no effect because the widget (which is called nextPage in this example) lost its context and the animations controller animated another copy of the widget that was not displayed until the pop transition ended. While the pop transition ran it displayed an old copy of the "nextPage"

    The second idea was to use a GlobalKey to keep state of the widget which had also failed with an error of using 2 duplicate global keys.

    The ideal option would be something like this in my FromMenuRoute

    SlideTransition(
       position: Tween<Offset>(
        begin: const Offset(0.0, 0.0),
        end: const Offset(-0.3, 0.0),
      ).animate(animation),
      child: prevPage.copyWith(animation: animation), // but I understand this can't be done
    ),
    

    So I've run out of good ideas. I wouldn't like to make it in some very tricky way. Maybe there's some better solution I don't know about. Please, share it, if you know how to solve this


    Here's the detailed example of what I want to achieve

    import 'package:flutter/material.dart';
    
    class Page1 extends StatefulWidget {
      @override
      _Page1State createState() => _Page1State();
    }
    
    class _Page1State extends State<Page1> with SingleTickerProviderStateMixin {
    
    
      AnimationController _animationController;
    
      @override
      void initState() {
        _animationController = AnimationController(
          vsync: this,
          lowerBound: 0.0,
          upperBound: 1.0,
          duration: Duration(milliseconds: 3000)
        );
        super.initState();
      }
      @override
      void dispose() {
        _animationController.dispose();
        super.dispose();
      }
    
    
      @override
      Widget build(BuildContext context) {
        return Container(
          color: Colors.red,
          width: double.infinity,
          height: double.infinity,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Container(
                width: double.infinity,
                height: 60.0,
                color: Colors.amber,
                child: Stack(
                  children: <Widget>[
                    SlideTransition(
                      position: Tween<Offset>(
                        begin: const Offset(0.0, 0.0),
                        end: Offset(3.0, 0.0),
                      ).animate(_animationController),
                      child: Container(
                        height: 60.0,
                        width: 60.0,
                        color: Colors.blue,
                      ),
                    ),
                  ],
                ),
              ),
              SizedBox(height: 100.0,),
              RaisedButton(
                child: Text('Add new route'),
                onPressed: () {
                  /// calling here doe not affect animation because
                  /// a new widget is created on top of this
                  /// but is I call it from initState() I won't have access to the 
                  /// controller later, when I need to revert the animation
                  _animationController.forward();
                  Navigator.of(context).push(
                    FromMenuRoute(prevPage: widget, nextPage: Page2())
                  );
                },
              ),
            ],
          ),
        );
      }
    }
    
    
    class Page2 extends StatefulWidget {
      @override
      _Page2State createState() => _Page2State();
    }
    
    class _Page2State extends State<Page2> {
      @override
      Widget build(BuildContext context) {
        return Container(
          color: Colors.green,
          width: double.infinity,
          height: double.infinity,
        );
      }
    }
    
    
    class FromMenuRoute extends PageRouteBuilder {
    
      final Widget nextPage;
      final Widget prevPage;
    
      FromMenuRoute({this.prevPage, this.nextPage}) : super(
        transitionDuration: Duration(milliseconds: 3000),
        pageBuilder: (
          BuildContext context,
          Animation<double> animation,
          Animation<double> secondaryAnimation,
        ) {
          return nextPage;
        },
        maintainState: true,
        transitionsBuilder: (
          BuildContext context,
          Animation<double> animation,
          Animation<double> secondaryAnimation,
          Widget child,
        ) {
          return Material(
            child: Stack(
              overflow: Overflow.visible,
              children: <Widget>[
                SlideTransition(
                  position: Tween<Offset>(
                    begin: const Offset(0.0, 0.0),
                    end: const Offset(-0.3, 0.0),
                  ).animate(animation),
                  child: prevPage,
                ),
    
                SlideTransition(
                  position: Tween<Offset>(
                    begin: const Offset(1.0, 0.0),
                    end: Offset.zero,
                  ).animate(animation),
                  child: AnimatedBuilder(
                    animation: animation,
                    builder: (c, w) {
                      return Opacity(
                        opacity: .8,
                        child: Material(
                          shadowColor: Colors.black,
                          elevation: 30.0 * animation.value,
                          child: nextPage
                        ),
                      );
                    },
                  ),
                )
              ],
            ),
          );
        }
      );
    }
    

    I need to start moving a blue square when the page transition starts and revert the animation of that square on the route pop