Unverified Commit 3e5c700f authored by chunhtai's avatar chunhtai Committed by GitHub

Add material page, cupertino page, and transition page classes (#58511)

* Add material page, cupertino page, and transition page classes

* update

* update comments

* fix test

* addressing comments

* make page getter private
parent 8a5042b2
...@@ -1063,7 +1063,7 @@ class _NavigationBarStaticComponents { ...@@ -1063,7 +1063,7 @@ class _NavigationBarStaticComponents {
}) { }) {
// Auto use the CupertinoPageRoute's title if middle not provided. // Auto use the CupertinoPageRoute's title if middle not provided.
if (automaticallyImplyTitle && if (automaticallyImplyTitle &&
currentRoute is CupertinoPageRoute && currentRoute is CupertinoRouteTransitionMixin &&
currentRoute.title != null) { currentRoute.title != null) {
return Text(currentRoute.title); return Text(currentRoute.title);
} }
...@@ -1451,8 +1451,8 @@ class _BackLabel extends StatelessWidget { ...@@ -1451,8 +1451,8 @@ class _BackLabel extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (specifiedPreviousTitle != null) { if (specifiedPreviousTitle != null) {
return _buildPreviousTitleWidget(context, specifiedPreviousTitle, null); return _buildPreviousTitleWidget(context, specifiedPreviousTitle, null);
} else if (route is CupertinoPageRoute<dynamic> && !route.isFirst) { } else if (route is CupertinoRouteTransitionMixin<dynamic> && !route.isFirst) {
final CupertinoPageRoute<dynamic> cupertinoRoute = route as CupertinoPageRoute<dynamic>; final CupertinoRouteTransitionMixin<dynamic> cupertinoRoute = route as CupertinoRouteTransitionMixin<dynamic>;
// There is no timing issue because the previousTitle Listenable changes // There is no timing issue because the previousTitle Listenable changes
// happen during route modifications before the ValueListenableBuilder // happen during route modifications before the ValueListenableBuilder
// is built. // is built.
......
...@@ -78,56 +78,35 @@ final DecorationTween _kGradientShadowTween = DecorationTween( ...@@ -78,56 +78,35 @@ final DecorationTween _kGradientShadowTween = DecorationTween(
), ),
); );
/// A modal route that replaces the entire screen with an iOS transition. /// A mixin that replaces the entire screen with an iOS transition for a
/// [PageRoute].
/// ///
/// {@template flutter.cupertino.cupertinoRouteTransitionMixin}
/// The page slides in from the right and exits in reverse. The page also shifts /// The page slides in from the right and exits in reverse. The page also shifts
/// to the left in parallax when another page enters to cover it. /// to the left in parallax when another page enters to cover it.
/// ///
/// The page slides in from the bottom and exits in reverse with no parallax /// The page slides in from the bottom and exits in reverse with no parallax
/// effect for fullscreen dialogs. /// effect for fullscreen dialogs.
/// /// {@endtemplate}
/// By default, when a modal route is replaced by another, the previous route
/// remains in memory. To free all the resources when this is not necessary, set
/// [maintainState] to false.
///
/// The type `T` specifies the return type of the route which can be supplied as
/// the route is popped from the stack via [Navigator.pop] when an optional
/// `result` can be provided.
/// ///
/// See also: /// See also:
/// ///
/// * [MaterialPageRoute], for an adaptive [PageRoute] that uses a /// * [MaterialRouteTransitionMixin], which is a mixin that provides
/// platform-appropriate transition. /// platform-appropriate transitions for a [PageRoute]
/// * [CupertinoPageScaffold], for applications that have one page with a fixed /// * [CupertinoPageRoute], which is a [PageRoute] that leverages this mixin.
/// navigation bar on top. mixin CupertinoRouteTransitionMixin<T> on PageRoute<T> {
/// * [CupertinoTabScaffold], for applications that have a tab bar at the
/// bottom with multiple pages.
class CupertinoPageRoute<T> extends PageRoute<T> {
/// Creates a page route for use in an iOS designed app.
///
/// The [builder], [maintainState], and [fullscreenDialog] arguments must not
/// be null.
CupertinoPageRoute({
@required this.builder,
this.title,
RouteSettings settings,
this.maintainState = true,
bool fullscreenDialog = false,
}) : assert(builder != null),
assert(maintainState != null),
assert(fullscreenDialog != null),
assert(opaque),
super(settings: settings, fullscreenDialog: fullscreenDialog);
/// Builds the primary contents of the route. /// Builds the primary contents of the route.
final WidgetBuilder builder; WidgetBuilder get builder;
/// {@template flutter.cupertino.cupertinoRouteTransitionMixin.title}
/// A title string for this route. /// A title string for this route.
/// ///
/// Used to auto-populate [CupertinoNavigationBar] and /// Used to auto-populate [CupertinoNavigationBar] and
/// [CupertinoSliverNavigationBar]'s `middle`/`largeTitle` widgets when /// [CupertinoSliverNavigationBar]'s `middle`/`largeTitle` widgets when
/// one is not manually supplied. /// one is not manually supplied.
final String title; /// {@endtemplate}
String get title;
ValueNotifier<String> _previousTitle; ValueNotifier<String> _previousTitle;
...@@ -155,9 +134,9 @@ class CupertinoPageRoute<T> extends PageRoute<T> { ...@@ -155,9 +134,9 @@ class CupertinoPageRoute<T> extends PageRoute<T> {
@override @override
void didChangePrevious(Route<dynamic> previousRoute) { void didChangePrevious(Route<dynamic> previousRoute) {
final String previousTitleString = previousRoute is CupertinoPageRoute final String previousTitleString = previousRoute is CupertinoRouteTransitionMixin
? previousRoute.title ? previousRoute.title
: null; : null;
if (_previousTitle == null) { if (_previousTitle == null) {
_previousTitle = ValueNotifier<String>(previousTitleString); _previousTitle = ValueNotifier<String>(previousTitleString);
} else { } else {
...@@ -166,9 +145,6 @@ class CupertinoPageRoute<T> extends PageRoute<T> { ...@@ -166,9 +145,6 @@ class CupertinoPageRoute<T> extends PageRoute<T> {
super.didChangePrevious(previousRoute); super.didChangePrevious(previousRoute);
} }
@override
final bool maintainState;
@override @override
// A relatively rigorous eyeball estimation. // A relatively rigorous eyeball estimation.
Duration get transitionDuration => const Duration(milliseconds: 400); Duration get transitionDuration => const Duration(milliseconds: 400);
...@@ -182,7 +158,7 @@ class CupertinoPageRoute<T> extends PageRoute<T> { ...@@ -182,7 +158,7 @@ class CupertinoPageRoute<T> extends PageRoute<T> {
@override @override
bool canTransitionTo(TransitionRoute<dynamic> nextRoute) { bool canTransitionTo(TransitionRoute<dynamic> nextRoute) {
// Don't perform outgoing animation if the next route is a fullscreen dialog. // Don't perform outgoing animation if the next route is a fullscreen dialog.
return nextRoute is CupertinoPageRoute && !nextRoute.fullscreenDialog; return nextRoute is CupertinoRouteTransitionMixin && !nextRoute.fullscreenDialog;
} }
/// True if an iOS-style back swipe pop gesture is currently underway for [route]. /// True if an iOS-style back swipe pop gesture is currently underway for [route].
...@@ -334,11 +310,139 @@ class CupertinoPageRoute<T> extends PageRoute<T> { ...@@ -334,11 +310,139 @@ class CupertinoPageRoute<T> extends PageRoute<T> {
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) { Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
return buildPageTransitions<T>(this, context, animation, secondaryAnimation, child); return buildPageTransitions<T>(this, context, animation, secondaryAnimation, child);
} }
}
/// A modal route that replaces the entire screen with an iOS transition.
///
/// {@macro flutter.cupertino.cupertinoRouteTransitionMixin}
///
/// By default, when a modal route is replaced by another, the previous route
/// remains in memory. To free all the resources when this is not necessary, set
/// [maintainState] to false.
///
/// The type `T` specifies the return type of the route which can be supplied as
/// the route is popped from the stack via [Navigator.pop] when an optional
/// `result` can be provided.
///
/// See also:
///
/// * [CupertinoRouteTransitionMixin], for a mixin that provides iOS transition
/// for this modal route.
/// * [MaterialPageRoute], for an adaptive [PageRoute] that uses a
/// platform-appropriate transition.
/// * [CupertinoPageScaffold], for applications that have one page with a fixed
/// navigation bar on top.
/// * [CupertinoTabScaffold], for applications that have a tab bar at the
/// bottom with multiple pages.
/// * [CupertinoPage], for a [Page] version of this class.
class CupertinoPageRoute<T> extends PageRoute<T> with CupertinoRouteTransitionMixin<T> {
/// Creates a page route for use in an iOS designed app.
///
/// The [builder], [maintainState], and [fullscreenDialog] arguments must not
/// be null.
CupertinoPageRoute({
@required this.builder,
this.title,
RouteSettings settings,
this.maintainState = true,
bool fullscreenDialog = false,
}) : assert(builder != null),
assert(maintainState != null),
assert(fullscreenDialog != null),
assert(opaque),
super(settings: settings, fullscreenDialog: fullscreenDialog);
@override
final WidgetBuilder builder;
@override
final String title;
@override
final bool maintainState;
@override @override
String get debugLabel => '${super.debugLabel}(${settings.name})'; String get debugLabel => '${super.debugLabel}(${settings.name})';
} }
// A page-based version of CupertinoPageRoute.
//
// This route uses the builder from the page to build its content. This ensures
// the content is up to date after page updates.
class _PageBasedCupertinoPageRoute<T> extends PageRoute<T> with CupertinoRouteTransitionMixin<T> {
_PageBasedCupertinoPageRoute({
@required CupertinoPage<T> page,
}) : assert(page != null),
assert(opaque),
super(settings: page);
CupertinoPage<T> get _page => settings as CupertinoPage<T>;
@override
WidgetBuilder get builder => _page.builder;
@override
String get title => _page.title;
@override
bool get maintainState => _page.maintainState;
@override
bool get fullscreenDialog => _page.fullscreenDialog;
@override
String get debugLabel => '${super.debugLabel}(${_page.name})';
}
/// A page that creates a cupertino style [PageRoute].
///
/// {@macro flutter.cupertino.cupertinoRouteTransitionMixin}
///
/// By default, when a created modal route is replaced by another, the previous
/// route remains in memory. To free all the resources when this is not
/// necessary, set [maintainState] to false.
///
/// The type `T` specifies the return type of the route which can be supplied as
/// the route is popped from the stack via [Navigator.transitionDelegate] by
/// providing the optional `result` argument to the
/// [RouteTransitionRecord.markForPop] in the [TransitionDelegate.resolve].
///
/// See also:
///
/// * [CupertinoPageRoute], for a [PageRoute] version of this class.
class CupertinoPage<T> extends Page<T> {
/// Creates a cupertino page.
const CupertinoPage({
@required this.builder,
this.maintainState = true,
this.title,
this.fullscreenDialog = false,
LocalKey key,
String name,
Object arguments,
}) : assert(builder != null),
assert(maintainState != null),
assert(fullscreenDialog != null),
super(key: key, name: name, arguments: arguments);
/// Builds the primary contents of the route.
final WidgetBuilder builder;
/// {@macro flutter.cupertino.cupertinoRouteTransitionMixin.title}
final String title;
/// {@macro flutter.widgets.modalRoute.maintainState}
final bool maintainState;
/// {@macro flutter.widgets.pageRoute.fullscreenDialog}
final bool fullscreenDialog;
@override
Route<T> createRoute(BuildContext context) {
return _PageBasedCupertinoPageRoute<T>(page: this);
}
}
/// Provides an iOS-style page transition animation. /// Provides an iOS-style page transition animation.
/// ///
/// The page slides in from the right and exits in reverse. It also shifts to the left in /// The page slides in from the right and exits in reverse. It also shifts to the left in
......
...@@ -13,20 +13,14 @@ import 'theme.dart'; ...@@ -13,20 +13,14 @@ import 'theme.dart';
/// A modal route that replaces the entire screen with a platform-adaptive /// A modal route that replaces the entire screen with a platform-adaptive
/// transition. /// transition.
/// ///
/// For Android, the entrance transition for the page slides the page upwards /// {@macro flutter.material.materialRouteTransitionMixin}
/// and fades it in. The exit transition is the same, but in reverse.
///
/// The transition is adaptive to the platform and on iOS, the page slides in
/// from the right and exits in reverse. The page also shifts to the left in
/// parallax when another page enters to cover it. (These directions are flipped
/// in environments with a right-to-left reading direction.)
/// ///
/// By default, when a modal route is replaced by another, the previous route /// By default, when a modal route is replaced by another, the previous route
/// remains in memory. To free all the resources when this is not necessary, set /// remains in memory. To free all the resources when this is not necessary, set
/// [maintainState] to false. /// [maintainState] to false.
/// ///
/// The `fullscreenDialog` property specifies whether the incoming page is a /// The `fullscreenDialog` property specifies whether the incoming route is a
/// fullscreen modal dialog. On iOS, those pages animate from the bottom to the /// fullscreen modal dialog. On iOS, those routes animate from the bottom to the
/// top rather than horizontally. /// top rather than horizontally.
/// ///
/// The type `T` specifies the return type of the route which can be supplied as /// The type `T` specifies the return type of the route which can be supplied as
...@@ -35,9 +29,10 @@ import 'theme.dart'; ...@@ -35,9 +29,10 @@ import 'theme.dart';
/// ///
/// See also: /// See also:
/// ///
/// * [PageTransitionsTheme], which defines the default page transitions used /// * [MaterialRouteTransitionMixin], which provides the material transition
/// by [MaterialPageRoute.buildTransitions]. /// for this route.
class MaterialPageRoute<T> extends PageRoute<T> { /// * [MaterialPage], which is a [Page] of this class.
class MaterialPageRoute<T> extends PageRoute<T> with MaterialRouteTransitionMixin<T> {
/// Construct a MaterialPageRoute whose contents are defined by [builder]. /// Construct a MaterialPageRoute whose contents are defined by [builder].
/// ///
/// The values of [builder], [maintainState], and [fullScreenDialog] must not /// The values of [builder], [maintainState], and [fullScreenDialog] must not
...@@ -53,12 +48,37 @@ class MaterialPageRoute<T> extends PageRoute<T> { ...@@ -53,12 +48,37 @@ class MaterialPageRoute<T> extends PageRoute<T> {
assert(opaque), assert(opaque),
super(settings: settings, fullscreenDialog: fullscreenDialog); super(settings: settings, fullscreenDialog: fullscreenDialog);
/// Builds the primary contents of the route. @override
final WidgetBuilder builder; final WidgetBuilder builder;
@override @override
final bool maintainState; final bool maintainState;
@override
String get debugLabel => '${super.debugLabel}(${settings.name})';
}
/// A mixin that provides platform-adaptive transitions for a [PageRoute].
///
/// {@template flutter.material.materialRouteTransitionMixin}
/// For Android, the entrance transition for the page slides the route upwards
/// and fades it in. The exit transition is the same, but in reverse.
///
/// The transition is adaptive to the platform and on iOS, the route slides in
/// from the right and exits in reverse. The route also shifts to the left in
/// parallax when another page enters to cover it. (These directions are flipped
/// in environments with a right-to-left reading direction.)
/// {@endtemplate}
///
/// See also:
///
/// * [PageTransitionsTheme], which defines the default page transitions used
/// by the [MaterialRouteTransitionMixin.buildTransitions].
mixin MaterialRouteTransitionMixin<T> on PageRoute<T> {
/// Builds the primary contents of the route.
WidgetBuilder get builder;
@override @override
Duration get transitionDuration => const Duration(milliseconds: 300); Duration get transitionDuration => const Duration(milliseconds: 300);
...@@ -71,8 +91,8 @@ class MaterialPageRoute<T> extends PageRoute<T> { ...@@ -71,8 +91,8 @@ class MaterialPageRoute<T> extends PageRoute<T> {
@override @override
bool canTransitionTo(TransitionRoute<dynamic> nextRoute) { bool canTransitionTo(TransitionRoute<dynamic> nextRoute) {
// Don't perform outgoing animation if the next route is a fullscreen dialog. // Don't perform outgoing animation if the next route is a fullscreen dialog.
return (nextRoute is MaterialPageRoute && !nextRoute.fullscreenDialog) return (nextRoute is MaterialRouteTransitionMixin && !nextRoute.fullscreenDialog)
|| (nextRoute is CupertinoPageRoute && !nextRoute.fullscreenDialog); || (nextRoute is CupertinoRouteTransitionMixin && !nextRoute.fullscreenDialog);
} }
@override @override
...@@ -103,7 +123,79 @@ class MaterialPageRoute<T> extends PageRoute<T> { ...@@ -103,7 +123,79 @@ class MaterialPageRoute<T> extends PageRoute<T> {
final PageTransitionsTheme theme = Theme.of(context).pageTransitionsTheme; final PageTransitionsTheme theme = Theme.of(context).pageTransitionsTheme;
return theme.buildTransitions<T>(this, context, animation, secondaryAnimation, child); return theme.buildTransitions<T>(this, context, animation, secondaryAnimation, child);
} }
}
/// A page that creates a material style [PageRoute].
///
/// {@macro flutter.material.materialRouteTransitionMixin}
///
/// By default, when the created route is replaced by another, the previous
/// route remains in memory. To free all the resources when this is not
/// necessary, set [maintainState] to false.
///
/// The `fullscreenDialog` property specifies whether the created route is a
/// fullscreen modal dialog. On iOS, those routes animate from the bottom to the
/// top rather than horizontally.
///
/// The type `T` specifies the return type of the route which can be supplied as
/// the route is popped from the stack via [Navigator.transitionDelegate] by
/// providing the optional `result` argument to the
/// [RouteTransitionRecord.markForPop] in the [TransitionDelegate.resolve].
///
/// See also:
///
/// * [MaterialPageRoute], which is the [PageRoute] version of this class
class MaterialPage<T> extends Page<T> {
/// Creates a material page.
const MaterialPage({
@required this.builder,
this.maintainState = true,
this.fullscreenDialog = false,
LocalKey key,
String name,
Object arguments,
}) : assert(builder != null),
assert(maintainState != null),
assert(fullscreenDialog != null),
super(key: key, name: name, arguments: arguments);
/// Builds the primary contents of the route.
final WidgetBuilder builder;
/// {@macro flutter.widgets.modalRoute.maintainState}
final bool maintainState;
/// {@macro flutter.widgets.pageRoute.fullscreenDialog}
final bool fullscreenDialog;
@override @override
String get debugLabel => '${super.debugLabel}(${settings.name})'; Route<T> createRoute(BuildContext context) {
return _PageBasedMaterialPageRoute<T>(page: this);
}
}
// A page-based version of MaterialPageRoute.
//
// This route uses the builder from the page to build its content. This ensures
// the content is up to date after page updates.
class _PageBasedMaterialPageRoute<T> extends PageRoute<T> with MaterialRouteTransitionMixin<T> {
_PageBasedMaterialPageRoute({
@required MaterialPage<T> page,
}) : assert(page != null),
assert(opaque),
super(settings: page);
MaterialPage<T> get _page => settings as MaterialPage<T>;
@override
WidgetBuilder get builder => _page.builder;
@override
bool get maintainState => _page.maintainState;
@override
bool get fullscreenDialog => _page.fullscreenDialog;
@override
String get debugLabel => '${super.debugLabel}(${_page.name})';
} }
...@@ -536,7 +536,7 @@ class CupertinoPageTransitionsBuilder extends PageTransitionsBuilder { ...@@ -536,7 +536,7 @@ class CupertinoPageTransitionsBuilder extends PageTransitionsBuilder {
Animation<double> secondaryAnimation, Animation<double> secondaryAnimation,
Widget child, Widget child,
) { ) {
return CupertinoPageRoute.buildPageTransitions<T>(route, context, animation, secondaryAnimation, child); return CupertinoRouteTransitionMixin.buildPageTransitions<T>(route, context, animation, secondaryAnimation, child);
} }
} }
...@@ -593,7 +593,7 @@ class PageTransitionsTheme with Diagnosticable { ...@@ -593,7 +593,7 @@ class PageTransitionsTheme with Diagnosticable {
) { ) {
TargetPlatform platform = Theme.of(context).platform; TargetPlatform platform = Theme.of(context).platform;
if (CupertinoPageRoute.isPopGestureInProgress(route)) if (CupertinoRouteTransitionMixin.isPopGestureInProgress(route))
platform = TargetPlatform.iOS; platform = TargetPlatform.iOS;
final PageTransitionsBuilder matchingBuilder = final PageTransitionsBuilder matchingBuilder =
......
...@@ -3004,8 +3004,8 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -3004,8 +3004,8 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
needsExplicitDecision = true; needsExplicitDecision = true;
assert( assert(
newEntry.route.settings == nextPage, newEntry.route.settings == nextPage,
'If a route is created from a page, its must have that page as its ' 'The settings getter of a page-based Route must return a Page object. '
'settings.', 'Please set the settings to the Page in the Page.createRoute method.'
); );
newHistory.add(newEntry); newHistory.add(newEntry);
} else { } else {
......
...@@ -17,12 +17,14 @@ abstract class PageRoute<T> extends ModalRoute<T> { ...@@ -17,12 +17,14 @@ abstract class PageRoute<T> extends ModalRoute<T> {
this.fullscreenDialog = false, this.fullscreenDialog = false,
}) : super(settings: settings); }) : super(settings: settings);
/// {@template flutter.widgets.pageRoute.fullscreenDialog}
/// Whether this page route is a full-screen dialog. /// Whether this page route is a full-screen dialog.
/// ///
/// In Material and Cupertino, being fullscreen has the effects of making /// In Material and Cupertino, being fullscreen has the effects of making
/// the app bars have a close button instead of a back button. On /// the app bars have a close button instead of a back button. On
/// iOS, dialogs transitions animate differently and are also not closeable /// iOS, dialogs transitions animate differently and are also not closeable
/// with the back swipe gesture. /// with the back swipe gesture.
/// {@endtemplate}
final bool fullscreenDialog; final bool fullscreenDialog;
@override @override
...@@ -46,6 +48,10 @@ Widget _defaultTransitionsBuilder(BuildContext context, Animation<double> animat ...@@ -46,6 +48,10 @@ Widget _defaultTransitionsBuilder(BuildContext context, Animation<double> animat
/// ///
/// Callers must define the [pageBuilder] function which creates the route's /// Callers must define the [pageBuilder] function which creates the route's
/// primary contents. To add transitions define the [transitionsBuilder] function. /// primary contents. To add transitions define the [transitionsBuilder] function.
///
/// See also:
///
/// * [TransitionBuilderPage], which is a [Page] of this class.
class PageRouteBuilder<T> extends PageRoute<T> { class PageRouteBuilder<T> extends PageRoute<T> {
/// Creates a route that delegates to builder callbacks. /// Creates a route that delegates to builder callbacks.
/// ///
...@@ -70,14 +76,18 @@ class PageRouteBuilder<T> extends PageRoute<T> { ...@@ -70,14 +76,18 @@ class PageRouteBuilder<T> extends PageRoute<T> {
assert(fullscreenDialog != null), assert(fullscreenDialog != null),
super(settings: settings, fullscreenDialog: fullscreenDialog); super(settings: settings, fullscreenDialog: fullscreenDialog);
/// {@template flutter.widgets.pageRouteBuilder.pageBuilder}
/// Used build the route's primary contents. /// Used build the route's primary contents.
/// ///
/// See [ModalRoute.buildPage] for complete definition of the parameters. /// See [ModalRoute.buildPage] for complete definition of the parameters.
/// {@endtemplate}
final RoutePageBuilder pageBuilder; final RoutePageBuilder pageBuilder;
/// {@template flutter.widgets.pageRouteBuilder.transitionsBuilder}
/// Used to build the route's transitions. /// Used to build the route's transitions.
/// ///
/// See [ModalRoute.buildTransitions] for complete definition of the parameters. /// See [ModalRoute.buildTransitions] for complete definition of the parameters.
/// {@endtemplate}
final RouteTransitionsBuilder transitionsBuilder; final RouteTransitionsBuilder transitionsBuilder;
@override @override
...@@ -107,5 +117,108 @@ class PageRouteBuilder<T> extends PageRoute<T> { ...@@ -107,5 +117,108 @@ class PageRouteBuilder<T> extends PageRoute<T> {
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) { Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
return transitionsBuilder(context, animation, secondaryAnimation, child); return transitionsBuilder(context, animation, secondaryAnimation, child);
} }
}
/// A page that creates a [PageRoute] with customizable transition.
///
/// Similar to the [PageRouteBuilder], callers must define the [pageBuilder]
/// function which creates the route's primary contents. To add transitions
/// define the [transitionsBuilder] function.
///
/// See also:
///
/// * [PageRouteBuilder], which is a [PageRoute] version of this class.
class TransitionBuilderPage<T> extends Page<T> {
/// Creates a [TransitionBuilderPage].
const TransitionBuilderPage({
@required this.pageBuilder,
this.transitionsBuilder = _defaultTransitionsBuilder,
this.transitionDuration = const Duration(milliseconds: 300),
this.opaque = true,
this.barrierDismissible = false,
this.barrierColor,
this.barrierLabel,
this.maintainState = true,
this.fullscreenDialog = false,
LocalKey key,
String name,
Object arguments,
}) : assert(pageBuilder != null),
assert(transitionsBuilder != null),
assert(opaque != null),
assert(barrierDismissible != null),
assert(maintainState != null),
assert(fullscreenDialog != null),
super(key: key, name: name, arguments: arguments);
/// {@macro flutter.widgets.pageRouteBuilder.pageBuilder}
final RoutePageBuilder pageBuilder;
/// {@macro flutter.widgets.pageRouteBuilder.transitionsBuilder}
final RouteTransitionsBuilder transitionsBuilder;
/// {@macro flutter.widgets.transitionRoute.transitionDuration}
final Duration transitionDuration;
/// {@macro flutter.widgets.transitionRoute.opaque}
final bool opaque;
/// {@macro flutter.widgets.modalRoute.barrierDismissible}
final bool barrierDismissible;
/// {@macro flutter.widgets.modalRoute.barrierColor}
final Color barrierColor;
/// {@macro flutter.widgets.modalRoute.barrierLabel}
final String barrierLabel;
/// {@macro flutter.widgets.modalRoute.maintainState}
final bool maintainState;
/// {@macro flutter.widgets.pageRoute.fullscreenDialog}
final bool fullscreenDialog;
@override
Route<T> createRoute(BuildContext context) => _PageBasedPageRouteBuilder<T>(this);
}
// A page-based version of the [PageRouteBuilder].
//
// This class gets its builder and settings directly from the [TransitionBuilderPage],
// so that its content updates accordingly to the [TransitionBuilderPage].
class _PageBasedPageRouteBuilder<T> extends PageRoute<T>{
_PageBasedPageRouteBuilder(
TransitionBuilderPage<T> page,
) : assert(page != null),
super(settings: page, fullscreenDialog: page.fullscreenDialog);
TransitionBuilderPage<T> get _page => settings as TransitionBuilderPage<T>;
@override
Duration get transitionDuration => _page.transitionDuration;
@override
bool get opaque => _page.opaque;
@override
bool get barrierDismissible => _page.barrierDismissible;
@override
Color get barrierColor => _page.barrierColor;
@override
String get barrierLabel => _page.barrierLabel;
@override
bool get maintainState => _page.maintainState;
@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return _page.pageBuilder(context, animation, secondaryAnimation);
}
@override
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
return _page.transitionsBuilder(context, animation, secondaryAnimation, child);
}
} }
...@@ -93,12 +93,14 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> { ...@@ -93,12 +93,14 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
Future<T> get completed => _transitionCompleter.future; Future<T> get completed => _transitionCompleter.future;
final Completer<T> _transitionCompleter = Completer<T>(); final Completer<T> _transitionCompleter = Completer<T>();
/// {@template flutter.widgets.transitionRoute.transitionDuration}
/// The duration the transition going forwards. /// The duration the transition going forwards.
/// ///
/// See also: /// See also:
/// ///
/// * [reverseTransitionDuration], which controls the duration of the /// * [reverseTransitionDuration], which controls the duration of the
/// transition when it is in reverse. /// transition when it is in reverse.
/// {@endtemplate}
Duration get transitionDuration; Duration get transitionDuration;
/// The duration the transition going in reverse. /// The duration the transition going in reverse.
...@@ -107,10 +109,12 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> { ...@@ -107,10 +109,12 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
/// the forwards [transitionDuration]. /// the forwards [transitionDuration].
Duration get reverseTransitionDuration => transitionDuration; Duration get reverseTransitionDuration => transitionDuration;
/// {@template flutter.widgets.transitionRoute.opaque}
/// Whether the route obscures previous routes when the transition is complete. /// Whether the route obscures previous routes when the transition is complete.
/// ///
/// When an opaque route's entrance transition is complete, the routes behind /// When an opaque route's entrance transition is complete, the routes behind
/// the opaque route will not be built to save resources. /// the opaque route will not be built to save resources.
/// {@endtemplate}
bool get opaque; bool get opaque;
// This ensures that if we got to the dismissed state while still current, // This ensures that if we got to the dismissed state while still current,
...@@ -1092,6 +1096,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -1092,6 +1096,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
// The API for subclasses to override - used by this class // The API for subclasses to override - used by this class
/// {@template flutter.widgets.modalRoute.barrierDismissible}
/// Whether you can dismiss this route by tapping the modal barrier. /// Whether you can dismiss this route by tapping the modal barrier.
/// ///
/// The modal barrier is the scrim that is rendered behind each route, which /// The modal barrier is the scrim that is rendered behind each route, which
...@@ -1106,7 +1111,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -1106,7 +1111,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// ///
/// If [barrierDismissible] is false, then tapping the barrier has no effect. /// If [barrierDismissible] is false, then tapping the barrier has no effect.
/// ///
/// If this getter would ever start returning a different value, /// If this getter would ever start returning a different value, the
/// [changedInternalState] should be invoked so that the change can take /// [changedInternalState] should be invoked so that the change can take
/// effect. /// effect.
/// ///
...@@ -1114,6 +1119,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -1114,6 +1119,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// ///
/// * [barrierColor], which controls the color of the scrim for this route. /// * [barrierColor], which controls the color of the scrim for this route.
/// * [ModalBarrier], the widget that implements this feature. /// * [ModalBarrier], the widget that implements this feature.
/// {@endtemplate}
bool get barrierDismissible; bool get barrierDismissible;
/// Whether the semantics of the modal barrier are included in the /// Whether the semantics of the modal barrier are included in the
...@@ -1131,6 +1137,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -1131,6 +1137,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// has no effect. /// has no effect.
bool get semanticsDismissible => true; bool get semanticsDismissible => true;
/// {@template flutter.widgets.modalRoute.barrierColor}
/// The color to use for the modal barrier. If this is null, the barrier will /// The color to use for the modal barrier. If this is null, the barrier will
/// be transparent. /// be transparent.
/// ///
...@@ -1147,7 +1154,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -1147,7 +1154,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// While the route is animating into position, the color is animated from /// While the route is animating into position, the color is animated from
/// transparent to the specified color. /// transparent to the specified color.
/// ///
/// If this getter would ever start returning a different color, /// If this getter would ever start returning a different color, the
/// [changedInternalState] should be invoked so that the change can take /// [changedInternalState] should be invoked so that the change can take
/// effect. /// effect.
/// ///
...@@ -1156,8 +1163,10 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -1156,8 +1163,10 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// * [barrierDismissible], which controls the behavior of the barrier when /// * [barrierDismissible], which controls the behavior of the barrier when
/// tapped. /// tapped.
/// * [ModalBarrier], the widget that implements this feature. /// * [ModalBarrier], the widget that implements this feature.
/// {@endtemplate}
Color get barrierColor; Color get barrierColor;
/// {@template flutter.widgets.modalRoute.barrierLabel}
/// The semantic label used for a dismissible barrier. /// The semantic label used for a dismissible barrier.
/// ///
/// If the barrier is dismissible, this label will be read out if /// If the barrier is dismissible, this label will be read out if
...@@ -1170,7 +1179,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -1170,7 +1179,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// For example, when a dialog is on the screen, the page below the dialog is /// For example, when a dialog is on the screen, the page below the dialog is
/// usually darkened by the modal barrier. /// usually darkened by the modal barrier.
/// ///
/// If this getter would ever start returning a different label, /// If this getter would ever start returning a different label, the
/// [changedInternalState] should be invoked so that the change can take /// [changedInternalState] should be invoked so that the change can take
/// effect. /// effect.
/// ///
...@@ -1179,6 +1188,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -1179,6 +1188,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// * [barrierDismissible], which controls the behavior of the barrier when /// * [barrierDismissible], which controls the behavior of the barrier when
/// tapped. /// tapped.
/// * [ModalBarrier], the widget that implements this feature. /// * [ModalBarrier], the widget that implements this feature.
/// {@endtemplate}
String get barrierLabel; String get barrierLabel;
/// The curve that is used for animating the modal barrier in and out. /// The curve that is used for animating the modal barrier in and out.
...@@ -1193,7 +1203,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -1193,7 +1203,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// While the route is animating into position, the color is animated from /// While the route is animating into position, the color is animated from
/// transparent to the specified [barrierColor]. /// transparent to the specified [barrierColor].
/// ///
/// If this getter would ever start returning a different curve, /// If this getter would ever start returning a different curve, the
/// [changedInternalState] should be invoked so that the change can take /// [changedInternalState] should be invoked so that the change can take
/// effect. /// effect.
/// ///
...@@ -1207,6 +1217,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -1207,6 +1217,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// * [AnimatedModalBarrier], the widget that implements this feature. /// * [AnimatedModalBarrier], the widget that implements this feature.
Curve get barrierCurve => Curves.ease; Curve get barrierCurve => Curves.ease;
/// {@template flutter.widgets.modalRoute.maintainState}
/// Whether the route should remain in memory when it is inactive. /// Whether the route should remain in memory when it is inactive.
/// ///
/// If this is true, then the route is maintained, so that any futures it is /// If this is true, then the route is maintained, so that any futures it is
...@@ -1215,9 +1226,10 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -1215,9 +1226,10 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// framework to entirely discard the route's widget hierarchy when it is not /// framework to entirely discard the route's widget hierarchy when it is not
/// visible. /// visible.
/// ///
/// The value of this getter should not change during the lifetime of the /// If this getter would ever start returning a different value, the
/// object. It is used by [createOverlayEntries], which is called by /// [changedInternalState] should be invoked so that the change can take
/// [install] near the beginning of the route lifecycle. /// effect.
/// {@endtemplate}
bool get maintainState; bool get maintainState;
...@@ -1397,6 +1409,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -1397,6 +1409,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
super.changedInternalState(); super.changedInternalState();
setState(() { /* internal state already changed */ }); setState(() { /* internal state already changed */ });
_modalBarrier.markNeedsBuild(); _modalBarrier.markNeedsBuild();
_modalScope.maintainState = maintainState;
} }
@override @override
...@@ -1481,10 +1494,12 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -1481,10 +1494,12 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
); );
} }
OverlayEntry _modalScope;
@override @override
Iterable<OverlayEntry> createOverlayEntries() sync* { Iterable<OverlayEntry> createOverlayEntries() sync* {
yield _modalBarrier = OverlayEntry(builder: _buildModalBarrier); yield _modalBarrier = OverlayEntry(builder: _buildModalBarrier);
yield OverlayEntry(builder: _buildModalScope, maintainState: maintainState); yield _modalScope = OverlayEntry(builder: _buildModalScope, maintainState: maintainState);
} }
@override @override
......
...@@ -1374,6 +1374,104 @@ void main() { ...@@ -1374,6 +1374,104 @@ void main() {
)); ));
debugDefaultTargetPlatformOverride = null; debugDefaultTargetPlatformOverride = null;
}); });
testWidgets('CupertinoPage works', (WidgetTester tester) async {
final LocalKey pageKey = UniqueKey();
final TransitionDetector detector = TransitionDetector();
List<Page<void>> myPages = <Page<void>>[
CupertinoPage<void>(
key: pageKey,
title: 'title one',
builder: (BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(key: UniqueKey()),
child: const Text('first'),
);
}
),
];
await tester.pumpWidget(
buildNavigator(
pages: myPages,
onPopPage: (Route<dynamic> route, dynamic result) => null,
transitionDelegate: detector,
)
);
expect(detector.hasTransition, isFalse);
expect(find.widgetWithText(CupertinoNavigationBar, 'title one'), findsOneWidget);
expect(find.text('first'), findsOneWidget);
myPages = <Page<void>>[
CupertinoPage<void>(
key: pageKey,
title: 'title two',
builder: (BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(key: UniqueKey()),
child: const Text('second'),
);
}
),
];
await tester.pumpWidget(
buildNavigator(
pages: myPages,
onPopPage: (Route<dynamic> route, dynamic result) => null,
transitionDelegate: detector,
)
);
// There should be no transition because the page has the same key.
expect(detector.hasTransition, isFalse);
// The content does update.
expect(find.text('first'), findsNothing);
expect(find.widgetWithText(CupertinoNavigationBar, 'title one'), findsNothing);
expect(find.text('second'), findsOneWidget);
expect(find.widgetWithText(CupertinoNavigationBar, 'title two'), findsOneWidget);
});
testWidgets('CupertinoPage can toggle MaintainState', (WidgetTester tester) async {
final LocalKey pageKeyOne = UniqueKey();
final LocalKey pageKeyTwo = UniqueKey();
final TransitionDetector detector = TransitionDetector();
List<Page<void>> myPages = <Page<void>>[
CupertinoPage<void>(key: pageKeyOne, maintainState: false, builder: (BuildContext context) => const Text('first')),
CupertinoPage<void>(key: pageKeyTwo, builder: (BuildContext context) => const Text('second')),
];
await tester.pumpWidget(
buildNavigator(
pages: myPages,
onPopPage: (Route<dynamic> route, dynamic result) => null,
transitionDelegate: detector,
)
);
expect(detector.hasTransition, isFalse);
// Page one does not maintain state.
expect(find.text('first', skipOffstage: false), findsNothing);
expect(find.text('second'), findsOneWidget);
myPages = <Page<void>>[
CupertinoPage<void>(key: pageKeyOne, maintainState: true, builder: (BuildContext context) => const Text('first')),
CupertinoPage<void>(key: pageKeyTwo, builder: (BuildContext context) => const Text('second')),
];
await tester.pumpWidget(
buildNavigator(
pages: myPages,
onPopPage: (Route<dynamic> route, dynamic result) => null,
transitionDelegate: detector,
)
);
// There should be no transition because the page has the same key.
expect(detector.hasTransition, isFalse);
// Page one sets the maintain state to be true, its widget tree should be
// built.
expect(find.text('first', skipOffstage: false), findsOneWidget);
expect(find.text('second'), findsOneWidget);
});
} }
class MockNavigatorObserver extends Mock implements NavigatorObserver {} class MockNavigatorObserver extends Mock implements NavigatorObserver {}
...@@ -1401,3 +1499,47 @@ class DialogObserver extends NavigatorObserver { ...@@ -1401,3 +1499,47 @@ class DialogObserver extends NavigatorObserver {
super.didPush(route, previousRoute); super.didPush(route, previousRoute);
} }
} }
class TransitionDetector extends DefaultTransitionDelegate<void> {
bool hasTransition = false;
@override
Iterable<RouteTransitionRecord> resolve({
List<RouteTransitionRecord> newPageRouteHistory,
Map<RouteTransitionRecord, RouteTransitionRecord> locationToExitingPageRoute,
Map<RouteTransitionRecord, List<RouteTransitionRecord>> pageRouteToPagelessRoutes
}) {
hasTransition = true;
return super.resolve(
newPageRouteHistory: newPageRouteHistory,
locationToExitingPageRoute: locationToExitingPageRoute,
pageRouteToPagelessRoutes: pageRouteToPagelessRoutes
);
}
}
Widget buildNavigator({
List<Page<dynamic>> pages,
PopPageCallback onPopPage,
GlobalKey<NavigatorState> key,
TransitionDelegate<dynamic> transitionDelegate
}) {
return MediaQuery(
data: MediaQueryData.fromWindow(WidgetsBinding.instance.window),
child: Localizations(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultCupertinoLocalizations.delegate,
DefaultWidgetsLocalizations.delegate
],
child: Directionality(
textDirection: TextDirection.ltr,
child: Navigator(
key: key,
pages: pages,
onPopPage: onPopPage,
transitionDelegate: transitionDelegate ?? const DefaultTransitionDelegate<dynamic>(),
),
),
),
);
}
...@@ -836,4 +836,124 @@ void main() { ...@@ -836,4 +836,124 @@ void main() {
expect(titleInitialTopLeft.dy, equals(titleTransientTopLeft.dy)); expect(titleInitialTopLeft.dy, equals(titleTransientTopLeft.dy));
expect(titleInitialTopLeft.dx, greaterThan(titleTransientTopLeft.dx)); expect(titleInitialTopLeft.dx, greaterThan(titleTransientTopLeft.dx));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('MaterialPage works', (WidgetTester tester) async {
final LocalKey pageKey = UniqueKey();
final TransitionDetector detector = TransitionDetector();
List<Page<void>> myPages = <Page<void>>[
MaterialPage<void>(key: pageKey, builder: (BuildContext context) => const Text('first')),
];
await tester.pumpWidget(
buildNavigator(
pages: myPages,
onPopPage: (Route<dynamic> route, dynamic result) => null,
transitionDelegate: detector,
)
);
expect(detector.hasTransition, isFalse);
expect(find.text('first'), findsOneWidget);
myPages = <Page<void>>[
MaterialPage<void>(key: pageKey, builder: (BuildContext context) => const Text('second')),
];
await tester.pumpWidget(
buildNavigator(
pages: myPages,
onPopPage: (Route<dynamic> route, dynamic result) => null,
transitionDelegate: detector,
)
);
// There should be no transition because the page has the same key.
expect(detector.hasTransition, isFalse);
// The content does update.
expect(find.text('first'), findsNothing);
expect(find.text('second'), findsOneWidget);
});
testWidgets('MaterialPage can toggle MaintainState', (WidgetTester tester) async {
final LocalKey pageKeyOne = UniqueKey();
final LocalKey pageKeyTwo = UniqueKey();
final TransitionDetector detector = TransitionDetector();
List<Page<void>> myPages = <Page<void>>[
MaterialPage<void>(key: pageKeyOne, maintainState: false, builder: (BuildContext context) => const Text('first')),
MaterialPage<void>(key: pageKeyTwo, builder: (BuildContext context) => const Text('second')),
];
await tester.pumpWidget(
buildNavigator(
pages: myPages,
onPopPage: (Route<dynamic> route, dynamic result) => null,
transitionDelegate: detector,
)
);
expect(detector.hasTransition, isFalse);
// Page one does not maintain state.
expect(find.text('first', skipOffstage: false), findsNothing);
expect(find.text('second'), findsOneWidget);
myPages = <Page<void>>[
MaterialPage<void>(key: pageKeyOne, maintainState: true, builder: (BuildContext context) => const Text('first')),
MaterialPage<void>(key: pageKeyTwo, builder: (BuildContext context) => const Text('second')),
];
await tester.pumpWidget(
buildNavigator(
pages: myPages,
onPopPage: (Route<dynamic> route, dynamic result) => null,
transitionDelegate: detector,
)
);
// There should be no transition because the page has the same key.
expect(detector.hasTransition, isFalse);
// Page one sets the maintain state to be true, its widget tree should be
// built.
expect(find.text('first', skipOffstage: false), findsOneWidget);
expect(find.text('second'), findsOneWidget);
});
}
class TransitionDetector extends DefaultTransitionDelegate<void> {
bool hasTransition = false;
@override
Iterable<RouteTransitionRecord> resolve({
List<RouteTransitionRecord> newPageRouteHistory,
Map<RouteTransitionRecord, RouteTransitionRecord> locationToExitingPageRoute,
Map<RouteTransitionRecord, List<RouteTransitionRecord>> pageRouteToPagelessRoutes
}) {
hasTransition = true;
return super.resolve(
newPageRouteHistory: newPageRouteHistory,
locationToExitingPageRoute: locationToExitingPageRoute,
pageRouteToPagelessRoutes: pageRouteToPagelessRoutes
);
}
}
Widget buildNavigator({
List<Page<dynamic>> pages,
PopPageCallback onPopPage,
GlobalKey<NavigatorState> key,
TransitionDelegate<dynamic> transitionDelegate
}) {
return MediaQuery(
data: MediaQueryData.fromWindow(WidgetsBinding.instance.window),
child: Localizations(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate
],
child: Directionality(
textDirection: TextDirection.ltr,
child: Navigator(
key: key,
pages: pages,
onPopPage: onPopPage,
transitionDelegate: transitionDelegate ?? const DefaultTransitionDelegate<dynamic>(),
),
),
),
);
} }
...@@ -538,6 +538,142 @@ void main() { ...@@ -538,6 +538,142 @@ void main() {
expect(focusNode.hasPrimaryFocus, isTrue); expect(focusNode.hasPrimaryFocus, isTrue);
}); });
testWidgets('TransitionBuilderPage works', (WidgetTester tester) async {
final LocalKey pageKey = UniqueKey();
final TransitionDetector detector = TransitionDetector();
List<Page<void>> myPages = <Page<void>>[
TransitionBuilderPage<void>(
key: pageKey,
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return const Text('first');
},
)
];
await tester.pumpWidget(
buildNavigator(
pages: myPages,
onPopPage: (Route<dynamic> route, dynamic result) => null,
transitionDelegate: detector,
)
);
expect(detector.hasTransition, isFalse);
expect(find.text('first'), findsOneWidget);
myPages = <Page<void>>[
TransitionBuilderPage<void>(
key: pageKey,
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return const Text('second');
},
)
];
await tester.pumpWidget(
buildNavigator(
pages: myPages,
onPopPage: (Route<dynamic> route, dynamic result) => null,
transitionDelegate: detector,
)
);
// There should be no transition because the page has the same key.
expect(detector.hasTransition, isFalse);
// The content does update.
expect(find.text('first'), findsNothing);
expect(find.text('second'), findsOneWidget);
myPages = <Page<void>>[
TransitionBuilderPage<void>(
key: pageKey,
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return const Text('dummy');
},
transitionsBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
// Purposely discard the input child.
return const Text('third');
}
)
];
await tester.pumpWidget(
buildNavigator(
pages: myPages,
onPopPage: (Route<dynamic> route, dynamic result) => null,
transitionDelegate: detector,
)
);
// There should be no transition because the page has the same key.
expect(detector.hasTransition, isFalse);
// Makes sure transitionsBuilder works.
expect(find.text('second'), findsNothing);
expect(find.text('dummy'), findsNothing);
expect(find.text('third'), findsOneWidget);
});
testWidgets('TransitionBuilderPage can toggle MaintainState', (WidgetTester tester) async {
final LocalKey pageKeyOne = UniqueKey();
final LocalKey pageKeyTwo = UniqueKey();
final TransitionDetector detector = TransitionDetector();
List<Page<void>> myPages = <Page<void>>[
TransitionBuilderPage<void>(
key: pageKeyOne,
maintainState: false,
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return const Text('first');
},
),
TransitionBuilderPage<void>(
key: pageKeyTwo,
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return const Text('second');
},
)
];
await tester.pumpWidget(
buildNavigator(
pages: myPages,
onPopPage: (Route<dynamic> route, dynamic result) => null,
transitionDelegate: detector,
)
);
expect(detector.hasTransition, isFalse);
// Page one does not maintain state.
expect(find.text('first', skipOffstage: false), findsNothing);
expect(find.text('second'), findsOneWidget);
myPages = <Page<void>>[
TransitionBuilderPage<void>(
key: pageKeyOne,
maintainState: true,
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return const Text('first');
},
),
TransitionBuilderPage<void>(
key: pageKeyTwo,
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return const Text('second');
},
)
];
await tester.pumpWidget(
buildNavigator(
pages: myPages,
onPopPage: (Route<dynamic> route, dynamic result) => null,
transitionDelegate: detector,
)
);
// There should be no transition because the page has the same key.
expect(detector.hasTransition, isFalse);
// Page one sets the maintain state to be true, its widget tree should be
// built.
expect(find.text('first', skipOffstage: false), findsOneWidget);
expect(find.text('second'), findsOneWidget);
});
group('TransitionRoute', () { group('TransitionRoute', () {
testWidgets('secondary animation is kDismissed when next route finishes pop', (WidgetTester tester) async { testWidgets('secondary animation is kDismissed when next route finishes pop', (WidgetTester tester) async {
final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
...@@ -1539,3 +1675,47 @@ class WidgetWithLocalHistoryState extends State<WidgetWithLocalHistory> { ...@@ -1539,3 +1675,47 @@ class WidgetWithLocalHistoryState extends State<WidgetWithLocalHistory> {
return const Text('dummy'); return const Text('dummy');
} }
} }
class TransitionDetector extends DefaultTransitionDelegate<void> {
bool hasTransition = false;
@override
Iterable<RouteTransitionRecord> resolve({
List<RouteTransitionRecord> newPageRouteHistory,
Map<RouteTransitionRecord, RouteTransitionRecord> locationToExitingPageRoute,
Map<RouteTransitionRecord, List<RouteTransitionRecord>> pageRouteToPagelessRoutes
}) {
hasTransition = true;
return super.resolve(
newPageRouteHistory: newPageRouteHistory,
locationToExitingPageRoute: locationToExitingPageRoute,
pageRouteToPagelessRoutes: pageRouteToPagelessRoutes
);
}
}
Widget buildNavigator({
List<Page<dynamic>> pages,
PopPageCallback onPopPage,
GlobalKey<NavigatorState> key,
TransitionDelegate<dynamic> transitionDelegate
}) {
return MediaQuery(
data: MediaQueryData.fromWindow(WidgetsBinding.instance.window),
child: Localizations(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate
],
child: Directionality(
textDirection: TextDirection.ltr,
child: Navigator(
key: key,
pages: pages,
onPopPage: onPopPage,
transitionDelegate: transitionDelegate ?? const DefaultTransitionDelegate<dynamic>(),
),
),
),
);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment