Create a CupertinoTab to support parallel navigation trees in iOS (#12130)
* Refactor CupertinoScaffold * Rename rootTabPageBuilder to tabBuilder * fix tab transparency padding * add a CupertinoTab * Add default background color * Add a bunch of examples * A bunch of tests * Refactor CupertinoScaffold * Rename rootTabPageBuilder to tabBuilder * fix tab transparency padding * Add default background color * review notes * fix test * review * Rename CupertinoTab to CupertinoTabView * remove final ; in sample code for analyzer
This commit is contained in:
parent
6d47092772
commit
f9e8da6d32
@ -18,6 +18,7 @@ export 'src/cupertino/route.dart';
|
|||||||
export 'src/cupertino/slider.dart';
|
export 'src/cupertino/slider.dart';
|
||||||
export 'src/cupertino/switch.dart';
|
export 'src/cupertino/switch.dart';
|
||||||
export 'src/cupertino/tab_scaffold.dart';
|
export 'src/cupertino/tab_scaffold.dart';
|
||||||
|
export 'src/cupertino/tab_view.dart';
|
||||||
export 'src/cupertino/text_selection.dart';
|
export 'src/cupertino/text_selection.dart';
|
||||||
export 'src/cupertino/thumb_painter.dart';
|
export 'src/cupertino/thumb_painter.dart';
|
||||||
export 'widgets.dart';
|
export 'widgets.dart';
|
||||||
|
@ -29,8 +29,11 @@ const Color _kDefaultTabBarBorderColor = const Color(0x4C000000);
|
|||||||
///
|
///
|
||||||
/// If the given [backgroundColor]'s opacity is not 1.0 (which is the case by
|
/// If the given [backgroundColor]'s opacity is not 1.0 (which is the case by
|
||||||
/// default), it will produce a blurring effect to the content behind it.
|
/// default), it will produce a blurring effect to the content behind it.
|
||||||
//
|
///
|
||||||
// TODO(xster): document using with a CupertinoScaffold.
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [CupertinoTabScaffold] which hosts the [CupertinoTabBar] at the bottom.
|
||||||
|
/// * [BottomNavigationBarItem] typical item in a [CupertinoTabBar].
|
||||||
class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget {
|
class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
/// Creates a tab bar in the iOS style.
|
/// Creates a tab bar in the iOS style.
|
||||||
CupertinoTabBar({
|
CupertinoTabBar({
|
||||||
|
@ -50,8 +50,13 @@ const TextStyle _kLargeTitleTextStyle = const TextStyle(
|
|||||||
/// If the given [backgroundColor]'s opacity is not 1.0 (which is the case by
|
/// If the given [backgroundColor]'s opacity is not 1.0 (which is the case by
|
||||||
/// default), it will produce a blurring effect to the content behind it.
|
/// default), it will produce a blurring effect to the content behind it.
|
||||||
///
|
///
|
||||||
|
/// Enabling [largeTitle] will create a scrollable second row showing the title
|
||||||
|
/// in a larger font introduced in iOS 11. The [middle] widget must be a text
|
||||||
|
/// and the [CupertinoNavigationBar] must be placed in a sliver group in this case.
|
||||||
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
|
/// * [CupertinoPageScaffold] page layout helper typically hosting the [CupertinoNavigationBar].
|
||||||
/// * [CupertinoSliverNavigationBar] for a nav bar to be placed in a sliver and
|
/// * [CupertinoSliverNavigationBar] for a nav bar to be placed in a sliver and
|
||||||
/// that supports iOS 11 style large titles.
|
/// that supports iOS 11 style large titles.
|
||||||
//
|
//
|
||||||
|
@ -12,7 +12,11 @@ import 'nav_bar.dart';
|
|||||||
///
|
///
|
||||||
/// The scaffold lays out the navigation bar on top and the content between or
|
/// The scaffold lays out the navigation bar on top and the content between or
|
||||||
/// behind the navigation bar.
|
/// behind the navigation bar.
|
||||||
// TODO(xster): add an example.
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [CupertinoPageRoute] a modal page route that typically hosts a [CupertinoPageRoute]
|
||||||
|
/// with support for iOS style page transitions.
|
||||||
class CupertinoPageScaffold extends StatelessWidget {
|
class CupertinoPageScaffold extends StatelessWidget {
|
||||||
const CupertinoPageScaffold({
|
const CupertinoPageScaffold({
|
||||||
Key key,
|
Key key,
|
||||||
@ -37,20 +41,30 @@ class CupertinoPageScaffold extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final List<Widget> stacked = <Widget>[];
|
final List<Widget> stacked = <Widget>[];
|
||||||
|
Widget childWithMediaQuery = child;
|
||||||
|
|
||||||
double topPadding = 0.0;
|
double topPadding = 0.0;
|
||||||
if (navigationBar != null) {
|
if (navigationBar != null) {
|
||||||
topPadding += navigationBar.preferredSize.height;
|
topPadding += navigationBar.preferredSize.height;
|
||||||
// If the navigation bar has a preferred size, pad it and the OS status
|
// If the navigation bar has a preferred size, pad it and the OS status
|
||||||
// bar as well. Otherwise, let the content extend to the complete top
|
// bar as well. Otherwise, let the content extend to the complete top
|
||||||
// of the page.
|
// of the page.
|
||||||
if (topPadding > 0.0)
|
if (topPadding > 0.0) {
|
||||||
topPadding += MediaQuery.of(context).padding.top;
|
final EdgeInsets mediaQueryPadding = MediaQuery.of(context).padding;
|
||||||
|
topPadding += mediaQueryPadding.top;
|
||||||
|
childWithMediaQuery = new MediaQuery(
|
||||||
|
data: MediaQuery.of(context).copyWith(
|
||||||
|
padding: mediaQueryPadding.copyWith(top: 0.0),
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The main content being at the bottom is added to the stack first.
|
// The main content being at the bottom is added to the stack first.
|
||||||
stacked.add(new Padding(
|
stacked.add(new Padding(
|
||||||
padding: new EdgeInsets.only(top: topPadding),
|
padding: new EdgeInsets.only(top: topPadding),
|
||||||
child: child,
|
child: childWithMediaQuery,
|
||||||
));
|
));
|
||||||
|
|
||||||
if (navigationBar != null) {
|
if (navigationBar != null) {
|
||||||
|
@ -69,6 +69,8 @@ final DecorationTween _kGradientShadowTween = new DecorationTween(
|
|||||||
///
|
///
|
||||||
/// * [MaterialPageRoute] for an adaptive [PageRoute] that uses a platform
|
/// * [MaterialPageRoute] for an adaptive [PageRoute] that uses a platform
|
||||||
/// appropriate transition.
|
/// appropriate transition.
|
||||||
|
/// * [CupertinoPageScaffold] typical content of a [CupertinoPageRoute] implementing
|
||||||
|
/// iOS style layout with navigation bar on top.
|
||||||
class CupertinoPageRoute<T> extends PageRoute<T> {
|
class CupertinoPageRoute<T> extends PageRoute<T> {
|
||||||
/// Creates a page route for use in an iOS designed app.
|
/// Creates a page route for use in an iOS designed app.
|
||||||
///
|
///
|
||||||
|
@ -19,8 +19,67 @@ import 'bottom_tab_bar.dart';
|
|||||||
/// tab index. [tabBuilder] must be able to build the same number of
|
/// tab index. [tabBuilder] must be able to build the same number of
|
||||||
/// pages as the [tabBar.items.length]. Inactive tabs will be moved [Offstage]
|
/// pages as the [tabBar.items.length]. Inactive tabs will be moved [Offstage]
|
||||||
/// and its animations disabled.
|
/// and its animations disabled.
|
||||||
// TODO(xster): describe navigator handlings.
|
///
|
||||||
// TODO(xster): add an example.
|
/// Use [CupertinoTabView] as the content of each tab to support tabs with parallel
|
||||||
|
/// navigation state and history.
|
||||||
|
///
|
||||||
|
/// ## Sample code
|
||||||
|
///
|
||||||
|
/// A sample code implementing a typical iOS information architecture with tabs.
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// new CupertinoTabScaffold(
|
||||||
|
/// tabBar: new CupertinoTabBar(
|
||||||
|
/// items: <BottomNavigationBarItem> [
|
||||||
|
/// // ...
|
||||||
|
/// ],
|
||||||
|
/// ),
|
||||||
|
/// tabBuilder: (BuildContext context, int index) {
|
||||||
|
/// return new CupertinoTabView(
|
||||||
|
/// builder: (BuildContext context) {
|
||||||
|
/// return new CupertinoPageScaffold(
|
||||||
|
/// navigationBar: new CupertinoNavigationBar(
|
||||||
|
/// middle: new Text('Page 1 of tab $index'),
|
||||||
|
/// ),
|
||||||
|
/// child: new Center(
|
||||||
|
/// child: new CupertinoButton(
|
||||||
|
/// child: const Text('Next page'),
|
||||||
|
/// onPressed: () {
|
||||||
|
/// Navigator.of(context).push(
|
||||||
|
/// new CupertinoPageRoute<Null>(
|
||||||
|
/// builder: (BuildContext context) {
|
||||||
|
/// return new CupertinoPageScaffold(
|
||||||
|
/// navigationBar: new CupertinoNavigationBar(
|
||||||
|
/// middle: new Text('Page 2 of tab $index'),
|
||||||
|
/// ),
|
||||||
|
/// child: new Center(
|
||||||
|
/// child: new CupertinoButton(
|
||||||
|
/// child: const Text('Back'),
|
||||||
|
/// onPressed: () { Navigator.of(context).pop(); },
|
||||||
|
/// ),
|
||||||
|
/// ),
|
||||||
|
/// );
|
||||||
|
/// },
|
||||||
|
/// ),
|
||||||
|
/// );
|
||||||
|
/// },
|
||||||
|
/// ),
|
||||||
|
/// ),
|
||||||
|
/// );
|
||||||
|
/// },
|
||||||
|
/// );
|
||||||
|
/// },
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [CupertinoTabBar] bottom tab bars inserted in the scaffold.
|
||||||
|
/// * [CupertinoTabView] a typical root content of each tap that holds its own
|
||||||
|
/// [Navigator] stack.
|
||||||
|
/// * [CupertinoPageRoute] a route hosting modal pages with iOS style transitions.
|
||||||
|
/// * [CupertinoPageScaffold] typical contents of an iOS modal page implementing
|
||||||
|
/// layout with a navigation bar on top.
|
||||||
class CupertinoTabScaffold extends StatefulWidget {
|
class CupertinoTabScaffold extends StatefulWidget {
|
||||||
const CupertinoTabScaffold({
|
const CupertinoTabScaffold({
|
||||||
Key key,
|
Key key,
|
||||||
@ -48,6 +107,10 @@ class CupertinoTabScaffold extends StatefulWidget {
|
|||||||
|
|
||||||
/// An [IndexedWidgetBuilder] that's called when tabs become active.
|
/// An [IndexedWidgetBuilder] that's called when tabs become active.
|
||||||
///
|
///
|
||||||
|
/// The widgets built by [IndexedWidgetBuilder] is typically a [CupertinoTabView]
|
||||||
|
/// in order to achieve the parallel hierarchies information architecture seen
|
||||||
|
/// on iOS apps with tab bars.
|
||||||
|
///
|
||||||
/// When the tab becomes inactive, its content is still cached in the widget
|
/// When the tab becomes inactive, its content is still cached in the widget
|
||||||
/// tree [Offstage] and its animations disabled.
|
/// tree [Offstage] and its animations disabled.
|
||||||
///
|
///
|
||||||
@ -138,7 +201,6 @@ class _TabViewState extends State<_TabView> {
|
|||||||
children: new List<Widget>.generate(widget.tabNumber, (int index) {
|
children: new List<Widget>.generate(widget.tabNumber, (int index) {
|
||||||
final bool active = index == widget.currentTabIndex;
|
final bool active = index == widget.currentTabIndex;
|
||||||
|
|
||||||
// TODO(xster): lazily replace empty tabs with Navigators instead.
|
|
||||||
if (active || tabs[index] != null)
|
if (active || tabs[index] != null)
|
||||||
tabs[index] = widget.tabBuilder(context, index);
|
tabs[index] = widget.tabBuilder(context, index);
|
||||||
|
|
||||||
|
154
packages/flutter/lib/src/cupertino/tab_view.dart
Normal file
154
packages/flutter/lib/src/cupertino/tab_view.dart
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
// Copyright 2017 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/foundation.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import 'route.dart';
|
||||||
|
|
||||||
|
/// A single tab view with its own [Navigator] state and history.
|
||||||
|
///
|
||||||
|
/// A typical tab view used as the content of each tab in a [CupertinoTabScaffold]
|
||||||
|
/// where multiple tabs with parallel navigation states and history can
|
||||||
|
/// co-exist.
|
||||||
|
///
|
||||||
|
/// [CupertinoTabView] configures the top-level [Navigator] to search for routes
|
||||||
|
/// in the following order:
|
||||||
|
///
|
||||||
|
/// 1. For the `/` route, the [builder] property, if non-null, is used.
|
||||||
|
///
|
||||||
|
/// 2. Otherwise, the [routes] table is used, if it has an entry for the route,
|
||||||
|
/// including `/` if [builder] is not specified.
|
||||||
|
///
|
||||||
|
/// 3. Otherwise, [onGenerateRoute] is called, if provided. It should return a
|
||||||
|
/// non-null value for any _valid_ route not handled by [builder] and [routes].
|
||||||
|
///
|
||||||
|
/// 4. Finally if all else fails [onUnknownRoute] is called.
|
||||||
|
///
|
||||||
|
/// These navigation properties are not shared with any sibling [CupertinoTabView]
|
||||||
|
/// nor any ancestor or descendent [Navigator] instances.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [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 {
|
||||||
|
const CupertinoTabView({
|
||||||
|
Key key,
|
||||||
|
this.builder,
|
||||||
|
this.routes,
|
||||||
|
this.onGenerateRoute,
|
||||||
|
this.onUnknownRoute,
|
||||||
|
this.navigatorObservers: const <NavigatorObserver>[],
|
||||||
|
}) : assert(navigatorObservers != null),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
/// The widget builder for the default route of the tab view
|
||||||
|
/// ([Navigator.defaultRouteName], which is `/`).
|
||||||
|
///
|
||||||
|
/// If a [builder] is specified, then [routes] must not include an entry for `/`,
|
||||||
|
/// as [builder] takes its place.
|
||||||
|
final WidgetBuilder builder;
|
||||||
|
|
||||||
|
/// This tab view's routing table.
|
||||||
|
///
|
||||||
|
/// When a named route is pushed with [Navigator.pushNamed] inside this tab view,
|
||||||
|
/// the route name is looked up in this map. If the name is present,
|
||||||
|
/// the associated [WidgetBuilder] is used to construct a [CupertinoPageRoute]
|
||||||
|
/// that performs an appropriate transition to the new route.
|
||||||
|
///
|
||||||
|
/// If the tab view only has one page, then you can specify it using [builder] instead.
|
||||||
|
///
|
||||||
|
/// If [builder] is specified, then it implies an entry in this table for the
|
||||||
|
/// [Navigator.defaultRouteName] route (`/`), and it is an error to
|
||||||
|
/// redundantly provide such a route in the [routes] table.
|
||||||
|
///
|
||||||
|
/// If a route is requested that is not specified in this table (or by
|
||||||
|
/// [builder]), then the [onGenerateRoute] callback is called to build the page
|
||||||
|
/// instead.
|
||||||
|
///
|
||||||
|
/// This routing table is not shared with any routing tables of ancestor or
|
||||||
|
/// descendent [Navigator]s.
|
||||||
|
final Map<String, WidgetBuilder> routes;
|
||||||
|
|
||||||
|
/// The route generator callback used when the tab view is navigated to a named route.
|
||||||
|
///
|
||||||
|
/// This is used if [routes] does not contain the requested route.
|
||||||
|
final RouteFactory onGenerateRoute;
|
||||||
|
|
||||||
|
/// Called when [onGenerateRoute] also fails to generate a route.
|
||||||
|
///
|
||||||
|
/// This callback is typically used for error handling. For example, this
|
||||||
|
/// callback might always generate a "not found" page that describes the route
|
||||||
|
/// that wasn't found.
|
||||||
|
///
|
||||||
|
/// The default implementation pushes a route that displays an ugly error
|
||||||
|
/// message.
|
||||||
|
final RouteFactory onUnknownRoute;
|
||||||
|
|
||||||
|
/// The list of observers for the [Navigator] created in this tab view.
|
||||||
|
///
|
||||||
|
/// This list of observers is not shared with ancestor or descendent [Navigator]s.
|
||||||
|
final List<NavigatorObserver> navigatorObservers;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return new Navigator(
|
||||||
|
onGenerateRoute: _onGenerateRoute,
|
||||||
|
onUnknownRoute: _onUnknownRoute,
|
||||||
|
observers: navigatorObservers,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Route<dynamic> _onGenerateRoute(RouteSettings settings) {
|
||||||
|
final String name = settings.name;
|
||||||
|
WidgetBuilder routeBuilder;
|
||||||
|
if (name == Navigator.defaultRouteName && builder != null)
|
||||||
|
routeBuilder = builder;
|
||||||
|
else if (routes != null)
|
||||||
|
routeBuilder = routes[name];
|
||||||
|
if (routeBuilder != null) {
|
||||||
|
return new CupertinoPageRoute<dynamic>(
|
||||||
|
builder: routeBuilder,
|
||||||
|
settings: settings,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (onGenerateRoute != null)
|
||||||
|
return onGenerateRoute(settings);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Route<dynamic> _onUnknownRoute(RouteSettings settings) {
|
||||||
|
assert(() {
|
||||||
|
if (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'
|
||||||
|
' 1. For the "/" route, the "builder" property, if non-null, is used.\n'
|
||||||
|
' 2. Otherwise, the "routes" table is used, if it has an entry for '
|
||||||
|
'the route.\n'
|
||||||
|
' 3. Otherwise, onGenerateRoute is called. It should return a '
|
||||||
|
'non-null value for any valid route not handled by "builder" and "routes".\n'
|
||||||
|
' 4. Finally if all else fails onUnknownRoute is called.\n'
|
||||||
|
'Unfortunately, onUnknownRoute was not set.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
final Route<dynamic> result = onUnknownRoute(settings);
|
||||||
|
assert(() {
|
||||||
|
if (result == null) {
|
||||||
|
throw new FlutterError(
|
||||||
|
'The onUnknownRoute callback returned null.\n'
|
||||||
|
'When the $runtimeType requested the route $settings from its '
|
||||||
|
'onUnknownRoute callback, the callback returned null. Such callbacks '
|
||||||
|
'must never return null.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
@ -488,7 +488,6 @@ class _MaterialAppState extends State<MaterialApp> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Route<dynamic> _onUnknownRoute(RouteSettings settings) {
|
Route<dynamic> _onUnknownRoute(RouteSettings settings) {
|
||||||
assert(() {
|
assert(() {
|
||||||
if (widget.onUnknownRoute == null) {
|
if (widget.onUnknownRoute == null) {
|
||||||
|
@ -559,6 +559,22 @@ class EdgeInsets extends EdgeInsetsGeometry {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
EdgeInsets resolve(TextDirection direction) => this;
|
EdgeInsets resolve(TextDirection direction) => this;
|
||||||
|
|
||||||
|
/// Creates a copy of this EdgeInsets but with the given fields replaced
|
||||||
|
/// with the new values.
|
||||||
|
EdgeInsets copyWith({
|
||||||
|
double left,
|
||||||
|
double top,
|
||||||
|
double right,
|
||||||
|
double bottom,
|
||||||
|
}) {
|
||||||
|
return new EdgeInsets.only(
|
||||||
|
left: left ?? this.left,
|
||||||
|
top: top ?? this.top,
|
||||||
|
right: right ?? this.right,
|
||||||
|
bottom: bottom ?? this.bottom,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An immutable set of offsets in each of the four cardinal directions, but
|
/// An immutable set of offsets in each of the four cardinal directions, but
|
||||||
|
@ -77,4 +77,121 @@ void main() {
|
|||||||
|
|
||||||
expect(tester.getSize(find.byWidget(page1Center)).height, 600.0 - 44.0 - 50.0);
|
expect(tester.getSize(find.byWidget(page1Center)).height, 600.0 - 44.0 - 50.0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('iOS independent tab navigation', (WidgetTester tester) async {
|
||||||
|
// A full on iOS information architecture app with 2 tabs, and 2 pages
|
||||||
|
// in each with independent navigation states.
|
||||||
|
await tester.pumpWidget(
|
||||||
|
new WidgetsApp(
|
||||||
|
color: const Color(0xFFFFFFFF),
|
||||||
|
onGenerateRoute: (RouteSettings settings) {
|
||||||
|
return new CupertinoPageRoute<Null>(
|
||||||
|
settings: settings,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return new CupertinoTabScaffold(
|
||||||
|
tabBar: new CupertinoTabBar(
|
||||||
|
items: <BottomNavigationBarItem>[
|
||||||
|
const BottomNavigationBarItem(
|
||||||
|
icon: const ImageIcon(const TestImageProvider(24, 24)),
|
||||||
|
title: const Text('Tab 1'),
|
||||||
|
),
|
||||||
|
const BottomNavigationBarItem(
|
||||||
|
icon: const ImageIcon(const TestImageProvider(24, 24)),
|
||||||
|
title: const Text('Tab 2'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
tabBuilder: (BuildContext context, int index) {
|
||||||
|
// For 1-indexed readability.
|
||||||
|
++index;
|
||||||
|
return new CupertinoTabView(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return new CupertinoPageScaffold(
|
||||||
|
navigationBar: new CupertinoNavigationBar(
|
||||||
|
middle: new Text('Page 1 of tab $index'),
|
||||||
|
),
|
||||||
|
child: new Center(
|
||||||
|
child: new CupertinoButton(
|
||||||
|
child: const Text('Next'),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
new CupertinoPageRoute<Null>(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return new CupertinoPageScaffold(
|
||||||
|
navigationBar: new CupertinoNavigationBar(
|
||||||
|
middle: new Text('Page 2 of tab $index'),
|
||||||
|
),
|
||||||
|
child: new Center(
|
||||||
|
child: new CupertinoButton(
|
||||||
|
child: const Text('Back'),
|
||||||
|
onPressed: (){
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('Page 1 of tab 1'), findsOneWidget);
|
||||||
|
expect(find.text('Page 1 of tab 2'), findsNothing); // Lazy building so not built yet.
|
||||||
|
|
||||||
|
await tester.tap(find.text('Tab 2'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.text('Page 1 of tab 1'), findsNothing); // It's offstage now.
|
||||||
|
expect(find.text('Page 1 of tab 1', skipOffstage: false), findsOneWidget);
|
||||||
|
expect(find.text('Page 1 of tab 2'), findsOneWidget);
|
||||||
|
|
||||||
|
// Navigate in tab 2.
|
||||||
|
await tester.tap(find.text('Next'));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 300));
|
||||||
|
|
||||||
|
expect(find.text('Page 2 of tab 2'), isOnstage);
|
||||||
|
expect(find.text('Page 1 of tab 1', skipOffstage: false), isOffstage);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Tab 1'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Independent navigation stacks.
|
||||||
|
expect(find.text('Page 1 of tab 1'), isOnstage);
|
||||||
|
expect(find.text('Page 2 of tab 2', skipOffstage: false), isOffstage);
|
||||||
|
|
||||||
|
// Navigate in tab 1.
|
||||||
|
await tester.tap(find.text('Next'));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 300));
|
||||||
|
|
||||||
|
expect(find.text('Page 2 of tab 1'), isOnstage);
|
||||||
|
expect(find.text('Page 2 of tab 2', skipOffstage: false), isOffstage);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Tab 2'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.text('Page 2 of tab 2'), isOnstage);
|
||||||
|
expect(find.text('Page 2 of tab 1', skipOffstage: false), isOffstage);
|
||||||
|
|
||||||
|
// Pop in tab 2
|
||||||
|
await tester.tap(find.text('Back'));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 300));
|
||||||
|
|
||||||
|
expect(find.text('Page 1 of tab 2'), isOnstage);
|
||||||
|
expect(find.text('Page 2 of tab 1', skipOffstage: false), isOffstage);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
137
packages/flutter/test/cupertino/tab_test.dart
Normal file
137
packages/flutter/test/cupertino/tab_test.dart
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
// Copyright 2017 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_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('Use home', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
new WidgetsApp(
|
||||||
|
color: const Color(0xFFFFFFFF),
|
||||||
|
onGenerateRoute: (RouteSettings settings) {
|
||||||
|
return new CupertinoPageRoute<Null>(
|
||||||
|
settings: settings,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return new CupertinoTabView(
|
||||||
|
builder: (BuildContext context) => const Text('home'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('home'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Use routes', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
new WidgetsApp(
|
||||||
|
color: const Color(0xFFFFFFFF),
|
||||||
|
onGenerateRoute: (RouteSettings settings) {
|
||||||
|
return new CupertinoPageRoute<Null>(
|
||||||
|
settings: settings,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return new CupertinoTabView(
|
||||||
|
routes: <String, WidgetBuilder>{
|
||||||
|
'/': (BuildContext context) => const Text('first route'),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('first route'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Use home and named routes', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
new WidgetsApp(
|
||||||
|
color: const Color(0xFFFFFFFF),
|
||||||
|
onGenerateRoute: (RouteSettings settings) {
|
||||||
|
return new CupertinoPageRoute<Null>(
|
||||||
|
settings: settings,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return new CupertinoTabView(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return new CupertinoButton(
|
||||||
|
child: const Text('go to second page'),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pushNamed('/2');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
routes: <String, WidgetBuilder>{
|
||||||
|
'/2': (BuildContext context) => const Text('second named route'),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('go to second page'), findsOneWidget);
|
||||||
|
await tester.tap(find.text('go to second page'));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 300));
|
||||||
|
expect(find.text('second named route'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Use onGenerateRoute', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
new WidgetsApp(
|
||||||
|
color: const Color(0xFFFFFFFF),
|
||||||
|
onGenerateRoute: (RouteSettings settings) {
|
||||||
|
return new CupertinoPageRoute<Null>(
|
||||||
|
settings: settings,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return new CupertinoTabView(
|
||||||
|
onGenerateRoute: (RouteSettings settings) {
|
||||||
|
if (settings.name == Navigator.defaultRouteName) {
|
||||||
|
return new CupertinoPageRoute<Null>(
|
||||||
|
settings: settings,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return const Text('generated home');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('generated home'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Use onUnknownRoute', (WidgetTester tester) async {
|
||||||
|
String unknownForRouteCalled;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
new WidgetsApp(
|
||||||
|
color: const Color(0xFFFFFFFF),
|
||||||
|
onGenerateRoute: (RouteSettings settings) {
|
||||||
|
return new CupertinoPageRoute<Null>(
|
||||||
|
settings: settings,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return new CupertinoTabView(
|
||||||
|
onUnknownRoute: (RouteSettings settings) {
|
||||||
|
unknownForRouteCalled = settings.name;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(tester.takeException(), isFlutterError);
|
||||||
|
expect(unknownForRouteCalled, '/');
|
||||||
|
});
|
||||||
|
}
|
@ -94,6 +94,12 @@ void main() {
|
|||||||
expect(const EdgeInsetsDirectional.only(top: 1.0).add(const EdgeInsets.only(top: 2.0)), const EdgeInsets.only(top: 3.0));
|
expect(const EdgeInsetsDirectional.only(top: 1.0).add(const EdgeInsets.only(top: 2.0)), const EdgeInsets.only(top: 3.0));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('EdgeInsets copyWith', () {
|
||||||
|
final EdgeInsets sourceEdgeInsets = const EdgeInsets.only(left: 1.0, top: 2.0, bottom: 3.0, right: 4.0);
|
||||||
|
final EdgeInsets copy = sourceEdgeInsets.copyWith(left: 5.0, top: 6.0);
|
||||||
|
expect(copy, const EdgeInsets.only(left: 5.0, top: 6.0, bottom: 3.0, right: 4.0));
|
||||||
|
});
|
||||||
|
|
||||||
test('EdgeInsetsGeometry.lerp(...)', () {
|
test('EdgeInsetsGeometry.lerp(...)', () {
|
||||||
expect(EdgeInsetsGeometry.lerp(const EdgeInsetsDirectional.only(end: 10.0), null, 0.5), const EdgeInsetsDirectional.only(end: 5.0));
|
expect(EdgeInsetsGeometry.lerp(const EdgeInsetsDirectional.only(end: 10.0), null, 0.5), const EdgeInsetsDirectional.only(end: 5.0));
|
||||||
expect(EdgeInsetsGeometry.lerp(const EdgeInsetsDirectional.only(start: 10.0), null, 0.5), const EdgeInsetsDirectional.only(start: 5.0));
|
expect(EdgeInsetsGeometry.lerp(const EdgeInsetsDirectional.only(start: 10.0), null, 0.5), const EdgeInsetsDirectional.only(start: 5.0));
|
||||||
|
Loading…
x
Reference in New Issue
Block a user