diff --git a/examples/material_gallery/flutter.yaml b/examples/material_gallery/flutter.yaml index 72d5732578c..bfe9151df78 100644 --- a/examples/material_gallery/flutter.yaml +++ b/examples/material_gallery/flutter.yaml @@ -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 diff --git a/examples/material_gallery/lib/demo/tabs_demo.dart b/examples/material_gallery/lib/demo/tabs_demo.dart new file mode 100644 index 00000000000..0f2ae8e36e4 --- /dev/null +++ b/examples/material_gallery/lib/demo/tabs_demo.dart @@ -0,0 +1,59 @@ +// 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 _iconNames = ["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 { + double _viewWidth = 100.0; + + void _handleSizeChanged(Size newSize) { + setState(() { + _viewWidth = newSize.width; + }); + } + + Widget build(_) { + return new SizeObserver( + onSizeChanged: _handleSizeChanged, + child: new TabBarView( + selection: _selection, + items: _iconNames, + itemExtent: _viewWidth, + itemBuilder: (BuildContext context, String iconName, int index) { + return new Container( + key: new ValueKey(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() +); diff --git a/examples/material_gallery/lib/demo/widget_demo.dart b/examples/material_gallery/lib/demo/widget_demo.dart index 533c520189f..7d4daf44a35 100644 --- a/examples/material_gallery/lib/demo/widget_demo.dart +++ b/examples/material_gallery/lib/demo/widget_demo.dart @@ -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; } diff --git a/examples/material_gallery/lib/gallery_page.dart b/examples/material_gallery/lib/gallery_page.dart index 2936ba259f5..4b5d36e308e 100644 --- a/examples/material_gallery/lib/gallery_page.dart +++ b/examples/material_gallery/lib/gallery_page.dart @@ -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) ); diff --git a/examples/material_gallery/lib/main.dart b/examples/material_gallery/lib/main.dart index f7d4be98409..e80c5ce7279 100644 --- a/examples/material_gallery/lib/main.dart +++ b/examples/material_gallery/lib/main.dart @@ -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 _kDemos = [ kSelectionControlsDemo, kSliderDemo, kDatePickerDemo, + kTabsDemo, kTimePickerDemo, kDropDownDemo, ]; diff --git a/examples/stocks/lib/stock_home.dart b/examples/stocks/lib/stock_home.dart index 2ee05649da3..dfe23f44363 100644 --- a/examples/stocks/lib/stock_home.dart +++ b/examples/stocks/lib/stock_home.dart @@ -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 { final GlobalKey scaffoldKey = new GlobalKey(); + final TabBarSelection _tabBarSelection = new TabBarSelection(); bool _isSearching = false; String _searchQuery; @@ -160,7 +163,13 @@ class StockHomeState extends State { icon: "navigation/more_vert", onPressed: _handleMenuShow ) - ] + ], + tabBar: new TabBar( + selection: _tabBarSelection, + labels: [ + const TabLabel(text: 'MARKET'), + const TabLabel(text: 'PORTFOLIO')] + ) ); } @@ -208,25 +217,6 @@ class StockHomeState extends State { static const List portfolioSymbols = const ["AAPL","FIZZ", "FIVE", "FLAT", "ZINC", "ZNGA"]; - Widget buildTabNavigator() { - return new TabNavigator( - views: [ - 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 { ); } + Widget buildStockTab(BuildContext context, StockHomeTab tab, List stockSymbols) { + return new Container( + key: new ValueKey(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( + 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); + } + } + ) + ) ); } } diff --git a/examples/widgets/tabs.dart b/examples/widgets/tabs.dart deleted file mode 100644 index cd78032b29e..00000000000 --- a/examples/widgets/tabs.dart +++ /dev/null @@ -1,146 +0,0 @@ -// 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 { - // The index of the selected tab for each of the TabNavigators constructed below. - List selectedIndices = new List.filled(5, 0); - - TabNavigator _buildTabNavigator(int n, List 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 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('textLabelsTabNavigator')); - } - - TabNavigator _buildIconLabelsTabNavigator(int n) { - Iterable 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('iconLabelsTabNavigator')); - } - - TabNavigator _buildTextAndIconLabelsTabNavigator(int n) { - List views = [ - 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('textAndIconLabelsTabNavigator')); - } - - TabNavigator _buildScrollableTabNavigator(int n) { - Iterable 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('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 views = [ - 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('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: { - '/': (RouteArguments args) => new TabbedNavigatorApp(), - } - )); -} diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart index db49e9e3403..759fb5ee64b 100644 --- a/packages/flutter/lib/src/material/tabs.dart +++ b/packages/flutter/lib/src/material/tabs.dart @@ -292,14 +292,12 @@ class TabLabel { } } -class Tab extends StatelessComponent { - Tab({ +class _Tab extends StatelessComponent { + _Tab({ Key key, this.onSelected, this.label, - this.color, - this.selected: false, - this.selectedColor + this.color }) : super(key: key) { assert(label.text != null || label.icon != null); } @@ -307,19 +305,16 @@ class Tab extends StatelessComponent { final VoidCallback onSelected; final TabLabel label; final Color color; - final bool selected; - final Color selectedColor; Widget _buildLabelText() { 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); } Widget _buildLabelIcon() { assert(label.icon != null); - Color iconColor = selected ? selectedColor : color; - ColorFilter filter = new ColorFilter.mode(iconColor, TransferMode.srcATop); + ColorFilter filter = new ColorFilter.mode(color, TransferMode.srcATop); return new Icon(icon: label.icon, colorFilter: filter); } @@ -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 { TabBar({ Key key, this.labels, - this.selectedIndex: 0, - this.onChanged, + this.selection, this.isScrollable: false - }) : super(key: key, scrollDirection: ScrollDirection.horizontal); + }) : super(key: key, scrollDirection: ScrollDirection.horizontal) { + assert(labels != null); + assert(selection != null); + } final Iterable labels; - final int selectedIndex; - final TabSelectedIndexChanged onChanged; + final TabBarSelection selection; final bool isScrollable; _TabBarState createState() => new _TabBarState(); @@ -401,36 +424,53 @@ class TabBar extends Scrollable { class _TabBarState extends ScrollableState { void initState() { super.initState(); - _indicatorAnimation = new ValuePerformance() - ..duration = _kTabBarScroll - ..variable = new AnimatedRectValue(null, curve: Curves.ease); scrollBehavior.isScrollable = config.isScrollable; + config.selection._performance + ..addStatusListener(_handleStatusChange) + ..addListener(_handleProgressChange); + } + + void dispose() { + config.selection._performance + ..removeStatusListener(_handleStatusChange) + ..removeListener(_handleProgressChange) + ..stop(); + super.dispose(); + } + + Performance get _performance => config.selection._performance; + + bool _indicatorRectIsValid = false; + + // 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 + ..begin = _indicatorRect.value ?? _tabIndicatorRect(config.selection.previousIndex) + ..end = _tabIndicatorRect(config.selection.index); + }); + } + } + + void _handleProgressChange() { + // Performance listeners are notified before statusListeners. + if (_indicatorRectIsValid && _performance.status == PerformanceStatus.forward) { + setState(() { + _indicatorRect.setProgress(_performance.progress, AnimationDirection.forward); + }); + } } - Size _tabBarSize; Size _viewportSize = Size.zero; + Size _tabBarSize; List _tabWidths; - ValuePerformance _indicatorAnimation; - - void didUpdateConfig(TabBar oldConfig) { - super.didUpdateConfig(oldConfig); - if (!config.isScrollable) - scrollTo(0.0); - } - - AnimatedRectValue get _indicatorRect => _indicatorAnimation.variable; - - void _startIndicatorAnimation(int fromTabIndex, int toTabIndex) { - _indicatorRect - ..begin = (_indicatorRect.value == null ? _tabIndicatorRect(fromTabIndex) : _indicatorRect.value) - ..end = _tabIndicatorRect(toTabIndex); - _indicatorAnimation - ..progress = 0.0 - ..play(); - } - - ScrollBehavior createScrollBehavior() => new _TabsScrollBehavior(); - _TabsScrollBehavior get scrollBehavior => super.scrollBehavior; + AnimatedRectValue _indicatorRect = new AnimatedRectValue(null, curve: Curves.ease); Rect _tabRect(int tabIndex) { assert(_tabBarSize != null); @@ -450,31 +490,40 @@ class _TabBarState extends ScrollableState { 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 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); } void _handleTabSelected(int tabIndex) { - if (tabIndex != config.selectedIndex) { - if (_tabWidths != null) { - if (config.isScrollable) - scrollTo(_centeredTabScrollOffset(tabIndex), duration: _kTabBarScroll); - _startIndicatorAnimation(config.selectedIndex, tabIndex); - } - if (config.onChanged != null) - config.onChanged(tabIndex); - } + if (tabIndex != config.selection.index) + setState(() { + config.selection.index = tabIndex; + }); } Widget _toTab(TabLabel label, int tabIndex, Color color, Color selectedColor) { - return new Tab( - onSelected: () => _handleTabSelected(tabIndex), + Color labelColor = color; + 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, - color: color, - selected: tabIndex == config.selectedIndex, - selectedColor: selectedColor + color: labelColor ); } @@ -496,6 +545,8 @@ class _TabBarState extends ScrollableState { void _handleViewportSizeChanged(Size newSize) { _viewportSize = newSize; _updateScrollBehavior(); + if (config.isScrollable) + scrollTo(_centeredTabScrollOffset(config.selection.index), duration: _kTabBarScroll); } Widget buildContent(BuildContext context) { @@ -526,11 +577,11 @@ class _TabBarState extends ScrollableState { style: textStyle, child: new BuilderTransition( variables: >[_indicatorRect], - performance: _indicatorAnimation.view, + performance: config.selection.performance, builder: (BuildContext context) { return new _TabBarWrapper( children: tabs, - selectedIndex: config.selectedIndex, + selectedIndex: config.selection.index, indicatorColor: indicatorColor, indicatorRect: _indicatorRect.value, textAndIcons: textAndIcons, @@ -563,46 +614,111 @@ class _TabBarState extends ScrollableState { } } -class TabNavigatorView { - TabNavigatorView({ this.label, this.builder }) { - assert(builder != null); - } - - // this uses a builder for the contents, rather than a raw Widget child, - // 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; - final WidgetBuilder builder; -} - -class TabNavigator extends StatelessComponent { - TabNavigator({ +class TabBarView extends ScrollableList { + TabBarView({ Key key, - this.views, - this.selectedIndex: 0, - this.onChanged, - this.isScrollable: false - }) : super(key: key); + this.selection, + List items, + ItemBuilder itemBuilder, + double itemExtent + }) : super( + key: key, + scrollDirection: ScrollDirection.horizontal, + items: items, + itemBuilder: itemBuilder, + itemExtent: itemExtent, + itemsWrap: false + ) { + assert(selection != null); + } - final List views; - final int selectedIndex; - final TabSelectedIndexChanged onChanged; - final bool isScrollable; + final TabBarSelection selection; - Widget build(BuildContext context) { - assert(views != null && views.isNotEmpty); - assert(selectedIndex >= 0 && selectedIndex < views.length); - return new Column([ - new TabBar( - labels: views.map((TabNavigatorView view) => view.label), - onChanged: onChanged, - selectedIndex: selectedIndex, - isScrollable: isScrollable - ), - new Flexible(child: views[selectedIndex].builder(context)) - ], - alignItems: FlexAlignItems.stretch - ); + _TabBarViewState createState() => new _TabBarViewState(); +} + +// TODO(hansmuller): horizontal scrolling should drive the TabSelection's performance. +class _NotScrollable extends BoundedBehavior { + bool get isScrollable => false; +} + +class _TabBarViewState extends ScrollableListState> { + + ScrollBehavior createScrollBehavior() => new _NotScrollable(); + + List _itemIndices = [0, 1]; + AnimationDirection _scrollDirection = AnimationDirection.forward; + + void _initItemIndicesAndScrollPosition() { + final int selectedIndex = config.selection.index; + + if (selectedIndex == 0) { + _itemIndices = [0, 1]; + scrollTo(0.0); + } else if (selectedIndex == config.items.length - 1) { + _itemIndices = [selectedIndex - 1, selectedIndex]; + scrollTo(config.itemExtent); + } else { + _itemIndices = [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 = [selectedIndex, previousSelectedIndex]; + _scrollDirection = AnimationDirection.reverse; + } else { + _itemIndices = [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 buildItems(BuildContext context, int start, int count) { + return _itemIndices + .skip(start) + .take(count) + .map((int i) => config.itemBuilder(context, config.items[i], i)) + .toList(); } } diff --git a/packages/flutter/lib/src/material/tool_bar.dart b/packages/flutter/lib/src/material/tool_bar.dart index 99d89d13f51..bdb8803e5cb 100644 --- a/packages/flutter/lib/src/material/tool_bar.dart +++ b/packages/flutter/lib/src/material/tool_bar.dart @@ -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 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), diff --git a/packages/unit/test/widget/tabs_test.dart b/packages/unit/test/widget/tabs_test.dart index 2c2c2500ba6..7be3a9c30bc 100644 --- a/packages/unit/test/widget/tabs_test.dart +++ b/packages/unit/test/widget/tabs_test.dart @@ -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 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 tabs = ['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 tabs = ['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)); }); }); }