Unverified Commit f23c9ae5 authored by xster's avatar xster Committed by GitHub

Cupertino nav bar transitions between routes (#20322)

parent 05b4bd74
......@@ -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);
......
......@@ -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(
......
......@@ -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(),
);
......
This diff is collapsed.
......@@ -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);
});
}
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