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: ...@@ -3,3 +3,9 @@ material-design-icons:
- name: navigation/arrow_drop_down - name: navigation/arrow_drop_down
- name: navigation/cancel - name: navigation/cancel
- name: navigation/menu - 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 @@ ...@@ -5,9 +5,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class WidgetDemo { class WidgetDemo {
WidgetDemo({ this.title, this.routeName, this.builder }); WidgetDemo({ this.title, this.routeName, this.tabBarBuilder, this.builder });
final String title; final String title;
final String routeName; final String routeName;
final WidgetBuilder tabBarBuilder;
final WidgetBuilder builder; final WidgetBuilder builder;
} }
...@@ -40,6 +40,11 @@ class GalleryPage extends StatelessComponent { ...@@ -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) { Widget build(BuildContext context) {
return new Scaffold( return new Scaffold(
toolBar: new ToolBar( toolBar: new ToolBar(
...@@ -47,7 +52,8 @@ class GalleryPage extends StatelessComponent { ...@@ -47,7 +52,8 @@ class GalleryPage extends StatelessComponent {
icon: 'navigation/menu', icon: 'navigation/menu',
onPressed: () { _showDrawer(context); } onPressed: () { _showDrawer(context); }
), ),
center: new Text(active?.title ?? 'Material gallery') center: new Text(active?.title ?? 'Material gallery'),
tabBar: _tabBar(context)
), ),
body: _body(context) body: _body(context)
); );
......
...@@ -9,6 +9,7 @@ import 'demo/date_picker_demo.dart'; ...@@ -9,6 +9,7 @@ import 'demo/date_picker_demo.dart';
import 'demo/drop_down_demo.dart'; import 'demo/drop_down_demo.dart';
import 'demo/selection_controls_demo.dart'; import 'demo/selection_controls_demo.dart';
import 'demo/slider_demo.dart'; import 'demo/slider_demo.dart';
import 'demo/tabs_demo.dart';
import 'demo/time_picker_demo.dart'; import 'demo/time_picker_demo.dart';
import 'demo/widget_demo.dart'; import 'demo/widget_demo.dart';
import 'gallery_page.dart'; import 'gallery_page.dart';
...@@ -18,6 +19,7 @@ final List<WidgetDemo> _kDemos = <WidgetDemo>[ ...@@ -18,6 +19,7 @@ final List<WidgetDemo> _kDemos = <WidgetDemo>[
kSelectionControlsDemo, kSelectionControlsDemo,
kSliderDemo, kSliderDemo,
kDatePickerDemo, kDatePickerDemo,
kTabsDemo,
kTimePickerDemo, kTimePickerDemo,
kDropDownDemo, kDropDownDemo,
]; ];
......
...@@ -6,6 +6,8 @@ part of stocks; ...@@ -6,6 +6,8 @@ part of stocks;
typedef void ModeUpdater(StockMode mode); typedef void ModeUpdater(StockMode mode);
enum StockHomeTab { market, portfolio }
class StockHome extends StatefulComponent { class StockHome extends StatefulComponent {
StockHome(this.stocks, this.symbols, this.stockMode, this.modeUpdater); StockHome(this.stocks, this.symbols, this.stockMode, this.modeUpdater);
...@@ -20,6 +22,7 @@ class StockHome extends StatefulComponent { ...@@ -20,6 +22,7 @@ class StockHome extends StatefulComponent {
class StockHomeState extends State<StockHome> { class StockHomeState extends State<StockHome> {
final GlobalKey scaffoldKey = new GlobalKey(); final GlobalKey scaffoldKey = new GlobalKey();
final TabBarSelection _tabBarSelection = new TabBarSelection();
bool _isSearching = false; bool _isSearching = false;
String _searchQuery; String _searchQuery;
...@@ -160,7 +163,13 @@ class StockHomeState extends State<StockHome> { ...@@ -160,7 +163,13 @@ class StockHomeState extends State<StockHome> {
icon: "navigation/more_vert", icon: "navigation/more_vert",
onPressed: _handleMenuShow 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> { ...@@ -208,25 +217,6 @@ class StockHomeState extends State<StockHome> {
static const List<String> portfolioSymbols = const <String>["AAPL","FIZZ", "FIVE", "FLAT", "ZINC", "ZNGA"]; 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 searchFieldKey = new GlobalKey();
static GlobalKey companyNameKey = new GlobalKey(); static GlobalKey companyNameKey = new GlobalKey();
...@@ -270,12 +260,43 @@ class StockHomeState extends State<StockHome> { ...@@ -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) { Widget build(BuildContext context) {
return new Scaffold( return new Scaffold(
key: scaffoldKey, key: scaffoldKey,
toolBar: _isSearching ? buildSearchBar() : buildToolBar(), 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(),
}
));
}
...@@ -292,14 +292,12 @@ class TabLabel { ...@@ -292,14 +292,12 @@ class TabLabel {
} }
} }
class Tab extends StatelessComponent { class _Tab extends StatelessComponent {
Tab({ _Tab({
Key key, Key key,
this.onSelected, this.onSelected,
this.label, this.label,
this.color, this.color
this.selected: false,
this.selectedColor
}) : super(key: key) { }) : super(key: key) {
assert(label.text != null || label.icon != null); assert(label.text != null || label.icon != null);
} }
...@@ -307,19 +305,16 @@ class Tab extends StatelessComponent { ...@@ -307,19 +305,16 @@ class Tab extends StatelessComponent {
final VoidCallback onSelected; final VoidCallback onSelected;
final TabLabel label; final TabLabel label;
final Color color; final Color color;
final bool selected;
final Color selectedColor;
Widget _buildLabelText() { Widget _buildLabelText() {
assert(label.text != null); assert(label.text != null);
TextStyle style = new TextStyle(color: selected ? selectedColor : color); TextStyle style = new TextStyle(color: color);
return new Text(label.text, style: style); return new Text(label.text, style: style);
} }
Widget _buildLabelIcon() { Widget _buildLabelIcon() {
assert(label.icon != null); assert(label.icon != null);
Color iconColor = selected ? selectedColor : color; ColorFilter filter = new ColorFilter.mode(color, TransferMode.srcATop);
ColorFilter filter = new ColorFilter.mode(iconColor, TransferMode.srcATop);
return new Icon(icon: label.icon, colorFilter: filter); return new Icon(icon: label.icon, colorFilter: filter);
} }
...@@ -381,18 +376,46 @@ class _TabsScrollBehavior extends BoundedBehavior { ...@@ -381,18 +376,46 @@ class _TabsScrollBehavior extends BoundedBehavior {
} }
} }
class TabBarSelection {
TabBarSelection({ int index: 0, this.onChanged }) : _index = index;
final VoidCallback onChanged;
PerformanceView get performance => _performance.view;
final _performance = new Performance(duration: _kTabBarScroll, progress: 1.0);
int get index => _index;
int _index;
void set index(int value) {
if (value == _index)
return;
_previousIndex = _index;
_index = value;
_performance
..progress = 0.0
..play().then((_) {
if (onChanged != null)
onChanged();
});
}
int get previousIndex => _previousIndex;
int _previousIndex = 0;
}
class TabBar extends Scrollable { class TabBar extends Scrollable {
TabBar({ TabBar({
Key key, Key key,
this.labels, this.labels,
this.selectedIndex: 0, this.selection,
this.onChanged,
this.isScrollable: false this.isScrollable: false
}) : super(key: key, scrollDirection: ScrollDirection.horizontal); }) : super(key: key, scrollDirection: ScrollDirection.horizontal) {
assert(labels != null);
assert(selection != null);
}
final Iterable<TabLabel> labels; final Iterable<TabLabel> labels;
final int selectedIndex; final TabBarSelection selection;
final TabSelectedIndexChanged onChanged;
final bool isScrollable; final bool isScrollable;
_TabBarState createState() => new _TabBarState(); _TabBarState createState() => new _TabBarState();
...@@ -401,36 +424,53 @@ class TabBar extends Scrollable { ...@@ -401,36 +424,53 @@ class TabBar extends Scrollable {
class _TabBarState extends ScrollableState<TabBar> { class _TabBarState extends ScrollableState<TabBar> {
void initState() { void initState() {
super.initState(); super.initState();
_indicatorAnimation = new ValuePerformance<Rect>()
..duration = _kTabBarScroll
..variable = new AnimatedRectValue(null, curve: Curves.ease);
scrollBehavior.isScrollable = config.isScrollable; scrollBehavior.isScrollable = config.isScrollable;
config.selection._performance
..addStatusListener(_handleStatusChange)
..addListener(_handleProgressChange);
} }
Size _tabBarSize; void dispose() {
Size _viewportSize = Size.zero; config.selection._performance
List<double> _tabWidths; ..removeStatusListener(_handleStatusChange)
ValuePerformance<Rect> _indicatorAnimation; ..removeListener(_handleProgressChange)
..stop();
void didUpdateConfig(TabBar oldConfig) { super.dispose();
super.didUpdateConfig(oldConfig);
if (!config.isScrollable)
scrollTo(0.0);
} }
AnimatedRectValue get _indicatorRect => _indicatorAnimation.variable; Performance get _performance => config.selection._performance;
bool _indicatorRectIsValid = false;
void _startIndicatorAnimation(int fromTabIndex, int toTabIndex) { // The performance's status change is our indication that the selection index has
// changed. We don't start animating the _indicatorRect until after we've reset
// _indicatorRect here.
void _handleStatusChange(PerformanceStatus status) {
_indicatorRectIsValid = status == PerformanceStatus.forward;
if (status == PerformanceStatus.forward) {
if (config.isScrollable)
scrollTo(_centeredTabScrollOffset(config.selection.index), duration: _kTabBarScroll);
setState(() {
_indicatorRect _indicatorRect
..begin = (_indicatorRect.value == null ? _tabIndicatorRect(fromTabIndex) : _indicatorRect.value) ..begin = _indicatorRect.value ?? _tabIndicatorRect(config.selection.previousIndex)
..end = _tabIndicatorRect(toTabIndex); ..end = _tabIndicatorRect(config.selection.index);
_indicatorAnimation });
..progress = 0.0 }
..play();
} }
ScrollBehavior createScrollBehavior() => new _TabsScrollBehavior(); void _handleProgressChange() {
_TabsScrollBehavior get scrollBehavior => super.scrollBehavior; // Performance listeners are notified before statusListeners.
if (_indicatorRectIsValid && _performance.status == PerformanceStatus.forward) {
setState(() {
_indicatorRect.setProgress(_performance.progress, AnimationDirection.forward);
});
}
}
Size _viewportSize = Size.zero;
Size _tabBarSize;
List<double> _tabWidths;
AnimatedRectValue _indicatorRect = new AnimatedRectValue(null, curve: Curves.ease);
Rect _tabRect(int tabIndex) { Rect _tabRect(int tabIndex) {
assert(_tabBarSize != null); assert(_tabBarSize != null);
...@@ -450,31 +490,40 @@ class _TabBarState extends ScrollableState<TabBar> { ...@@ -450,31 +490,40 @@ class _TabBarState extends ScrollableState<TabBar> {
return new Rect.fromLTRB(r.left, r.bottom, r.right, r.bottom + _kTabIndicatorHeight); return new Rect.fromLTRB(r.left, r.bottom, r.right, r.bottom + _kTabIndicatorHeight);
} }
void didUpdateConfig(TabBar oldConfig) {
super.didUpdateConfig(oldConfig);
if (!config.isScrollable)
scrollTo(0.0);
}
ScrollBehavior createScrollBehavior() => new _TabsScrollBehavior();
_TabsScrollBehavior get scrollBehavior => super.scrollBehavior;
double _centeredTabScrollOffset(int tabIndex) { double _centeredTabScrollOffset(int tabIndex) {
double viewportWidth = scrollBehavior.containerExtent; double viewportWidth = scrollBehavior.containerExtent;
return (_tabRect(tabIndex).left + _tabWidths[tabIndex] / 2.0 - viewportWidth / 2.0) Rect tabRect = _tabRect(tabIndex);
return (tabRect.left + tabRect.width / 2.0 - viewportWidth / 2.0)
.clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset); .clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset);
} }
void _handleTabSelected(int tabIndex) { void _handleTabSelected(int tabIndex) {
if (tabIndex != config.selectedIndex) { if (tabIndex != config.selection.index)
if (_tabWidths != null) { setState(() {
if (config.isScrollable) config.selection.index = tabIndex;
scrollTo(_centeredTabScrollOffset(tabIndex), duration: _kTabBarScroll); });
_startIndicatorAnimation(config.selectedIndex, tabIndex);
}
if (config.onChanged != null)
config.onChanged(tabIndex);
}
} }
Widget _toTab(TabLabel label, int tabIndex, Color color, Color selectedColor) { Widget _toTab(TabLabel label, int tabIndex, Color color, Color selectedColor) {
return new Tab( Color labelColor = color;
onSelected: () => _handleTabSelected(tabIndex), if (tabIndex == config.selection.index)
labelColor = Color.lerp(color, selectedColor, _performance.progress);
else if (tabIndex == config.selection.previousIndex)
labelColor = Color.lerp(selectedColor, color, _performance.progress);
return new _Tab(
onSelected: () { _handleTabSelected(tabIndex); },
label: label, label: label,
color: color, color: labelColor
selected: tabIndex == config.selectedIndex,
selectedColor: selectedColor
); );
} }
...@@ -496,6 +545,8 @@ class _TabBarState extends ScrollableState<TabBar> { ...@@ -496,6 +545,8 @@ class _TabBarState extends ScrollableState<TabBar> {
void _handleViewportSizeChanged(Size newSize) { void _handleViewportSizeChanged(Size newSize) {
_viewportSize = newSize; _viewportSize = newSize;
_updateScrollBehavior(); _updateScrollBehavior();
if (config.isScrollable)
scrollTo(_centeredTabScrollOffset(config.selection.index), duration: _kTabBarScroll);
} }
Widget buildContent(BuildContext context) { Widget buildContent(BuildContext context) {
...@@ -526,11 +577,11 @@ class _TabBarState extends ScrollableState<TabBar> { ...@@ -526,11 +577,11 @@ class _TabBarState extends ScrollableState<TabBar> {
style: textStyle, style: textStyle,
child: new BuilderTransition( child: new BuilderTransition(
variables: <AnimatedValue<Rect>>[_indicatorRect], variables: <AnimatedValue<Rect>>[_indicatorRect],
performance: _indicatorAnimation.view, performance: config.selection.performance,
builder: (BuildContext context) { builder: (BuildContext context) {
return new _TabBarWrapper( return new _TabBarWrapper(
children: tabs, children: tabs,
selectedIndex: config.selectedIndex, selectedIndex: config.selection.index,
indicatorColor: indicatorColor, indicatorColor: indicatorColor,
indicatorRect: _indicatorRect.value, indicatorRect: _indicatorRect.value,
textAndIcons: textAndIcons, textAndIcons: textAndIcons,
...@@ -563,46 +614,111 @@ class _TabBarState extends ScrollableState<TabBar> { ...@@ -563,46 +614,111 @@ class _TabBarState extends ScrollableState<TabBar> {
} }
} }
class TabNavigatorView { class TabBarView<T> extends ScrollableList<T> {
TabNavigatorView({ this.label, this.builder }) { TabBarView({
assert(builder != null); Key key,
this.selection,
List<T> items,
ItemBuilder<T> itemBuilder,
double itemExtent
}) : super(
key: key,
scrollDirection: ScrollDirection.horizontal,
items: items,
itemBuilder: itemBuilder,
itemExtent: itemExtent,
itemsWrap: false
) {
assert(selection != null);
} }
// this uses a builder for the contents, rather than a raw Widget child, final TabBarSelection selection;
// because there might be many, many tabs and some might be relatively
// expensive to create up front. This way, the view is only created lazily.
final TabLabel label; _TabBarViewState createState() => new _TabBarViewState<T>();
final WidgetBuilder builder;
} }
class TabNavigator extends StatelessComponent { // TODO(hansmuller): horizontal scrolling should drive the TabSelection's performance.
TabNavigator({ class _NotScrollable extends BoundedBehavior {
Key key, bool get isScrollable => false;
this.views, }
this.selectedIndex: 0,
this.onChanged,
this.isScrollable: false
}) : super(key: key);
final List<TabNavigatorView> views; class _TabBarViewState<T> extends ScrollableListState<T, TabBarView<T>> {
final int selectedIndex;
final TabSelectedIndexChanged onChanged;
final bool isScrollable;
Widget build(BuildContext context) { ScrollBehavior createScrollBehavior() => new _NotScrollable();
assert(views != null && views.isNotEmpty);
assert(selectedIndex >= 0 && selectedIndex < views.length); List<int> _itemIndices = [0, 1];
return new Column(<Widget>[ AnimationDirection _scrollDirection = AnimationDirection.forward;
new TabBar(
labels: views.map((TabNavigatorView view) => view.label), void _initItemIndicesAndScrollPosition() {
onChanged: onChanged, final int selectedIndex = config.selection.index;
selectedIndex: selectedIndex,
isScrollable: isScrollable if (selectedIndex == 0) {
), _itemIndices = <int>[0, 1];
new Flexible(child: views[selectedIndex].builder(context)) scrollTo(0.0);
], } else if (selectedIndex == config.items.length - 1) {
alignItems: FlexAlignItems.stretch _itemIndices = <int>[selectedIndex - 1, selectedIndex];
); scrollTo(config.itemExtent);
} else {
_itemIndices = <int>[selectedIndex - 1, selectedIndex, selectedIndex + 1];
scrollTo(config.itemExtent);
}
}
Performance get _performance => config.selection._performance;
void initState() {
super.initState();
_initItemIndicesAndScrollPosition();
_performance
..addStatusListener(_handleStatusChange)
..addListener(_handleProgressChange);
}
void dispose() {
_performance
..removeStatusListener(_handleStatusChange)
..removeListener(_handleProgressChange)
..stop();
super.dispose();
}
void didUpdateConfig(TabBarView oldConfig) {
super.didUpdateConfig(oldConfig);
if (oldConfig.itemExtent != config.itemExtent && !_performance.isAnimating)
_initItemIndicesAndScrollPosition();
}
void _handleStatusChange(PerformanceStatus status) {
final int selectedIndex = config.selection.index;
final int previousSelectedIndex = config.selection.previousIndex;
if (status == PerformanceStatus.forward) {
if (selectedIndex < previousSelectedIndex) {
_itemIndices = <int>[selectedIndex, previousSelectedIndex];
_scrollDirection = AnimationDirection.reverse;
} else {
_itemIndices = <int>[previousSelectedIndex, selectedIndex];
_scrollDirection = AnimationDirection.forward;
}
} else if (status == PerformanceStatus.completed) {
_initItemIndicesAndScrollPosition();
}
}
void _handleProgressChange() {
if (_scrollDirection == AnimationDirection.forward)
scrollTo(config.itemExtent * _performance.progress);
else
scrollTo(config.itemExtent * (1.0 - _performance.progress));
}
int get itemCount => _itemIndices.length;
List<Widget> buildItems(BuildContext context, int start, int count) {
return _itemIndices
.skip(start)
.take(count)
.map((int i) => config.itemBuilder(context, config.items[i], i))
.toList();
} }
} }
...@@ -8,6 +8,7 @@ import 'constants.dart'; ...@@ -8,6 +8,7 @@ import 'constants.dart';
import 'icon_theme.dart'; import 'icon_theme.dart';
import 'icon_theme_data.dart'; import 'icon_theme_data.dart';
import 'shadows.dart'; import 'shadows.dart';
import 'tabs.dart';
import 'theme.dart'; import 'theme.dart';
import 'typography.dart'; import 'typography.dart';
...@@ -18,6 +19,7 @@ class ToolBar extends StatelessComponent { ...@@ -18,6 +19,7 @@ class ToolBar extends StatelessComponent {
this.center, this.center,
this.right, this.right,
this.bottom, this.bottom,
this.tabBar,
this.elevation: 4, this.elevation: 4,
this.backgroundColor, this.backgroundColor,
this.textTheme, this.textTheme,
...@@ -28,6 +30,7 @@ class ToolBar extends StatelessComponent { ...@@ -28,6 +30,7 @@ class ToolBar extends StatelessComponent {
final Widget center; final Widget center;
final List<Widget> right; final List<Widget> right;
final Widget bottom; final Widget bottom;
final TabBar tabBar;
final int elevation; final int elevation;
final Color backgroundColor; final Color backgroundColor;
final TextTheme textTheme; final TextTheme textTheme;
...@@ -40,6 +43,7 @@ class ToolBar extends StatelessComponent { ...@@ -40,6 +43,7 @@ class ToolBar extends StatelessComponent {
center: center, center: center,
right: right, right: right,
bottom: bottom, bottom: bottom,
tabBar: tabBar,
elevation: elevation, elevation: elevation,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
textTheme: textTheme, textTheme: textTheme,
...@@ -90,6 +94,9 @@ class ToolBar extends StatelessComponent { ...@@ -90,6 +94,9 @@ class ToolBar extends StatelessComponent {
child: new Container(height: kExtendedToolBarHeight - kToolBarHeight, child: bottom) child: new Container(height: kExtendedToolBarHeight - kToolBarHeight, child: bottom)
)); ));
if (tabBar != null)
columnChildren.add(tabBar);
Widget content = new AnimatedContainer( Widget content = new AnimatedContainer(
duration: kThemeChangeDuration, duration: kThemeChangeDuration,
padding: new EdgeDims.symmetric(horizontal: 8.0), padding: new EdgeDims.symmetric(horizontal: 8.0),
......
...@@ -7,16 +7,13 @@ import 'package:flutter/material.dart'; ...@@ -7,16 +7,13 @@ import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
int selectedIndex = 2; TabBarSelection selection;
Widget buildFrame({ List<String> tabs, bool isScrollable: false }) { Widget buildFrame({ List<String> tabs, bool isScrollable: false }) {
return new TabBar( return new TabBar(
labels: tabs.map((String tab) => new TabLabel(text: tab)).toList(), labels: tabs.map((String tab) => new TabLabel(text: tab)).toList(),
selectedIndex: selectedIndex, selection: selection,
isScrollable: isScrollable, isScrollable: isScrollable
onChanged: (int tabIndex) {
selectedIndex = tabIndex;
}
); );
} }
...@@ -24,56 +21,56 @@ void main() { ...@@ -24,56 +21,56 @@ void main() {
test('TabBar tap selects tab', () { test('TabBar tap selects tab', () {
testWidgets((WidgetTester tester) { testWidgets((WidgetTester tester) {
List<String> tabs = <String>['A', 'B', 'C']; List<String> tabs = <String>['A', 'B', 'C'];
selectedIndex = 2; selection = new TabBarSelection(index: 2);
tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: false)); tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: false));
expect(tester.findText('A'), isNotNull); expect(tester.findText('A'), isNotNull);
expect(tester.findText('B'), isNotNull); expect(tester.findText('B'), isNotNull);
expect(tester.findText('C'), isNotNull); expect(tester.findText('C'), isNotNull);
expect(selectedIndex, equals(2)); expect(selection.index, equals(2));
tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: false)); tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: false));
tester.tap(tester.findText('B')); tester.tap(tester.findText('B'));
tester.pump(); tester.pump();
expect(selectedIndex, equals(1)); expect(selection.index, equals(1));
tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: false)); tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: false));
tester.tap(tester.findText('C')); tester.tap(tester.findText('C'));
tester.pump(); tester.pump();
expect(selectedIndex, equals(2)); expect(selection.index, equals(2));
tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: false)); tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: false));
tester.tap(tester.findText('A')); tester.tap(tester.findText('A'));
tester.pump(); tester.pump();
expect(selectedIndex, equals(0)); expect(selection.index, equals(0));
}); });
}); });
test('Scrollable TabBar tap selects tab', () { test('Scrollable TabBar tap selects tab', () {
testWidgets((WidgetTester tester) { testWidgets((WidgetTester tester) {
List<String> tabs = <String>['A', 'B', 'C']; List<String> tabs = <String>['A', 'B', 'C'];
selectedIndex = 2; selection = new TabBarSelection(index: 2);
tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: true)); tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: true));
expect(tester.findText('A'), isNotNull); expect(tester.findText('A'), isNotNull);
expect(tester.findText('B'), isNotNull); expect(tester.findText('B'), isNotNull);
expect(tester.findText('C'), isNotNull); expect(tester.findText('C'), isNotNull);
expect(selectedIndex, equals(2)); expect(selection.index, equals(2));
tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: true)); tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: true));
tester.tap(tester.findText('B')); tester.tap(tester.findText('B'));
tester.pump(); tester.pump();
expect(selectedIndex, equals(1)); expect(selection.index, equals(1));
tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: true)); tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: true));
tester.tap(tester.findText('C')); tester.tap(tester.findText('C'));
tester.pump(); tester.pump();
expect(selectedIndex, equals(2)); expect(selection.index, equals(2));
tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: true)); tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: true));
tester.tap(tester.findText('A')); tester.tap(tester.findText('A'));
tester.pump(); 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