// Copyright 2014 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/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; /// This sample demonstrates creating a custom page transition that is able to /// override the outgoing transition of the route behind it in the navigation /// stack using [DelegatedTransitionBuilder], using a [MaterialApp.router] /// pattern of navigation. void main() { runApp(FlexibleRouteTransitionsApp()); } class FlexibleRouteTransitionsApp extends StatelessWidget { FlexibleRouteTransitionsApp({super.key}); final _MyRouteInformationParser _routeInformationParser = _MyRouteInformationParser(); final MyRouterDelegate _routerDelegate = MyRouterDelegate(); @override Widget build(BuildContext context) { return MaterialApp.router( title: 'Mixing Routes', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), pageTransitionsTheme: const PageTransitionsTheme( builders: { // By default the zoom builder is used on all platforms but iOS. Normally // on iOS the default is the Cupertino sliding transition. Setting // it to use zoom on all platforms allows the example to show multiple // transitions in one app for all platforms. TargetPlatform.iOS: ZoomPageTransitionsBuilder(), }, ), ), routeInformationParser: _routeInformationParser, routerDelegate: _routerDelegate, ); } } class _MyRouteInformationParser extends RouteInformationParser { @override SynchronousFuture parseRouteInformation(RouteInformation routeInformation) { return SynchronousFuture(MyPageConfiguration.values.firstWhere((MyPageConfiguration pageConfiguration) { return pageConfiguration.uriString == routeInformation.uri.toString(); }, orElse: () => MyPageConfiguration.unknown, )); } @override RouteInformation? restoreRouteInformation(MyPageConfiguration configuration) { return RouteInformation(uri: configuration.uri); } } class MyRouterDelegate extends RouterDelegate { final Set _listeners = {}; final List _pages = []; void _notifyListeners() { for (final VoidCallback listener in _listeners) { listener(); } } void onNavigateToHome() { _pages.clear(); _pages.add(MyPageConfiguration.home); _notifyListeners(); } void onNavigateToZoom() { _pages.add(MyPageConfiguration.zoom); _notifyListeners(); } void onNavigateToIOS() { _pages.add(MyPageConfiguration.iOS); _notifyListeners(); } void onNavigateToVertical() { _pages.add(MyPageConfiguration.vertical); _notifyListeners(); } @override void addListener(VoidCallback listener) { _listeners.add(listener); } @override void removeListener(VoidCallback listener) { _listeners.remove(listener); } @override Future popRoute() { if (_pages.isEmpty) { return SynchronousFuture(false); } _pages.removeLast(); _notifyListeners(); return SynchronousFuture(true); } @override Future setNewRoutePath(MyPageConfiguration configuration) { _pages.add(configuration); _notifyListeners(); return SynchronousFuture(null); } @override Widget build(BuildContext context) { return Navigator( restorationScopeId: 'root', onDidRemovePage: (Page page) { _pages.remove(MyPageConfiguration.fromName(page.name!)); }, pages: _pages.map((MyPageConfiguration page) => switch (page) { MyPageConfiguration.unknown => _MyUnknownPage(), MyPageConfiguration.home => _MyHomePage(routerDelegate: this), MyPageConfiguration.zoom => _ZoomPage(routerDelegate: this), MyPageConfiguration.iOS => _IOSPage(routerDelegate: this), MyPageConfiguration.vertical => _VerticalPage(routerDelegate: this), }).toList(), ); } } class _MyUnknownPage extends MaterialPage { _MyUnknownPage() : super( restorationId: 'unknown-page', child: Scaffold( appBar: AppBar(title: const Text('404')), body: const Center( child: Text('404'), ), ), ); @override String get name => MyPageConfiguration.unknown.name; } class _MyHomePage extends MaterialPage { _MyHomePage({required this.routerDelegate}) : super( restorationId: 'home-page', child: _MyPageScaffold(title: 'Home', routerDelegate: routerDelegate), ); final MyRouterDelegate routerDelegate; @override String get name => MyPageConfiguration.home.name; } class _ZoomPage extends MaterialPage { _ZoomPage({required this.routerDelegate}) : super( restorationId: 'zoom-page', child: _MyPageScaffold(title: 'Zoom Route', routerDelegate: routerDelegate), ); final MyRouterDelegate routerDelegate; @override String get name => MyPageConfiguration.zoom.name; } class _IOSPage extends CupertinoPage { _IOSPage({required this.routerDelegate}) : super( restorationId: 'ios-page', child: _MyPageScaffold(title: 'Cupertino Route', routerDelegate: routerDelegate), ); final MyRouterDelegate routerDelegate; @override String get name => MyPageConfiguration.iOS.name; } class _VerticalPage extends _VerticalTransitionPage { _VerticalPage({required this.routerDelegate}) : super( restorationId: 'vertical-page', child: _MyPageScaffold(title: 'Vertical Route', routerDelegate: routerDelegate), ); final MyRouterDelegate routerDelegate; @override String get name => MyPageConfiguration.vertical.name; } class _MyPageScaffold extends StatelessWidget { const _MyPageScaffold({required this.title, required this.routerDelegate}); final String title; final MyRouterDelegate routerDelegate; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text(title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ TextButton( onPressed: () { routerDelegate.onNavigateToVertical(); }, child: const Text('Crazy Vertical Transition'), ), TextButton( onPressed: () { routerDelegate.onNavigateToZoom(); }, child: const Text('Zoom Transition'), ), TextButton( onPressed: () { routerDelegate.onNavigateToIOS(); }, child: const Text('Cupertino Transition'), ), ], ), ), ); } } // A Page that applies a _VerticalPageTransition. class _VerticalTransitionPage extends Page { const _VerticalTransitionPage({ required this.child, this.maintainState = true, this.fullscreenDialog = false, this.allowSnapshotting = true, super.key, super.canPop, super.onPopInvoked, super.name, super.arguments, super.restorationId, }); final Widget child; final bool maintainState; final bool fullscreenDialog; final bool allowSnapshotting; @override Route createRoute(BuildContext context) { return _PageBasedVerticalPageRoute(page: this); } } class _PageBasedVerticalPageRoute extends PageRoute { _PageBasedVerticalPageRoute({ required _VerticalTransitionPage page, super.allowSnapshotting, }) : super(settings: page); _VerticalTransitionPage get _page => settings as _VerticalTransitionPage; @override bool get maintainState => _page.maintainState; @override bool get fullscreenDialog => _page.fullscreenDialog; @override String get debugLabel => '${super.debugLabel}(${_page.name})'; @override DelegatedTransitionBuilder? get delegatedTransition => _VerticalPageTransition._delegatedTransitionBuilder; @override Color? get barrierColor => const Color(0x00000000); @override bool get barrierDismissible => false; @override String? get barrierLabel => 'Should be no visible barrier...'; @override bool get opaque => false; @override Duration get transitionDuration => const Duration(milliseconds: 2000); @override Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) { return _page.child; } @override Widget buildTransitions(BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { return _VerticalPageTransition( primaryRouteAnimation: animation, secondaryRouteAnimation: secondaryAnimation, child: child, ); } } // A page transition that slides off the screen vertically, and uses // delegatedTransition to ensure that the outgoing route slides with it. class _VerticalPageTransition extends StatelessWidget { _VerticalPageTransition({ required Animation primaryRouteAnimation, required this.secondaryRouteAnimation, required this.child, }) : _primaryPositionAnimation = CurvedAnimation( parent: primaryRouteAnimation, curve: _curve, reverseCurve: _curve, ).drive(_kBottomUpTween), _secondaryPositionAnimation = CurvedAnimation( parent: secondaryRouteAnimation, curve: _curve, reverseCurve: _curve, ) .drive(_kTopDownTween); final Animation _primaryPositionAnimation; final Animation _secondaryPositionAnimation; final Animation secondaryRouteAnimation; final Widget child; static const Curve _curve = Curves.decelerate; static final Animatable _kBottomUpTween = Tween( begin: const Offset(0.0, 1.0), end: Offset.zero, ); static final Animatable _kTopDownTween = Tween( begin: Offset.zero, end: const Offset(0.0, -1.0), ); // When the _VerticalTransitionPageRoute animates onto or off of the navigation // stack, this transition is given to the route below it so that they animate in // sync. static Widget _delegatedTransitionBuilder( BuildContext context, Animation animation, Animation secondaryAnimation, bool allowSnapshotting, Widget? child ) { final Animatable tween = Tween( begin: Offset.zero, end: const Offset(0.0, -1.0), ).chain(CurveTween(curve: _curve)); return SlideTransition( position: secondaryAnimation.drive(tween), child: child, ); } @override Widget build(BuildContext context) { assert(debugCheckHasDirectionality(context)); final TextDirection textDirection = Directionality.of(context); return SlideTransition( position: _secondaryPositionAnimation, textDirection: textDirection, transformHitTests: false, child: SlideTransition( position: _primaryPositionAnimation, textDirection: textDirection, child: ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), child: child, ) ), ); } } enum MyPageConfiguration { home(uriString: '/'), zoom(uriString: '/zoom'), iOS(uriString: '/iOS'), vertical(uriString: '/vertical'), unknown(uriString: '/404'); const MyPageConfiguration({ required this.uriString, }); final String uriString; static MyPageConfiguration fromName(String testName) { return values.firstWhere((MyPageConfiguration page) => page.name == testName); } Uri get uri => Uri.parse(uriString); }