5

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

Konstantin
  • 1,150
  • 13
  • 31
  • @pskink Have you read the question? – Konstantin Apr 05 '20 at 18:13
  • Yes. Animating of the route itself is not what I need. I need to run animation inside the widgets when the route animation starts. Neither animation nor secondaryAnimation can help with that. I pass a widget thru the nextPage parameter and then it's used for push and pop routes but the problem is that it loses its context after the push route is ended and so I can't access it anymore to run some animation inside it. Instead a new widget is created in a background and the context is attached to it. That's the problem I've been struggling with for 2 days already – Konstantin Apr 06 '20 at 03:38
  • If I could do it as in your example it would be an easy task. You pass animation to the widget's constructor. I can't do it since I use a widget that has already been built before and so there's no way to pass the animation to that widget – Konstantin Apr 06 '20 at 05:24
  • I've probably misexplained the issue or you have misunderstood it. Here's the datailed example https://paste.ubuntu.com/p/d6vGMMG9Tj/ I need to animate the blue square when the transition starts and revert its animation on the route pop, and that's what I can't do because of a new widget created with the same state in a background – Konstantin Apr 06 '20 at 06:05
  • Yes, I did. It was not what I was asking for because it constructs the page contents inside the route but in my case the route has no access to the page's contents. Look at the answer I've provided. I've found the solution. The route animation can be accessed via ModalRoute.of(context).animation. And that solved the problem – Konstantin Apr 06 '20 at 11:17

1 Answers1

5

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
                  );
                },
              ),
            )
          ],
        ),
      );
    }
  );
}
Konstantin
  • 1,150
  • 13
  • 31