Commit 6d470927 authored by xster's avatar xster Committed by GitHub

Split CupertinoScaffold into CupertinoTabScaffold and CupertinoPageScaffold (#12106)

* Refactor CupertinoScaffold

* Rename rootTabPageBuilder to tabBuilder

* fix tab transparency padding

* Add default background color

* review notes

* fix test
parent e28765a9
......@@ -13,10 +13,11 @@ export 'src/cupertino/button.dart';
export 'src/cupertino/colors.dart';
export 'src/cupertino/dialog.dart';
export 'src/cupertino/nav_bar.dart';
export 'src/cupertino/page.dart';
export 'src/cupertino/scaffold.dart';
export 'src/cupertino/page_scaffold.dart';
export 'src/cupertino/route.dart';
export 'src/cupertino/slider.dart';
export 'src/cupertino/switch.dart';
export 'src/cupertino/tab_scaffold.dart';
export 'src/cupertino/text_selection.dart';
export 'src/cupertino/thumb_painter.dart';
export 'widgets.dart';
// 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 'colors.dart';
import 'nav_bar.dart';
/// Implements a single iOS application page's layout.
///
/// The scaffold lays out the navigation bar on top and the content between or
/// behind the navigation bar.
// TODO(xster): add an example.
class CupertinoPageScaffold extends StatelessWidget {
const CupertinoPageScaffold({
Key key,
this.navigationBar,
@required this.child,
}) : assert(child != null),
super(key: key);
/// The [navigationBar], typically a [CupertinoNavigationBar], is drawn at the
/// top of the screen.
///
/// If translucent, the main content may slide behind it.
/// Otherwise, the main content's top margin will be offset by its height.
// TODO(xster): document its page transition animation when ready
final PreferredSizeWidget navigationBar;
/// Widget to show in the main content area.
///
/// Content can slide under the [navigationBar] when they're translucent.
final Widget child;
@override
Widget build(BuildContext context) {
final List<Widget> stacked = <Widget>[];
double topPadding = 0.0;
if (navigationBar != null) {
topPadding += navigationBar.preferredSize.height;
// 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
// of the page.
if (topPadding > 0.0)
topPadding += MediaQuery.of(context).padding.top;
}
// The main content being at the bottom is added to the stack first.
stacked.add(new Padding(
padding: new EdgeInsets.only(top: topPadding),
child: child,
));
if (navigationBar != null) {
stacked.add(new Positioned(
top: 0.0,
left: 0.0,
right: 0.0,
child: navigationBar,
));
}
return new DecoratedBox(
decoration: const BoxDecoration(color: CupertinoColors.white),
child: new Stack(
children: stacked,
),
);
}
}
\ No newline at end of file
......@@ -5,76 +5,41 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'bottom_tab_bar.dart';
import 'nav_bar.dart';
/// Implements a basic iOS application's layout and behavior structure.
/// Implements a tabbed iOS application's root layout and behavior structure.
///
/// The scaffold lays out the navigation bar on top, the tab bar at the bottom
/// and tabbed or untabbed content between or behind the bars.
/// The scaffold lays out the tab bar at the bottom and the content between or
/// behind the tab bar.
///
/// For tabbed scaffolds, the tab's active item and the actively showing tab
/// in the content area are automatically connected.
/// A [tabBar] and a [tabBuilder] are required. The [CupertinoTabScaffold]
/// will automatically listen to the provided [CupertinoTabBar]'s tap callbacks
/// to change the active tab.
///
/// Tabs' contents are built with the provided [tabBuilder] at the active
/// tab index. [tabBuilder] must be able to build the same number of
/// pages as the [tabBar.items.length]. Inactive tabs will be moved [Offstage]
/// and its animations disabled.
// TODO(xster): describe navigator handlings.
// TODO(xster): add an example.
class CupertinoScaffold extends StatefulWidget {
/// Construct a [CupertinoScaffold] without tabs.
///
/// The [tabBar] and [rootTabPageBuilder] fields are not used in a [CupertinoScaffold]
/// without tabs.
// TODO(xster): document that page transitions will happen behind the navigation
// bar.
const CupertinoScaffold({
Key key,
this.navigationBar,
@required this.child,
}) : assert(child != null),
tabBar = null,
rootTabPageBuilder = null,
super(key: key);
/// Construct a [CupertinoScaffold] with tabs.
///
/// A [tabBar] and a [rootTabPageBuilder] are required. The [CupertinoScaffold]
/// will automatically listen to the provided [CupertinoTabBar]'s tap callbacks
/// to change the active tab.
///
/// Tabs' contents are built with the provided [rootTabPageBuilder] at the active
/// tab index. [rootTabPageBuilder] must be able to build the same number of
/// pages as the [tabBar.items.length]. Inactive tabs will be moved [Offstage]
/// and its animations disabled.
///
/// The [child] field is not used in a [CupertinoScaffold] with tabs.
const CupertinoScaffold.tabbed({
class CupertinoTabScaffold extends StatefulWidget {
const CupertinoTabScaffold({
Key key,
this.navigationBar,
@required this.tabBar,
@required this.rootTabPageBuilder,
@required this.tabBuilder,
}) : assert(tabBar != null),
assert(rootTabPageBuilder != null),
child = null,
assert(tabBuilder != null),
super(key: key);
/// The [navigationBar], typically a [CupertinoNavigationBar], is drawn at the
/// top of the screen.
///
/// If translucent, the main content may slide behind it.
/// Otherwise, the main content's top margin will be offset by its height.
// TODO(xster): document its page transition animation when ready
final PreferredSizeWidget navigationBar;
/// The [tabBar] is a [CupertinoTabBar] drawn at the bottom of the screen
/// that lets the user switch between different tabs in the main content area
/// when present.
///
/// This parameter is required and must be non-null when the [new CupertinoScaffold.tabbed]
/// constructor is used.
///
/// When provided, [CupertinoTabBar.currentIndex] will be ignored and will
/// be managed by the [CupertinoScaffold] to show the currently selected page
/// be managed by the [CupertinoTabScaffold] to show the currently selected page
/// as the active item index. If [CupertinoTabBar.onTap] is provided, it will
/// still be called. [CupertinoScaffold] automatically also listen to the
/// still be called. [CupertinoTabScaffold] automatically also listen to the
/// [CupertinoTabBar]'s `onTap` to change the [CupertinoTabBar]'s `currentIndex`
/// and change the actively displayed tab in [CupertinoScaffold]'s own
/// and change the actively displayed tab in [CupertinoTabScaffold]'s own
/// main content area.
///
/// If translucent, the main content may slide behind it.
......@@ -83,73 +48,34 @@ class CupertinoScaffold extends StatefulWidget {
/// An [IndexedWidgetBuilder] that's called when tabs become active.
///
/// Used when a tabbed scaffold is constructed via the [new CupertinoScaffold.tabbed]
/// constructor and must be non-null.
///
/// When the tab becomes inactive, its content is still cached in the widget
/// tree [Offstage] and its animations disabled.
///
/// Content can slide under the [navigationBar] or the [tabBar] when they're
/// translucent.
final IndexedWidgetBuilder rootTabPageBuilder;
/// Widget to show in the main content area when the scaffold is used without
/// tabs.
///
/// Used when the default [new CupertinoScaffold] constructor is used and must
/// be non-null.
///
/// Content can slide under the [navigationBar] or the [tabBar] when they're
/// translucent.
final Widget child;
/// Content can slide under the [tabBar] when it's translucent.
final IndexedWidgetBuilder tabBuilder;
@override
_CupertinoScaffoldState createState() => new _CupertinoScaffoldState();
_CupertinoTabScaffoldState createState() => new _CupertinoTabScaffoldState();
}
class _CupertinoScaffoldState extends State<CupertinoScaffold> {
class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> {
int _currentPage = 0;
/// Pad the given middle widget with or without top and bottom offsets depending
/// on whether the middle widget should slide behind translucent bars.
Widget _padMiddle(Widget middle) {
double topPadding = 0.0;
if (widget.navigationBar != null) {
topPadding += MediaQuery.of(context).padding.top;
topPadding += widget.navigationBar.preferredSize.height;
}
double bottomPadding = 0.0;
if (widget.tabBar?.opaque ?? false)
bottomPadding = widget.tabBar.preferredSize.height;
return new Padding(
padding: new EdgeInsets.only(top: topPadding, bottom: bottomPadding),
child: middle,
);
}
@override
Widget build(BuildContext context) {
final List<Widget> stacked = <Widget>[];
// The main content being at the bottom is added to the stack first.
if (widget.child != null) {
stacked.add(_padMiddle(widget.child));
} else if (widget.rootTabPageBuilder != null) {
stacked.add(_padMiddle(new _TabView(
currentTabIndex: _currentPage,
tabNumber: widget.tabBar.items.length,
rootTabPageBuilder: widget.rootTabPageBuilder,
)));
}
if (widget.navigationBar != null) {
stacked.add(new Align(
alignment: FractionalOffset.topCenter,
child: widget.navigationBar,
));
}
stacked.add(
new Padding(
padding: new EdgeInsets.only(bottom: widget.tabBar.opaque ? widget.tabBar.preferredSize.height : 0.0),
child: new _TabView(
currentTabIndex: _currentPage,
tabNumber: widget.tabBar.items.length,
tabBuilder: widget.tabBuilder,
)
),
);
if (widget.tabBar != null) {
stacked.add(new Align(
......@@ -158,15 +84,15 @@ class _CupertinoScaffoldState extends State<CupertinoScaffold> {
// our own listener to update the _currentPage on top of a possibly user
// provided callback.
child: widget.tabBar.copyWith(
currentIndex: _currentPage,
onTap: (int newIndex) {
setState(() {
_currentPage = newIndex;
});
// Chain the user's original callback.
if (widget.tabBar.onTap != null)
widget.tabBar.onTap(newIndex);
}
currentIndex: _currentPage,
onTap: (int newIndex) {
setState(() {
_currentPage = newIndex;
});
// Chain the user's original callback.
if (widget.tabBar.onTap != null)
widget.tabBar.onTap(newIndex);
}
),
));
}
......@@ -183,14 +109,14 @@ class _TabView extends StatefulWidget {
_TabView({
@required this.currentTabIndex,
@required this.tabNumber,
@required this.rootTabPageBuilder,
@required this.tabBuilder,
}) : assert(currentTabIndex != null),
assert(tabNumber != null && tabNumber > 0),
assert(rootTabPageBuilder != null);
assert(tabBuilder != null);
final int currentTabIndex;
final int tabNumber;
final IndexedWidgetBuilder rootTabPageBuilder;
final IndexedWidgetBuilder tabBuilder;
@override
_TabViewState createState() => new _TabViewState();
......@@ -208,12 +134,13 @@ class _TabViewState extends State<_TabView> {
@override
Widget build(BuildContext context) {
return new Stack(
fit: StackFit.expand,
children: new List<Widget>.generate(widget.tabNumber, (int index) {
final bool active = index == widget.currentTabIndex;
// TODO(xster): lazily replace empty tabs with Navigators instead.
if (active || tabs[index] != null)
tabs[index] = widget.rootTabPageBuilder(context, index);
tabs[index] = widget.tabBuilder(context, index);
return new Offstage(
offstage: !active,
......
......@@ -101,7 +101,7 @@ void main() {
return new CupertinoPageRoute<Null>(
settings: settings,
builder: (BuildContext context) {
return const CupertinoScaffold(
return const CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
middle: const Text('Title'),
),
......@@ -125,7 +125,7 @@ void main() {
return new CupertinoPageRoute<Null>(
settings: settings,
builder: (BuildContext context) {
return new CupertinoScaffold(
return new CupertinoPageScaffold(
child: new CustomScrollView(
controller: scrollController,
slivers: <Widget>[
......@@ -213,7 +213,7 @@ void main() {
return new CupertinoPageRoute<Null>(
settings: settings,
builder: (BuildContext context) {
return new CupertinoScaffold(
return new CupertinoPageScaffold(
child: new CustomScrollView(
controller: scrollController,
slivers: <Widget>[
......
......@@ -5,16 +5,10 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/rendering_tester.dart';
import '../services/mocks_for_image_cache.dart';
List<int> selectedTabs;
/// Integration tests testing both [CupertinoPageScaffold] and [CupertinoTabScaffold].
void main() {
setUp(() {
selectedTabs = <int>[];
});
testWidgets('Contents are behind translucent bar', (WidgetTester tester) async {
await tester.pumpWidget(
new WidgetsApp(
......@@ -23,7 +17,7 @@ void main() {
return new CupertinoPageRoute<Null>(
settings: settings,
builder: (BuildContext context) {
return const CupertinoScaffold(
return const CupertinoPageScaffold(
// Default nav bar is translucent.
navigationBar: const CupertinoNavigationBar(
middle: const Text('Title'),
......@@ -49,14 +43,30 @@ void main() {
return new CupertinoPageRoute<Null>(
settings: settings,
builder: (BuildContext context) {
return new CupertinoScaffold.tabbed(
navigationBar: const CupertinoNavigationBar(
return new CupertinoTabScaffold(
tabBar: new CupertinoTabBar(
backgroundColor: CupertinoColors.white,
middle: const Text('Title'),
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'),
),
],
),
tabBar: _buildTabBar(),
rootTabPageBuilder: (BuildContext context, int index) {
return index == 0 ? page1Center : new Stack();
tabBuilder: (BuildContext context, int index) {
return index == 0
? new CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
backgroundColor: CupertinoColors.white,
middle: const Text('Title'),
),
child: page1Center,
)
: new Stack();
}
);
},
......@@ -67,132 +77,4 @@ void main() {
expect(tester.getSize(find.byWidget(page1Center)).height, 600.0 - 44.0 - 50.0);
});
testWidgets('Tab switching', (WidgetTester tester) async {
final List<int> tabsPainted = <int>[];
await tester.pumpWidget(
new WidgetsApp(
color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>(
settings: settings,
builder: (BuildContext context) {
return new CupertinoScaffold.tabbed(
navigationBar: const CupertinoNavigationBar(
backgroundColor: CupertinoColors.white,
middle: const Text('Title'),
),
tabBar: _buildTabBar(),
rootTabPageBuilder: (BuildContext context, int index) {
return new CustomPaint(
child: new Text('Page ${index + 1}'),
painter: new TestCallbackPainter(
onPaint: () { tabsPainted.add(index); }
)
);
}
);
},
);
},
),
);
expect(tabsPainted, <int>[0]);
RichText tab1 = tester.widget(find.descendant(
of: find.text('Tab 1'),
matching: find.byType(RichText),
));
expect(tab1.text.style.color, CupertinoColors.activeBlue);
RichText tab2 = tester.widget(find.descendant(
of: find.text('Tab 2'),
matching: find.byType(RichText),
));
expect(tab2.text.style.color, CupertinoColors.inactiveGray);
await tester.tap(find.text('Tab 2'));
await tester.pump();
expect(tabsPainted, <int>[0, 1]);
tab1 = tester.widget(find.descendant(
of: find.text('Tab 1'),
matching: find.byType(RichText),
));
expect(tab1.text.style.color, CupertinoColors.inactiveGray);
tab2 = tester.widget(find.descendant(
of: find.text('Tab 2'),
matching: find.byType(RichText),
));
expect(tab2.text.style.color, CupertinoColors.activeBlue);
await tester.tap(find.text('Tab 1'));
await tester.pump();
expect(tabsPainted, <int>[0, 1, 0]);
});
testWidgets('Tabs are lazy built and moved offstage when inactive', (WidgetTester tester) async {
final List<int> tabsBuilt = <int>[];
await tester.pumpWidget(
new WidgetsApp(
color: const Color(0xFFFFFFFF),
onGenerateRoute: (RouteSettings settings) {
return new CupertinoPageRoute<Null>(
settings: settings,
builder: (BuildContext context) {
return new CupertinoScaffold.tabbed(
navigationBar: const CupertinoNavigationBar(
backgroundColor: CupertinoColors.white,
middle: const Text('Title'),
),
tabBar: _buildTabBar(),
rootTabPageBuilder: (BuildContext context, int index) {
tabsBuilt.add(index);
return new Text('Page ${index + 1}');
}
);
},
);
},
),
);
expect(tabsBuilt, <int>[0]);
expect(find.text('Page 1'), findsOneWidget);
expect(find.text('Page 2'), findsNothing);
await tester.tap(find.text('Tab 2'));
await tester.pump();
// Both tabs are built but only one is onstage.
expect(tabsBuilt, <int>[0, 0, 1]);
expect(find.text('Page 1', skipOffstage: false), isOffstage);
expect(find.text('Page 2'), findsOneWidget);
await tester.tap(find.text('Tab 1'));
await tester.pump();
expect(tabsBuilt, <int>[0, 0, 1, 0, 1]);
expect(find.text('Page 1'), findsOneWidget);
expect(find.text('Page 2', skipOffstage: false), isOffstage);
});
}
CupertinoTabBar _buildTabBar() {
return 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'),
),
],
backgroundColor: CupertinoColors.white,
onTap: (int newTab) => selectedTabs.add(newTab),
);
}
// 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';
import '../rendering/rendering_tester.dart';
import '../services/mocks_for_image_cache.dart';
List<int> selectedTabs;
void main() {
setUp(() {
selectedTabs = <int>[];
});
testWidgets('Tab switching', (WidgetTester tester) async {
final List<int> tabsPainted = <int>[];
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: _buildTabBar(),
tabBuilder: (BuildContext context, int index) {
return new CustomPaint(
child: new Text('Page ${index + 1}'),
painter: new TestCallbackPainter(
onPaint: () { tabsPainted.add(index); }
)
);
}
);
},
);
},
),
);
expect(tabsPainted, <int>[0]);
RichText tab1 = tester.widget(find.descendant(
of: find.text('Tab 1'),
matching: find.byType(RichText),
));
expect(tab1.text.style.color, CupertinoColors.activeBlue);
RichText tab2 = tester.widget(find.descendant(
of: find.text('Tab 2'),
matching: find.byType(RichText),
));
expect(tab2.text.style.color, CupertinoColors.inactiveGray);
await tester.tap(find.text('Tab 2'));
await tester.pump();
expect(tabsPainted, <int>[0, 1]);
tab1 = tester.widget(find.descendant(
of: find.text('Tab 1'),
matching: find.byType(RichText),
));
expect(tab1.text.style.color, CupertinoColors.inactiveGray);
tab2 = tester.widget(find.descendant(
of: find.text('Tab 2'),
matching: find.byType(RichText),
));
expect(tab2.text.style.color, CupertinoColors.activeBlue);
await tester.tap(find.text('Tab 1'));
await tester.pump();
expect(tabsPainted, <int>[0, 1, 0]);
// CupertinoTabBar's onTap callbacks are passed on.
expect(selectedTabs, <int>[1, 0]);
});
testWidgets('Tabs are lazy built and moved offstage when inactive', (WidgetTester tester) async {
final List<int> tabsBuilt = <int>[];
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: _buildTabBar(),
tabBuilder: (BuildContext context, int index) {
tabsBuilt.add(index);
return new Text('Page ${index + 1}');
}
);
},
);
},
),
);
expect(tabsBuilt, <int>[0]);
expect(find.text('Page 1'), findsOneWidget);
expect(find.text('Page 2'), findsNothing);
await tester.tap(find.text('Tab 2'));
await tester.pump();
// Both tabs are built but only one is onstage.
expect(tabsBuilt, <int>[0, 0, 1]);
expect(find.text('Page 1', skipOffstage: false), isOffstage);
expect(find.text('Page 2'), findsOneWidget);
await tester.tap(find.text('Tab 1'));
await tester.pump();
expect(tabsBuilt, <int>[0, 0, 1, 0, 1]);
expect(find.text('Page 1'), findsOneWidget);
expect(find.text('Page 2', skipOffstage: false), isOffstage);
});
}
CupertinoTabBar _buildTabBar() {
return 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'),
),
],
backgroundColor: CupertinoColors.white,
onTap: (int newTab) => selectedTabs.add(newTab),
);
}
\ No newline at end of file
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