Commit 9de4df1e authored by Hans Muller's avatar Hans Muller

TabNavigator animates selected TabView

The TabBar's selection is now represented by a TabBarSelection object which encapsulates both the previous and currently selected indices and the Performance used to animate the selection indicator.

Added a TabBarView class which displays a tab's contents. It uses a shared TabBarSelection to stay in sync with a TabBar. The TabBarView scrolls in sync with the TabBar when the selection changes. Eventually it will allow one to fling the selection forward or backwards.

Added a tabBar property to ToolBar. Typically the corresponding TabBarView will be the body of the toolbar's Scaffold.

Removed TabNavigatorView and TabNavigator.

Added a widget gallery tabs demo page. Removed the old tabs demo.
parent 40b3cf3f
......@@ -3,3 +3,9 @@ material-design-icons:
- name: navigation/arrow_drop_down
- name: navigation/cancel
- name: navigation/menu
- name: action/event
- name: action/home
- name: action/android
- name: action/alarm
- name: action/face
- name: action/language
// Copyright 2015 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/material.dart';
import 'widget_demo.dart';
final TabBarSelection _selection = new TabBarSelection();
final List<String> _iconNames = <String>["event", "home", "android", "alarm", "face", "language"];
Widget buildTabBar(_) {
return new TabBar(
selection: _selection,
isScrollable: true,
labels: _iconNames.map((String iconName) => new TabLabel(text: iconName, icon: "action/$iconName")).toList()
);
}
class TabsDemo extends StatefulComponent {
_TabsDemoState createState() => new _TabsDemoState();
}
class _TabsDemoState extends State<TabsDemo> {
double _viewWidth = 100.0;
void _handleSizeChanged(Size newSize) {
setState(() {
_viewWidth = newSize.width;
});
}
Widget build(_) {
return new SizeObserver(
onSizeChanged: _handleSizeChanged,
child: new TabBarView<String>(
selection: _selection,
items: _iconNames,
itemExtent: _viewWidth,
itemBuilder: (BuildContext context, String iconName, int index) {
return new Container(
key: new ValueKey<String>(iconName),
padding: const EdgeDims.all(12.0),
child: new Card(
child: new Center(child: new Icon(icon: "action/$iconName", size:IconSize.s48))
)
);
}
)
);
}
}
final WidgetDemo kTabsDemo = new WidgetDemo(
title: 'Tabs',
routeName: '/tabs',
tabBarBuilder: buildTabBar,
builder: (_) => new TabsDemo()
);
......@@ -5,9 +5,10 @@
import 'package:flutter/material.dart';
class WidgetDemo {
WidgetDemo({ this.title, this.routeName, this.builder });
WidgetDemo({ this.title, this.routeName, this.tabBarBuilder, this.builder });
final String title;
final String routeName;
final WidgetBuilder tabBarBuilder;
final WidgetBuilder builder;
}
......@@ -40,6 +40,11 @@ class GalleryPage extends StatelessComponent {
);
}
Widget _tabBar(BuildContext context) {
final WidgetBuilder builder = active?.tabBarBuilder;
return builder != null ? builder(context) : null;
}
Widget build(BuildContext context) {
return new Scaffold(
toolBar: new ToolBar(
......@@ -47,7 +52,8 @@ class GalleryPage extends StatelessComponent {
icon: 'navigation/menu',
onPressed: () { _showDrawer(context); }
),
center: new Text(active?.title ?? 'Material gallery')
center: new Text(active?.title ?? 'Material gallery'),
tabBar: _tabBar(context)
),
body: _body(context)
);
......
......@@ -9,6 +9,7 @@ import 'demo/date_picker_demo.dart';
import 'demo/drop_down_demo.dart';
import 'demo/selection_controls_demo.dart';
import 'demo/slider_demo.dart';
import 'demo/tabs_demo.dart';
import 'demo/time_picker_demo.dart';
import 'demo/widget_demo.dart';
import 'gallery_page.dart';
......@@ -18,6 +19,7 @@ final List<WidgetDemo> _kDemos = <WidgetDemo>[
kSelectionControlsDemo,
kSliderDemo,
kDatePickerDemo,
kTabsDemo,
kTimePickerDemo,
kDropDownDemo,
];
......
......@@ -6,6 +6,8 @@ part of stocks;
typedef void ModeUpdater(StockMode mode);
enum StockHomeTab { market, portfolio }
class StockHome extends StatefulComponent {
StockHome(this.stocks, this.symbols, this.stockMode, this.modeUpdater);
......@@ -20,6 +22,7 @@ class StockHome extends StatefulComponent {
class StockHomeState extends State<StockHome> {
final GlobalKey scaffoldKey = new GlobalKey();
final TabBarSelection _tabBarSelection = new TabBarSelection();
bool _isSearching = false;
String _searchQuery;
......@@ -160,7 +163,13 @@ class StockHomeState extends State<StockHome> {
icon: "navigation/more_vert",
onPressed: _handleMenuShow
)
]
],
tabBar: new TabBar(
selection: _tabBarSelection,
labels: <TabLabel>[
const TabLabel(text: 'MARKET'),
const TabLabel(text: 'PORTFOLIO')]
)
);
}
......@@ -208,25 +217,6 @@ class StockHomeState extends State<StockHome> {
static const List<String> portfolioSymbols = const <String>["AAPL","FIZZ", "FIVE", "FLAT", "ZINC", "ZNGA"];
Widget buildTabNavigator() {
return new TabNavigator(
views: <TabNavigatorView>[
new TabNavigatorView(
label: const TabLabel(text: 'MARKET'),
builder: (BuildContext context) => buildStockList(context, _filterBySearchQuery(_getStockList(config.symbols)).toList())
),
new TabNavigatorView(
label: const TabLabel(text: 'PORTFOLIO'),
builder: (BuildContext context) => buildStockList(context, _filterBySearchQuery(_getStockList(portfolioSymbols)).toList())
)
],
selectedIndex: selectedTabIndex,
onChanged: (int tabIndex) {
setState(() { selectedTabIndex = tabIndex; } );
}
);
}
static GlobalKey searchFieldKey = new GlobalKey();
static GlobalKey companyNameKey = new GlobalKey();
......@@ -270,12 +260,43 @@ class StockHomeState extends State<StockHome> {
);
}
Widget buildStockTab(BuildContext context, StockHomeTab tab, List<String> stockSymbols) {
return new Container(
key: new ValueKey<StockHomeTab>(tab),
child: buildStockList(context, _filterBySearchQuery(_getStockList(stockSymbols)).toList())
);
}
double _viewWidth = 100.0;
void _handleSizeChanged(Size newSize) {
setState(() {
_viewWidth = newSize.width;
});
}
Widget build(BuildContext context) {
return new Scaffold(
key: scaffoldKey,
toolBar: _isSearching ? buildSearchBar() : buildToolBar(),
body: buildTabNavigator(),
floatingActionButton: buildFloatingActionButton()
floatingActionButton: buildFloatingActionButton(),
body: new SizeObserver(
onSizeChanged: _handleSizeChanged,
child: new TabBarView<StockHomeTab>(
selection: _tabBarSelection,
items: [StockHomeTab.market, StockHomeTab.portfolio],
itemExtent: _viewWidth,
itemBuilder: (BuildContext context, StockHomeTab tab, _) {
switch (tab) {
case StockHomeTab.market:
return buildStockTab(context, tab, config.symbols);
case StockHomeTab.portfolio:
return buildStockTab(context, tab, portfolioSymbols);
default:
assert(false);
}
}
)
)
);
}
}
// Copyright 2015 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/material.dart';
import 'package:flutter/painting.dart';
class TabbedNavigatorApp extends StatefulComponent {
TabbedNavigatorAppState createState() => new TabbedNavigatorAppState();
}
class TabbedNavigatorAppState extends State<TabbedNavigatorApp> {
// The index of the selected tab for each of the TabNavigators constructed below.
List<int> selectedIndices = new List<int>.filled(5, 0);
TabNavigator _buildTabNavigator(int n, List<TabNavigatorView> views, Key key, {isScrollable: false}) {
return new TabNavigator(
key: key,
views: views,
selectedIndex: selectedIndices[n],
isScrollable: isScrollable,
onChanged: (int tabIndex) {
setState(() { selectedIndices[n] = tabIndex; } );
}
);
}
Widget _buildContent(String label) {
return new Center(
child: new Text(label, style: const TextStyle(fontSize: 48.0, fontWeight: FontWeight.w800))
);
}
TabNavigator _buildTextLabelsTabNavigator(int n) {
Iterable<TabNavigatorView> views = ["ONE", "TWO", "FREE", "FOUR"]
.map((text) {
return new TabNavigatorView(
label: new TabLabel(text: text),
builder: (BuildContext context) => _buildContent(text)
);
});
return _buildTabNavigator(n, views.toList(), const ValueKey<String>('textLabelsTabNavigator'));
}
TabNavigator _buildIconLabelsTabNavigator(int n) {
Iterable<TabNavigatorView> views = ["event", "home", "android", "alarm", "face", "language"]
.map((String iconName) {
return new TabNavigatorView(
label: new TabLabel(icon: "action/$iconName"),
builder: (BuildContext context) => _buildContent(iconName)
);
});
return _buildTabNavigator(n, views.toList(), const ValueKey<String>('iconLabelsTabNavigator'));
}
TabNavigator _buildTextAndIconLabelsTabNavigator(int n) {
List<TabNavigatorView> views = <TabNavigatorView>[
new TabNavigatorView(
label: const TabLabel(text: 'STOCKS', icon: 'action/list'),
builder: (BuildContext context) => _buildContent("Stocks")
),
new TabNavigatorView(
label: const TabLabel(text: 'PORTFOLIO', icon: 'action/account_circle'),
builder: (BuildContext context) => _buildContent("Portfolio")
),
new TabNavigatorView(
label: const TabLabel(text: 'SUMMARY', icon: 'action/assessment'),
builder: (BuildContext context) => _buildContent("Summary")
)
];
return _buildTabNavigator(n, views, const ValueKey<String>('textAndIconLabelsTabNavigator'));
}
TabNavigator _buildScrollableTabNavigator(int n) {
Iterable<TabNavigatorView> views = [
"MIN WIDTH",
"THIS TAB LABEL IS SO WIDE THAT IT OCCUPIES TWO LINES",
"THIS TAB IS PRETTY WIDE TOO",
"MORE",
"TABS",
"TO",
"STRETCH",
"OUT",
"THE",
"TAB BAR"
]
.map((text) {
return new TabNavigatorView(
label: new TabLabel(text: text),
builder: (BuildContext context) => _buildContent(text)
);
});
return _buildTabNavigator(n, views.toList(), const ValueKey<String>('scrollableTabNavigator'), isScrollable: true);
}
Container _buildCard(BuildContext context, TabNavigator tabNavigator) {
return new Container(
padding: const EdgeDims.all(12.0),
child: new Card(child: new Padding(child: tabNavigator, padding: const EdgeDims.all(8.0)))
);
}
Widget build(BuildContext context) {
List<TabNavigatorView> views = <TabNavigatorView>[
new TabNavigatorView(
label: const TabLabel(text: 'TEXT'),
builder: (BuildContext context) => _buildCard(context, _buildTextLabelsTabNavigator(0))
),
new TabNavigatorView(
label: const TabLabel(text: 'ICONS'),
builder: (BuildContext context) => _buildCard(context, _buildIconLabelsTabNavigator(1))
),
new TabNavigatorView(
label: const TabLabel(text: 'BOTH'),
builder: (BuildContext context) => _buildCard(context, _buildTextAndIconLabelsTabNavigator(2))
),
new TabNavigatorView(
label: const TabLabel(text: 'SCROLL'),
builder: (BuildContext context) => _buildCard(context, _buildScrollableTabNavigator(3))
)
];
TabNavigator tabNavigator = _buildTabNavigator(4, views, const ValueKey<String>('tabs'));
assert(selectedIndices.length == 5);
ToolBar toolbar = new ToolBar(
center: new Text('Tabbed Navigator', style: Typography.white.title),
elevation: 0
);
return new Scaffold(
toolBar: toolbar,
body: tabNavigator
);
}
}
void main() {
runApp(new MaterialApp(
title: 'Tabs',
routes: <String, RouteBuilder>{
'/': (RouteArguments args) => new TabbedNavigatorApp(),
}
));
}
This diff is collapsed.
......@@ -8,6 +8,7 @@ import 'constants.dart';
import 'icon_theme.dart';
import 'icon_theme_data.dart';
import 'shadows.dart';
import 'tabs.dart';
import 'theme.dart';
import 'typography.dart';
......@@ -18,6 +19,7 @@ class ToolBar extends StatelessComponent {
this.center,
this.right,
this.bottom,
this.tabBar,
this.elevation: 4,
this.backgroundColor,
this.textTheme,
......@@ -28,6 +30,7 @@ class ToolBar extends StatelessComponent {
final Widget center;
final List<Widget> right;
final Widget bottom;
final TabBar tabBar;
final int elevation;
final Color backgroundColor;
final TextTheme textTheme;
......@@ -40,6 +43,7 @@ class ToolBar extends StatelessComponent {
center: center,
right: right,
bottom: bottom,
tabBar: tabBar,
elevation: elevation,
backgroundColor: backgroundColor,
textTheme: textTheme,
......@@ -90,6 +94,9 @@ class ToolBar extends StatelessComponent {
child: new Container(height: kExtendedToolBarHeight - kToolBarHeight, child: bottom)
));
if (tabBar != null)
columnChildren.add(tabBar);
Widget content = new AnimatedContainer(
duration: kThemeChangeDuration,
padding: new EdgeDims.symmetric(horizontal: 8.0),
......
......@@ -7,16 +7,13 @@ import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:test/test.dart';
int selectedIndex = 2;
TabBarSelection selection;
Widget buildFrame({ List<String> tabs, bool isScrollable: false }) {
return new TabBar(
labels: tabs.map((String tab) => new TabLabel(text: tab)).toList(),
selectedIndex: selectedIndex,
isScrollable: isScrollable,
onChanged: (int tabIndex) {
selectedIndex = tabIndex;
}
selection: selection,
isScrollable: isScrollable
);
}
......@@ -24,56 +21,56 @@ void main() {
test('TabBar tap selects tab', () {
testWidgets((WidgetTester tester) {
List<String> tabs = <String>['A', 'B', 'C'];
selectedIndex = 2;
selection = new TabBarSelection(index: 2);
tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: false));
expect(tester.findText('A'), isNotNull);
expect(tester.findText('B'), isNotNull);
expect(tester.findText('C'), isNotNull);
expect(selectedIndex, equals(2));
expect(selection.index, equals(2));
tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: false));
tester.tap(tester.findText('B'));
tester.pump();
expect(selectedIndex, equals(1));
expect(selection.index, equals(1));
tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: false));
tester.tap(tester.findText('C'));
tester.pump();
expect(selectedIndex, equals(2));
expect(selection.index, equals(2));
tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: false));
tester.tap(tester.findText('A'));
tester.pump();
expect(selectedIndex, equals(0));
expect(selection.index, equals(0));
});
});
test('Scrollable TabBar tap selects tab', () {
testWidgets((WidgetTester tester) {
List<String> tabs = <String>['A', 'B', 'C'];
selectedIndex = 2;
selection = new TabBarSelection(index: 2);
tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: true));
expect(tester.findText('A'), isNotNull);
expect(tester.findText('B'), isNotNull);
expect(tester.findText('C'), isNotNull);
expect(selectedIndex, equals(2));
expect(selection.index, equals(2));
tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: true));
tester.tap(tester.findText('B'));
tester.pump();
expect(selectedIndex, equals(1));
expect(selection.index, equals(1));
tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: true));
tester.tap(tester.findText('C'));
tester.pump();
expect(selectedIndex, equals(2));
expect(selection.index, equals(2));
tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: true));
tester.tap(tester.findText('A'));
tester.pump();
expect(selectedIndex, equals(0));
expect(selection.index, equals(0));
});
});
}
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