Unverified Commit 582f35df authored by Hans Muller's avatar Hans Muller Committed by GitHub

PageTransitionsTheme, new MountainView page transition (#21715)

MaterialPageRoute transitions are now defined by the Theme. Added (optional) support for Android P style page transitions.
parent eadd59a9
......@@ -69,6 +69,7 @@ export 'src/material/material_localizations.dart';
export 'src/material/mergeable_material.dart';
export 'src/material/outline_button.dart';
export 'src/material/page.dart';
export 'src/material/page_transitions_theme.dart';
export 'src/material/paginated_data_table.dart';
export 'src/material/popup_menu.dart';
export 'src/material/progress_indicator.dart';
......
......@@ -92,7 +92,6 @@ class CupertinoPageRoute<T> extends PageRoute<T> {
RouteSettings settings,
this.maintainState = true,
bool fullscreenDialog = false,
this.hostRoute,
}) : assert(builder != null),
assert(maintainState != null),
assert(fullscreenDialog != null),
......@@ -151,16 +150,6 @@ class CupertinoPageRoute<T> extends PageRoute<T> {
@override
final bool maintainState;
/// The route that owns this one.
///
/// The [MaterialPageRoute] creates a [CupertinoPageRoute] to handle iOS-style
/// navigation. When this happens, the [MaterialPageRoute] is the [hostRoute]
/// of this [CupertinoPageRoute].
///
/// The [hostRoute] is responsible for calling [dispose] on the route. When
/// there is a [hostRoute], the [CupertinoPageRoute] must not be [install]ed.
final PageRoute<T> hostRoute;
@override
Duration get transitionDuration => const Duration(milliseconds: 350);
......@@ -181,52 +170,43 @@ class CupertinoPageRoute<T> extends PageRoute<T> {
return nextRoute is CupertinoPageRoute && !nextRoute.fullscreenDialog;
}
@override
void install(OverlayEntry insertionPoint) {
assert(() {
if (hostRoute == null)
return true;
throw FlutterError(
'Cannot install a subsidiary route (one with a hostRoute).\n'
'This route ($this) cannot be installed, because it has a host route ($hostRoute).'
);
}());
super.install(insertionPoint);
}
@override
void dispose() {
_backGestureController?.dispose();
_backGestureController = null;
_popGestureInProgress.remove(this);
super.dispose();
}
_CupertinoBackGestureController<T> _backGestureController;
/// Whether a pop gesture is currently underway.
/// True if a Cupertino pop gesture is currently underway for [route].
///
/// See also:
///
/// This starts returning true when pop gesture is started by the user. It
/// returns false if that has not yet occurred or if the most recent such
/// gesture has completed.
/// * [popGestureEnabled], which returns true if a user-triggered pop gesture
/// would be allowed.
static bool isPopGestureInProgress(PageRoute<dynamic> route) => _popGestureInProgress.contains(route);
static final Set<PageRoute<dynamic>> _popGestureInProgress = Set<PageRoute<dynamic>>();
/// True if a Cupertino pop gesture is currently underway for this route.
///
/// See also:
///
/// * [popGestureEnabled], which returns whether a pop gesture is appropriate
/// in the first place.
bool get popGestureInProgress => _backGestureController != null;
/// * [isPopGestureInProgress], which returns true if a Cupertino pop gesture
/// is currently underway for specific route.
/// * [popGestureEnabled], which returns true if a user-triggered pop gesture
/// would be allowed.
bool get popGestureInProgress => isPopGestureInProgress(this);
/// Whether a pop gesture can be started by the user.
///
/// This returns true if the user can edge-swipe to a previous route,
/// otherwise false.
/// Returns true if the user can edge-swipe to a previous route.
///
/// This will return false once [popGestureInProgress] is true, but
/// [popGestureInProgress] can only become true if [popGestureEnabled] was
/// Returns false once [isPopGestureInProgress] is true, but
/// [isPopGestureInProgress] can only become true if [popGestureEnabled] was
/// true first.
///
/// This should only be used between frames, not during build.
bool get popGestureEnabled {
final PageRoute<T> route = hostRoute ?? this;
bool get popGestureEnabled => _isPopGestureEnabled(this);
static bool _isPopGestureEnabled<T>(PageRoute<T> route) {
// If there's nothing to go back to, then obviously we don't support
// the back gesture.
if (route.isFirst)
......@@ -240,54 +220,19 @@ class CupertinoPageRoute<T> extends PageRoute<T> {
if (route.hasScopedWillPopCallback)
return false;
// Fullscreen dialogs aren't dismissable by back swipe.
if (fullscreenDialog)
if (route.fullscreenDialog)
return false;
// If we're in an animation already, we cannot be manually swiped.
if (route.controller.status != AnimationStatus.completed)
return false;
// If we're in a gesture already, we cannot start another.
if (popGestureInProgress)
if (_popGestureInProgress.contains(route))
return false;
// Looks like a back gesture would be welcome!
return true;
}
/// Begin dismissing this route from a horizontal swipe, if appropriate.
///
/// Swiping will be disabled if the page is a fullscreen dialog or if
/// dismissals can be overridden because a [WillPopCallback] was
/// defined for the route.
///
/// When this method decides a pop gesture is appropriate, it returns a
/// [CupertinoBackGestureController].
///
/// See also:
///
/// * [hasScopedWillPopCallback], which is true if a `willPop` callback
/// is defined for this route.
/// * [popGestureEnabled], which returns whether a pop gesture is
/// appropriate.
/// * [Route.startPopGesture], which describes the contract that this method
/// must implement.
_CupertinoBackGestureController<T> _startPopGesture() {
assert(!popGestureInProgress);
assert(popGestureEnabled);
final PageRoute<T> route = hostRoute ?? this;
_backGestureController = _CupertinoBackGestureController<T>(
navigator: route.navigator,
controller: route.controller,
onEnded: _endPopGesture,
);
return _backGestureController;
}
void _endPopGesture() {
// In practice this only gets called if for some reason popping the route
// did not cause this route to get disposed.
_backGestureController?.dispose();
_backGestureController = null;
}
@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
final Widget result = Semantics(
......@@ -307,9 +252,49 @@ class CupertinoPageRoute<T> extends PageRoute<T> {
return result;
}
@override
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
if (fullscreenDialog) {
// Called by _CupertinoBackGestureDetector when a pop ("back") drag start
// gesture is detected. The returned controller handles all of the subsquent
// drag events.
static _CupertinoBackGestureController<T> _startPopGesture<T>(PageRoute<T> route) {
assert(!_popGestureInProgress.contains(route));
assert(_isPopGestureEnabled(route));
_popGestureInProgress.add(route);
_CupertinoBackGestureController<T> backController;
backController = _CupertinoBackGestureController<T>(
navigator: route.navigator,
controller: route.controller,
onEnded: () {
backController?.dispose();
backController = null;
_popGestureInProgress.remove(route);
},
);
return backController;
}
/// Returns a [CupertinoFullscreenDialogTransition] if [route] is a full
/// screen dialog, otherwise a [CupertinoPageTransition] is returned.
///
/// Used by [CupertinoPageRoute.buildTransitions].
///
/// This method can be applied to any [PageRoute], not just
/// [CupertinoPageRoute]. It's typically used to provide a Cupertino style
/// horizontal transition for material widgets when the target platform
/// is [TargetPlatform.iOS].
///
/// See also:
///
/// * [CupertinoPageTransitionsBuilder], which uses this method to define a
/// [PageTransitionsBuilder] for the [PageTransitionsTheme].
static Widget buildPageTransitions<T>(
PageRoute<T> route,
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
if (route.fullscreenDialog) {
return CupertinoFullscreenDialogTransition(
animation: animation,
child: child,
......@@ -320,16 +305,21 @@ class CupertinoPageRoute<T> extends PageRoute<T> {
secondaryRouteAnimation: secondaryAnimation,
// In the middle of a back gesture drag, let the transition be linear to
// match finger motions.
linearTransition: popGestureInProgress,
linearTransition: _popGestureInProgress.contains(route),
child: _CupertinoBackGestureDetector<T>(
enabledCallback: () => popGestureEnabled,
onStartPopGesture: _startPopGesture,
enabledCallback: () => _isPopGestureEnabled<T>(route),
onStartPopGesture: () => _startPopGesture<T>(route),
child: child,
),
);
}
}
@override
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
return buildPageTransitions<T>(this, context, animation, secondaryAnimation, child);
}
@override
String get debugLabel => '${super.debugLabel}(${settings.name})';
}
......@@ -385,11 +375,10 @@ class CupertinoPageTransition extends StatelessWidget {
Widget build(BuildContext context) {
assert(debugCheckHasDirectionality(context));
final TextDirection textDirection = Directionality.of(context);
// TODO(ianh): tell the transform to be un-transformed for hit testing
// but not while being controlled by a gesture.
return SlideTransition(
position: _secondaryPositionAnimation,
textDirection: textDirection,
transformHitTests: false,
child: SlideTransition(
position: _primaryPositionAnimation,
textDirection: textDirection,
......
......@@ -5,47 +5,9 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'page_transitions_theme.dart';
import 'theme.dart';
// Fractional offset from 1/4 screen below the top to fully on screen.
final Animatable<Offset> _kBottomUpTween = Tween<Offset>(
begin: const Offset(0.0, 0.25),
end: Offset.zero,
);
// Used for Android and Fuchsia.
class _MountainViewPageTransition extends StatelessWidget {
_MountainViewPageTransition({
Key key,
@required bool fade,
@required Animation<double> routeAnimation, // The route's linear 0.0 - 1.0 animation.
@required this.child,
}) : _positionAnimation = routeAnimation.drive(_kBottomUpTween.chain(_fastOutSlowInTween)),
_opacityAnimation = fade
? routeAnimation.drive(_easeInTween) // Eyeballed from other Material apps.
: const AlwaysStoppedAnimation<double>(1.0),
super(key: key);
static final Animatable<double> _fastOutSlowInTween = CurveTween(curve: Curves.fastOutSlowIn);
static final Animatable<double> _easeInTween = CurveTween(curve: Curves.easeIn);
final Animation<Offset> _positionAnimation;
final Animation<double> _opacityAnimation;
final Widget child;
@override
Widget build(BuildContext context) {
// TODO(ianh): tell the transform to be un-transformed for hit testing
return SlideTransition(
position: _positionAnimation,
child: FadeTransition(
opacity: _opacityAnimation,
child: child,
),
);
}
}
/// A modal route that replaces the entire screen with a platform-adaptive
/// transition.
///
......@@ -71,16 +33,22 @@ class _MountainViewPageTransition extends StatelessWidget {
///
/// See also:
///
/// * [CupertinoPageRoute], which this [PageRoute] delegates transition
/// animations to for iOS.
/// * [PageTransitionsTheme], which defines the default page transitions used
/// by [MaterialPageRoute.buildTransitions].
///
class MaterialPageRoute<T> extends PageRoute<T> {
/// Creates a page route for use in a material design app.
/// Construct a MaterialPageRoute whose contents are defined by [builder].
///
/// The values of [builder], [maintainState], and [fullScreenDialog] must not
/// be null.
MaterialPageRoute({
@required this.builder,
RouteSettings settings,
this.maintainState = true,
bool fullscreenDialog = false,
}) : assert(builder != null),
assert(maintainState != null),
assert(fullscreenDialog != null),
super(settings: settings, fullscreenDialog: fullscreenDialog) {
// ignore: prefer_asserts_in_initializer_lists , https://github.com/dart-lang/sdk/issues/31223
assert(opaque);
......@@ -92,26 +60,6 @@ class MaterialPageRoute<T> extends PageRoute<T> {
@override
final bool maintainState;
/// A delegate PageRoute to which iOS themed page operations are delegated to.
/// It's lazily created on first use.
CupertinoPageRoute<T> get _cupertinoPageRoute {
assert(_useCupertinoTransitions);
_internalCupertinoPageRoute ??= CupertinoPageRoute<T>(
builder: builder, // Not used.
fullscreenDialog: fullscreenDialog,
hostRoute: this,
);
return _internalCupertinoPageRoute;
}
CupertinoPageRoute<T> _internalCupertinoPageRoute;
/// Whether we should currently be using Cupertino transitions. This is true
/// if the theme says we're on iOS, or if we're in an active gesture.
bool get _useCupertinoTransitions {
return _internalCupertinoPageRoute?.popGestureInProgress == true
|| Theme.of(navigator.context).platform == TargetPlatform.iOS;
}
@override
Duration get transitionDuration => const Duration(milliseconds: 300);
......@@ -133,12 +81,6 @@ class MaterialPageRoute<T> extends PageRoute<T> {
|| (nextRoute is CupertinoPageRoute && !nextRoute.fullscreenDialog);
}
@override
void dispose() {
_internalCupertinoPageRoute?.dispose();
super.dispose();
}
@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
final Widget result = Semantics(
......@@ -160,15 +102,8 @@ class MaterialPageRoute<T> extends PageRoute<T> {
@override
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
if (_useCupertinoTransitions) {
return _cupertinoPageRoute.buildTransitions(context, animation, secondaryAnimation, child);
} else {
return _MountainViewPageTransition(
routeAnimation: animation,
child: child,
fade: true,
);
}
final PageTransitionsTheme theme = Theme.of(context).pageTransitionsTheme;
return theme.buildTransitions<T>(this, context, animation, secondaryAnimation, child);
}
@override
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'theme.dart';
// Slides the page upwards and fades it in, starting from 1/4 screen
// below the top.
class _FadeUpwardsPageTransition extends StatelessWidget {
_FadeUpwardsPageTransition({
Key key,
@required Animation<double> routeAnimation, // The route's linear 0.0 - 1.0 animation.
@required this.child,
}) : _positionAnimation = routeAnimation.drive(_bottomUpTween.chain(_fastOutSlowInTween)),
_opacityAnimation = routeAnimation.drive(_easeInTween),
super(key: key);
// Fractional offset from 1/4 screen below the top to fully on screen.
static final Tween<Offset> _bottomUpTween = Tween<Offset>(
begin: const Offset(0.0, 0.25),
end: Offset.zero,
);
static final Animatable<double> _fastOutSlowInTween = CurveTween(curve: Curves.fastOutSlowIn);
static final Animatable<double> _easeInTween = CurveTween(curve: Curves.easeIn);
final Animation<Offset> _positionAnimation;
final Animation<double> _opacityAnimation;
final Widget child;
@override
Widget build(BuildContext context) {
return SlideTransition(
position: _positionAnimation,
// TODO(ianh): tell the transform to be un-transformed for hit testing
child: FadeTransition(
opacity: _opacityAnimation,
child: child,
),
);
}
}
// This transition is intended to match the default for Android P.
class _OpenUpwardsPageTransition extends StatelessWidget {
const _OpenUpwardsPageTransition({
Key key,
this.animation,
this.secondaryAnimation,
this.child,
}) : super(key: key);
// The new page slides upwards just a little as its clip
// rectangle exposes the page from bottom to top.
static final Tween<Offset> _primaryTranslationTween = Tween<Offset>(
begin: const Offset(0.0, 0.05),
end: Offset.zero,
);
// The old page slides upwards a little as the new page appears.
static final Tween<Offset> _secondaryTranslationTween = Tween<Offset>(
begin: Offset.zero,
end: const Offset(0.0, -0.025),
);
// The scrim obscures the old page by becoming increasingly opaque.
static final Tween<double> _scrimOpacityTween = Tween<double>(
begin: 0.0,
end: 0.25,
);
// Used by all of the transition animations.
static const Curve _transitionCurve = Cubic(0.20, 0.00, 0.00, 1.00);
final Animation<double> animation;
final Animation<double> secondaryAnimation;
final Widget child;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final Size size = constraints.biggest;
final CurvedAnimation primaryAnimation = CurvedAnimation(
parent: animation,
curve: _transitionCurve,
reverseCurve: _transitionCurve.flipped,
);
// Gradually expose the new page from bottom to top.
final Animation<double> clipAnimation = Tween<double>(
begin: 0.0,
end: size.height,
).animate(primaryAnimation);
final Animation<double> opacityAnimation = _scrimOpacityTween.animate(primaryAnimation);
final Animation<Offset> primaryTranslationAnimation = _primaryTranslationTween.animate(primaryAnimation);
final Animation<Offset> secondaryTranslationAnimation = _secondaryTranslationTween.animate(
CurvedAnimation(
parent: secondaryAnimation,
curve: _transitionCurve,
reverseCurve: _transitionCurve.flipped,
),
);
return AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget child) {
return Container(
color: Colors.black.withOpacity(opacityAnimation.value),
alignment: Alignment.bottomLeft,
child: ClipRect(
child: SizedBox(
height: clipAnimation.value,
child: OverflowBox(
alignment: Alignment.bottomLeft,
maxHeight: size.height,
child: child,
),
),
),
);
},
child: AnimatedBuilder(
animation: secondaryAnimation,
child: FractionalTranslation(
translation: primaryTranslationAnimation.value,
child: child,
),
builder: (BuildContext context, Widget child) {
return FractionalTranslation(
translation: secondaryTranslationAnimation.value,
child: child,
);
},
),
);
},
);
}
}
/// Used by [PageTransitionsTheme] to define a [MaterialPageRoute] page
/// transition animation.
///
/// Apps can configure the map of builders for [ThemeData.platformTheme]
/// to customize the default [MaterialPageRoute] page transition animation
/// for different platforms.
///
/// See also:
///
/// * [FadeUpwardsPageTransitionsBuilder], which defines a default page transition.
/// * [OpenUpwardsPageTransitionsBuilder], which defines a page transition
/// that's similar to the one provided by Android P.
/// * [CupertinoPageTransitionsBuilder], which defines a horizontal page
/// transition that matches native iOS page transitions.
abstract class PageTransitionsBuilder {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const PageTransitionsBuilder();
/// Wraps the child with one or more transition widgets which define how [route]
/// arrives on and leaves the screen.
///
/// The [MaterialPageRoute.buildTransitions] method looks up the current
/// current [PageTransitionsTheme] with `Theme.of(context).pageTransitionsTheme`
/// and delegates to this method with a [PageTransitionsBuilder] based
/// on the theme's [ThemeData.platform].
Widget buildTransitions<T>(
PageRoute<T> route,
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
);
}
/// Used by [PageTransitionsTheme] to define a default [MaterialPageRoute] page
/// transition animation.
///
/// The default animation fades the new page in while translating it upwards,
/// starting from about 25% below the top of the screen.
///
/// See also:
///
/// * [OpenUpwardsPageTransitionsBuilder], which defines a page transition
/// that's similar to the one provided by Android P.
/// * [CupertinoPageTransitionsBuilder], which defines a horizontal page
/// transition that matches native iOS page transitions.
class FadeUpwardsPageTransitionsBuilder extends PageTransitionsBuilder {
/// Construct a [FadeUpwardsPageTransitionsBuilder].
const FadeUpwardsPageTransitionsBuilder();
@override
Widget buildTransitions<T>(
PageRoute<T> route,
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return _FadeUpwardsPageTransition(routeAnimation: animation, child: child);
}
}
/// Used by [PageTransitionsTheme] to define a vertical [MaterialPageRoute] page
/// transition animation that looks like the default page transition
/// used on Android P.
///
/// See also:
/// * [FadeUpwardsPageTransitionsBuilder], which defines a default page transition.
/// * [CupertinoPageTransitionsBuilder], which defines a horizontal page
/// transition that matches native iOS page transitions.
class OpenUpwardsPageTransitionsBuilder extends PageTransitionsBuilder {
/// Construct a [OpenUpwardsPageTransitionsBuilder].
const OpenUpwardsPageTransitionsBuilder();
@override
Widget buildTransitions<T>(
PageRoute<T> route,
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return _OpenUpwardsPageTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
}
}
/// Used by [PageTransitionsTheme] to define a horizontal [MaterialPageRoute]
/// page transition animation that matches native iOS page transitions.
///
/// See also:
///
/// * [FadeUpwardsPageTransitionsBuilder], which defines a default page transition.
/// * [OpenUpwardsPageTransitionsBuilder], which defines a page transition
/// that's similar to the one provided by Android P.
class CupertinoPageTransitionsBuilder extends PageTransitionsBuilder {
/// Construct a [CupertinoPageTransitionsBuilder].
const CupertinoPageTransitionsBuilder();
@override
Widget buildTransitions<T>(
PageRoute<T> route,
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return CupertinoPageRoute.buildPageTransitions<T>(route, context, animation, secondaryAnimation, child);
}
}
/// Defines the page transition animations used by [MaterialPageRoute]
/// for different [TargetPlatform]s.
///
/// The [MaterialPageRoute.buildTransitions] method looks up the current
/// current [PageTransitionsTheme] with `Theme.of(context).pageTransitionsTheme`
/// and delegates to [buildTransitions].
///
/// If a builder with a matching platform is not found, then the
/// [FadeUpwardsPageTransitionsBuilder] is used.
///
/// See also:
///
/// * [ThemeData.pageTransitionsTheme], which defines the default page
/// transitions for the overall theme.
/// * [FadeUpwardsPageTransitionsBuilder], which defines a default page transition.
/// * [OpenUpwardsPageTransitionsBuilder], which defines a page transition
/// that's similar to the one provided by Android P.
/// * [CupertinoPageTransitionsBuilder], which defines a horizontal page
/// transition that matches native iOS page transitions.
@immutable
class PageTransitionsTheme extends Diagnosticable {
/// Construct a PageTransitionsTheme.
///
/// By default the list of builders is: [FadeUpwardsPageTransitionsBuilder],
/// [CupertinoPageTransitionsBuilder] for [TargetPlatform.android]
/// and [TargetPlatform.iOS] respectively.
const PageTransitionsTheme({ Map<TargetPlatform, PageTransitionsBuilder> builders }) : _builders = builders;
static const Map<TargetPlatform, PageTransitionsBuilder> _defaultBuilders = <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
};
/// The [PageTransitionsBuilder]s supported by this theme.
Map<TargetPlatform, PageTransitionsBuilder> get builders => _builders ?? _defaultBuilders;
final Map<TargetPlatform, PageTransitionsBuilder> _builders;
/// Delegates to the builder for the current [ThemeData.platform]
/// or [FadeUpwardsPageTransitionsBuilder].
///
/// [MaterialPageRoute.buildTransitions] delegates to this method.
Widget buildTransitions<T>(
PageRoute<T> route,
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
TargetPlatform platform = Theme.of(context).platform;
if (CupertinoPageRoute.isPopGestureInProgress(route))
platform = TargetPlatform.iOS;
final PageTransitionsBuilder matchingBuilder =
builders[platform] ?? const FadeUpwardsPageTransitionsBuilder();
return matchingBuilder.buildTransitions<T>(route, context, animation, secondaryAnimation, child);
}
// Just used to the buidlers Map to a list with one PageTransitionsBuilder per platform
// for the operator == overload.
List<PageTransitionsBuilder> _all(Map<TargetPlatform, PageTransitionsBuilder> builders) {
return TargetPlatform.values.map((TargetPlatform platform) => builders[platform]).toList();
}
@override
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (other.runtimeType != runtimeType)
return false;
final PageTransitionsTheme typedOther = other;
if (identical(builders, other.builders))
return true;
return listEquals<PageTransitionsBuilder>(_all(builders), _all(typedOther.builders));
}
@override
int get hashCode => hashList(_all(builders));
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(
DiagnosticsProperty<Map<TargetPlatform, PageTransitionsBuilder>>(
'builders',
builders,
defaultValue: PageTransitionsTheme._defaultBuilders
),
);
}
}
......@@ -14,6 +14,7 @@ import 'colors.dart';
import 'ink_splash.dart';
import 'ink_well.dart' show InteractiveInkFeatureFactory;
import 'input_decorator.dart';
import 'page_transitions_theme.dart';
import 'slider_theme.dart';
import 'typography.dart';
......@@ -143,6 +144,7 @@ class ThemeData extends Diagnosticable {
ChipThemeData chipTheme,
TargetPlatform platform,
MaterialTapTargetSize materialTapTargetSize,
PageTransitionsTheme pageTransitionsTheme,
}) {
materialTapTargetSize ??= MaterialTapTargetSize.padded;
brightness ??= Brightness.light;
......@@ -182,6 +184,7 @@ class ThemeData extends Diagnosticable {
hintColor ??= isDark ? const Color(0x80FFFFFF) : const Color(0x8A000000);
errorColor ??= Colors.red[700];
inputDecorationTheme ??= const InputDecorationTheme();
pageTransitionsTheme ??= const PageTransitionsTheme();
primaryIconTheme ??= primaryIsDark ? const IconThemeData(color: Colors.white) : const IconThemeData(color: Colors.black);
accentIconTheme ??= accentIsDark ? const IconThemeData(color: Colors.white) : const IconThemeData(color: Colors.black);
iconTheme ??= isDark ? const IconThemeData(color: Colors.white) : const IconThemeData(color: Colors.black87);
......@@ -209,6 +212,7 @@ class ThemeData extends Diagnosticable {
brightness: brightness,
labelStyle: textTheme.body2,
);
return ThemeData.raw(
brightness: brightness,
primaryColor: primaryColor,
......@@ -251,6 +255,7 @@ class ThemeData extends Diagnosticable {
chipTheme: chipTheme,
platform: platform,
materialTapTargetSize: materialTapTargetSize,
pageTransitionsTheme: pageTransitionsTheme,
);
}
......@@ -261,6 +266,9 @@ class ThemeData extends Diagnosticable {
/// create intermediate themes based on two themes created with the
/// [new ThemeData] constructor.
const ThemeData.raw({
// Warning: make sure these properties are in the exact same order as in
// operator == and in the hashValues method and in the order of fields
// in this class, and in the lerp() method.
@required this.brightness,
@required this.primaryColor,
@required this.primaryColorBrightness,
......@@ -302,6 +310,7 @@ class ThemeData extends Diagnosticable {
@required this.chipTheme,
@required this.platform,
@required this.materialTapTargetSize,
@required this.pageTransitionsTheme,
}) : assert(brightness != null),
assert(primaryColor != null),
assert(primaryColorBrightness != null),
......@@ -341,7 +350,8 @@ class ThemeData extends Diagnosticable {
assert(sliderTheme != null),
assert(chipTheme != null),
assert(platform != null),
assert(materialTapTargetSize != null);
assert(materialTapTargetSize != null),
assert(pageTransitionsTheme != null);
/// A default light blue theme.
///
......@@ -543,6 +553,13 @@ class ThemeData extends Diagnosticable {
/// Configures the hit test size of certain Material widgets.
final MaterialTapTargetSize materialTapTargetSize;
/// Default [MaterialPageRoute] transitions per [TargetPlatform].
///
/// [MaterialPageRoute.buildTransitions] delegates to a [PageTransitionsBuilder]
/// whose [PageTransitionsBuilder.platform] matches [platform]. If a matching
/// builder is not found, a builder whose platform is null is used.
final PageTransitionsTheme pageTransitionsTheme;
/// Creates a copy of this theme but with the given fields replaced with the new values.
ThemeData copyWith({
Brightness brightness,
......@@ -586,6 +603,7 @@ class ThemeData extends Diagnosticable {
ChipThemeData chipTheme,
TargetPlatform platform,
MaterialTapTargetSize materialTapTargetSize,
PageTransitionsTheme pageTransitionsTheme,
}) {
return ThemeData.raw(
brightness: brightness ?? this.brightness,
......@@ -629,6 +647,7 @@ class ThemeData extends Diagnosticable {
chipTheme: chipTheme ?? this.chipTheme,
platform: platform ?? this.platform,
materialTapTargetSize: materialTapTargetSize ?? this.materialTapTargetSize,
pageTransitionsTheme: pageTransitionsTheme ?? this.pageTransitionsTheme,
);
}
......@@ -723,6 +742,8 @@ class ThemeData extends Diagnosticable {
primaryColorLight: Color.lerp(a.primaryColorLight, b.primaryColorLight, t),
primaryColorDark: Color.lerp(a.primaryColorDark, b.primaryColorDark, t),
canvasColor: Color.lerp(a.canvasColor, b.canvasColor, t),
accentColor: Color.lerp(a.accentColor, b.accentColor, t),
accentColorBrightness: t < 0.5 ? a.accentColorBrightness : b.accentColorBrightness,
scaffoldBackgroundColor: Color.lerp(a.scaffoldBackgroundColor, b.scaffoldBackgroundColor, t),
bottomAppBarColor: Color.lerp(a.bottomAppBarColor, b.bottomAppBarColor, t),
cardColor: Color.lerp(a.cardColor, b.cardColor, t),
......@@ -734,6 +755,7 @@ class ThemeData extends Diagnosticable {
unselectedWidgetColor: Color.lerp(a.unselectedWidgetColor, b.unselectedWidgetColor, t),
disabledColor: Color.lerp(a.disabledColor, b.disabledColor, t),
buttonColor: Color.lerp(a.buttonColor, b.buttonColor, t),
toggleableActiveColor: Color.lerp(a.toggleableActiveColor, b.toggleableActiveColor, t),
buttonTheme: t < 0.5 ? a.buttonTheme : b.buttonTheme,
secondaryHeaderColor: Color.lerp(a.secondaryHeaderColor, b.secondaryHeaderColor, t),
textSelectionColor: Color.lerp(a.textSelectionColor, b.textSelectionColor, t),
......@@ -741,12 +763,9 @@ class ThemeData extends Diagnosticable {
textSelectionHandleColor: Color.lerp(a.textSelectionHandleColor, b.textSelectionHandleColor, t),
backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t),
dialogBackgroundColor: Color.lerp(a.dialogBackgroundColor, b.dialogBackgroundColor, t),
accentColor: Color.lerp(a.accentColor, b.accentColor, t),
accentColorBrightness: t < 0.5 ? a.accentColorBrightness : b.accentColorBrightness,
indicatorColor: Color.lerp(a.indicatorColor, b.indicatorColor, t),
hintColor: Color.lerp(a.hintColor, b.hintColor, t),
errorColor: Color.lerp(a.errorColor, b.errorColor, t),
toggleableActiveColor: Color.lerp(a.toggleableActiveColor, b.toggleableActiveColor, t),
textTheme: TextTheme.lerp(a.textTheme, b.textTheme, t),
primaryTextTheme: TextTheme.lerp(a.primaryTextTheme, b.primaryTextTheme, t),
accentTextTheme: TextTheme.lerp(a.accentTextTheme, b.accentTextTheme, t),
......@@ -758,6 +777,7 @@ class ThemeData extends Diagnosticable {
chipTheme: ChipThemeData.lerp(a.chipTheme, b.chipTheme, t),
platform: t < 0.5 ? a.platform : b.platform,
materialTapTargetSize: t < 0.5 ? a.materialTapTargetSize : b.materialTapTargetSize,
pageTransitionsTheme: t < 0.5 ? a.pageTransitionsTheme : b.pageTransitionsTheme,
);
}
......@@ -766,9 +786,16 @@ class ThemeData extends Diagnosticable {
if (other.runtimeType != runtimeType)
return false;
final ThemeData otherData = other;
// Warning: make sure these properties are in the exact same order as in
// hashValues() and in the raw constructor and in the order of fields in
// the class and in the lerp() method.
return (otherData.brightness == brightness) &&
(otherData.primaryColor == primaryColor) &&
(otherData.primaryColorBrightness == primaryColorBrightness) &&
(otherData.primaryColorLight == primaryColorLight) &&
(otherData.primaryColorDark == primaryColorDark) &&
(otherData.accentColor == accentColor) &&
(otherData.accentColorBrightness == accentColorBrightness) &&
(otherData.canvasColor == canvasColor) &&
(otherData.scaffoldBackgroundColor == scaffoldBackgroundColor) &&
(otherData.bottomAppBarColor == bottomAppBarColor) &&
......@@ -789,8 +816,6 @@ class ThemeData extends Diagnosticable {
(otherData.textSelectionHandleColor == textSelectionHandleColor) &&
(otherData.backgroundColor == backgroundColor) &&
(otherData.dialogBackgroundColor == dialogBackgroundColor) &&
(otherData.accentColor == accentColor) &&
(otherData.accentColorBrightness == accentColorBrightness) &&
(otherData.indicatorColor == indicatorColor) &&
(otherData.hintColor == hintColor) &&
(otherData.errorColor == errorColor) &&
......@@ -804,15 +829,24 @@ class ThemeData extends Diagnosticable {
(otherData.sliderTheme == sliderTheme) &&
(otherData.chipTheme == chipTheme) &&
(otherData.platform == platform) &&
(otherData.materialTapTargetSize == materialTapTargetSize);
(otherData.materialTapTargetSize == materialTapTargetSize) &&
(otherData.pageTransitionsTheme == pageTransitionsTheme);
}
@override
int get hashCode {
// The hashValues() function supports up to 20 arguments.
return hashValues(
// Warning: make sure these properties are in the exact same order as in
// operator == and in the raw constructor and in the order of fields in
// the class and in the lerp() method.
brightness,
primaryColor,
primaryColorBrightness,
primaryColorLight,
primaryColorDark,
accentColor,
accentColorBrightness,
canvasColor,
scaffoldBackgroundColor,
bottomAppBarColor,
......@@ -823,33 +857,33 @@ class ThemeData extends Diagnosticable {
splashFactory,
selectedRowColor,
unselectedWidgetColor,
disabledColor,
buttonColor,
buttonTheme,
hashValues(
secondaryHeaderColor,
textSelectionColor,
cursorColor,
textSelectionHandleColor,
hashValues( // Too many values.
toggleableActiveColor,
backgroundColor,
accentColor,
accentColorBrightness,
indicatorColor,
dialogBackgroundColor,
indicatorColor,
hintColor,
errorColor,
toggleableActiveColor,
textTheme,
primaryTextTheme,
accentTextTheme,
iconTheme,
inputDecorationTheme,
iconTheme,
primaryIconTheme,
accentIconTheme,
sliderTheme,
chipTheme,
hashValues(
platform,
materialTapTargetSize,
cursorColor
pageTransitionsTheme,
),
),
);
}
......@@ -896,6 +930,7 @@ class ThemeData extends Diagnosticable {
properties.add(DiagnosticsProperty<SliderThemeData>('sliderTheme', sliderTheme));
properties.add(DiagnosticsProperty<ChipThemeData>('chipTheme', chipTheme));
properties.add(DiagnosticsProperty<MaterialTapTargetSize>('materialTapTargetSize', materialTapTargetSize));
properties.add(DiagnosticsProperty<PageTransitionsTheme>('pageTransitionsTheme', pageTransitionsTheme));
}
}
......
......@@ -917,7 +917,7 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
/// The parameters `viewport` and `offset` are required and cannot be null.
/// If `descendant` is null, this is a no-op and `rect` is returned.
///
/// If both `decedent` and `rect` are null, null is returned because there is
/// If both `descendant` and `rect` are null, null is returned because there is
/// nothing to be shown in the viewport.
///
/// The `duration` parameter can be set to a non-zero value to animate the
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Default PageTranstionsTheme platform', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(home: const Text('home')));
final PageTransitionsTheme theme = Theme.of(tester.element(find.text('home'))).pageTransitionsTheme;
expect(theme.builders, isNotNull);
expect(theme.builders[TargetPlatform.android], isNotNull);
expect(theme.builders[TargetPlatform.iOS], isNotNull);
});
testWidgets('Default PageTranstionsTheme builds a CupertionPageTransition for iOS', (WidgetTester tester) async {
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
'/': (BuildContext context) => Material(
child: FlatButton(
child: const Text('push'),
onPressed: () { Navigator.of(context).pushNamed('/b'); },
),
),
'/b': (BuildContext context) => const Text('page b'),
};
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(platform: TargetPlatform.iOS),
routes: routes,
),
);
expect(Theme.of(tester.element(find.text('push'))).platform, TargetPlatform.iOS);
expect(find.byType(CupertinoPageTransition), findsOneWidget);
await tester.tap(find.text('push'));
await tester.pumpAndSettle();
expect(find.text('page b'), findsOneWidget);
expect(find.byType(CupertinoPageTransition), findsOneWidget);
});
testWidgets('Default PageTranstionsTheme builds a _FadeUpwardsPageTransition for android', (WidgetTester tester) async {
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
'/': (BuildContext context) => Material(
child: FlatButton(
child: const Text('push'),
onPressed: () { Navigator.of(context).pushNamed('/b'); },
),
),
'/b': (BuildContext context) => const Text('page b'),
};
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(platform: TargetPlatform.android),
routes: routes,
),
);
Finder findFadeUpwardsPageTransition() {
return find.descendant(
of: find.byType(MaterialApp),
matching: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_FadeUpwardsPageTransition'),
);
}
expect(Theme.of(tester.element(find.text('push'))).platform, TargetPlatform.android);
expect(findFadeUpwardsPageTransition(), findsOneWidget);
await tester.tap(find.text('push'));
await tester.pumpAndSettle();
expect(find.text('page b'), findsOneWidget);
expect(findFadeUpwardsPageTransition(), findsOneWidget);
});
testWidgets('pageTranstionsTheme override builds a _OpenUpwardsPageTransition for android', (WidgetTester tester) async {
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
'/': (BuildContext context) => Material(
child: FlatButton(
child: const Text('push'),
onPressed: () { Navigator.of(context).pushNamed('/b'); },
),
),
'/b': (BuildContext context) => const Text('page b'),
};
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
platform: TargetPlatform.android,
pageTransitionsTheme: const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: OpenUpwardsPageTransitionsBuilder(), // creates a _OpenUpwardsPageTransition
},
),
),
routes: routes,
),
);
Finder findOpenUpwardsPageTransition() {
return find.descendant(
of: find.byType(MaterialApp),
matching: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_OpenUpwardsPageTransition'),
);
}
expect(Theme.of(tester.element(find.text('push'))).platform, TargetPlatform.android);
expect(findOpenUpwardsPageTransition(), findsOneWidget);
await tester.tap(find.text('push'));
await tester.pumpAndSettle();
expect(find.text('page b'), findsOneWidget);
expect(findOpenUpwardsPageTransition(), findsOneWidget);
});
}
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