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 {
}) {
// Auto use the CupertinoPageRoute's title if middle not provided.
if (automaticallyImplyTitle &&
currentRoute is CupertinoPageRoute &&
currentRoute is CupertinoRouteTransitionMixin &&
currentRoute.title != null) {
return Text(currentRoute.title);
}
......@@ -1451,8 +1451,8 @@ class _BackLabel extends StatelessWidget {
Widget build(BuildContext context) {
if (specifiedPreviousTitle != null) {
return _buildPreviousTitleWidget(context, specifiedPreviousTitle, null);
} else if (route is CupertinoPageRoute<dynamic> && !route.isFirst) {
final CupertinoPageRoute<dynamic> cupertinoRoute = route as CupertinoPageRoute<dynamic>;
} else if (route is CupertinoRouteTransitionMixin<dynamic> && !route.isFirst) {
final CupertinoRouteTransitionMixin<dynamic> cupertinoRoute = route as CupertinoRouteTransitionMixin<dynamic>;
// There is no timing issue because the previousTitle Listenable changes
// happen during route modifications before the ValueListenableBuilder
// is built.
......
......@@ -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
/// 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
/// effect for fullscreen dialogs.
///
/// 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.
/// {@endtemplate}
///
/// See also:
///
/// * [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.
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);
/// * [MaterialRouteTransitionMixin], which is a mixin that provides
/// platform-appropriate transitions for a [PageRoute]
/// * [CupertinoPageRoute], which is a [PageRoute] that leverages this mixin.
mixin CupertinoRouteTransitionMixin<T> on PageRoute<T> {
/// Builds the primary contents of the route.
final WidgetBuilder builder;
WidgetBuilder get builder;
/// {@template flutter.cupertino.cupertinoRouteTransitionMixin.title}
/// A title string for this route.
///
/// Used to auto-populate [CupertinoNavigationBar] and
/// [CupertinoSliverNavigationBar]'s `middle`/`largeTitle` widgets when
/// one is not manually supplied.
final String title;
/// {@endtemplate}
String get title;
ValueNotifier<String> _previousTitle;
......@@ -155,9 +134,9 @@ class CupertinoPageRoute<T> extends PageRoute<T> {
@override
void didChangePrevious(Route<dynamic> previousRoute) {
final String previousTitleString = previousRoute is CupertinoPageRoute
? previousRoute.title
: null;
final String previousTitleString = previousRoute is CupertinoRouteTransitionMixin
? previousRoute.title
: null;
if (_previousTitle == null) {
_previousTitle = ValueNotifier<String>(previousTitleString);
} else {
......@@ -166,9 +145,6 @@ class CupertinoPageRoute<T> extends PageRoute<T> {
super.didChangePrevious(previousRoute);
}
@override
final bool maintainState;
@override
// A relatively rigorous eyeball estimation.
Duration get transitionDuration => const Duration(milliseconds: 400);
......@@ -182,7 +158,7 @@ class CupertinoPageRoute<T> extends PageRoute<T> {
@override
bool canTransitionTo(TransitionRoute<dynamic> nextRoute) {
// 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].
......@@ -334,11 +310,139 @@ class CupertinoPageRoute<T> extends PageRoute<T> {
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget 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
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.
///
/// 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';
/// A modal route that replaces the entire screen with a platform-adaptive
/// transition.
///
/// For Android, the entrance transition for the page slides the page 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 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.)
/// {@macro flutter.material.materialRouteTransitionMixin}
///
/// 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 `fullscreenDialog` property specifies whether the incoming page is a
/// fullscreen modal dialog. On iOS, those pages animate from the bottom to the
/// The `fullscreenDialog` property specifies whether the incoming 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
......@@ -35,9 +29,10 @@ import 'theme.dart';
///
/// See also:
///
/// * [PageTransitionsTheme], which defines the default page transitions used
/// by [MaterialPageRoute.buildTransitions].
class MaterialPageRoute<T> extends PageRoute<T> {
/// * [MaterialRouteTransitionMixin], which provides the material transition
/// for this route.
/// * [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].
///
/// The values of [builder], [maintainState], and [fullScreenDialog] must not
......@@ -53,12 +48,37 @@ class MaterialPageRoute<T> extends PageRoute<T> {
assert(opaque),
super(settings: settings, fullscreenDialog: fullscreenDialog);
/// Builds the primary contents of the route.
@override
final WidgetBuilder builder;
@override
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
Duration get transitionDuration => const Duration(milliseconds: 300);
......@@ -71,8 +91,8 @@ class MaterialPageRoute<T> extends PageRoute<T> {
@override
bool canTransitionTo(TransitionRoute<dynamic> nextRoute) {
// Don't perform outgoing animation if the next route is a fullscreen dialog.
return (nextRoute is MaterialPageRoute && !nextRoute.fullscreenDialog)
|| (nextRoute is CupertinoPageRoute && !nextRoute.fullscreenDialog);
return (nextRoute is MaterialRouteTransitionMixin && !nextRoute.fullscreenDialog)
|| (nextRoute is CupertinoRouteTransitionMixin && !nextRoute.fullscreenDialog);
}
@override
......@@ -103,7 +123,79 @@ class MaterialPageRoute<T> extends PageRoute<T> {
final PageTransitionsTheme theme = Theme.of(context).pageTransitionsTheme;
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
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 {
Animation<double> secondaryAnimation,
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 {
) {
TargetPlatform platform = Theme.of(context).platform;
if (CupertinoPageRoute.isPopGestureInProgress(route))
if (CupertinoRouteTransitionMixin.isPopGestureInProgress(route))
platform = TargetPlatform.iOS;
final PageTransitionsBuilder matchingBuilder =
......
......@@ -3004,8 +3004,8 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
needsExplicitDecision = true;
assert(
newEntry.route.settings == nextPage,
'If a route is created from a page, its must have that page as its '
'settings.',
'The settings getter of a page-based Route must return a Page object. '
'Please set the settings to the Page in the Page.createRoute method.'
);
newHistory.add(newEntry);
} else {
......
......@@ -17,12 +17,14 @@ abstract class PageRoute<T> extends ModalRoute<T> {
this.fullscreenDialog = false,
}) : super(settings: settings);
/// {@template flutter.widgets.pageRoute.fullscreenDialog}
/// Whether this page route is a full-screen dialog.
///
/// In Material and Cupertino, being fullscreen has the effects of making
/// the app bars have a close button instead of a back button. On
/// iOS, dialogs transitions animate differently and are also not closeable
/// with the back swipe gesture.
/// {@endtemplate}
final bool fullscreenDialog;
@override
......@@ -46,6 +48,10 @@ Widget _defaultTransitionsBuilder(BuildContext context, Animation<double> animat
///
/// Callers must define the [pageBuilder] function which creates the route's
/// 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> {
/// Creates a route that delegates to builder callbacks.
///
......@@ -70,14 +76,18 @@ class PageRouteBuilder<T> extends PageRoute<T> {
assert(fullscreenDialog != null),
super(settings: settings, fullscreenDialog: fullscreenDialog);
/// {@template flutter.widgets.pageRouteBuilder.pageBuilder}
/// Used build the route's primary contents.
///
/// See [ModalRoute.buildPage] for complete definition of the parameters.
/// {@endtemplate}
final RoutePageBuilder pageBuilder;
/// {@template flutter.widgets.pageRouteBuilder.transitionsBuilder}
/// Used to build the route's transitions.
///
/// See [ModalRoute.buildTransitions] for complete definition of the parameters.
/// {@endtemplate}
final RouteTransitionsBuilder transitionsBuilder;
@override
......@@ -107,5 +117,108 @@ class PageRouteBuilder<T> extends PageRoute<T> {
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget 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> {
Future<T> get completed => _transitionCompleter.future;
final Completer<T> _transitionCompleter = Completer<T>();
/// {@template flutter.widgets.transitionRoute.transitionDuration}
/// The duration the transition going forwards.
///
/// See also:
///
/// * [reverseTransitionDuration], which controls the duration of the
/// transition when it is in reverse.
/// {@endtemplate}
Duration get transitionDuration;
/// The duration the transition going in reverse.
......@@ -107,10 +109,12 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
/// the forwards [transitionDuration].
Duration get reverseTransitionDuration => transitionDuration;
/// {@template flutter.widgets.transitionRoute.opaque}
/// Whether the route obscures previous routes when the transition is complete.
///
/// When an opaque route's entrance transition is complete, the routes behind
/// the opaque route will not be built to save resources.
/// {@endtemplate}
bool get opaque;
// 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
// 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.
///
/// 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
///
/// 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
/// effect.
///
......@@ -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.
/// * [ModalBarrier], the widget that implements this feature.
/// {@endtemplate}
bool get barrierDismissible;
/// 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
/// has no effect.
bool get semanticsDismissible => true;
/// {@template flutter.widgets.modalRoute.barrierColor}
/// The color to use for the modal barrier. If this is null, the barrier will
/// be transparent.
///
......@@ -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
/// 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
/// effect.
///
......@@ -1156,8 +1163,10 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// * [barrierDismissible], which controls the behavior of the barrier when
/// tapped.
/// * [ModalBarrier], the widget that implements this feature.
/// {@endtemplate}
Color get barrierColor;
/// {@template flutter.widgets.modalRoute.barrierLabel}
/// The semantic label used for a dismissible barrier.
///
/// 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
/// For example, when a dialog is on the screen, the page below the dialog is
/// 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
/// effect.
///
......@@ -1179,6 +1188,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// * [barrierDismissible], which controls the behavior of the barrier when
/// tapped.
/// * [ModalBarrier], the widget that implements this feature.
/// {@endtemplate}
String get barrierLabel;
/// 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
/// While the route is animating into position, the color is animated from
/// 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
/// effect.
///
......@@ -1207,6 +1217,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// * [AnimatedModalBarrier], the widget that implements this feature.
Curve get barrierCurve => Curves.ease;
/// {@template flutter.widgets.modalRoute.maintainState}
/// 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
......@@ -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
/// visible.
///
/// The value of this getter should not change during the lifetime of the
/// object. It is used by [createOverlayEntries], which is called by
/// [install] near the beginning of the route lifecycle.
/// If this getter would ever start returning a different value, the
/// [changedInternalState] should be invoked so that the change can take
/// effect.
/// {@endtemplate}
bool get maintainState;
......@@ -1397,6 +1409,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
super.changedInternalState();
setState(() { /* internal state already changed */ });
_modalBarrier.markNeedsBuild();
_modalScope.maintainState = maintainState;
}
@override
......@@ -1481,10 +1494,12 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
);
}
OverlayEntry _modalScope;
@override
Iterable<OverlayEntry> createOverlayEntries() sync* {
yield _modalBarrier = OverlayEntry(builder: _buildModalBarrier);
yield OverlayEntry(builder: _buildModalScope, maintainState: maintainState);
yield _modalScope = OverlayEntry(builder: _buildModalScope, maintainState: maintainState);
}
@override
......
......@@ -1374,6 +1374,104 @@ void main() {
));
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 {}
......@@ -1401,3 +1499,47 @@ class DialogObserver extends NavigatorObserver {
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() {
expect(titleInitialTopLeft.dy, equals(titleTransientTopLeft.dy));
expect(titleInitialTopLeft.dx, greaterThan(titleTransientTopLeft.dx));
}, 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() {
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', () {
testWidgets('secondary animation is kDismissed when next route finishes pop', (WidgetTester tester) async {
final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
......@@ -1539,3 +1675,47 @@ class WidgetWithLocalHistoryState extends State<WidgetWithLocalHistory> {
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