diff --git a/examples/material_gallery/lib/demo/tabs_demo.dart b/examples/material_gallery/lib/demo/tabs_demo.dart index 3d2e3ff2dd..e4105d54df 100644 --- a/examples/material_gallery/lib/demo/tabs_demo.dart +++ b/examples/material_gallery/lib/demo/tabs_demo.dart @@ -7,11 +7,16 @@ import 'package:flutter/material.dart'; import 'widget_demo.dart'; final List _iconNames = ["event", "home", "android", "alarm", "face", "language"]; -final TabBarSelection _selection = new TabBarSelection(maxIndex: _iconNames.length - 1); -Widget buildTabBar(_) { +Widget _buildTabBarSelection(_, Widget child) { + return new TabBarSelection( + maxIndex: _iconNames.length - 1, + child: child + ); +} + +Widget _buildTabBar(_) { return new TabBar( - selection: _selection, isScrollable: true, labels: _iconNames.map((String iconName) => new TabLabel(text: iconName, icon: "action/$iconName")).toList() ); @@ -24,7 +29,6 @@ class TabsDemo extends StatefulComponent { class _TabsDemoState extends State { Widget build(_) { return new TabBarView( - selection: _selection, items: _iconNames, itemBuilder: (BuildContext context, String iconName, int index) { return new Container( @@ -42,6 +46,7 @@ class _TabsDemoState extends State { final WidgetDemo kTabsDemo = new WidgetDemo( title: 'Tabs', routeName: '/tabs', - tabBarBuilder: buildTabBar, + tabBarBuilder: _buildTabBar, + pageWrapperBuilder: _buildTabBarSelection, builder: (_) => new TabsDemo() ); diff --git a/examples/material_gallery/lib/demo/widget_demo.dart b/examples/material_gallery/lib/demo/widget_demo.dart index d22c6775bb..1b6884b586 100644 --- a/examples/material_gallery/lib/demo/widget_demo.dart +++ b/examples/material_gallery/lib/demo/widget_demo.dart @@ -4,12 +4,22 @@ import 'package:flutter/material.dart'; +typedef Widget PageWrapperBuilder(BuildContext context, Widget child); + class WidgetDemo { - WidgetDemo({ this.title, this.routeName, this.tabBarBuilder, this.floatingActionButtonBuilder, this.builder }); + WidgetDemo({ + this.title, + this.routeName, + this.tabBarBuilder, + this.pageWrapperBuilder, + this.floatingActionButtonBuilder, + this.builder + }); final String title; final String routeName; final WidgetBuilder tabBarBuilder; + final PageWrapperBuilder pageWrapperBuilder; final WidgetBuilder floatingActionButtonBuilder; final WidgetBuilder builder; } diff --git a/examples/material_gallery/lib/gallery_page.dart b/examples/material_gallery/lib/gallery_page.dart index de6c4f79b5..175899e18c 100644 --- a/examples/material_gallery/lib/gallery_page.dart +++ b/examples/material_gallery/lib/gallery_page.dart @@ -63,20 +63,27 @@ class _GalleryPageState extends State { return builder != null ? builder(context) : null; } + Widget _buildPageWrapper(BuildContext context, Widget child) { + final PageWrapperBuilder builder = config.active?.pageWrapperBuilder; + return builder != null ? builder(context, child) : child; + } + Widget _buildFloatingActionButton() { final WidgetBuilder builder = config.active?.floatingActionButtonBuilder; return builder != null ? builder(context) : null; } Widget build(BuildContext context) { - return new Scaffold( - toolBar: new ToolBar( - center: new Text(config.active?.title ?? 'Flutter Material gallery'), - tabBar: _buildTabBar() - ), - drawer: _buildDrawer(), - floatingActionButton: _buildFloatingActionButton(), - body: _buildBody() + return _buildPageWrapper(context, + new Scaffold( + toolBar: new ToolBar( + center: new Text(config.active?.title ?? 'Flutter Material gallery'), + tabBar: _buildTabBar() + ), + drawer: _buildDrawer(), + floatingActionButton: _buildFloatingActionButton(), + body: _buildBody() + ) ); } } diff --git a/examples/stocks/lib/stock_home.dart b/examples/stocks/lib/stock_home.dart index 4975aacc58..5b0f1899eb 100644 --- a/examples/stocks/lib/stock_home.dart +++ b/examples/stocks/lib/stock_home.dart @@ -24,15 +24,9 @@ class StockHomeState extends State { final GlobalKey _scaffoldKey = new GlobalKey(); bool _isSearching = false; String _searchQuery; - TabBarSelection _tabBarSelection; void initState() { super.initState(); - _tabBarSelection = PageStorage.of(context)?.readState(context); - if (_tabBarSelection == null) { - _tabBarSelection = new TabBarSelection(maxIndex: 1); - PageStorage.of(context)?.writeState(context, _tabBarSelection); - } } void _handleSearchBegin() { @@ -168,7 +162,6 @@ class StockHomeState extends State { ) ], tabBar: new TabBar( - selection: _tabBarSelection, labels: [ new TabLabel(text: StockStrings.of(context).market()), new TabLabel(text: StockStrings.of(context).portfolio()) @@ -273,24 +266,26 @@ class StockHomeState extends State { } Widget build(BuildContext context) { - return new Scaffold( - key: _scaffoldKey, - toolBar: _isSearching ? buildSearchBar() : buildToolBar(), - floatingActionButton: buildFloatingActionButton(), - drawer: _buildDrawer(context), - body: new TabBarView( - selection: _tabBarSelection, - items: [StockHomeTab.market, StockHomeTab.portfolio], - 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); + return new TabBarSelection( + maxIndex: 1, + child: new Scaffold( + key: _scaffoldKey, + toolBar: _isSearching ? buildSearchBar() : buildToolBar(), + floatingActionButton: buildFloatingActionButton(), + drawer: _buildDrawer(context), + body: new TabBarView( + items: [StockHomeTab.market, StockHomeTab.portfolio], + 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/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart index 0dfbaf42bb..6b199b0518 100644 --- a/packages/flutter/lib/src/material/tabs.dart +++ b/packages/flutter/lib/src/material/tabs.dart @@ -381,32 +381,26 @@ class _TabsScrollBehavior extends BoundedBehavior { } } -class _TabBarSelection extends InheritedWidget { - _TabBarSelection({ - this.selection, - Key key, - Widget child - }) : super(key: key, child: child); - - final TabBarSelectionState selection; - - bool updateShouldNotify(_TabBarSelection oldWidget) => selection != oldWidget.selection; +abstract class TabBarSelectionPerformanceListener { + void handleStatusChange(PerformanceStatus status); + void handleProgressChange(); + void handleSelectionDeactivate(); } class TabBarSelection extends StatefulComponent { TabBarSelection({ Key key, - this.index: 0, + this.index, this.maxIndex, this.onChanged, - Widget this.child - }) { + this.child + }) : super(key: key) { + assert(child != null); assert(maxIndex != null); - assert(index != null); - assert(index >= 0 && index <= maxIndex); + assert((index != null) ? index >= 0 && index <= maxIndex : true); } - final int index; // TBD: this doesn't work yet... + final int index; final int maxIndex; final Widget child; final ValueChanged onChanged; @@ -414,20 +408,25 @@ class TabBarSelection extends StatefulComponent { TabBarSelectionState createState() => new TabBarSelectionState(); static TabBarSelectionState of(BuildContext context) { - _TabBarSelection widget = context.inheritFromWidgetOfType(_TabBarSelection); - return widget?.selection; + return context.ancestorStateOfType(TabBarSelectionState); } } 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. - PerformanceView get performance => _performance.view; final _performance = new Performance(duration: _kTabBarScroll, progress: 1.0); + void initState() { + super.initState(); + _index = config.index ?? PageStorage.of(context)?.readState(context) ?? 0; + } + void dispose() { _performance.stop(); + PageStorage.of(context)?.writeState(context, _index); super.dispose(); } @@ -435,11 +434,12 @@ class TabBarSelectionState extends State { bool get indexIsChanging => _indexIsChanging; int get index => _index; - int _index = 0; + int _index; void set index(int value) { if (value == _index) return; - _previousIndex = _index; + if (!_indexIsChanging) + _previousIndex = _index; _index = value; _indexIsChanging = true; @@ -469,17 +469,43 @@ class TabBarSelectionState extends State { _performance ..progress = progress ..forward().then((_) { - if (config.onChanged != null) - config.onChanged(_index); - _indexIsChanging = false; + if (_performance.progress == 1.0) { + if (config.onChanged != null) + config.onChanged(_index); + _indexIsChanging = false; + } }); } int get previousIndex => _previousIndex; int _previousIndex = 0; + final List _performanceListeners = []; + + void registerPerformanceListener(TabBarSelectionPerformanceListener listener) { + _performanceListeners.add(listener); + _performance + ..addStatusListener(listener.handleStatusChange) + ..addListener(listener.handleProgressChange); + } + + void unregisterPerformanceListener(TabBarSelectionPerformanceListener listener) { + _performanceListeners.remove(listener); + _performance + ..removeStatusListener(listener.handleStatusChange) + ..removeListener(listener.handleProgressChange); + } + + void deactivate() { + for (TabBarSelectionPerformanceListener listener in _performanceListeners.toList()) { + listener.handleSelectionDeactivate(); + unregisterPerformanceListener(listener); + } + assert(_performanceListeners.isEmpty); + } + Widget build(BuildContext context) { - return new _TabBarSelection(selection: this, child: config.child); + return config.child; } } @@ -507,40 +533,30 @@ class TabBar extends Scrollable { _TabBarState createState() => new _TabBarState(); } -class _TabBarState extends ScrollableState { +class _TabBarState extends ScrollableState implements TabBarSelectionPerformanceListener { TabBarSelectionState _selection; bool _indexIsChanging = false; int get _tabCount => config.labels.length; - void _addSelectionListeners(TabBarSelectionState selection) { - if (selection != null) { - selection._performance - ..addStatusListener(_handleStatusChange) - ..addListener(_handleProgressChange); - } - } - - void _removeSelectionListeners(TabBarSelectionState selection) { - if (selection != null) { - selection._performance - ..removeStatusListener(_handleStatusChange) - ..removeListener(_handleProgressChange); - } - } - void initState() { super.initState(); scrollBehavior.isScrollable = config.isScrollable; + _selection = TabBarSelection.of(context); + _selection?.registerPerformanceListener(this); } void dispose() { - _removeSelectionListeners(_selection); + _selection?.unregisterPerformanceListener(this); super.dispose(); } - void _handleStatusChange(PerformanceStatus status) { + void handleSelectionDeactivate() { + _selection = null; + } + + void handleStatusChange(PerformanceStatus status) { if (_tabCount == 0) return; @@ -561,7 +577,7 @@ class _TabBarState extends ScrollableState { } } - void _handleProgressChange() { + void handleProgressChange() { if (_tabCount == 0 || _selection == null) return; @@ -574,9 +590,11 @@ class _TabBarState extends ScrollableState { ..curve = Curves.ease; _indexIsChanging = true; } - setState(() { - _indicatorRect.setProgress(_selection.performance.progress, AnimationDirection.forward); - }); + Rect oldRect = _indicatorRect.value; + _indicatorRect.setProgress(_selection.performance.progress, AnimationDirection.forward); + Rect newRect = _indicatorRect.value; + if (oldRect != newRect) + setState(() { }); } Size _viewportSize = Size.zero; @@ -671,8 +689,8 @@ class _TabBarState extends ScrollableState { TabBarSelectionState oldSelection = _selection; _selection = TabBarSelection.of(context); if (oldSelection != _selection) { - _removeSelectionListeners(oldSelection); - _addSelectionListeners(_selection); + oldSelection?.registerPerformanceListener(this); + _selection?.registerPerformanceListener(this); } assert(config.labels != null && config.labels.isNotEmpty); @@ -746,7 +764,7 @@ class TabBarView extends PageableList { _TabBarViewState createState() => new _TabBarViewState(); } -class _TabBarViewState extends PageableListState> { +class _TabBarViewState extends PageableListState> implements TabBarSelectionPerformanceListener { TabBarSelectionState _selection; List _itemIndices = [0, 1]; @@ -754,16 +772,6 @@ class _TabBarViewState extends PageableListState> { int get _tabCount => config.items.length; - void _addSelectionListeners(TabBarSelectionState selection) { - if (selection != null) - selection._performance.addListener(_handleProgressChange); - } - - void _removeSelectionListeners(TabBarSelectionState selection) { - if (selection != null) - selection._performance.removeListener(_handleProgressChange); - } - BoundedBehavior _boundedBehavior; ExtentScrollBehavior get scrollBehavior { @@ -771,11 +779,25 @@ class _TabBarViewState extends PageableListState> { return _boundedBehavior; } + + void initState() { + super.initState(); + _selection = TabBarSelection.of(context); + if (_selection != null) { + _selection.registerPerformanceListener(this); + _initItemIndicesAndScrollPosition(); + } + } + void dispose() { - _removeSelectionListeners(_selection); + _selection?.unregisterPerformanceListener(this); super.dispose(); } + void handleSelectionDeactivate() { + _selection = null; + } + void _initItemIndicesAndScrollPosition() { assert(_selection != null); final int selectedIndex = _selection.index; @@ -791,7 +813,10 @@ class _TabBarViewState extends PageableListState> { } } - void _handleProgressChange() { + void handleStatusChange(PerformanceStatus status) { + } + + void handleProgressChange() { if (_selection == null || !_selection.indexIsChanging) return; // The TabBar is driving the TabBarSelection performance. @@ -865,10 +890,8 @@ class _TabBarViewState extends PageableListState> { TabBarSelectionState oldSelection = _selection; _selection = TabBarSelection.of(context); if (oldSelection != _selection) { - _removeSelectionListeners(oldSelection); - _addSelectionListeners(_selection); - if (_selection != null) - _initItemIndicesAndScrollPosition(); + oldSelection?.unregisterPerformanceListener(this); + _selection?.registerPerformanceListener(this); } return _itemIndices diff --git a/packages/flutter/lib/src/material/tool_bar.dart b/packages/flutter/lib/src/material/tool_bar.dart index f0a682977c..2b28ef42a8 100644 --- a/packages/flutter/lib/src/material/tool_bar.dart +++ b/packages/flutter/lib/src/material/tool_bar.dart @@ -8,7 +8,6 @@ import 'constants.dart'; import 'icon_theme.dart'; import 'icon_theme_data.dart'; import 'material.dart'; -import 'tabs.dart'; import 'theme.dart'; import 'typography.dart'; @@ -30,7 +29,7 @@ class ToolBar extends StatelessComponent { final Widget center; final List right; final Widget bottom; - final TabBar tabBar; + final Widget tabBar; final int elevation; final Color backgroundColor; final TextTheme textTheme; diff --git a/packages/flutter/test/widget/tabs_test.dart b/packages/flutter/test/widget/tabs_test.dart index 1844b39062..cfc923fabf 100644 --- a/packages/flutter/test/widget/tabs_test.dart +++ b/packages/flutter/test/widget/tabs_test.dart @@ -7,14 +7,15 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:test/test.dart'; -TabBarSelection selection; - Widget buildFrame({ List tabs, bool isScrollable: false }) { return new Material( - child: new TabBar( - labels: tabs.map((String tab) => new TabLabel(text: tab)).toList(), - selection: selection, - isScrollable: isScrollable + child: new TabBarSelection( + index: 2, + maxIndex: tabs.length - 1, + child: new TabBar( + labels: tabs.map((String tab) => new TabLabel(text: tab)).toList(), + isScrollable: isScrollable + ) ) ); } @@ -23,9 +24,10 @@ void main() { test('TabBar tap selects tab', () { testWidgets((WidgetTester tester) { List tabs = ['A', 'B', 'C']; - selection = new TabBarSelection(index: 2, maxIndex: tabs.length - 1); tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: false)); + TabBarSelectionState selection = tester.findStateOfType(TabBarSelectionState); + expect(selection, isNotNull); expect(tester.findText('A'), isNotNull); expect(tester.findText('B'), isNotNull); expect(tester.findText('C'), isNotNull); @@ -51,9 +53,10 @@ void main() { test('Scrollable TabBar tap selects tab', () { testWidgets((WidgetTester tester) { List tabs = ['A', 'B', 'C']; - selection = new TabBarSelection(index: 2, maxIndex: tabs.length - 1); tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: true)); + TabBarSelectionState selection = tester.findStateOfType(TabBarSelectionState); + expect(selection, isNotNull); expect(tester.findText('A'), isNotNull); expect(tester.findText('B'), isNotNull); expect(tester.findText('C'), isNotNull);