diff --git a/examples/material_gallery/lib/demo/tabs_demo.dart b/examples/material_gallery/lib/demo/tabs_demo.dart index e4105d54df..74b1151586 100644 --- a/examples/material_gallery/lib/demo/tabs_demo.dart +++ b/examples/material_gallery/lib/demo/tabs_demo.dart @@ -9,16 +9,15 @@ import 'widget_demo.dart'; final List _iconNames = ["event", "home", "android", "alarm", "face", "language"]; Widget _buildTabBarSelection(_, Widget child) { - return new TabBarSelection( - maxIndex: _iconNames.length - 1, - child: child - ); + return new TabBarSelection(values: _iconNames, child: child); } Widget _buildTabBar(_) { - return new TabBar( + return new TabBar( isScrollable: true, - labels: _iconNames.map((String iconName) => new TabLabel(text: iconName, icon: "action/$iconName")).toList() + labels: new Map.fromIterable( + _iconNames, + value: (String iconName) => new TabLabel(text: iconName, icon: "action/$iconName")) ); } diff --git a/examples/stocks/lib/stock_home.dart b/examples/stocks/lib/stock_home.dart index 5b0f1899eb..3ecdac413b 100644 --- a/examples/stocks/lib/stock_home.dart +++ b/examples/stocks/lib/stock_home.dart @@ -161,17 +161,15 @@ class StockHomeState extends State { onPressed: _handleMenuShow ) ], - tabBar: new TabBar( - labels: [ - new TabLabel(text: StockStrings.of(context).market()), - new TabLabel(text: StockStrings.of(context).portfolio()) - ] + tabBar: new TabBar( + labels: { + StockHomeTab.market: new TabLabel(text: StockStrings.of(context).market()), + StockHomeTab.portfolio: new TabLabel(text: StockStrings.of(context).portfolio()) + } ) ); } - int selectedTabIndex = 0; - Iterable _getStockList(Iterable symbols) { return symbols.map((String symbol) => config.stocks[symbol]) .where((Stock stock) => stock != null); @@ -266,8 +264,8 @@ class StockHomeState extends State { } Widget build(BuildContext context) { - return new TabBarSelection( - maxIndex: 1, + return new TabBarSelection( + values: [StockHomeTab.market, StockHomeTab.portfolio], child: new Scaffold( key: _scaffoldKey, toolBar: _isSearching ? buildSearchBar() : buildToolBar(), diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart index 930b9716fb..ecbdfe39b9 100644 --- a/packages/flutter/lib/src/material/tabs.dart +++ b/packages/flutter/lib/src/material/tabs.dart @@ -387,81 +387,115 @@ abstract class TabBarSelectionPerformanceListener { void handleSelectionDeactivate(); } -class TabBarSelection extends StatefulComponent { +class TabBarSelection extends StatefulComponent { TabBarSelection({ Key key, - this.index, - this.maxIndex, + this.value, + this.values, this.onChanged, this.child }) : super(key: key) { + assert(values != null && values.length > 0); + assert(new Set.from(values).length == values.length); + assert(value == null ? true : values.where((T e) => e == value).length == 1); assert(child != null); - assert(maxIndex != null); - assert((index != null) ? index >= 0 && index <= maxIndex : true); } - final int index; - final int maxIndex; + final T value; + List values; + final ValueChanged onChanged; final Widget child; - final ValueChanged onChanged; - TabBarSelectionState createState() => new TabBarSelectionState(); + TabBarSelectionState createState() => new TabBarSelectionState(); static TabBarSelectionState of(BuildContext context) { - return context.ancestorStateOfType(TabBarSelectionState); + TabBarSelectionState result = null; + context.visitAncestorElements((ancestor) { + if (ancestor is StatefulComponentElement && ancestor.state is TabBarSelectionState) { + result = ancestor.state; + return false; + } + return true; + }); + return result; } } -class TabBarSelectionState extends State { +class TabBarSelectionState extends State> { PerformanceView get performance => _performance.view; // Both the TabBar and TabBarView classes access _performance because they // alternately drive selection progress between tabs. final _performance = new Performance(duration: _kTabBarScroll, progress: 1.0); + final Map _valueToIndex = new Map(); + + void _initValueToIndex() { + _valueToIndex.clear(); + int index = 0; + for(T value in values) + _valueToIndex[value] = index++; + } void initState() { super.initState(); - _index = config.index ?? PageStorage.of(context)?.readState(context) ?? 0; + _value = config.value ?? PageStorage.of(context)?.readState(context) ?? values.first; + _previousValue = _value; + _initValueToIndex(); + } + + void didUpdateConfig(TabBarSelection oldConfig) { + super.didUpdateConfig(oldConfig); + if (values != oldConfig.values) + _initValueToIndex(); } void dispose() { _performance.stop(); - PageStorage.of(context)?.writeState(context, _index); + PageStorage.of(context)?.writeState(context, _value); super.dispose(); } - bool _indexIsChanging = false; - bool get indexIsChanging => _indexIsChanging; + List get values => config.values; - int get index => _index; - int _index; - void set index(int value) { - if (value == _index) + T get previousValue => _previousValue; + T _previousValue; + + bool _valueIsChanging = false; + bool get valueIsChanging => _valueIsChanging; + + int indexOf(T tabValue) => _valueToIndex[tabValue]; + int get index => _valueToIndex[value]; + int get previousIndex => indexOf(_previousValue); + + T get value => _value; + T _value; + void set value(T newValue) { + if (newValue == _value) return; - if (!_indexIsChanging) - _previousIndex = _index; - _index = value; - _indexIsChanging = true; + if (!_valueIsChanging) + _previousValue = _value; + _value = newValue; + _valueIsChanging = true; - // If the selected index change was triggered by a drag gesture, the current + // If the selected value change was triggered by a drag gesture, the current // value of _performance.progress will reflect where the gesture ended. While // the drag was underway progress indicates where the indicator and TabBarView // scrollPosition are vis the indices of the two tabs adjacent to the selected // one. So 0.5 means the drag didn't move at all, 0.0 means the drag extended // to the beginning of the tab on the left and 1.0 likewise for the tab on the - // right. That is unless the selected index was 0 or maxIndex. In those cases - // progress just moves between the selected tab and the adjacent one. - // Convert progress to reflect the fact that we're now moving between (just) + // right. That is unless the index of the selected value was 0 or values.length - 1. + // In those cases progress just moves between the selected tab and the adjacent + // one. Convert progress to reflect the fact that we're now moving between (just) // the previous and current selection index. double progress; if (_performance.status == PerformanceStatus.completed) progress = 0.0; - else if (_previousIndex == 0) + else if (_previousValue == values.first) progress = _performance.progress; - else if (_previousIndex == config.maxIndex) + else if (_previousValue == values.last) progress = 1.0 - _performance.progress; - else if (_previousIndex < _index) + else if (previousIndex < index) progress = (_performance.progress - 0.5) * 2.0; else progress = 1.0 - _performance.progress * 2.0; @@ -471,15 +505,12 @@ class TabBarSelectionState extends State { ..forward().then((_) { if (_performance.progress == 1.0) { if (config.onChanged != null) - config.onChanged(_index); - _indexIsChanging = false; + config.onChanged(_value); + _valueIsChanging = false; } }); } - int get previousIndex => _previousIndex; - int _previousIndex = 0; - final List _performanceListeners = []; void registerPerformanceListener(TabBarSelectionPerformanceListener listener) { @@ -509,7 +540,6 @@ class TabBarSelectionState extends State { } } - /// Displays a horizontal row of tabs, one per label. If isScrollable is /// true then each tab is as wide as needed for its label and the entire /// [TabBar] is scrollable. Otherwise each tab gets an equal share of the @@ -517,34 +547,40 @@ class TabBarSelectionState extends State { /// built to enable saving and monitoring the selected tab. /// /// Tabs must always have an ancestor Material object. -class TabBar extends Scrollable { +class TabBar extends Scrollable { TabBar({ Key key, this.labels, this.isScrollable: false - }) : super(key: key, scrollDirection: ScrollDirection.horizontal) { - assert(labels != null); - assert(labels.length > 1); - } + }) : super(key: key, scrollDirection: ScrollDirection.horizontal); - final Iterable labels; + final Map labels; final bool isScrollable; _TabBarState createState() => new _TabBarState(); } -class _TabBarState extends ScrollableState implements TabBarSelectionPerformanceListener { +class _TabBarState extends ScrollableState> implements TabBarSelectionPerformanceListener { TabBarSelectionState _selection; - bool _indexIsChanging = false; + bool _valueIsChanging = false; - int get _tabCount => config.labels.length; + void _initSelection(TabBarSelectionState selection) { + _selection?.unregisterPerformanceListener(this); + _selection = selection; + _selection?.registerPerformanceListener(this); + } void initState() { super.initState(); scrollBehavior.isScrollable = config.isScrollable; - _selection = TabBarSelection.of(context); - _selection?.registerPerformanceListener(this); + _initSelection(TabBarSelection.of(context)); + } + + void didUpdateConfig(TabBar oldConfig) { + super.didUpdateConfig(oldConfig); + if (!config.isScrollable) + scrollTo(0.0); } void dispose() { @@ -557,20 +593,20 @@ class _TabBarState extends ScrollableState implements TabBarSelectionPer } void handleStatusChange(PerformanceStatus status) { - if (_tabCount == 0) + if (config.labels.length == 0) return; - if (_indexIsChanging && status == PerformanceStatus.completed) { - _indexIsChanging = false; + if (_valueIsChanging && status == PerformanceStatus.completed) { + _valueIsChanging = false; double progress = 0.5; if (_selection.index == 0) progress = 0.0; - else if (_selection.index == _tabCount - 1) + else if (_selection.index == config.labels.length - 1) progress = 1.0; setState(() { _indicatorRect ..begin = _tabIndicatorRect(math.max(0, _selection.index - 1)) - ..end = _tabIndicatorRect(math.min(_tabCount - 1, _selection.index + 1)) + ..end = _tabIndicatorRect(math.min(config.labels.length - 1, _selection.index + 1)) ..curve = null ..setProgress(progress, AnimationDirection.forward); }); @@ -578,17 +614,17 @@ class _TabBarState extends ScrollableState implements TabBarSelectionPer } void handleProgressChange() { - if (_tabCount == 0 || _selection == null) + if (config.labels.length == 0 || _selection == null) return; - if (!_indexIsChanging && _selection.indexIsChanging) { + if (!_valueIsChanging && _selection.valueIsChanging) { if (config.isScrollable) scrollTo(_centeredTabScrollOffset(_selection.index), duration: _kTabBarScroll); _indicatorRect ..begin = _indicatorRect.value ?? _tabIndicatorRect(_selection.previousIndex) ..end = _tabIndicatorRect(_selection.index) ..curve = Curves.ease; - _indexIsChanging = true; + _valueIsChanging = true; } Rect oldRect = _indicatorRect.value; _indicatorRect.setProgress(_selection.performance.progress, AnimationDirection.forward); @@ -620,12 +656,6 @@ class _TabBarState extends ScrollableState implements TabBarSelectionPer 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; @@ -639,7 +669,7 @@ class _TabBarState extends ScrollableState implements TabBarSelectionPer void _handleTabSelected(int tabIndex) { if (_selection != null && tabIndex != _selection.index) setState(() { - _selection.index = tabIndex; + _selection.value = _selection.values[tabIndex]; }); } @@ -649,7 +679,7 @@ class _TabBarState extends ScrollableState implements TabBarSelectionPer final bool isSelectedTab = tabIndex == _selection.index; final bool isPreviouslySelectedTab = tabIndex == _selection.previousIndex; labelColor = isSelectedTab ? selectedColor : color; - if (_selection.indexIsChanging) { + if (_selection.valueIsChanging) { if (isSelectedTab) labelColor = Color.lerp(color, selectedColor, _selection.performance.progress); else if (isPreviouslySelectedTab) @@ -686,14 +716,11 @@ class _TabBarState extends ScrollableState implements TabBarSelectionPer } Widget buildContent(BuildContext context) { - TabBarSelectionState oldSelection = _selection; - _selection = TabBarSelection.of(context); - if (oldSelection != _selection) { - oldSelection?.registerPerformanceListener(this); - _selection?.registerPerformanceListener(this); - } + TabBarSelectionState newSelection = TabBarSelection.of(context); + if (_selection != newSelection) + _initSelection(newSelection); - assert(config.labels != null && config.labels.isNotEmpty); + assert(config.labels.isNotEmpty); assert(Material.of(context) != null); ThemeData themeData = Theme.of(context); @@ -708,7 +735,7 @@ class _TabBarState extends ScrollableState implements TabBarSelectionPer List tabs = []; bool textAndIcons = false; int tabIndex = 0; - for (TabLabel label in config.labels) { + for (TabLabel label in config.labels.values) { tabs.add(_toTab(label, tabIndex++, textStyle.color, indicatorColor)); if (label.text != null && label.icon != null) textAndIcons = true; @@ -780,15 +807,19 @@ class _TabBarViewState extends PageableListState> implements } - void initState() { - super.initState(); - _selection = TabBarSelection.of(context); + void _initSelection(TabBarSelectionState selection) { + _selection = selection; if (_selection != null) { _selection.registerPerformanceListener(this); _initItemIndicesAndScrollPosition(); } } + void initState() { + super.initState(); + _initSelection(TabBarSelection.of(context)); + } + void dispose() { _selection?.unregisterPerformanceListener(this); super.dispose(); @@ -817,7 +848,7 @@ class _TabBarViewState extends PageableListState> implements } void handleProgressChange() { - if (_selection == null || !_selection.indexIsChanging) + if (_selection == null || !_selection.valueIsChanging) return; // The TabBar is driving the TabBarSelection performance. @@ -851,7 +882,7 @@ class _TabBarViewState extends PageableListState> implements int get itemCount => _itemIndices.length; void dispatchOnScroll() { - if (_selection == null || _selection.indexIsChanging) + if (_selection == null || _selection.valueIsChanging) return; // This class is driving the TabBarSelection's performance. @@ -864,36 +895,31 @@ class _TabBarViewState extends PageableListState> implements } Future fling(Offset scrollVelocity) { - // TODO(hansmuller): should not short-circuit in this case. - if (_selection == null || _selection.indexIsChanging) + if (_selection == null || _selection.valueIsChanging) return new Future.value(); if (scrollVelocity.dx.abs() > _kMinFlingVelocity) { final int selectionDelta = scrollVelocity.dx > 0 ? -1 : 1; - _selection.index = (_selection.index + selectionDelta).clamp(0, _tabCount - 1); + _selection.value = _selection.values[(_selection.index + selectionDelta).clamp(0, _tabCount - 1)]; return new Future.value(); } final int selectionIndex = _selection.index; final int settleIndex = snapScrollOffset(scrollOffset).toInt(); if (selectionIndex > 0 && settleIndex != 1) { - _selection.index += settleIndex == 2 ? 1 : -1; - return new Future.value(); + _selection.value = _selection.values[selectionIndex + (settleIndex == 2 ? 1 : -1)]; + return new Future.value(); } else if (selectionIndex == 0 && settleIndex == 1) { - _selection.index = 1; + _selection.value = _selection.values[1]; return new Future.value(); } return settleScrollOffset(); } List buildItems(BuildContext context, int start, int count) { - TabBarSelectionState oldSelection = _selection; - _selection = TabBarSelection.of(context); - if (oldSelection != _selection) { - oldSelection?.unregisterPerformanceListener(this); - _selection?.registerPerformanceListener(this); - } - + TabBarSelectionState newSelection = TabBarSelection.of(context); + if (_selection != newSelection) + _initSelection(newSelection); return _itemIndices .skip(start) .take(count) diff --git a/packages/flutter/test/widget/tabs_test.dart b/packages/flutter/test/widget/tabs_test.dart index cfc923fabf..ed1d45d0df 100644 --- a/packages/flutter/test/widget/tabs_test.dart +++ b/packages/flutter/test/widget/tabs_test.dart @@ -7,13 +7,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:test/test.dart'; -Widget buildFrame({ List tabs, bool isScrollable: false }) { +Widget buildFrame({ List tabs, String value, bool isScrollable: false }) { return new Material( - child: new TabBarSelection( - index: 2, - maxIndex: tabs.length - 1, - child: new TabBar( - labels: tabs.map((String tab) => new TabLabel(text: tab)).toList(), + child: new TabBarSelection( + value: value, + values: tabs, + child: new TabBar( + labels: new Map.fromIterable(tabs, value: (String tab) => new TabLabel(text: tab)), isScrollable: isScrollable ) ) @@ -25,28 +25,48 @@ void main() { testWidgets((WidgetTester tester) { List tabs = ['A', 'B', 'C']; - tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: false)); - TabBarSelectionState selection = tester.findStateOfType(TabBarSelectionState); + tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: false)); + TabBarSelectionState selection = TabBarSelection.of(tester.findText('A')); expect(selection, isNotNull); + expect(selection.indexOf('A'), equals(0)); + expect(selection.indexOf('B'), equals(1)); + expect(selection.indexOf('C'), equals(2)); expect(tester.findText('A'), isNotNull); expect(tester.findText('B'), isNotNull); expect(tester.findText('C'), isNotNull); expect(selection.index, equals(2)); + expect(selection.previousIndex, equals(2)); + expect(selection.value, equals('C')); + expect(selection.previousValue, equals('C')); - tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: false)); + tester.pumpWidget(buildFrame(tabs: tabs, value: 'C' ,isScrollable: false)); tester.tap(tester.findText('B')); tester.pump(); + expect(selection.valueIsChanging, true); + tester.pump(const Duration(seconds: 1)); // finish the animation + expect(selection.valueIsChanging, false); + expect(selection.value, equals('B')); + expect(selection.previousValue, equals('C')); expect(selection.index, equals(1)); + expect(selection.previousIndex, equals(2)); - tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: false)); + tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: false)); tester.tap(tester.findText('C')); tester.pump(); + tester.pump(const Duration(seconds: 1)); + expect(selection.value, equals('C')); + expect(selection.previousValue, equals('B')); expect(selection.index, equals(2)); + expect(selection.previousIndex, equals(1)); - tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: false)); + tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: false)); tester.tap(tester.findText('A')); tester.pump(); + tester.pump(const Duration(seconds: 1)); + expect(selection.value, equals('A')); + expect(selection.previousValue, equals('C')); expect(selection.index, equals(0)); + expect(selection.previousIndex, equals(2)); }); }); @@ -54,28 +74,28 @@ void main() { testWidgets((WidgetTester tester) { List tabs = ['A', 'B', 'C']; - tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: true)); - TabBarSelectionState selection = tester.findStateOfType(TabBarSelectionState); + tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: true)); + TabBarSelectionState selection = TabBarSelection.of(tester.findText('A')); expect(selection, isNotNull); expect(tester.findText('A'), isNotNull); expect(tester.findText('B'), isNotNull); expect(tester.findText('C'), isNotNull); - expect(selection.index, equals(2)); + expect(selection.value, equals('C')); - tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: true)); + tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: true)); tester.tap(tester.findText('B')); tester.pump(); - expect(selection.index, equals(1)); + expect(selection.value, equals('B')); - tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: true)); + tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: true)); tester.tap(tester.findText('C')); tester.pump(); - expect(selection.index, equals(2)); + expect(selection.value, equals('C')); - tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: true)); + tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: true)); tester.tap(tester.findText('A')); tester.pump(); - expect(selection.index, equals(0)); + expect(selection.value, equals('A')); }); }); }