Cupertino nav bar transitions between routes (#20322)
This commit is contained in:
parent
05b4bd748c
commit
f23c9ae5f8
@ -55,7 +55,11 @@ class _CupertinoRefreshControlDemoState extends State<CupertinoRefreshControlDem
|
||||
new CupertinoSliverRefreshControl(
|
||||
onRefresh: () {
|
||||
return new Future<void>.delayed(const Duration(seconds: 2))
|
||||
..then((_) => setState(() => repopulateList()));
|
||||
..then((_) {
|
||||
if (mounted) {
|
||||
setState(() => repopulateList());
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
new SliverSafeArea(
|
||||
|
@ -320,13 +320,10 @@ class _AlwaysCupertinoScrollBehavior extends ScrollBehavior {
|
||||
}
|
||||
|
||||
class _CupertinoAppState extends State<CupertinoApp> {
|
||||
HeroController _heroController;
|
||||
List<NavigatorObserver> _navigatorObservers;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_heroController = new HeroController(); // Linear tweening.
|
||||
_updateNavigator();
|
||||
}
|
||||
|
||||
@ -342,9 +339,6 @@ class _CupertinoAppState extends State<CupertinoApp> {
|
||||
widget.routes.isNotEmpty ||
|
||||
widget.onGenerateRoute != null ||
|
||||
widget.onUnknownRoute != null;
|
||||
_navigatorObservers =
|
||||
new List<NavigatorObserver>.from(widget.navigatorObservers)
|
||||
..add(_heroController);
|
||||
}
|
||||
|
||||
Widget defaultBuilder(BuildContext context, Widget child) {
|
||||
@ -361,7 +355,7 @@ class _CupertinoAppState extends State<CupertinoApp> {
|
||||
routes: widget.routes,
|
||||
onGenerateRoute: widget.onGenerateRoute,
|
||||
onUnknownRoute: widget.onUnknownRoute,
|
||||
navigatorObservers: _navigatorObservers,
|
||||
navigatorObservers: widget.navigatorObservers,
|
||||
);
|
||||
if (widget.builder != null) {
|
||||
return widget.builder(context, navigator);
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -37,7 +37,7 @@ import 'route.dart';
|
||||
/// * [CupertinoTabScaffold], a typical host that supports switching between tabs.
|
||||
/// * [CupertinoPageRoute], a typical modal page route pushed onto the
|
||||
/// [CupertinoTabView]'s [Navigator].
|
||||
class CupertinoTabView extends StatelessWidget {
|
||||
class CupertinoTabView extends StatefulWidget {
|
||||
/// Creates the content area for a tab in a [CupertinoTabScaffold].
|
||||
const CupertinoTabView({
|
||||
Key key,
|
||||
@ -101,12 +101,41 @@ class CupertinoTabView extends StatelessWidget {
|
||||
/// This list of observers is not shared with ancestor or descendant [Navigator]s.
|
||||
final List<NavigatorObserver> navigatorObservers;
|
||||
|
||||
@override
|
||||
_CupertinoTabViewState createState() {
|
||||
return new _CupertinoTabViewState();
|
||||
}
|
||||
}
|
||||
|
||||
class _CupertinoTabViewState extends State<CupertinoTabView> {
|
||||
HeroController _heroController;
|
||||
List<NavigatorObserver> _navigatorObservers;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_heroController = new HeroController(); // Linear tweening.
|
||||
_updateObservers();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CupertinoTabView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_updateObservers();
|
||||
}
|
||||
|
||||
void _updateObservers() {
|
||||
_navigatorObservers =
|
||||
new List<NavigatorObserver>.from(widget.navigatorObservers)
|
||||
..add(_heroController);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return new Navigator(
|
||||
onGenerateRoute: _onGenerateRoute,
|
||||
onUnknownRoute: _onUnknownRoute,
|
||||
observers: navigatorObservers,
|
||||
observers: _navigatorObservers,
|
||||
);
|
||||
}
|
||||
|
||||
@ -114,12 +143,12 @@ class CupertinoTabView extends StatelessWidget {
|
||||
final String name = settings.name;
|
||||
WidgetBuilder routeBuilder;
|
||||
String title;
|
||||
if (name == Navigator.defaultRouteName && builder != null) {
|
||||
routeBuilder = builder;
|
||||
title = defaultTitle;
|
||||
if (name == Navigator.defaultRouteName && widget.builder != null) {
|
||||
routeBuilder = widget.builder;
|
||||
title = widget.defaultTitle;
|
||||
}
|
||||
else if (routes != null)
|
||||
routeBuilder = routes[name];
|
||||
else if (widget.routes != null)
|
||||
routeBuilder = widget.routes[name];
|
||||
if (routeBuilder != null) {
|
||||
return new CupertinoPageRoute<dynamic>(
|
||||
builder: routeBuilder,
|
||||
@ -127,14 +156,14 @@ class CupertinoTabView extends StatelessWidget {
|
||||
settings: settings,
|
||||
);
|
||||
}
|
||||
if (onGenerateRoute != null)
|
||||
return onGenerateRoute(settings);
|
||||
if (widget.onGenerateRoute != null)
|
||||
return widget.onGenerateRoute(settings);
|
||||
return null;
|
||||
}
|
||||
|
||||
Route<dynamic> _onUnknownRoute(RouteSettings settings) {
|
||||
assert(() {
|
||||
if (onUnknownRoute == null) {
|
||||
if (widget.onUnknownRoute == null) {
|
||||
throw new FlutterError(
|
||||
'Could not find a generator for route $settings in the $runtimeType.\n'
|
||||
'Generators for routes are searched for in the following order:\n'
|
||||
@ -149,7 +178,7 @@ class CupertinoTabView extends StatelessWidget {
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
final Route<dynamic> result = onUnknownRoute(settings);
|
||||
final Route<dynamic> result = widget.onUnknownRoute(settings);
|
||||
assert(() {
|
||||
if (result == null) {
|
||||
throw new FlutterError(
|
||||
|
@ -20,11 +20,36 @@ import 'transitions.dart';
|
||||
/// [MaterialRectArcTween].
|
||||
typedef Tween<Rect> CreateRectTween(Rect begin, Rect end);
|
||||
|
||||
/// A function that lets [Hero]s self supply a [Widget] that is shown during the
|
||||
/// hero's flight from one route to another instead of default (which is to
|
||||
/// show the destination route's instance of the Hero).
|
||||
typedef Widget HeroFlightShuttleBuilder(
|
||||
BuildContext flightContext,
|
||||
Animation<double> animation,
|
||||
HeroFlightDirection flightDirection,
|
||||
BuildContext fromHeroContext,
|
||||
BuildContext toHeroContext,
|
||||
);
|
||||
|
||||
typedef void _OnFlightEnded(_HeroFlight flight);
|
||||
|
||||
enum _HeroFlightType {
|
||||
push, // Fly the "to" hero and animate with the "to" route.
|
||||
pop, // Fly the "to" hero and animate with the "from" route.
|
||||
/// Direction of the hero's flight based on the navigation operation.
|
||||
enum HeroFlightDirection {
|
||||
/// A flight triggered by a route push.
|
||||
///
|
||||
/// The animation goes from 0 to 1.
|
||||
///
|
||||
/// If no custom [HeroFlightShuttleBuilder] is supplied, the top route's
|
||||
/// [Hero] child is shown in flight.
|
||||
push,
|
||||
|
||||
/// A flight triggered by a route pop.
|
||||
///
|
||||
/// The animation goes from 1 to 0.
|
||||
///
|
||||
/// If no custom [HeroFlightShuttleBuilder] is supplied, the bottom route's
|
||||
/// [Hero] child is shown in flight.
|
||||
pop,
|
||||
}
|
||||
|
||||
// The bounding box for context in global coordinates.
|
||||
@ -42,8 +67,8 @@ Rect _globalBoundingBoxFor(BuildContext context) {
|
||||
/// be helpful for orienting the user for the feature to physically move from
|
||||
/// one page to the other during the routes' transition. Such an animation
|
||||
/// is called a *hero animation*. The hero widgets "fly" in the Navigator's
|
||||
/// overlay during the transition and while they're in-flight they're
|
||||
/// not shown in their original locations in the old and new routes.
|
||||
/// overlay during the transition and while they're in-flight they're, by
|
||||
/// default, not shown in their original locations in the old and new routes.
|
||||
///
|
||||
/// To label a widget as such a feature, wrap it in a [Hero] widget. When
|
||||
/// navigation happens, the [Hero] widgets on each route are identified
|
||||
@ -52,6 +77,9 @@ Rect _globalBoundingBoxFor(BuildContext context) {
|
||||
///
|
||||
/// If a [Hero] is already in flight when navigation occurs, its
|
||||
/// flight animation will be redirected to its new destination.
|
||||
|
||||
/// The widget shown in-flight during the transition is, by default, the
|
||||
/// destination route's [Hero]'s child.
|
||||
///
|
||||
/// Routes must not contain more than one [Hero] for each [tag].
|
||||
///
|
||||
@ -67,12 +95,24 @@ Rect _globalBoundingBoxFor(BuildContext context) {
|
||||
///
|
||||
/// To make the animations look good, it's critical that the widget tree for the
|
||||
/// hero in both locations be essentially identical. The widget of the *target*
|
||||
/// is used to do the transition: when going from route A to route B, route B's
|
||||
/// hero's widget is placed over route A's hero's widget, and route A's hero is
|
||||
/// hidden. Then the widget is animated to route B's hero's position, and then
|
||||
/// the widget is inserted into route B. When going back from B to A, route A's
|
||||
/// hero's widget is placed over where route B's hero's widget was, and then the
|
||||
/// animation goes the other way.
|
||||
/// is, by default, used to do the transition: when going from route A to route
|
||||
/// B, route B's hero's widget is placed over route A's hero's widget. If a
|
||||
/// [flightShuttleBuilder] is supplied, its output widget is shown during the
|
||||
/// flight transition instead.
|
||||
///
|
||||
/// By default, both route A and route B's heroes are hidden while the
|
||||
/// transitioning widget is animating in-flight above the 2 routes.
|
||||
/// [placeholderBuilder] can be used to show a custom widget in their place
|
||||
/// instead once the transition has taken flight.
|
||||
///
|
||||
/// During the transition, the transition widget is animated to route B's hero's
|
||||
/// position, and then the widget is inserted into route B. When going back from
|
||||
/// B to A, route A's hero's widget is, by default, placed over where route B's
|
||||
/// hero's widget was, and then the animation goes the other way.
|
||||
///
|
||||
/// ## Parts of a Hero Transition
|
||||
///
|
||||
/// 
|
||||
class Hero extends StatefulWidget {
|
||||
/// Create a hero.
|
||||
///
|
||||
@ -81,6 +121,8 @@ class Hero extends StatefulWidget {
|
||||
Key key,
|
||||
@required this.tag,
|
||||
this.createRectTween,
|
||||
this.flightShuttleBuilder,
|
||||
this.placeholderBuilder,
|
||||
@required this.child,
|
||||
}) : assert(tag != null),
|
||||
assert(child != null),
|
||||
@ -115,6 +157,25 @@ class Hero extends StatefulWidget {
|
||||
/// {@macro flutter.widgets.child}
|
||||
final Widget child;
|
||||
|
||||
/// Optional override to supply a widget that's shown during the hero's flight.
|
||||
///
|
||||
/// This in-flight widget can depend on the route transition's animation as
|
||||
/// well as the incoming and outgoing routes' [Hero] descendants' widgets and
|
||||
/// layout.
|
||||
///
|
||||
/// When both the source and destination [Hero]s provide a [flightShuttleBuilder],
|
||||
/// the destination's [flightShuttleBuilder] takes precedence.
|
||||
///
|
||||
/// If none is provided, the destination route's Hero child is shown in-flight
|
||||
/// by default.
|
||||
final HeroFlightShuttleBuilder flightShuttleBuilder;
|
||||
|
||||
/// Placeholder widget left in place as the Hero's child once the flight takes off.
|
||||
///
|
||||
/// By default, an empty SizedBox keeping the Hero child's original size is
|
||||
/// left in place once the Hero shuttle has taken flight.
|
||||
final TransitionBuilder placeholderBuilder;
|
||||
|
||||
// Returns a map of all of the heroes in context, indexed by hero tag.
|
||||
static Map<Object, _HeroState> _allHeroesFor(BuildContext context) {
|
||||
assert(context != null);
|
||||
@ -141,6 +202,10 @@ class Hero extends StatefulWidget {
|
||||
final _HeroState heroState = hero.state;
|
||||
result[tag] = heroState;
|
||||
}
|
||||
// Don't perform transitions across different Navigators.
|
||||
if (element.widget is Navigator) {
|
||||
return;
|
||||
}
|
||||
element.visitChildren(visitor);
|
||||
}
|
||||
context.visitChildElements(visitor);
|
||||
@ -181,10 +246,14 @@ class _HeroState extends State<Hero> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_placeholderSize != null) {
|
||||
return new SizedBox(
|
||||
width: _placeholderSize.width,
|
||||
height: _placeholderSize.height
|
||||
);
|
||||
if (widget.placeholderBuilder == null) {
|
||||
return new SizedBox(
|
||||
width: _placeholderSize.width,
|
||||
height: _placeholderSize.height
|
||||
);
|
||||
} else {
|
||||
return widget.placeholderBuilder(context, widget.child);
|
||||
}
|
||||
}
|
||||
return new KeyedSubtree(
|
||||
key: _key,
|
||||
@ -204,9 +273,10 @@ class _HeroFlightManifest {
|
||||
@required this.fromHero,
|
||||
@required this.toHero,
|
||||
@required this.createRectTween,
|
||||
@required this.shuttleBuilder,
|
||||
}) : assert(fromHero.widget.tag == toHero.widget.tag);
|
||||
|
||||
final _HeroFlightType type;
|
||||
final HeroFlightDirection type;
|
||||
final OverlayState overlay;
|
||||
final Rect navigatorRect;
|
||||
final PageRoute<dynamic> fromRoute;
|
||||
@ -214,19 +284,21 @@ class _HeroFlightManifest {
|
||||
final _HeroState fromHero;
|
||||
final _HeroState toHero;
|
||||
final CreateRectTween createRectTween;
|
||||
final HeroFlightShuttleBuilder shuttleBuilder;
|
||||
|
||||
Object get tag => fromHero.widget.tag;
|
||||
|
||||
Animation<double> get animation {
|
||||
return new CurvedAnimation(
|
||||
parent: (type == _HeroFlightType.push) ? toRoute.animation : fromRoute.animation,
|
||||
parent: (type == HeroFlightDirection.push) ? toRoute.animation : fromRoute.animation,
|
||||
curve: Curves.fastOutSlowIn,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '_HeroFlightManifest($type hero: $tag from: ${fromRoute.settings} to: ${toRoute.settings})';
|
||||
return '_HeroFlightManifest($type tag: $tag from route: ${fromRoute.settings} '
|
||||
'to route: ${toRoute.settings} with hero: $fromHero to $toHero)';
|
||||
}
|
||||
}
|
||||
|
||||
@ -238,7 +310,9 @@ class _HeroFlight {
|
||||
|
||||
final _OnFlightEnded onFlightEnded;
|
||||
|
||||
Tween<Rect> heroRect;
|
||||
Tween<Rect> heroRectTween;
|
||||
Widget shuttle;
|
||||
|
||||
Animation<double> _heroOpacity = kAlwaysCompleteAnimation;
|
||||
ProxyAnimation _proxyAnimation;
|
||||
_HeroFlightManifest manifest;
|
||||
@ -255,9 +329,18 @@ class _HeroFlight {
|
||||
// The OverlayEntry WidgetBuilder callback for the hero's overlay.
|
||||
Widget _buildOverlay(BuildContext context) {
|
||||
assert(manifest != null);
|
||||
shuttle ??= manifest.shuttleBuilder(
|
||||
context,
|
||||
manifest.animation,
|
||||
manifest.type,
|
||||
manifest.fromHero.context,
|
||||
manifest.toHero.context,
|
||||
);
|
||||
assert(shuttle != null);
|
||||
|
||||
return new AnimatedBuilder(
|
||||
animation: _proxyAnimation,
|
||||
child: manifest.toHero.widget,
|
||||
child: shuttle,
|
||||
builder: (BuildContext context, Widget child) {
|
||||
final RenderBox toHeroBox = manifest.toHero.context?.findRenderObject();
|
||||
if (_aborted || toHeroBox == null || !toHeroBox.attached) {
|
||||
@ -273,13 +356,13 @@ class _HeroFlight {
|
||||
// supposed to end up then recreate the heroRect tween.
|
||||
final RenderBox finalRouteBox = manifest.toRoute.subtreeContext?.findRenderObject();
|
||||
final Offset toHeroOrigin = toHeroBox.localToGlobal(Offset.zero, ancestor: finalRouteBox);
|
||||
if (toHeroOrigin != heroRect.end.topLeft) {
|
||||
final Rect heroRectEnd = toHeroOrigin & heroRect.end.size;
|
||||
heroRect = _doCreateRectTween(heroRect.begin, heroRectEnd);
|
||||
if (toHeroOrigin != heroRectTween.end.topLeft) {
|
||||
final Rect heroRectEnd = toHeroOrigin & heroRectTween.end.size;
|
||||
heroRectTween = _doCreateRectTween(heroRectTween.begin, heroRectEnd);
|
||||
}
|
||||
}
|
||||
|
||||
final Rect rect = heroRect.evaluate(_proxyAnimation);
|
||||
final Rect rect = heroRectTween.evaluate(_proxyAnimation);
|
||||
final Size size = manifest.navigatorRect.size;
|
||||
final RelativeRect offsets = new RelativeRect.fromSize(rect, size);
|
||||
|
||||
@ -291,7 +374,6 @@ class _HeroFlight {
|
||||
child: new IgnorePointer(
|
||||
child: new RepaintBoundary(
|
||||
child: new Opacity(
|
||||
key: manifest.toHero._key,
|
||||
opacity: _heroOpacity.value,
|
||||
child: child,
|
||||
),
|
||||
@ -322,12 +404,12 @@ class _HeroFlight {
|
||||
assert(() {
|
||||
final Animation<double> initial = initialManifest.animation;
|
||||
assert(initial != null);
|
||||
final _HeroFlightType type = initialManifest.type;
|
||||
final HeroFlightDirection type = initialManifest.type;
|
||||
assert(type != null);
|
||||
switch (type) {
|
||||
case _HeroFlightType.pop:
|
||||
case HeroFlightDirection.pop:
|
||||
return initial.value == 1.0 && initial.status == AnimationStatus.reverse;
|
||||
case _HeroFlightType.push:
|
||||
case HeroFlightDirection.push:
|
||||
return initial.value == 0.0 && initial.status == AnimationStatus.forward;
|
||||
}
|
||||
return null;
|
||||
@ -335,7 +417,7 @@ class _HeroFlight {
|
||||
|
||||
manifest = initialManifest;
|
||||
|
||||
if (manifest.type == _HeroFlightType.pop)
|
||||
if (manifest.type == HeroFlightDirection.pop)
|
||||
_proxyAnimation.parent = new ReverseAnimation(manifest.animation);
|
||||
else
|
||||
_proxyAnimation.parent = manifest.animation;
|
||||
@ -343,7 +425,7 @@ class _HeroFlight {
|
||||
manifest.fromHero.startFlight();
|
||||
manifest.toHero.startFlight();
|
||||
|
||||
heroRect = _doCreateRectTween(
|
||||
heroRectTween = _doCreateRectTween(
|
||||
_globalBoundingBoxFor(manifest.fromHero.context),
|
||||
_globalBoundingBoxFor(manifest.toHero.context),
|
||||
);
|
||||
@ -357,7 +439,7 @@ class _HeroFlight {
|
||||
void divert(_HeroFlightManifest newManifest) {
|
||||
assert(manifest.tag == newManifest.tag);
|
||||
|
||||
if (manifest.type == _HeroFlightType.push && newManifest.type == _HeroFlightType.pop) {
|
||||
if (manifest.type == HeroFlightDirection.push && newManifest.type == HeroFlightDirection.pop) {
|
||||
// A push flight was interrupted by a pop.
|
||||
assert(newManifest.animation.status == AnimationStatus.reverse);
|
||||
assert(manifest.fromHero == newManifest.toHero);
|
||||
@ -371,8 +453,8 @@ class _HeroFlight {
|
||||
// path for swapped begin and end parameters. We want the pop flight
|
||||
// path to be the same (in reverse) as the push flight path.
|
||||
_proxyAnimation.parent = new ReverseAnimation(newManifest.animation);
|
||||
heroRect = new ReverseTween<Rect>(heroRect);
|
||||
} else if (manifest.type == _HeroFlightType.pop && newManifest.type == _HeroFlightType.push) {
|
||||
heroRectTween = new ReverseTween<Rect>(heroRectTween);
|
||||
} else if (manifest.type == HeroFlightDirection.pop && newManifest.type == HeroFlightDirection.push) {
|
||||
// A pop flight was interrupted by a push.
|
||||
assert(newManifest.animation.status == AnimationStatus.forward);
|
||||
assert(manifest.toHero == newManifest.fromHero);
|
||||
@ -386,10 +468,10 @@ class _HeroFlight {
|
||||
if (manifest.fromHero != newManifest.toHero) {
|
||||
manifest.fromHero.endFlight();
|
||||
newManifest.toHero.startFlight();
|
||||
heroRect = _doCreateRectTween(heroRect.end, _globalBoundingBoxFor(newManifest.toHero.context));
|
||||
heroRectTween = _doCreateRectTween(heroRectTween.end, _globalBoundingBoxFor(newManifest.toHero.context));
|
||||
} else {
|
||||
// TODO(hansmuller): Use ReverseTween here per github.com/flutter/flutter/pull/12203.
|
||||
heroRect = _doCreateRectTween(heroRect.end, heroRect.begin);
|
||||
heroRectTween = _doCreateRectTween(heroRectTween.end, heroRectTween.begin);
|
||||
}
|
||||
} else {
|
||||
// A push or a pop flight is heading to a new route, i.e.
|
||||
@ -398,17 +480,24 @@ class _HeroFlight {
|
||||
assert(manifest.fromHero != newManifest.fromHero);
|
||||
assert(manifest.toHero != newManifest.toHero);
|
||||
|
||||
heroRect = _doCreateRectTween(heroRect.evaluate(_proxyAnimation), _globalBoundingBoxFor(newManifest.toHero.context));
|
||||
heroRectTween = _doCreateRectTween(heroRectTween.evaluate(_proxyAnimation), _globalBoundingBoxFor(newManifest.toHero.context));
|
||||
shuttle = null;
|
||||
|
||||
if (newManifest.type == _HeroFlightType.pop)
|
||||
if (newManifest.type == HeroFlightDirection.pop)
|
||||
_proxyAnimation.parent = new ReverseAnimation(newManifest.animation);
|
||||
else
|
||||
_proxyAnimation.parent = newManifest.animation;
|
||||
|
||||
manifest.fromHero.endFlight();
|
||||
manifest.toHero.endFlight();
|
||||
|
||||
// Let the heroes in each of the routes rebuild with their placeholders.
|
||||
newManifest.fromHero.startFlight();
|
||||
newManifest.toHero.startFlight();
|
||||
|
||||
// Let the transition overlay on top of the routes also rebuild since
|
||||
// we cleared the old shuttle.
|
||||
overlayEntry.markNeedsBuild();
|
||||
}
|
||||
|
||||
_aborted = false;
|
||||
@ -455,14 +544,14 @@ class HeroController extends NavigatorObserver {
|
||||
void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
|
||||
assert(navigator != null);
|
||||
assert(route != null);
|
||||
_maybeStartHeroTransition(previousRoute, route, _HeroFlightType.push);
|
||||
_maybeStartHeroTransition(previousRoute, route, HeroFlightDirection.push);
|
||||
}
|
||||
|
||||
@override
|
||||
void didPop(Route<dynamic> route, Route<dynamic> previousRoute) {
|
||||
assert(navigator != null);
|
||||
assert(route != null);
|
||||
_maybeStartHeroTransition(route, previousRoute, _HeroFlightType.pop);
|
||||
_maybeStartHeroTransition(route, previousRoute, HeroFlightDirection.pop);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -477,14 +566,14 @@ class HeroController extends NavigatorObserver {
|
||||
|
||||
// If we're transitioning between different page routes, start a hero transition
|
||||
// after the toRoute has been laid out with its animation's value at 1.0.
|
||||
void _maybeStartHeroTransition(Route<dynamic> fromRoute, Route<dynamic> toRoute, _HeroFlightType flightType) {
|
||||
void _maybeStartHeroTransition(Route<dynamic> fromRoute, Route<dynamic> toRoute, HeroFlightDirection flightType) {
|
||||
if (_questsEnabled && toRoute != fromRoute && toRoute is PageRoute<dynamic> && fromRoute is PageRoute<dynamic>) {
|
||||
final PageRoute<dynamic> from = fromRoute;
|
||||
final PageRoute<dynamic> to = toRoute;
|
||||
final Animation<double> animation = (flightType == _HeroFlightType.push) ? to.animation : from.animation;
|
||||
final Animation<double> animation = (flightType == HeroFlightDirection.push) ? to.animation : from.animation;
|
||||
|
||||
// A user gesture may have already completed the pop.
|
||||
if (flightType == _HeroFlightType.pop && animation.status == AnimationStatus.dismissed)
|
||||
if (flightType == HeroFlightDirection.pop && animation.status == AnimationStatus.dismissed)
|
||||
return;
|
||||
|
||||
// Putting a route offstage changes its animation value to 1.0. Once this
|
||||
@ -493,14 +582,19 @@ class HeroController extends NavigatorObserver {
|
||||
to.offstage = to.animation.value == 0.0;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((Duration value) {
|
||||
_startHeroTransition(from, to, flightType);
|
||||
_startHeroTransition(from, to, animation, flightType);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Find the matching pairs of heros in from and to and either start or a new
|
||||
// hero flight, or divert an existing one.
|
||||
void _startHeroTransition(PageRoute<dynamic> from, PageRoute<dynamic> to, _HeroFlightType flightType) {
|
||||
void _startHeroTransition(
|
||||
PageRoute<dynamic> from,
|
||||
PageRoute<dynamic> to,
|
||||
Animation<double> animation,
|
||||
HeroFlightDirection flightType,
|
||||
) {
|
||||
// If the navigator or one of the routes subtrees was removed before this
|
||||
// end-of-frame callback was called, then don't actually start a transition.
|
||||
if (navigator == null || from.subtreeContext == null || to.subtreeContext == null) {
|
||||
@ -520,6 +614,9 @@ class HeroController extends NavigatorObserver {
|
||||
|
||||
for (Object tag in fromHeroes.keys) {
|
||||
if (toHeroes[tag] != null) {
|
||||
final HeroFlightShuttleBuilder fromShuttleBuilder = fromHeroes[tag].widget.flightShuttleBuilder;
|
||||
final HeroFlightShuttleBuilder toShuttleBuilder = toHeroes[tag].widget.flightShuttleBuilder;
|
||||
|
||||
final _HeroFlightManifest manifest = new _HeroFlightManifest(
|
||||
type: flightType,
|
||||
overlay: navigator.overlay,
|
||||
@ -529,7 +626,10 @@ class HeroController extends NavigatorObserver {
|
||||
fromHero: fromHeroes[tag],
|
||||
toHero: toHeroes[tag],
|
||||
createRectTween: createRectTween,
|
||||
shuttleBuilder:
|
||||
toShuttleBuilder ?? fromShuttleBuilder ?? _defaultHeroFlightShuttleBuilder,
|
||||
);
|
||||
|
||||
if (_flights[tag] != null)
|
||||
_flights[tag].divert(manifest);
|
||||
else
|
||||
@ -543,4 +643,15 @@ class HeroController extends NavigatorObserver {
|
||||
void _handleFlightEnded(_HeroFlight flight) {
|
||||
_flights.remove(flight.manifest.tag);
|
||||
}
|
||||
|
||||
static final HeroFlightShuttleBuilder _defaultHeroFlightShuttleBuilder = (
|
||||
BuildContext flightContext,
|
||||
Animation<double> animation,
|
||||
HeroFlightDirection flightDirection,
|
||||
BuildContext fromHeroContext,
|
||||
BuildContext toHeroContext,
|
||||
) {
|
||||
final Hero toHero = toHeroContext.widget;
|
||||
return toHero.child;
|
||||
};
|
||||
}
|
||||
|
@ -127,6 +127,24 @@ class BorderRadiusTween extends Tween<BorderRadius> {
|
||||
BorderRadius lerp(double t) => BorderRadius.lerp(begin, end, t);
|
||||
}
|
||||
|
||||
/// An interpolation between two [Border]s.
|
||||
///
|
||||
/// This class specializes the interpolation of [Tween<Border>] to use
|
||||
/// [Border.lerp].
|
||||
///
|
||||
/// See [Tween] for a discussion on how to use interpolation objects.
|
||||
class BorderTween extends Tween<Border> {
|
||||
/// Creates a [Border] tween.
|
||||
///
|
||||
/// The [begin] and [end] properties may be null; the null value
|
||||
/// is treated as having no border.
|
||||
BorderTween({ Border begin, Border end }) : super(begin: begin, end: end);
|
||||
|
||||
/// Returns the value this variable has at the given animation clock value.
|
||||
@override
|
||||
Border lerp(double t) => Border.lerp(begin, end, t);
|
||||
}
|
||||
|
||||
/// An interpolation between two [Matrix4]s.
|
||||
///
|
||||
/// This class specializes the interpolation of [Tween<Matrix4>] to be
|
||||
|
@ -188,6 +188,7 @@ class _SliverPersistentHeaderElement extends RenderObjectElement {
|
||||
|
||||
@override
|
||||
void performRebuild() {
|
||||
super.performRebuild();
|
||||
renderObject.triggerRebuild();
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@ import 'package:vector_math/vector_math_64.dart' show Matrix4;
|
||||
import 'basic.dart';
|
||||
import 'container.dart';
|
||||
import 'framework.dart';
|
||||
import 'text.dart';
|
||||
|
||||
export 'package:flutter/rendering.dart' show RelativeRect;
|
||||
|
||||
@ -671,6 +672,64 @@ class AlignTransition extends AnimatedWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Animated version of a [DefaultTextStyle] that animates the different properties
|
||||
/// of its [TextStyle].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [DefaultTextStyle], which also defines a [TextStyle] for its descendants
|
||||
/// but is not animated.
|
||||
class DefaultTextStyleTransition extends AnimatedWidget {
|
||||
/// Creates an animated [DefaultTextStyle] whose [TextStyle] animation updates
|
||||
/// the widget.
|
||||
const DefaultTextStyleTransition({
|
||||
Key key,
|
||||
@required Animation<TextStyle> style,
|
||||
@required this.child,
|
||||
this.textAlign,
|
||||
this.softWrap = true,
|
||||
this.overflow = TextOverflow.clip,
|
||||
this.maxLines,
|
||||
}) : super(key: key, listenable: style);
|
||||
|
||||
/// The animation that controls the descendants' text style.
|
||||
Animation<TextStyle> get style => listenable;
|
||||
|
||||
/// How the text should be aligned horizontally.
|
||||
final TextAlign textAlign;
|
||||
|
||||
/// Whether the text should break at soft line breaks.
|
||||
///
|
||||
/// See [DefaultTextStyle.softWrap] for more details.
|
||||
final bool softWrap;
|
||||
|
||||
/// How visual overflow should be handled.
|
||||
///
|
||||
final TextOverflow overflow;
|
||||
|
||||
/// An optional maximum number of lines for the text to span, wrapping if necessary.
|
||||
///
|
||||
/// See [DefaultTextStyle.maxLines] for more details.
|
||||
final int maxLines;
|
||||
|
||||
/// The widget below this widget in the tree.
|
||||
///
|
||||
/// {@macro flutter.widgets.child}
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return new DefaultTextStyle(
|
||||
style: style.value,
|
||||
textAlign: textAlign,
|
||||
softWrap: softWrap,
|
||||
overflow: overflow,
|
||||
maxLines: maxLines,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A general-purpose widget for building animations.
|
||||
///
|
||||
/// AnimatedBuilder is useful for more complex widgets that wish to include
|
||||
|
@ -306,6 +306,65 @@ void main() {
|
||||
expect(tester.getSize(find.widgetWithText(OverflowBox, 'Title')).height, 0.0);
|
||||
});
|
||||
|
||||
testWidgets('User specified middle is always visible in sliver', (WidgetTester tester) async {
|
||||
final ScrollController scrollController = new ScrollController();
|
||||
final Key segmentedControlsKey = new UniqueKey();
|
||||
await tester.pumpWidget(
|
||||
new CupertinoApp(
|
||||
home: new CupertinoPageScaffold(
|
||||
child: new CustomScrollView(
|
||||
controller: scrollController,
|
||||
slivers: <Widget>[
|
||||
new CupertinoSliverNavigationBar(
|
||||
middle: new ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 200.0),
|
||||
child: new CupertinoSegmentedControl<int>(
|
||||
key: segmentedControlsKey,
|
||||
children: const <int, Widget>{
|
||||
0: Text('Option A'),
|
||||
1: Text('Option B'),
|
||||
},
|
||||
onValueChanged: (int selected) { },
|
||||
groupValue: 0,
|
||||
),
|
||||
),
|
||||
largeTitle: const Text('Title'),
|
||||
),
|
||||
new SliverToBoxAdapter(
|
||||
child: new Container(
|
||||
height: 1200.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(scrollController.offset, 0.0);
|
||||
expect(tester.getTopLeft(find.byType(NavigationToolbar)).dy, 0.0);
|
||||
expect(tester.getSize(find.byType(NavigationToolbar)).height, 44.0);
|
||||
|
||||
expect(find.text('Title'), findsOneWidget);
|
||||
expect(tester.getCenter(find.byKey(segmentedControlsKey)).dx, 400.0);
|
||||
|
||||
expect(tester.getTopLeft(find.widgetWithText(OverflowBox, 'Title')).dy, 44.0);
|
||||
expect(tester.getSize(find.widgetWithText(OverflowBox, 'Title')).height, 52.0);
|
||||
|
||||
scrollController.jumpTo(600.0);
|
||||
await tester.pump(); // Once to trigger the opacity animation.
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
|
||||
expect(tester.getCenter(find.byKey(segmentedControlsKey)).dx, 400.0);
|
||||
// The large title is invisible now.
|
||||
expect(
|
||||
tester.renderObject<RenderAnimatedOpacity>(
|
||||
find.widgetWithText(AnimatedOpacity, 'Title')
|
||||
).opacity.value,
|
||||
0.0,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Small title can be overridden', (WidgetTester tester) async {
|
||||
final ScrollController scrollController = new ScrollController();
|
||||
await tester.pumpWidget(
|
||||
@ -390,7 +449,7 @@ void main() {
|
||||
));
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
expect(find.byType(CupertinoButton), findsOneWidget);
|
||||
expect(find.text(new String.fromCharCode(CupertinoIcons.back.codePoint)), findsOneWidget);
|
||||
@ -405,23 +464,22 @@ void main() {
|
||||
));
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
expect(find.byType(CupertinoButton), findsNWidgets(2));
|
||||
expect(find.text('Close'), findsOneWidget);
|
||||
expect(find.widgetWithText(CupertinoButton, 'Close'), findsOneWidget);
|
||||
|
||||
// Test popping goes back correctly.
|
||||
await tester.tap(find.text('Close'));
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
expect(find.text('Page 2'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text(new String.fromCharCode(CupertinoIcons.back.codePoint)));
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
expect(find.text('Home page'), findsOneWidget);
|
||||
});
|
||||
@ -438,7 +496,7 @@ void main() {
|
||||
builder: (BuildContext context) {
|
||||
return const CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
previousPageTitle: '0123456789',
|
||||
previousPageTitle: '012345678901',
|
||||
),
|
||||
child: Placeholder(),
|
||||
);
|
||||
@ -449,14 +507,14 @@ void main() {
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
expect(find.widgetWithText(CupertinoButton, '0123456789'), findsOneWidget);
|
||||
expect(find.widgetWithText(CupertinoButton, '012345678901'), findsOneWidget);
|
||||
|
||||
tester.state<NavigatorState>(find.byType(Navigator)).push(
|
||||
new CupertinoPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
return const CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
previousPageTitle: '01234567890',
|
||||
previousPageTitle: '0123456789012',
|
||||
),
|
||||
child: Placeholder(),
|
||||
);
|
||||
|
811
packages/flutter/test/cupertino/nav_bar_transition_test.dart
Normal file
811
packages/flutter/test/cupertino/nav_bar_transition_test.dart
Normal file
@ -0,0 +1,811 @@
|
||||
// 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/rendering.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
Future<void> startTransitionBetween(
|
||||
WidgetTester tester, {
|
||||
Widget from,
|
||||
Widget to,
|
||||
String fromTitle,
|
||||
String toTitle,
|
||||
}) async {
|
||||
await tester.pumpWidget(
|
||||
new CupertinoApp(
|
||||
home: const Placeholder(),
|
||||
),
|
||||
);
|
||||
|
||||
tester
|
||||
.state<NavigatorState>(find.byType(Navigator))
|
||||
.push(new CupertinoPageRoute<void>(
|
||||
title: fromTitle,
|
||||
builder: (BuildContext context) => scaffoldForNavBar(from),
|
||||
));
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
tester
|
||||
.state<NavigatorState>(find.byType(Navigator))
|
||||
.push(new CupertinoPageRoute<void>(
|
||||
title: toTitle,
|
||||
builder: (BuildContext context) => scaffoldForNavBar(to),
|
||||
));
|
||||
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
CupertinoPageScaffold scaffoldForNavBar(Widget navBar) {
|
||||
if (navBar is CupertinoNavigationBar || navBar == null) {
|
||||
return new CupertinoPageScaffold(
|
||||
navigationBar: navBar ?? const CupertinoNavigationBar(),
|
||||
child: const Placeholder(),
|
||||
);
|
||||
} else if (navBar is CupertinoSliverNavigationBar) {
|
||||
return new CupertinoPageScaffold(
|
||||
child: new CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
navBar,
|
||||
// Add filler so it's scrollable.
|
||||
const SliverToBoxAdapter(
|
||||
child: Placeholder(fallbackHeight: 1000.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
assert(false, 'Unexpected nav bar type ${navBar.runtimeType}');
|
||||
return null;
|
||||
}
|
||||
|
||||
Finder flying(WidgetTester tester, Finder finder) {
|
||||
final RenderObjectWithChildMixin<RenderStack> theater =
|
||||
tester.renderObject(find.byType(Overlay));
|
||||
final RenderStack theaterStack = theater.child;
|
||||
final Finder lastOverlayFinder = find.byElementPredicate((Element element) {
|
||||
return element is RenderObjectElement &&
|
||||
element.renderObject == theaterStack.lastChild;
|
||||
});
|
||||
|
||||
assert(
|
||||
find
|
||||
.descendant(
|
||||
of: lastOverlayFinder,
|
||||
matching: find.byWidgetPredicate(
|
||||
(Widget widget) =>
|
||||
widget.runtimeType.toString() ==
|
||||
'_NavigationBarTransition',
|
||||
),
|
||||
)
|
||||
.evaluate()
|
||||
.length ==
|
||||
1,
|
||||
'The last overlay in the navigator was not a flying hero',);
|
||||
|
||||
return find.descendant(
|
||||
of: lastOverlayFinder,
|
||||
matching: finder,
|
||||
);
|
||||
}
|
||||
|
||||
void checkBackgroundBoxHeight(WidgetTester tester, double height) {
|
||||
final Widget transitionBackgroundBox =
|
||||
tester.widget<Stack>(flying(tester, find.byType(Stack))).children[0];
|
||||
expect(
|
||||
tester
|
||||
.widget<SizedBox>(
|
||||
find.descendant(
|
||||
of: find.byWidget(transitionBackgroundBox),
|
||||
matching: find.byType(SizedBox),
|
||||
),
|
||||
)
|
||||
.height,
|
||||
height,
|
||||
);
|
||||
}
|
||||
|
||||
void checkOpacity(WidgetTester tester, Finder finder, double opacity) {
|
||||
expect(
|
||||
tester
|
||||
.renderObject<RenderAnimatedOpacity>(find.ancestor(
|
||||
of: finder,
|
||||
matching: find.byType(FadeTransition),
|
||||
))
|
||||
.opacity
|
||||
.value,
|
||||
opacity,
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('Bottom middle moves between middle and back label',
|
||||
(WidgetTester tester) async {
|
||||
await startTransitionBetween(tester, fromTitle: 'Page 1');
|
||||
|
||||
// Be mid-transition.
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
// There's 2 of them. One from the top route's back label and one from the
|
||||
// bottom route's middle widget.
|
||||
expect(flying(tester, find.text('Page 1')), findsNWidgets(2));
|
||||
|
||||
// Since they have the same text, they should be more or less at the same
|
||||
// place.
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('Page 1')).first),
|
||||
const Offset(331.0724935531616, 13.5),
|
||||
);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('Page 1')).last),
|
||||
const Offset(331.0724935531616, 13.5),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Bottom middle and top back label transitions their font',
|
||||
(WidgetTester tester) async {
|
||||
await startTransitionBetween(tester, fromTitle: 'Page 1');
|
||||
|
||||
// Be mid-transition.
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
// The transition's stack is ordered. The bottom middle is inserted first.
|
||||
final RenderParagraph bottomMiddle =
|
||||
tester.renderObject(flying(tester, find.text('Page 1')).first);
|
||||
expect(bottomMiddle.text.style.color, const Color(0xFF00070F));
|
||||
expect(bottomMiddle.text.style.fontWeight, FontWeight.w600);
|
||||
expect(bottomMiddle.text.style.fontFamily, '.SF UI Text');
|
||||
expect(bottomMiddle.text.style.letterSpacing, -0.08952957153320312);
|
||||
|
||||
checkOpacity(
|
||||
tester, flying(tester, find.text('Page 1')).first, 0.8609542846679688);
|
||||
|
||||
// The top back label is styled exactly the same way. But the opacity tweens
|
||||
// are flipped.
|
||||
final RenderParagraph topBackLabel =
|
||||
tester.renderObject(flying(tester, find.text('Page 1')).last);
|
||||
expect(topBackLabel.text.style.color, const Color(0xFF00070F));
|
||||
expect(topBackLabel.text.style.fontWeight, FontWeight.w600);
|
||||
expect(topBackLabel.text.style.fontFamily, '.SF UI Text');
|
||||
expect(topBackLabel.text.style.letterSpacing, -0.08952957153320312);
|
||||
|
||||
checkOpacity(tester, flying(tester, find.text('Page 1')).last, 0.0);
|
||||
|
||||
// Move animation further a bit.
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
expect(bottomMiddle.text.style.color, const Color(0xFF0073F0));
|
||||
expect(bottomMiddle.text.style.fontWeight, FontWeight.w400);
|
||||
expect(bottomMiddle.text.style.fontFamily, '.SF UI Text');
|
||||
expect(bottomMiddle.text.style.letterSpacing, -0.231169798374176);
|
||||
|
||||
checkOpacity(tester, flying(tester, find.text('Page 1')).first, 0.0);
|
||||
|
||||
expect(topBackLabel.text.style.color, const Color(0xFF0073F0));
|
||||
expect(topBackLabel.text.style.fontWeight, FontWeight.w400);
|
||||
expect(topBackLabel.text.style.fontFamily, '.SF UI Text');
|
||||
expect(topBackLabel.text.style.letterSpacing, -0.231169798374176);
|
||||
|
||||
checkOpacity(
|
||||
tester, flying(tester, find.text('Page 1')).last, 0.8733493089675903);
|
||||
});
|
||||
|
||||
testWidgets('Fullscreen dialogs do not create heroes',
|
||||
(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
new CupertinoApp(
|
||||
home: const Placeholder(),
|
||||
),
|
||||
);
|
||||
|
||||
tester
|
||||
.state<NavigatorState>(find.byType(Navigator))
|
||||
.push(new CupertinoPageRoute<void>(
|
||||
title: 'Page 1',
|
||||
builder: (BuildContext context) => scaffoldForNavBar(null),
|
||||
));
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
tester
|
||||
.state<NavigatorState>(find.byType(Navigator))
|
||||
.push(new CupertinoPageRoute<void>(
|
||||
title: 'Page 2',
|
||||
fullscreenDialog: true,
|
||||
builder: (BuildContext context) => scaffoldForNavBar(null),
|
||||
));
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
// Only the first (non-fullscreen-dialog) page has a Hero.
|
||||
expect(find.byType(Hero), findsOneWidget);
|
||||
// No Hero transition happened.
|
||||
expect(() => flying(tester, find.text('Page 2')), throwsAssertionError);
|
||||
});
|
||||
|
||||
testWidgets('Turning off transition works', (WidgetTester tester) async {
|
||||
await startTransitionBetween(
|
||||
tester,
|
||||
from: const CupertinoNavigationBar(
|
||||
transitionBetweenRoutes: false,
|
||||
middle: Text('Page 1'),
|
||||
),
|
||||
toTitle: 'Page 2',
|
||||
);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
// Only the second page that doesn't have the transitionBetweenRoutes
|
||||
// override off has a Hero.
|
||||
expect(find.byType(Hero), findsOneWidget);
|
||||
expect(
|
||||
find.descendant(of: find.byType(Hero), matching: find.text('Page 2')),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
// No Hero transition happened.
|
||||
expect(() => flying(tester, find.text('Page 2')), throwsAssertionError);
|
||||
});
|
||||
|
||||
testWidgets('Popping mid-transition is symmetrical',
|
||||
(WidgetTester tester) async {
|
||||
await startTransitionBetween(tester, fromTitle: 'Page 1');
|
||||
|
||||
// Be mid-transition.
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
void checkColorAndPositionAt50ms() {
|
||||
// The transition's stack is ordered. The bottom middle is inserted first.
|
||||
final RenderParagraph bottomMiddle =
|
||||
tester.renderObject(flying(tester, find.text('Page 1')).first);
|
||||
expect(bottomMiddle.text.style.color, const Color(0xFF00070F));
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('Page 1')).first),
|
||||
const Offset(331.0724935531616, 13.5),
|
||||
);
|
||||
|
||||
// The top back label is styled exactly the same way. But the opacity tweens
|
||||
// are flipped.
|
||||
final RenderParagraph topBackLabel =
|
||||
tester.renderObject(flying(tester, find.text('Page 1')).last);
|
||||
expect(topBackLabel.text.style.color, const Color(0xFF00070F));
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('Page 1')).last),
|
||||
const Offset(331.0724935531616, 13.5),
|
||||
);
|
||||
}
|
||||
|
||||
checkColorAndPositionAt50ms();
|
||||
|
||||
// Advance more.
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
// Pop and reverse the same amount of time.
|
||||
tester.state<NavigatorState>(find.byType(Navigator)).pop();
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
// Check that everything's the same as on the way in.
|
||||
checkColorAndPositionAt50ms();
|
||||
});
|
||||
|
||||
testWidgets('There should be no global keys in the hero flight',
|
||||
(WidgetTester tester) async {
|
||||
await startTransitionBetween(tester, fromTitle: 'Page 1');
|
||||
|
||||
// Be mid-transition.
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
expect(
|
||||
flying(
|
||||
tester,
|
||||
find.byWidgetPredicate((Widget widget) => widget.key != null),
|
||||
),
|
||||
findsNothing,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Transition box grows to large title size',
|
||||
(WidgetTester tester) async {
|
||||
await startTransitionBetween(
|
||||
tester,
|
||||
fromTitle: 'Page 1',
|
||||
to: const CupertinoSliverNavigationBar(),
|
||||
toTitle: 'Page 2',
|
||||
);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
checkBackgroundBoxHeight(tester, 47.097110748291016);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
checkBackgroundBoxHeight(tester, 61.0267448425293);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
checkBackgroundBoxHeight(tester, 78.68475294113159);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
checkBackgroundBoxHeight(tester, 88.32722091674805);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
checkBackgroundBoxHeight(tester, 93.13018447160721);
|
||||
});
|
||||
|
||||
testWidgets('Large transition box shrinks to standard nav bar size',
|
||||
(WidgetTester tester) async {
|
||||
await startTransitionBetween(
|
||||
tester,
|
||||
from: const CupertinoSliverNavigationBar(),
|
||||
fromTitle: 'Page 1',
|
||||
toTitle: 'Page 2',
|
||||
);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
checkBackgroundBoxHeight(tester, 92.90288925170898);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
checkBackgroundBoxHeight(tester, 78.9732551574707);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
checkBackgroundBoxHeight(tester, 61.31524705886841);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
checkBackgroundBoxHeight(tester, 51.67277908325195);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
checkBackgroundBoxHeight(tester, 46.86981552839279);
|
||||
});
|
||||
|
||||
testWidgets('Hero flight removed at the end of page transition',
|
||||
(WidgetTester tester) async {
|
||||
await startTransitionBetween(tester, fromTitle: 'Page 1');
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
// There's 2 of them. One from the top route's back label and one from the
|
||||
// bottom route's middle widget.
|
||||
expect(flying(tester, find.text('Page 1')), findsNWidgets(2));
|
||||
|
||||
// End the transition.
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
expect(() => flying(tester, find.text('Page 1')), throwsAssertionError);
|
||||
});
|
||||
|
||||
testWidgets('Exact widget is reused to build inside the transition',
|
||||
(WidgetTester tester) async {
|
||||
const Widget userMiddle = Placeholder();
|
||||
await startTransitionBetween(
|
||||
tester,
|
||||
from: const CupertinoSliverNavigationBar(
|
||||
middle: userMiddle,
|
||||
),
|
||||
fromTitle: 'Page 1',
|
||||
toTitle: 'Page 2',
|
||||
);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
expect(flying(tester, find.byWidget(userMiddle)), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('First appearance of back chevron fades in from the right',
|
||||
(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
new CupertinoApp(
|
||||
home: scaffoldForNavBar(null),
|
||||
),
|
||||
);
|
||||
|
||||
tester
|
||||
.state<NavigatorState>(find.byType(Navigator))
|
||||
.push(new CupertinoPageRoute<void>(
|
||||
title: 'Page 1',
|
||||
builder: (BuildContext context) => scaffoldForNavBar(null),
|
||||
));
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
final Finder backChevron = flying(tester,
|
||||
find.text(new String.fromCharCode(CupertinoIcons.back.codePoint)));
|
||||
|
||||
expect(
|
||||
backChevron,
|
||||
// Only one exists from the top page. The bottom page has no back chevron.
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
// Come in from the right and fade in.
|
||||
checkOpacity(tester, backChevron, 0.0);
|
||||
expect(
|
||||
tester.getTopLeft(backChevron), const Offset(71.94993209838867, 5.0));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 150));
|
||||
checkOpacity(tester, backChevron, 0.32467134296894073);
|
||||
expect(
|
||||
tester.getTopLeft(backChevron), const Offset(18.033634185791016, 5.0));
|
||||
});
|
||||
|
||||
testWidgets('Back chevron fades out and in when both pages have it',
|
||||
(WidgetTester tester) async {
|
||||
await startTransitionBetween(tester, fromTitle: 'Page 1');
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
final Finder backChevrons = flying(tester,
|
||||
find.text(new String.fromCharCode(CupertinoIcons.back.codePoint)));
|
||||
|
||||
expect(
|
||||
backChevrons,
|
||||
findsNWidgets(2),
|
||||
);
|
||||
|
||||
checkOpacity(tester, backChevrons.first, 0.8393326997756958);
|
||||
checkOpacity(tester, backChevrons.last, 0.0);
|
||||
// Both overlap at the same place.
|
||||
expect(tester.getTopLeft(backChevrons.first), const Offset(8.0, 5.0));
|
||||
expect(tester.getTopLeft(backChevrons.last), const Offset(8.0, 5.0));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 150));
|
||||
checkOpacity(tester, backChevrons.first, 0.0);
|
||||
checkOpacity(tester, backChevrons.last, 0.6276369094848633);
|
||||
// Still in the same place.
|
||||
expect(tester.getTopLeft(backChevrons.first), const Offset(8.0, 5.0));
|
||||
expect(tester.getTopLeft(backChevrons.last), const Offset(8.0, 5.0));
|
||||
});
|
||||
|
||||
testWidgets('Bottom middle just fades if top page has a custom leading',
|
||||
(WidgetTester tester) async {
|
||||
await startTransitionBetween(
|
||||
tester,
|
||||
fromTitle: 'Page 1',
|
||||
to: const CupertinoSliverNavigationBar(
|
||||
leading: Text('custom'),
|
||||
),
|
||||
toTitle: 'Page 2',
|
||||
);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
// There's just 1 in flight because there's no back label on the top page.
|
||||
expect(flying(tester, find.text('Page 1')), findsOneWidget);
|
||||
|
||||
checkOpacity(
|
||||
tester, flying(tester, find.text('Page 1')), 0.8609542846679688);
|
||||
|
||||
// The middle widget doesn't move.
|
||||
expect(
|
||||
tester.getCenter(flying(tester, find.text('Page 1'))),
|
||||
const Offset(400.0, 22.0),
|
||||
);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 150));
|
||||
checkOpacity(tester, flying(tester, find.text('Page 1')), 0.0);
|
||||
expect(
|
||||
tester.getCenter(flying(tester, find.text('Page 1'))),
|
||||
const Offset(400.0, 22.0),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Bottom leading fades in place', (WidgetTester tester) async {
|
||||
await startTransitionBetween(
|
||||
tester,
|
||||
from: const CupertinoSliverNavigationBar(leading: Text('custom')),
|
||||
fromTitle: 'Page 1',
|
||||
toTitle: 'Page 2',
|
||||
);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
expect(flying(tester, find.text('custom')), findsOneWidget);
|
||||
|
||||
checkOpacity(
|
||||
tester, flying(tester, find.text('custom')), 0.7655444294214249);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('custom'))),
|
||||
const Offset(16.0, 0.0),
|
||||
);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 150));
|
||||
checkOpacity(tester, flying(tester, find.text('custom')), 0.0);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('custom'))),
|
||||
const Offset(16.0, 0.0),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Bottom trailing fades in place', (WidgetTester tester) async {
|
||||
await startTransitionBetween(
|
||||
tester,
|
||||
from: const CupertinoSliverNavigationBar(trailing: Text('custom')),
|
||||
fromTitle: 'Page 1',
|
||||
toTitle: 'Page 2',
|
||||
);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
expect(flying(tester, find.text('custom')), findsOneWidget);
|
||||
|
||||
checkOpacity(
|
||||
tester, flying(tester, find.text('custom')), 0.8393326997756958);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('custom'))),
|
||||
const Offset(683.0, 13.5),
|
||||
);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 150));
|
||||
checkOpacity(tester, flying(tester, find.text('custom')), 0.0);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('custom'))),
|
||||
const Offset(683.0, 13.5),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Bottom back label fades and slides to the left',
|
||||
(WidgetTester tester) async {
|
||||
await startTransitionBetween(
|
||||
tester,
|
||||
fromTitle: 'Page 1',
|
||||
toTitle: 'Page 2',
|
||||
);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
tester
|
||||
.state<NavigatorState>(find.byType(Navigator))
|
||||
.push(new CupertinoPageRoute<void>(
|
||||
title: 'Page 3',
|
||||
builder: (BuildContext context) => scaffoldForNavBar(null),
|
||||
));
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
// 'Page 1' appears once on Page 2 as the back label.
|
||||
expect(flying(tester, find.text('Page 1')), findsOneWidget);
|
||||
|
||||
// Back label fades out faster.
|
||||
checkOpacity(
|
||||
tester, flying(tester, find.text('Page 1')), 0.5584745407104492);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('Page 1'))),
|
||||
const Offset(24.176071166992188, 13.5),
|
||||
);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 150));
|
||||
checkOpacity(tester, flying(tester, find.text('Page 1')), 0.0);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('Page 1'))),
|
||||
const Offset(-292.97862243652344, 13.5),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Bottom large title moves to top back label',
|
||||
(WidgetTester tester) async {
|
||||
await startTransitionBetween(
|
||||
tester,
|
||||
from: const CupertinoSliverNavigationBar(),
|
||||
fromTitle: 'Page 1',
|
||||
toTitle: 'Page 2',
|
||||
);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
// There's 2, one from the bottom large title fading out and one from the
|
||||
// bottom back label fading in.
|
||||
expect(flying(tester, find.text('Page 1')), findsNWidgets(2));
|
||||
|
||||
checkOpacity(
|
||||
tester, flying(tester, find.text('Page 1')).first, 0.8393326997756958);
|
||||
checkOpacity(tester, flying(tester, find.text('Page 1')).last, 0.0);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('Page 1')).first),
|
||||
const Offset(17.905914306640625, 51.58156871795654),
|
||||
);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('Page 1')).last),
|
||||
const Offset(17.905914306640625, 51.58156871795654),
|
||||
);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 150));
|
||||
checkOpacity(tester, flying(tester, find.text('Page 1')).first, 0.0);
|
||||
checkOpacity(
|
||||
tester, flying(tester, find.text('Page 1')).last, 0.6276369094848633);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('Page 1')).first),
|
||||
const Offset(43.278289794921875, 19.23011875152588),
|
||||
);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('Page 1')).last),
|
||||
const Offset(43.278289794921875, 19.23011875152588),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Long title turns into the word back mid transition',
|
||||
(WidgetTester tester) async {
|
||||
await startTransitionBetween(
|
||||
tester,
|
||||
from: const CupertinoSliverNavigationBar(),
|
||||
fromTitle: 'A title too long to fit',
|
||||
toTitle: 'Page 2',
|
||||
);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
expect(
|
||||
flying(tester, find.text('A title too long to fit')), findsOneWidget);
|
||||
// Automatically changed to the word 'Back' in the back label.
|
||||
expect(flying(tester, find.text('Back')), findsOneWidget);
|
||||
|
||||
checkOpacity(tester, flying(tester, find.text('A title too long to fit')),
|
||||
0.8393326997756958);
|
||||
checkOpacity(tester, flying(tester, find.text('Back')), 0.0);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('A title too long to fit'))),
|
||||
const Offset(17.905914306640625, 51.58156871795654),
|
||||
);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('Back'))),
|
||||
const Offset(17.905914306640625, 51.58156871795654),
|
||||
);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 150));
|
||||
checkOpacity(
|
||||
tester, flying(tester, find.text('A title too long to fit')), 0.0);
|
||||
checkOpacity(tester, flying(tester, find.text('Back')), 0.6276369094848633);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('A title too long to fit'))),
|
||||
const Offset(43.278289794921875, 19.23011875152588),
|
||||
);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('Back'))),
|
||||
const Offset(43.278289794921875, 19.23011875152588),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Bottom large title and top back label transitions their font',
|
||||
(WidgetTester tester) async {
|
||||
await startTransitionBetween(
|
||||
tester,
|
||||
from: const CupertinoSliverNavigationBar(),
|
||||
fromTitle: 'Page 1',
|
||||
);
|
||||
|
||||
// Be mid-transition.
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
// The transition's stack is ordered. The bottom large title is inserted first.
|
||||
final RenderParagraph bottomLargeTitle =
|
||||
tester.renderObject(flying(tester, find.text('Page 1')).first);
|
||||
expect(bottomLargeTitle.text.style.color, const Color(0xFF00070F));
|
||||
expect(bottomLargeTitle.text.style.fontWeight, FontWeight.w700);
|
||||
expect(bottomLargeTitle.text.style.fontFamily, '.SF Pro Display');
|
||||
expect(bottomLargeTitle.text.style.letterSpacing, 0.21141128540039061);
|
||||
|
||||
// The top back label is styled exactly the same way.
|
||||
final RenderParagraph topBackLabel =
|
||||
tester.renderObject(flying(tester, find.text('Page 1')).last);
|
||||
expect(topBackLabel.text.style.color, const Color(0xFF00070F));
|
||||
expect(topBackLabel.text.style.fontWeight, FontWeight.w700);
|
||||
expect(topBackLabel.text.style.fontFamily, '.SF Pro Display');
|
||||
expect(topBackLabel.text.style.letterSpacing, 0.21141128540039061);
|
||||
|
||||
// Move animation further a bit.
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
expect(bottomLargeTitle.text.style.color, const Color(0xFF0073F0));
|
||||
expect(bottomLargeTitle.text.style.fontWeight, FontWeight.w400);
|
||||
expect(bottomLargeTitle.text.style.fontFamily, '.SF UI Text');
|
||||
expect(bottomLargeTitle.text.style.letterSpacing, -0.2135093951225281);
|
||||
|
||||
expect(topBackLabel.text.style.color, const Color(0xFF0073F0));
|
||||
expect(topBackLabel.text.style.fontWeight, FontWeight.w400);
|
||||
expect(topBackLabel.text.style.fontFamily, '.SF UI Text');
|
||||
expect(topBackLabel.text.style.letterSpacing, -0.2135093951225281);
|
||||
});
|
||||
|
||||
testWidgets('Top middle fades in and slides in from the right',
|
||||
(WidgetTester tester) async {
|
||||
await startTransitionBetween(
|
||||
tester,
|
||||
toTitle: 'Page 2',
|
||||
);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
expect(flying(tester, find.text('Page 2')), findsOneWidget);
|
||||
|
||||
checkOpacity(
|
||||
tester, flying(tester, find.text('Page 2')), 0.0);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('Page 2'))),
|
||||
const Offset(725.1760711669922, 13.5),
|
||||
);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 150));
|
||||
|
||||
checkOpacity(
|
||||
tester, flying(tester, find.text('Page 2')), 0.6972532719373703);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('Page 2'))),
|
||||
const Offset(408.02137756347656, 13.5),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Top large title fades in and slides in from the right',
|
||||
(WidgetTester tester) async {
|
||||
await startTransitionBetween(
|
||||
tester,
|
||||
to: const CupertinoSliverNavigationBar(),
|
||||
toTitle: 'Page 2',
|
||||
);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
expect(flying(tester, find.text('Page 2')), findsOneWidget);
|
||||
|
||||
checkOpacity(
|
||||
tester, flying(tester, find.text('Page 2')), 0.0);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('Page 2'))),
|
||||
const Offset(768.3521423339844, 54.0),
|
||||
);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 150));
|
||||
|
||||
checkOpacity(
|
||||
tester, flying(tester, find.text('Page 2')), 0.6753286570310593);
|
||||
expect(
|
||||
tester.getTopLeft(flying(tester, find.text('Page 2'))),
|
||||
const Offset(134.04275512695312, 54.0),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Components are not unnecessarily rebuilt during transitions',
|
||||
(WidgetTester tester) async {
|
||||
int bottomBuildTimes = 0;
|
||||
int topBuildTimes = 0;
|
||||
await startTransitionBetween(
|
||||
tester,
|
||||
from: new CupertinoNavigationBar(
|
||||
middle: new Builder(builder: (BuildContext context) {
|
||||
bottomBuildTimes++;
|
||||
return const Text('Page 1');
|
||||
}),
|
||||
),
|
||||
to: new CupertinoSliverNavigationBar(
|
||||
largeTitle: new Builder(builder: (BuildContext context) {
|
||||
topBuildTimes++;
|
||||
return const Text('Page 2');
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
expect(bottomBuildTimes, 1);
|
||||
// RenderSliverPersistentHeader.layoutChild causes 2 builds.
|
||||
expect(topBuildTimes, 2);
|
||||
|
||||
await tester.pump();
|
||||
|
||||
// The shuttle builder builds the component widgets one more time.
|
||||
expect(bottomBuildTimes, 2);
|
||||
expect(topBuildTimes, 3);
|
||||
|
||||
// Subsequent animation needs to use reprojection of children.
|
||||
await tester.pump();
|
||||
expect(bottomBuildTimes, 2);
|
||||
expect(topBuildTimes, 3);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
expect(bottomBuildTimes, 2);
|
||||
expect(topBuildTimes, 3);
|
||||
|
||||
// Finish animations.
|
||||
await tester.pump(const Duration(milliseconds: 400));
|
||||
|
||||
expect(bottomBuildTimes, 2);
|
||||
expect(topBuildTimes, 3);
|
||||
});
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
@ -36,6 +37,71 @@ void main() {
|
||||
expect(tester.getCenter(find.text('An iPod')).dx, 400.0);
|
||||
});
|
||||
|
||||
testWidgets('Large title auto-populates with title', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
new CupertinoApp(
|
||||
home: const Placeholder(),
|
||||
),
|
||||
);
|
||||
|
||||
tester.state<NavigatorState>(find.byType(Navigator)).push(
|
||||
new CupertinoPageRoute<void>(
|
||||
title: 'An iPod',
|
||||
builder: (BuildContext context) {
|
||||
return new CupertinoPageScaffold(
|
||||
child: new CustomScrollView(
|
||||
slivers: const <Widget>[
|
||||
CupertinoSliverNavigationBar(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
// There should be 2 Text widget with the title in the nav bar. One in the
|
||||
// large title position and one in the middle position (though the middle
|
||||
// position Text is initially invisible while the sliver is expanded).
|
||||
expect(
|
||||
find.widgetWithText(CupertinoSliverNavigationBar, 'An iPod'),
|
||||
findsNWidgets(2),
|
||||
);
|
||||
|
||||
final List<Element> titles = tester.elementList(find.text('An iPod'))
|
||||
.toList()
|
||||
..sort((Element a, Element b) {
|
||||
final RenderParagraph aParagraph = a.renderObject;
|
||||
final RenderParagraph bParagraph = b.renderObject;
|
||||
return aParagraph.text.style.fontSize.compareTo(
|
||||
bParagraph.text.style.fontSize
|
||||
);
|
||||
});
|
||||
|
||||
final Iterable<double> opacities = titles.map((Element element) {
|
||||
final RenderAnimatedOpacity renderOpacity =
|
||||
element.ancestorRenderObjectOfType(const TypeMatcher<RenderAnimatedOpacity>());
|
||||
return renderOpacity.opacity.value;
|
||||
});
|
||||
|
||||
expect(opacities, <double> [
|
||||
0.0, // Initially the smaller font title is invisible.
|
||||
1.0, // The larger font title is visible.
|
||||
]);
|
||||
|
||||
// Check that the large font title is at the right spot.
|
||||
expect(
|
||||
tester.getTopLeft(find.byWidget(titles[1].widget)),
|
||||
const Offset(16.0, 54.0),
|
||||
);
|
||||
|
||||
// The smaller, initially invisible title, should still be positioned in the
|
||||
// center.
|
||||
expect(tester.getCenter(find.byWidget(titles[0].widget)).dx, 400.0);
|
||||
});
|
||||
|
||||
testWidgets('Leading auto-populates with back button with previous title', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
new CupertinoApp(
|
||||
|
@ -239,7 +239,7 @@ void main() {
|
||||
// Navigate in tab 2.
|
||||
await tester.tap(find.text('Next'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
expect(find.text('Page 2 of tab 2'), isOnstage);
|
||||
expect(find.text('Page 1 of tab 1', skipOffstage: false), isOffstage);
|
||||
@ -254,7 +254,7 @@ void main() {
|
||||
// Navigate in tab 1.
|
||||
await tester.tap(find.text('Next'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
expect(find.text('Page 2 of tab 1'), isOnstage);
|
||||
expect(find.text('Page 2 of tab 2', skipOffstage: false), isOffstage);
|
||||
@ -268,7 +268,7 @@ void main() {
|
||||
// Pop in tab 2
|
||||
await tester.tap(find.text('Back'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
expect(find.text('Page 1 of tab 2'), isOnstage);
|
||||
expect(find.text('Page 2 of tab 1', skipOffstage: false), isOffstage);
|
||||
|
@ -1244,4 +1244,95 @@ void main() {
|
||||
await tester.pump(duration * 0.1);
|
||||
expect(tester.getTopLeft(find.byKey(firstKey)).dx, x0);
|
||||
});
|
||||
|
||||
testWidgets('Can override flight shuttle', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(new MaterialApp(
|
||||
home: new Material(
|
||||
child: new ListView(
|
||||
children: <Widget>[
|
||||
const Hero(tag: 'a', child: Text('foo')),
|
||||
new Builder(builder: (BuildContext context) {
|
||||
return new FlatButton(
|
||||
child: const Text('two'),
|
||||
onPressed: () => Navigator.push<void>(context, new MaterialPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
return new Material(
|
||||
child: new Hero(
|
||||
tag: 'a',
|
||||
child: const Text('bar'),
|
||||
flightShuttleBuilder: (
|
||||
BuildContext flightContext,
|
||||
Animation<double> animation,
|
||||
HeroFlightDirection flightDirection,
|
||||
BuildContext fromHeroContext,
|
||||
BuildContext toHeroContext,
|
||||
) {
|
||||
return const Text('baz');
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
)),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
await tester.tap(find.text('two'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 10));
|
||||
|
||||
expect(find.text('foo'), findsNothing);
|
||||
expect(find.text('bar'), findsNothing);
|
||||
expect(find.text('baz'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Can override flight launch pads', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(new MaterialApp(
|
||||
home: new Material(
|
||||
child: new ListView(
|
||||
children: <Widget>[
|
||||
new Hero(
|
||||
tag: 'a',
|
||||
child: const Text('Batman'),
|
||||
placeholderBuilder: (BuildContext context, Widget child) {
|
||||
return const Text('Venom');
|
||||
},
|
||||
),
|
||||
new Builder(builder: (BuildContext context) {
|
||||
return new FlatButton(
|
||||
child: const Text('two'),
|
||||
onPressed: () => Navigator.push<void>(context, new MaterialPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
return new Material(
|
||||
child: new Hero(
|
||||
tag: 'a',
|
||||
child: const Text('Wolverine'),
|
||||
placeholderBuilder: (BuildContext context, Widget child) {
|
||||
return const Text('Joker');
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
)),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
await tester.tap(find.text('two'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 10));
|
||||
|
||||
expect(find.text('Batman'), findsNothing);
|
||||
// This shows up once but in the Hero because by default, the destination
|
||||
// Hero child is the widget in flight.
|
||||
expect(find.text('Wolverine'), findsOneWidget);
|
||||
expect(find.text('Venom'), findsOneWidget);
|
||||
expect(find.text('Joker'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user