1

This problem has been discussed a bit here, but none of the solutions are clean or do weird things such as displaying the same page twice. Flutter Transition Exit

I'm trying to make it so when you navigate to the next page, the current page slides to the left while the next page comes in from the right. However, the best solution I've been able to come up with is to wrap the content of the current page in a SlideTransition. When you navigate to the next page, you start opening the new page in a "SlideRightRoute" animation at the same time that you're starting an animation to the slide the current page out.

While this works, it feels like I'm "working against the framework". My question is: Is there a "correct" way to animate a page out or is this currently a hole in the framework (and a solution like this is really the best we can do at the moment)?

abstract class SlideablePage<T extends StatefulWidget> 
extends State<T> with SingleTickerProviderStateMixin {
  AnimationController _controller;
  Animation<Offset> _offset;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 500),
    );
    _offset = Tween<Offset>(
      begin: Offset(0.0, 0.0),
      end: Offset(-1.0, 0.0),
    ).animate(_controller);
  }

  @override
  Widget build(BuildContext context) {
    return SlideTransition(
      position: _offset,
      child: buildContent(context),
    );
  }

  Widget buildContent(BuildContext context);

  Future push(Widget page) async {
    var result = Navigator.push(
      context,
      new SlideRightRoute(widget: page),
    );
    _controller.forward();
    await result;
    _controller.reverse();
  }
}

class SlideRightRoute extends PageRouteBuilder {
  final Widget widget;
  SlideRightRoute({this.widget})
      : super(
          transitionDuration: const Duration(milliseconds: 500),
          pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
            return widget;
          },
          transitionsBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
            return new SlideTransition(
              position: new Tween<Offset>(
                begin: const Offset(1.0, 0.0),
                end: Offset.zero,
              ).animate(animation),
              child: child,
            );
          },
        );
}
David
  • 131
  • 1
  • 5

1 Answers1

0

Hey @David,

I've been searching through the internet to try find an elegant solution to this problem. Firstly, I'm implementing a navigation solution that is using named routes. I've seen plenty of implementations that use Navigator.push(context, SlideAnimation(...)) with a stack, and parent + child parameters. Go here for that solution.

I'm sure this is not the most elegant solution, but I'm putting it here such that others can build upon it. I know it's long, and probably not the copy paste solution you're looking for (random internet person reading my answer). I'm hoping someone else out there can make this better.

For a little more context, I wanted to implement two different types of transitions, depending on the relationship between the current page and the page being transitioned to. Page transition guide here.

From the aforementioned site, I wanted 'Top-Level' and 'Peer' transitions. To create those relationships, I started by creating an enum of pages, and a map from those pages to paths.

app_router.dart

enum AppPageEnum {
  settings,
  home,
  homeNested,
  // ... etc
}

extension AppPageExtension on AppPageEnum {
  String get toPath {
    switch (this) {
      case AppPageEnum.settings:
        return '/settings';
      case AppPageEnum.home:
        return '/home';
      case AppPageEnum.homeNested:
        return '/home/nested';
      // ... etc
    }
  }
}

I also generated some utility methods to help identify if this was a base route (has only one /, or a nested route). Also, one to get the widget that some AppPageEnum points too.

app_router.dart

var appRouter = AppPageEnum.values.asNameMap().map((key, value) {
  String k = value.toPath;
  Widget v;
  switch (value) {
    case AppPageEnum.settings:
      v = const SettingsPage();
      break;
    case AppPageEnum.home:
      v = const HomePage();
      break;
    case AppPageEnum.homeNested:
      v = const HomeNestedPage();
      break;
    default:
      v = UnknownPageRoute(name: value.name.title());
  }
  return MapEntry(k, v);
});

bool isBaseString(String path) {
  return path.split("/").length <= 2;
}
// OR
bool isBasePage(AppPageEnum page) {
  return isBaseString(page.toPath);
}

With the above in mind, here is the transitions I came up with.

main.dart

return MaterialApp(
  // ...
  onGenerateRoute: (settings) {
    if (settings.name == null || appRouter[settings.name!] == null) {
      return null;
    }

    // Not a base page, so both push and pop transitions should be slides
    if (!isBaseString(settings.name!)){
      return siblingTransition(settings);
    }
    
    // A base page, so push and pop transitions should be relative
    return parentTransition(settings);
  },
  // ...
);

app_router.dart

PageRouteBuilder siblingTransition(RouteSettings settings) {
  Widget child = appRouter[settings.name!]!;
  Duration duration = const Duration(milliseconds: 500);
  return PageRouteBuilder(
    transitionDuration: duration,
    reverseTransitionDuration: duration,
    pageBuilder: (context, animation, secondaryAnimation) => child,
    settings: settings,
    transitionsBuilder: (
      BuildContext context,
      Animation<double> animation,
      Animation<double> secondaryAnimation,
      Widget child,
    ) {

// animation determines how the pages comes in and out of the screen.
// secondaryAnimation determines how the page exits the screen when another is pushed on top of it. 
// You can remove the nested SlideTransition if you won't have double nested pages (e.g. home/nested/nested).

      return SlideTransition(
        position: Tween<Offset>(
          begin: const Offset(1.0, 0.0),
          end: Offset.zero,
        ).animate(animation),
        child: SlideTransition(
          position: Tween<Offset>(
            begin: Offset.zero,
            end: const Offset(-1.0, 0.0),
          ).animate(secondaryAnimation),
          child: child,
        ),
      );
    },
  );
}

PageRouteBuilder parentTransition(RouteSettings settings) {
  Widget child = appRouter[settings.name!]!;
  Duration duration = const Duration(milliseconds: 300);
  return PageRouteBuilder(
    transitionDuration: duration,
    reverseTransitionDuration: duration,
    pageBuilder: (context, animation, secondaryAnimation) => child,
    settings: settings,
    transitionsBuilder: (
      BuildContext context,
      Animation<double> animation,
      Animation<double> secondaryAnimation,
      Widget child,
    ) {

// When the animation value is not 1.0, that means we are currently using the primary animation 
// which indicates the parent is not already on the stack.
// I'm following the material design instructions here, so I'm simply returning the child. 
// This could be some other type of transition between top-level pages. 
      if (animation.value != 1.0) {
        return child;
      }
// Here, we know that the secondary animation is being used (to push something on top of this page). 
// Thus, we know it's a peer (child) widget to this parent. 
// As such, we implement the slide transition as per the material design instructions. 
      return SlideTransition(
        position: Tween<Offset>(
          begin: Offset.zero,
          end: const Offset(-1.0, 0.0),
        ).animate(secondaryAnimation),
        child: child,
      );
    },
  );
}

Lastly, our navigation between pages looks like this

// Navigate to a top level page.
Navigator.of(context).popUntil((route) => route.settings.name == '/');
Navigator.of(context).pushNamed(AppPageEnum.toPath);

// Navigate to a peer
Navigator.of(context).pushNamed(AppPageEnum.toPath);

I know this is long, and full of potentially unnecessary context. I just wanted to provide a full guide for what I did, using named routes. If your implementation differs from this, I hope there is enough information here such that you can mold it to your need.