Commit b23aed7a authored by Hans Muller's avatar Hans Muller Committed by GitHub

New Tabs API (#7387)

parent e82b18d4
...@@ -470,12 +470,12 @@ class ItemGalleryBox extends StatelessWidget { ...@@ -470,12 +470,12 @@ class ItemGalleryBox extends StatelessWidget {
return new SizedBox( return new SizedBox(
height: 200.0, height: 200.0,
child: new TabBarSelection<String>( child: new DefaultTabController(
values: tabNames, length: tabNames.length,
child: new Column( child: new Column(
children: <Widget>[ children: <Widget>[
new Expanded( new Expanded(
child: new TabBarView<String>( child: new TabBarView(
children: tabNames.map((String tabName) { children: tabNames.map((String tabName) {
return new Container( return new Container(
key: new Key('Tab $index - $tabName'), key: new Key('Tab $index - $tabName'),
...@@ -521,7 +521,7 @@ class ItemGalleryBox extends StatelessWidget { ...@@ -521,7 +521,7 @@ class ItemGalleryBox extends StatelessWidget {
) )
), ),
new Container( new Container(
child: new TabPageSelector<String>() child: new TabPageSelector()
) )
] ]
) )
......
...@@ -412,8 +412,6 @@ class AnimationDemo extends StatefulWidget { ...@@ -412,8 +412,6 @@ class AnimationDemo extends StatefulWidget {
} }
class _AnimationDemoState extends State<AnimationDemo> with TickerProviderStateMixin { class _AnimationDemoState extends State<AnimationDemo> with TickerProviderStateMixin {
static final GlobalKey<TabBarSelectionState<_ArcDemo>> _tabsKey = new GlobalKey<TabBarSelectionState<_ArcDemo>>();
List<_ArcDemo> _allDemos; List<_ArcDemo> _allDemos;
@override @override
...@@ -435,8 +433,7 @@ class _AnimationDemoState extends State<AnimationDemo> with TickerProviderStateM ...@@ -435,8 +433,7 @@ class _AnimationDemoState extends State<AnimationDemo> with TickerProviderStateM
]; ];
} }
Future<Null> _play() async { Future<Null> _play(_ArcDemo demo) async {
_ArcDemo demo = _tabsKey.currentState.value;
await demo.controller.forward(); await demo.controller.forward();
if (demo.key.currentState != null && demo.key.currentState.mounted) if (demo.key.currentState != null && demo.key.currentState.mounted)
demo.controller.reverse(); demo.controller.reverse();
...@@ -444,23 +441,26 @@ class _AnimationDemoState extends State<AnimationDemo> with TickerProviderStateM ...@@ -444,23 +441,26 @@ class _AnimationDemoState extends State<AnimationDemo> with TickerProviderStateM
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new TabBarSelection<_ArcDemo>( return new DefaultTabController(
key: _tabsKey, length: _allDemos.length,
values: _allDemos,
child: new Scaffold( child: new Scaffold(
appBar: new AppBar( appBar: new AppBar(
title: new Text('Animation'), title: new Text('Animation'),
bottom: new TabBar<_ArcDemo>( bottom: new TabBar(
labels: new Map<_ArcDemo, TabLabel>.fromIterable(_allDemos, value: (_ArcDemo demo) { tabs: _allDemos.map((_ArcDemo demo) => new Tab(text: demo.title)).toList(),
return new TabLabel(text: demo.title); ),
})
)
), ),
floatingActionButton: new FloatingActionButton( floatingActionButton: new Builder(
onPressed: _play, builder: (BuildContext context) {
child: new Icon(Icons.refresh) return new FloatingActionButton(
child: new Icon(Icons.refresh),
onPressed: () {
_play(_allDemos[DefaultTabController.of(context).index]);
},
);
},
), ),
body: new TabBarView<_ArcDemo>( body: new TabBarView(
children: _allDemos.map((_ArcDemo demo) => demo.builder(demo)).toList() children: _allDemos.map((_ArcDemo demo) => demo.builder(demo)).toList()
) )
) )
......
...@@ -107,38 +107,28 @@ class ColorSwatchTabView extends StatelessWidget { ...@@ -107,38 +107,28 @@ class ColorSwatchTabView extends StatelessWidget {
} }
} }
class ColorsDemo extends StatefulWidget { class ColorsDemo extends StatelessWidget {
ColorsDemo({ Key key }) : super(key: key);
static const String routeName = '/colors'; static const String routeName = '/colors';
@override
_ColorsDemoState createState() => new _ColorsDemoState();
}
class _ColorsDemoState extends State<ColorsDemo> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new TabBarSelection<ColorSwatch>( return new DefaultTabController(
values: colorSwatches, length: colorSwatches.length,
child: new Scaffold( child: new Scaffold(
appBar: new AppBar( appBar: new AppBar(
elevation: 0, elevation: 0,
title: new Text('Colors'), title: new Text('Colors'),
bottom: new TabBar<ColorSwatch>( bottom: new TabBar(
isScrollable: true, isScrollable: true,
labels: new Map<ColorSwatch, TabLabel>.fromIterable(colorSwatches, value: (ColorSwatch swatch) { tabs: colorSwatches.map((ColorSwatch swatch) => new Tab(text: swatch.name)).toList(),
return new TabLabel(text: swatch.name);
})
) )
), ),
body: new TabBarView<ColorSwatch>( body: new TabBarView(
children: colorSwatches.map((ColorSwatch swatch) { children: colorSwatches.map((ColorSwatch swatch) {
return new ColorSwatchTabView(swatch: swatch); return new ColorSwatchTabView(swatch: swatch);
}) }).toList(),
.toList() ),
) ),
)
); );
} }
} }
...@@ -4,78 +4,83 @@ ...@@ -4,78 +4,83 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class PageSelectorDemo extends StatelessWidget { class _PageSelector extends StatelessWidget {
_PageSelector({ this.icons });
static const String routeName = '/page-selector'; final List<IconData> icons;
void _handleArrowButtonPress(BuildContext context, int delta) { void _handleArrowButtonPress(BuildContext context, int delta) {
final TabBarSelectionState<IconData> selection = TabBarSelection.of/*<IconData>*/(context); TabController controller = DefaultTabController.of(context);
if (!selection.valueIsChanging) if (!controller.indexIsChanging)
selection.value = selection.values[(selection.index + delta).clamp(0, selection.values.length - 1)]; controller.animateTo(controller.index + delta);
} }
@override @override
Widget build(BuildContext notUsed) { // Can't find the TabBarSelection from this context. Widget build(BuildContext context) {
final List<IconData> icons = <IconData>[ final TabController controller = DefaultTabController.of(context);
Icons.event, final Color color = Theme.of(context).accentColor;
Icons.home, return new Column(
Icons.android, children: <Widget>[
Icons.alarm, new Container(
Icons.face, margin: const EdgeInsets.only(top: 16.0),
Icons.language, child: new Row(
]; children: <Widget>[
new IconButton(
icon: new Icon(Icons.chevron_left),
color: color,
onPressed: () { _handleArrowButtonPress(context, -1); },
tooltip: 'Page back'
),
new TabPageSelector(controller: controller),
new IconButton(
icon: new Icon(Icons.chevron_right),
color: color,
onPressed: () { _handleArrowButtonPress(context, 1); },
tooltip: 'Page forward'
)
],
mainAxisAlignment: MainAxisAlignment.spaceBetween
)
),
new Expanded(
child: new TabBarView(
children: icons.map((IconData icon) {
return new Container(
key: new ObjectKey(icon),
padding: const EdgeInsets.all(12.0),
child: new Card(
child: new Center(
child: new Icon(icon, size: 128.0, color: color)
),
),
);
}).toList()
),
),
],
);
}
}
class PageSelectorDemo extends StatelessWidget {
static const String routeName = '/page-selector';
static final List<IconData> icons = <IconData>[
Icons.event,
Icons.home,
Icons.android,
Icons.alarm,
Icons.face,
Icons.language,
];
@override
Widget build(BuildContext context) {
return new Scaffold( return new Scaffold(
appBar: new AppBar(title: new Text('Page selector')), appBar: new AppBar(title: new Text('Page selector')),
body: new TabBarSelection<IconData>( body: new DefaultTabController(
values: icons, length: icons.length,
child: new Builder( child: new _PageSelector(icons: icons),
builder: (BuildContext context) { ),
final Color color = Theme.of(context).accentColor;
return new Column(
children: <Widget>[
new Container(
margin: const EdgeInsets.only(top: 16.0),
child: new Row(
children: <Widget>[
new IconButton(
icon: new Icon(Icons.chevron_left),
color: color,
onPressed: () { _handleArrowButtonPress(context, -1); },
tooltip: 'Page back'
),
new TabPageSelector<IconData>(),
new IconButton(
icon: new Icon(Icons.chevron_right),
color: color,
onPressed: () { _handleArrowButtonPress(context, 1); },
tooltip: 'Page forward'
)
],
mainAxisAlignment: MainAxisAlignment.spaceBetween
)
),
new Expanded(
child: new TabBarView<IconData>(
children: icons.map((IconData icon) {
return new Container(
key: new ObjectKey(icon),
padding: const EdgeInsets.all(12.0),
child: new Card(
child: new Center(
child: new Icon(icon, size: 128.0, color: color)
)
)
);
})
.toList()
)
)
]
);
}
)
)
); );
} }
} }
...@@ -10,6 +10,21 @@ enum TabsDemoStyle { ...@@ -10,6 +10,21 @@ enum TabsDemoStyle {
textOnly textOnly
} }
class _Page {
_Page({ this.icon, this.text });
final IconData icon;
final String text;
}
final List<_Page> _allPages = <_Page>[
new _Page(icon: Icons.event, text: 'EVENT'),
new _Page(icon: Icons.home, text: 'HOME'),
new _Page(icon: Icons.android, text: 'ANDROID'),
new _Page(icon: Icons.alarm, text: 'ALARM'),
new _Page(icon: Icons.face, text: 'FACE'),
new _Page(icon: Icons.language, text: 'LANGAUGE'),
];
class ScrollableTabsDemo extends StatefulWidget { class ScrollableTabsDemo extends StatefulWidget {
static const String routeName = '/scrollable-tabs'; static const String routeName = '/scrollable-tabs';
...@@ -17,26 +32,21 @@ class ScrollableTabsDemo extends StatefulWidget { ...@@ -17,26 +32,21 @@ class ScrollableTabsDemo extends StatefulWidget {
ScrollableTabsDemoState createState() => new ScrollableTabsDemoState(); ScrollableTabsDemoState createState() => new ScrollableTabsDemoState();
} }
class ScrollableTabsDemoState extends State<ScrollableTabsDemo> { class ScrollableTabsDemoState extends State<ScrollableTabsDemo> with SingleTickerProviderStateMixin {
final List<IconData> icons = <IconData>[ TabController _controller;
Icons.event, TabsDemoStyle _demoStyle = TabsDemoStyle.iconsAndText;
Icons.home,
Icons.android,
Icons.alarm,
Icons.face,
Icons.language,
];
final Map<IconData, String> labels = <IconData, String>{ @override
Icons.event: 'EVENT', void initState() {
Icons.home: 'HOME', super.initState();
Icons.android: 'ANDROID', _controller = new TabController(vsync: this, length: _allPages.length);
Icons.alarm: 'ALARM', }
Icons.face: 'FACE',
Icons.language: 'LANGUAGE',
};
TabsDemoStyle _demoStyle = TabsDemoStyle.iconsAndText; @override
void dispose() {
_controller.dispose();
super.dispose();
}
void changeDemoStyle(TabsDemoStyle style) { void changeDemoStyle(TabsDemoStyle style) {
setState(() { setState(() {
...@@ -47,65 +57,61 @@ class ScrollableTabsDemoState extends State<ScrollableTabsDemo> { ...@@ -47,65 +57,61 @@ class ScrollableTabsDemoState extends State<ScrollableTabsDemo> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Color iconColor = Theme.of(context).accentColor; final Color iconColor = Theme.of(context).accentColor;
return new TabBarSelection<IconData>( return new Scaffold(
values: icons, appBar: new AppBar(
child: new Scaffold( title: new Text('Scrollable tabs'),
appBar: new AppBar( actions: <Widget>[
title: new Text('Scrollable tabs'), new PopupMenuButton<TabsDemoStyle>(
actions: <Widget>[ onSelected: changeDemoStyle,
new PopupMenuButton<TabsDemoStyle>( itemBuilder: (BuildContext context) => <PopupMenuItem<TabsDemoStyle>>[
onSelected: changeDemoStyle, new PopupMenuItem<TabsDemoStyle>(
itemBuilder: (BuildContext context) => <PopupMenuItem<TabsDemoStyle>>[ value: TabsDemoStyle.iconsAndText,
new PopupMenuItem<TabsDemoStyle>( child: new Text('Icons and text')
value: TabsDemoStyle.iconsAndText, ),
child: new Text('Icons and text') new PopupMenuItem<TabsDemoStyle>(
), value: TabsDemoStyle.iconsOnly,
new PopupMenuItem<TabsDemoStyle>( child: new Text('Icons only')
value: TabsDemoStyle.iconsOnly, ),
child: new Text('Icons only') new PopupMenuItem<TabsDemoStyle>(
), value: TabsDemoStyle.textOnly,
new PopupMenuItem<TabsDemoStyle>( child: new Text('Text only')
value: TabsDemoStyle.textOnly, ),
child: new Text('Text only') ],
), ),
] ],
) bottom: new TabBar(
], controller: _controller,
bottom: new TabBar<IconData>( isScrollable: true,
isScrollable: true, tabs: _allPages.map((_Page page) {
labels: new Map<IconData, TabLabel>.fromIterable( switch(_demoStyle) {
icons, case TabsDemoStyle.iconsAndText:
value: (IconData icon) { return new Tab(text: page.text, icon: new Icon(page.icon));
switch(_demoStyle) { case TabsDemoStyle.iconsOnly:
case TabsDemoStyle.iconsAndText: return new Tab(icon: new Icon(page.icon));
return new TabLabel(text: labels[icon], icon: new Icon(icon)); case TabsDemoStyle.textOnly:
case TabsDemoStyle.iconsOnly: return new Tab(text: page.text);
return new TabLabel(icon: new Icon(icon)); }
case TabsDemoStyle.textOnly: }).toList(),
return new TabLabel(text: labels[icon]);
}
}
)
)
), ),
body: new TabBarView<IconData>( ),
children: icons.map((IconData icon) { body: new TabBarView(
return new Container( controller: _controller,
key: new ObjectKey(icon), children: _allPages.map((_Page page) {
padding: const EdgeInsets.all(12.0), return new Container(
child:new Card( key: new ObjectKey(page.icon),
child: new Center( padding: const EdgeInsets.all(12.0),
child: new Icon( child:new Card(
icon, child: new Center(
color: iconColor, child: new Icon(
size: 128.0 page.icon,
) color: iconColor,
) size: 128.0,
) ),
); ),
}).toList() ),
) );
) }).toList()
),
); );
} }
} }
...@@ -111,30 +111,21 @@ class _CardDataItem extends StatelessWidget { ...@@ -111,30 +111,21 @@ class _CardDataItem extends StatelessWidget {
} }
} }
class TabsDemo extends StatefulWidget { class TabsDemo extends StatelessWidget {
TabsDemo({ Key key }) : super(key: key);
static const String routeName = '/tabs'; static const String routeName = '/tabs';
@override
_TabsDemoState createState() => new _TabsDemoState();
}
class _TabsDemoState extends State<TabsDemo> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new TabBarSelection<_Page>( return new DefaultTabController(
values: _allPages.keys.toList(), length: _allPages.length,
child: new Scaffold( child: new Scaffold(
appBar: new AppBar( appBar: new AppBar(
title: new Text('Tabs and scrolling'), title: new Text('Tabs and scrolling'),
bottom: new TabBar<_Page>( bottom: new TabBar(
labels: new Map<_Page, TabLabel>.fromIterable(_allPages.keys, value: (_Page page) { tabs: _allPages.keys.map((_Page page) => new Tab(text: page.label)).toList(),
return new TabLabel(text: page.label); ),
})
)
), ),
body: new TabBarView<_Page>( body: new TabBarView(
children: _allPages.keys.map((_Page page) { children: _allPages.keys.map((_Page page) {
return new ScrollableList( return new ScrollableList(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
...@@ -144,11 +135,11 @@ class _TabsDemoState extends State<TabsDemo> { ...@@ -144,11 +135,11 @@ class _TabsDemoState extends State<TabsDemo> {
padding: const EdgeInsets.symmetric(vertical: 8.0), padding: const EdgeInsets.symmetric(vertical: 8.0),
child: new _CardDataItem(page: page, data: data) child: new _CardDataItem(page: page, data: data)
); );
}).toList() }).toList(),
); );
}).toList() }).toList(),
) ),
) ),
); );
} }
} }
...@@ -4,6 +4,12 @@ ...@@ -4,6 +4,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
const String _explanatoryText =
"When the Scaffold's floating action button changes, the new button fades and "
"turns into view. In this demo, changing tabs can cause the app to be rebuilt "
"with a FloatingActionButton that the Scaffold distinguishes from the others "
"by its key.";
class _Page { class _Page {
_Page({ this.label, this.colors, this.icon }); _Page({ this.label, this.colors, this.icon });
...@@ -11,7 +17,6 @@ class _Page { ...@@ -11,7 +17,6 @@ class _Page {
final Map<int, Color> colors; final Map<int, Color> colors;
final IconData icon; final IconData icon;
TabLabel get tabLabel => new TabLabel(text: label.toUpperCase());
Color get labelColor => colors != null ? colors[300] : Colors.grey[300]; Color get labelColor => colors != null ? colors[300] : Colors.grey[300];
bool get fabDefined => colors != null && icon != null; bool get fabDefined => colors != null && icon != null;
Color get fabColor => colors[400]; Color get fabColor => colors[400];
...@@ -19,11 +24,13 @@ class _Page { ...@@ -19,11 +24,13 @@ class _Page {
Key get fabKey => new ValueKey<Color>(fabColor); Key get fabKey => new ValueKey<Color>(fabColor);
} }
const String _explanatoryText = final List<_Page> _allPages = <_Page>[
"When the Scaffold's floating action button changes, the new button fades and " new _Page(label: 'Blue', colors: Colors.indigo, icon: Icons.add),
"turns into view. In this demo, changing tabs can cause the app to be rebuilt " new _Page(label: 'Eco', colors: Colors.green, icon: Icons.create),
"with a FloatingActionButton that the Scaffold distinguishes from the others " new _Page(label: 'No'),
"by its key."; new _Page(label: 'Teal', colors: Colors.teal, icon: Icons.add),
new _Page(label: 'Red', colors: Colors.red, icon: Icons.create),
];
class TabsFabDemo extends StatefulWidget { class TabsFabDemo extends StatefulWidget {
static const String routeName = '/tabs-fab'; static const String routeName = '/tabs-fab';
...@@ -32,31 +39,34 @@ class TabsFabDemo extends StatefulWidget { ...@@ -32,31 +39,34 @@ class TabsFabDemo extends StatefulWidget {
_TabsFabDemoState createState() => new _TabsFabDemoState(); _TabsFabDemoState createState() => new _TabsFabDemoState();
} }
class _TabsFabDemoState extends State<TabsFabDemo> { class _TabsFabDemoState extends State<TabsFabDemo> with SingleTickerProviderStateMixin {
final GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>(); final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
final List<_Page> pages = <_Page>[
new _Page(label: 'Blue', colors: Colors.indigo, icon: Icons.add), TabController _controller;
new _Page(label: 'Eco', colors: Colors.green, icon: Icons.create), _Page _selectedPage;
new _Page(label: 'No'),
new _Page(label: 'Teal', colors: Colors.teal, icon: Icons.add),
new _Page(label: 'Red', colors: Colors.red, icon: Icons.create),
];
_Page selectedPage;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
selectedPage = pages[0]; _controller = new TabController(vsync: this, length: _allPages.length);
_controller.addListener(_handleTabSelection);
_selectedPage = _allPages[0];
}
@override
void dispose() {
_controller.dispose();
super.dispose();
} }
void _handleTabSelection(_Page page) { void _handleTabSelection() {
setState(() { setState(() {
selectedPage = page; _selectedPage = _allPages[_controller.index];
}); });
} }
void _showExplanatoryText() { void _showExplanatoryText() {
scaffoldKey.currentState.showBottomSheet((BuildContext context) { _scaffoldKey.currentState.showBottomSheet((BuildContext context) {
return new Container( return new Container(
decoration: new BoxDecoration( decoration: new BoxDecoration(
border: new Border(top: new BorderSide(color: Theme.of(context).dividerColor)) border: new Border(top: new BorderSide(color: Theme.of(context).dividerColor))
...@@ -93,26 +103,26 @@ class _TabsFabDemoState extends State<TabsFabDemo> { ...@@ -93,26 +103,26 @@ class _TabsFabDemoState extends State<TabsFabDemo> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new TabBarSelection<_Page>( return new Scaffold(
values: pages, key: _scaffoldKey,
onChanged: _handleTabSelection, appBar: new AppBar(
child: new Scaffold( title: new Text('FAB per tab'),
key: scaffoldKey, bottom: new TabBar(
appBar: new AppBar( controller: _controller,
title: new Text('FAB per tab'), tabs: _allPages.map((_Page page) => new Tab(text: page.label.toUpperCase())).toList(),
bottom: new TabBar<_Page>( )
labels: new Map<_Page, TabLabel>.fromIterable(pages, value: (_Page page) => page.tabLabel) ),
) floatingActionButton: !_selectedPage.fabDefined ? null : new FloatingActionButton(
), key: _selectedPage.fabKey,
floatingActionButton: !selectedPage.fabDefined ? null : new FloatingActionButton( tooltip: 'Show explanation',
key: selectedPage.fabKey, backgroundColor: _selectedPage.fabColor,
tooltip: 'Show explanation', child: _selectedPage.fabIcon,
backgroundColor: selectedPage.fabColor, onPressed: _showExplanatoryText
child: selectedPage.fabIcon, ),
onPressed: _showExplanatoryText body: new TabBarView(
), controller: _controller,
body: new TabBarView<_Page>(children: pages.map(buildTabView).toList()) children: _allPages.map(buildTabView).toList()
) ),
); );
} }
} }
...@@ -20,13 +20,6 @@ class ComponentDemoTabData { ...@@ -20,13 +20,6 @@ class ComponentDemoTabData {
final String description; final String description;
final String tabName; final String tabName;
static Map<ComponentDemoTabData, TabLabel> buildTabLabels(List<ComponentDemoTabData> demos) {
return new Map<ComponentDemoTabData, TabLabel>.fromIterable(
demos,
value: (ComponentDemoTabData demo) => new TabLabel(text: demo.tabName)
);
}
@override @override
bool operator==(Object other) { bool operator==(Object other) {
if (other.runtimeType != runtimeType) if (other.runtimeType != runtimeType)
...@@ -49,8 +42,7 @@ class TabbedComponentDemoScaffold extends StatelessWidget { ...@@ -49,8 +42,7 @@ class TabbedComponentDemoScaffold extends StatelessWidget {
final String title; final String title;
void _showExampleCode(BuildContext context) { void _showExampleCode(BuildContext context) {
TabBarSelectionState<ComponentDemoTabData> selection = TabBarSelection.of(context); String tag = demos[DefaultTabController.of(context).index].exampleCodeTag;
String tag = selection.value?.exampleCodeTag;
if (tag != null) { if (tag != null) {
Navigator.push(context, new MaterialPageRoute<FullScreenCodeDialog>( Navigator.push(context, new MaterialPageRoute<FullScreenCodeDialog>(
builder: (BuildContext context) => new FullScreenCodeDialog(exampleCodeTag: tag) builder: (BuildContext context) => new FullScreenCodeDialog(exampleCodeTag: tag)
...@@ -60,8 +52,8 @@ class TabbedComponentDemoScaffold extends StatelessWidget { ...@@ -60,8 +52,8 @@ class TabbedComponentDemoScaffold extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new TabBarSelection<ComponentDemoTabData>( return new DefaultTabController(
values: demos, length: demos.length,
child: new Scaffold( child: new Scaffold(
appBar: new AppBar( appBar: new AppBar(
title: new Text(title), title: new Text(title),
...@@ -71,17 +63,19 @@ class TabbedComponentDemoScaffold extends StatelessWidget { ...@@ -71,17 +63,19 @@ class TabbedComponentDemoScaffold extends StatelessWidget {
return new IconButton( return new IconButton(
icon: new Icon(Icons.description), icon: new Icon(Icons.description),
tooltip: 'Show example code', tooltip: 'Show example code',
onPressed: () { _showExampleCode(context); } onPressed: () {
_showExampleCode(context);
},
); );
} },
) ),
], ],
bottom: new TabBar<ComponentDemoTabData>( bottom: new TabBar(
isScrollable: true, isScrollable: true,
labels: ComponentDemoTabData.buildTabLabels(demos) tabs: demos.map((ComponentDemoTabData data) => new Tab(text: data.tabName)).toList(),
) ),
), ),
body: new TabBarView<ComponentDemoTabData>( body: new TabBarView(
children: demos.map((ComponentDemoTabData demo) { children: demos.map((ComponentDemoTabData demo) {
return new Column( return new Column(
children: <Widget>[ children: <Widget>[
...@@ -92,11 +86,11 @@ class TabbedComponentDemoScaffold extends StatelessWidget { ...@@ -92,11 +86,11 @@ class TabbedComponentDemoScaffold extends StatelessWidget {
) )
), ),
new Expanded(child: demo.widget) new Expanded(child: demo.widget)
] ],
); );
}).toList() }).toList(),
) ),
) ),
); );
} }
} }
......
...@@ -222,11 +222,11 @@ class StockHomeState extends State<StockHome> { ...@@ -222,11 +222,11 @@ class StockHomeState extends State<StockHome> {
] ]
) )
], ],
bottom: new TabBar<StockHomeTab>( bottom: new TabBar(
labels: <StockHomeTab, TabLabel>{ tabs: <Widget>[
StockHomeTab.market: new TabLabel(text: StockStrings.of(context).market()), new Tab(text: StockStrings.of(context).market()),
StockHomeTab.portfolio: new TabLabel(text: StockStrings.of(context).portfolio()) new Tab(text: StockStrings.of(context).portfolio()),
} ]
) )
); );
} }
...@@ -318,14 +318,14 @@ class StockHomeState extends State<StockHome> { ...@@ -318,14 +318,14 @@ class StockHomeState extends State<StockHome> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new TabBarSelection<StockHomeTab>( return new DefaultTabController(
values: <StockHomeTab>[StockHomeTab.market, StockHomeTab.portfolio], length: 2,
child: new Scaffold( child: new Scaffold(
key: _scaffoldKey, key: _scaffoldKey,
appBar: _isSearching ? buildSearchBar() : buildAppBar(), appBar: _isSearching ? buildSearchBar() : buildAppBar(),
floatingActionButton: buildFloatingActionButton(), floatingActionButton: buildFloatingActionButton(),
drawer: _buildDrawer(context), drawer: _buildDrawer(context),
body: new TabBarView<StockHomeTab>( body: new TabBarView(
children: <Widget>[ children: <Widget>[
_buildStockTab(context, StockHomeTab.market, config.symbols), _buildStockTab(context, StockHomeTab.market, config.symbols),
_buildStockTab(context, StockHomeTab.portfolio, portfolioSymbols), _buildStockTab(context, StockHomeTab.portfolio, portfolioSymbols),
......
...@@ -71,6 +71,7 @@ export 'src/material/snack_bar.dart'; ...@@ -71,6 +71,7 @@ export 'src/material/snack_bar.dart';
export 'src/material/stepper.dart'; export 'src/material/stepper.dart';
export 'src/material/switch.dart'; export 'src/material/switch.dart';
export 'src/material/tabs.dart'; export 'src/material/tabs.dart';
export 'src/material/tab_controller.dart';
export 'src/material/theme.dart'; export 'src/material/theme.dart';
export 'src/material/theme_data.dart'; export 'src/material/theme_data.dart';
export 'src/material/time_picker.dart'; export 'src/material/time_picker.dart';
......
...@@ -22,3 +22,6 @@ const Duration kRadialReactionDuration = const Duration(milliseconds: 200); ...@@ -22,3 +22,6 @@ const Duration kRadialReactionDuration = const Duration(milliseconds: 200);
/// The value of the alpha channel to use when drawing a circular material ink response. /// The value of the alpha channel to use when drawing a circular material ink response.
const int kRadialReactionAlpha = 0x33; const int kRadialReactionAlpha = 0x33;
/// The duration
const Duration kTabScrollDuration = const Duration(milliseconds: 200);
// 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/foundation.dart';
import 'package:flutter/widgets.dart';
import 'constants.dart';
/// Coordinates tab selection between a [TabBar] and a [TabBarView].
///
/// The [index] property is the index of the selected tab and the [animation]
/// represents the current scroll positions of the tab bar and the tar bar view.
/// The selected tab's index can be changed with [animateTo].
///
/// See also:
///
/// * [DefaultTabController], which simplifies sharing a TabController with
/// its [TabBar] and a [TabBarView] descendants.
class TabController extends ChangeNotifier {
/// Creates an object that manages the state required by [TabBar] and a [TabBarView].
TabController({ int initialIndex: 0, @required this.length, @required TickerProvider vsync })
: _index = initialIndex,
_previousIndex = initialIndex,
_animationController = new AnimationController(
value: initialIndex.toDouble(),
upperBound: (length - 1).toDouble(),
vsync: vsync
) {
assert(length != null && length > 1);
assert(initialIndex != null && initialIndex >= 0 && initialIndex < length);
}
/// An animation whose value represents the current position of the [TabBar]'s
/// selected tab indicator as well as the scrollOffsets of the [TabBar]
/// and [TabBarView].
///
/// The animation's value ranges from 0.0 to [length] - 1.0. After the
/// selected tab is changed, the animation's value equals [index]. The
/// animation's value can be [offset] by +/- 1.0 to reflect [TabBarView]
/// drag scrolling.
final AnimationController _animationController;
Animation<double> get animation => _animationController.view;
/// The total number of tabs. Must be greater than one.
final int length;
void _changeIndex(int value, { Duration duration, Curve curve }) {
assert(value != null);
assert(value >= 0 && value < length);
assert(duration == null ? curve == null : true);
assert(_indexIsChangingCount >= 0);
if (value == _index)
return;
_previousIndex = index;
_index = value;
if (duration != null) {
_indexIsChangingCount += 1;
_animationController
..animateTo(_index.toDouble(), duration: duration, curve: curve).then((_) {
_indexIsChangingCount -= 1;
notifyListeners();
});
} else {
_indexIsChangingCount += 1;
_animationController.value = _index.toDouble();
_indexIsChangingCount -= 1;
notifyListeners();
}
}
/// The index of the currently selected tab. Changing the index also updates
/// [previousIndex], sets the [animation]'s value to index, resets
/// [indexIsChanging] to false, and notifies listeners.
///
/// To change the currently selected tab and play the [animation] use [animateTo].
int get index => _index;
int _index;
set index(int value) {
_changeIndex(value);
}
/// The index of the previously selected tab. Initially the same as [index].
int get previousIndex => _previousIndex;
int _previousIndex;
/// True while we're animating from [previousIndex] to [index].
bool get indexIsChanging => _indexIsChangingCount != 0;
int _indexIsChangingCount = 0;
/// Immediately sets [index] and [previousIndex] and then plays the
/// [animation] from its current value to [index].
///
/// While the animation is running [indexIsChanging] is true. When the
/// animation completes [offset] will be 0.0.
void animateTo(int value, { Duration duration: kTabScrollDuration, Curve curve: Curves.ease }) {
_changeIndex(value, duration: duration, curve: curve);
}
/// The difference between the [animation]'s value and [index]. The offset
/// value must be between -1.0 and 1.0.
///
/// This property is typically set by the [TabBarView] when the user
/// drags left or right. A value between -1.0 and 0.0 implies that the
/// TabBarView has been dragged to the left. Similarly a value between
/// 0.0 and 1.0 implies that the TabBarView has been dragged to the right.
double get offset => _animationController.value - _index.toDouble();
set offset(double newOffset) {
assert(newOffset != null);
assert(newOffset >= -1.0 && newOffset <= 1.0);
assert(!indexIsChanging);
if (newOffset == offset)
return;
_animationController.value = newOffset + _index.toDouble();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
}
class _TabControllerScope extends InheritedWidget {
_TabControllerScope({
Key key,
this.controller,
this.enabled,
Widget child
}) : super(key: key, child: child);
final TabController controller;
final bool enabled;
@override
bool updateShouldNotify(_TabControllerScope old) {
return enabled != old.enabled || controller != old.controller;
}
}
/// The [TabController] for descendant widgets that don't specify one explicitly.
class DefaultTabController extends StatefulWidget {
DefaultTabController({
Key key,
@required this.length,
this.initialIndex: 0,
this.child
}) : super(key: key);
/// The total number of tabs. Must be greater than one.
final int length;
/// The initial index of the selected tab.
final int initialIndex;
/// This widget's child. Often a [Scaffold] whose [AppBar] includes a [TabBar].
final Widget child;
/// The closest instance of this class that encloses the given context.
///
/// Typical usage:
///
/// ```dart
/// TabController controller = DefaultTabBarController.of(context);
/// ```
static TabController of(BuildContext context) {
_TabControllerScope scope = context.inheritFromWidgetOfExactType(_TabControllerScope);
return scope?.controller;
}
@override
_DefaultTabControllerState createState() => new _DefaultTabControllerState();
}
class _DefaultTabControllerState extends State<DefaultTabController> with SingleTickerProviderStateMixin {
TabController _controller;
@override
void initState() {
super.initState();
_controller = new TabController(
vsync: this,
length: config.length,
initialIndex: config.initialIndex,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return new _TabControllerScope(
controller: _controller,
enabled: TickerMode.of(context),
child: config.child,
);
}
}
...@@ -3,396 +3,80 @@ ...@@ -3,396 +3,80 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:math' as math; import 'dart:ui' show lerpDouble;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/physics.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'app_bar.dart'; import 'app_bar.dart';
import 'colors.dart'; import 'colors.dart';
import 'constants.dart';
import 'debug.dart'; import 'debug.dart';
import 'icon.dart';
import 'icon_theme.dart'; import 'icon_theme.dart';
import 'icon_theme_data.dart'; import 'icon_theme_data.dart';
import 'ink_well.dart'; import 'ink_well.dart';
import 'material.dart'; import 'material.dart';
import 'tab_controller.dart';
import 'theme.dart'; import 'theme.dart';
typedef void _TabLayoutChanged(Size size, List<double> widths);
// See https://material.google.com/components/tabs.html#tabs-specs
const double _kTabHeight = 46.0; const double _kTabHeight = 46.0;
const double _kTextAndIconTabHeight = 72.0; const double _kTextAndIconTabHeight = 72.0;
const double _kTabIndicatorHeight = 2.0; const double _kTabIndicatorHeight = 2.0;
const double _kMinTabWidth = 72.0; const double _kMinTabWidth = 72.0;
const double _kMaxTabWidth = 264.0; const double _kMaxTabWidth = 264.0;
const EdgeInsets _kTabLabelPadding = const EdgeInsets.symmetric(horizontal: 12.0); const EdgeInsets _kTabLabelPadding = const EdgeInsets.symmetric(horizontal: 12.0);
const double _kTabBarScrollDrag = 0.025;
const Duration _kTabBarScroll = const Duration(milliseconds: 200);
// Curves for the leading and trailing edge of the selected tab indicator.
const Curve _kTabIndicatorLeadingCurve = Curves.easeOut;
const Curve _kTabIndicatorTrailingCurve = Curves.easeIn;
// The additional factor of 5 is to further increase sensitivity to swipe
// gestures and was determined "experimentally".
final double _kMinFlingVelocity = kPixelScrollTolerance.velocity / 5.0;
class _TabBarParentData extends ContainerBoxParentDataMixin<RenderBox> { }
class _RenderTabBar extends RenderBox with
ContainerRenderObjectMixin<RenderBox, _TabBarParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, _TabBarParentData> {
_RenderTabBar(this.onLayoutChanged);
int _selectedIndex;
int get selectedIndex => _selectedIndex;
set selectedIndex(int value) {
if (_selectedIndex != value) {
_selectedIndex = value;
markNeedsPaint();
}
}
Color _indicatorColor;
Color get indicatorColor => _indicatorColor;
set indicatorColor(Color value) {
if (_indicatorColor != value) {
_indicatorColor = value;
markNeedsPaint();
}
}
Rect _indicatorRect;
Rect get indicatorRect => _indicatorRect;
set indicatorRect(Rect value) {
if (_indicatorRect != value) {
_indicatorRect = value;
markNeedsPaint();
}
}
bool _textAndIcons;
bool get textAndIcons => _textAndIcons;
set textAndIcons(bool value) {
if (_textAndIcons != value) {
_textAndIcons = value;
markNeedsLayout();
}
}
bool _isScrollable;
bool get isScrollable => _isScrollable;
set isScrollable(bool value) {
if (_isScrollable != value) {
_isScrollable = value;
markNeedsLayout();
}
}
@override
void setupParentData(RenderBox child) {
if (child.parentData is! _TabBarParentData)
child.parentData = new _TabBarParentData();
}
@override
double computeMinIntrinsicWidth(double height) {
double maxWidth = 0.0;
RenderBox child = firstChild;
while (child != null) {
maxWidth = math.max(maxWidth, child.getMinIntrinsicWidth(height));
final _TabBarParentData childParentData = child.parentData;
child = childParentData.nextSibling;
}
return isScrollable ? maxWidth : maxWidth * childCount;
}
@override
double computeMaxIntrinsicWidth(double height) {
double maxWidth = 0.0;
double totalWidth = 0.0;
RenderBox child = firstChild;
while (child != null) {
double childWidth = child.getMaxIntrinsicWidth(height);
maxWidth = math.max(maxWidth, childWidth);
totalWidth += childWidth;
final _TabBarParentData childParentData = child.parentData;
child = childParentData.nextSibling;
}
return isScrollable ? totalWidth : maxWidth * childCount;
}
double get _tabHeight => textAndIcons ? _kTextAndIconTabHeight : _kTabHeight; /// A material design [TabBar] tab. If both [icon] and [text] are
double get _tabBarHeight => _tabHeight + _kTabIndicatorHeight; /// provided, the text is displayed below the icon.
///
@override /// See also:
double computeMinIntrinsicHeight(double width) => _tabBarHeight; ///
/// * [TabBar], which displays a row of tabs.
@override /// * [TabBarView], which displays a widget for the currently selected tab.
double computeMaxIntrinsicHeight(double width) => _tabBarHeight; /// * [TabController], which coordinates tab selection between a [TabBar] and a [TabBarView].
/// * <https://material.google.com/components/tabs.html>
void layoutFixedWidthTabs() { class Tab extends StatelessWidget {
double tabWidth = size.width / childCount; /// Creates a material design [TabBar] tab. At least one of [text] and [icon]
BoxConstraints tabConstraints = /// must be non-null.
new BoxConstraints.tightFor(width: tabWidth, height: _tabHeight); Tab({
double x = 0.0;
RenderBox child = firstChild;
while (child != null) {
child.layout(tabConstraints);
final _TabBarParentData childParentData = child.parentData;
childParentData.offset = new Offset(x, 0.0);
x += tabWidth;
child = childParentData.nextSibling;
}
}
double layoutScrollableTabs() {
BoxConstraints tabConstraints = new BoxConstraints(
minWidth: _kMinTabWidth,
maxWidth: _kMaxTabWidth,
minHeight: _tabHeight,
maxHeight: _tabHeight
);
double x = 0.0;
RenderBox child = firstChild;
while (child != null) {
child.layout(tabConstraints, parentUsesSize: true);
final _TabBarParentData childParentData = child.parentData;
childParentData.offset = new Offset(x, 0.0);
x += child.size.width;
child = childParentData.nextSibling;
}
return x;
}
Size layoutSize;
List<double> layoutWidths;
_TabLayoutChanged onLayoutChanged;
void reportLayoutChangedIfNeeded() {
assert(onLayoutChanged != null);
List<double> widths = new List<double>(childCount);
if (!isScrollable && childCount > 0) {
double tabWidth = size.width / childCount;
widths.fillRange(0, widths.length, tabWidth);
} else if (isScrollable) {
RenderBox child = firstChild;
int childIndex = 0;
while (child != null) {
widths[childIndex++] = child.size.width;
final _TabBarParentData childParentData = child.parentData;
child = childParentData.nextSibling;
}
assert(childIndex == widths.length);
}
if (size != layoutSize || widths != layoutWidths) {
layoutSize = size;
layoutWidths = widths;
onLayoutChanged(layoutSize, layoutWidths);
}
}
@override
void performLayout() {
assert(constraints is BoxConstraints);
if (childCount == 0)
return;
if (isScrollable) {
double tabBarWidth = layoutScrollableTabs();
size = constraints.constrain(new Size(tabBarWidth, _tabBarHeight));
} else {
size = constraints.constrain(new Size(constraints.maxWidth, _tabBarHeight));
layoutFixedWidthTabs();
}
if (onLayoutChanged != null)
reportLayoutChangedIfNeeded();
}
@override
bool hitTestChildren(HitTestResult result, { Point position }) {
return defaultHitTestChildren(result, position: position);
}
void _paintIndicator(Canvas canvas, RenderBox selectedTab, Offset offset) {
if (indicatorColor == null)
return;
if (indicatorRect != null) {
canvas.drawRect(indicatorRect.shift(offset), new Paint()..color = indicatorColor);
return;
}
final Size size = new Size(selectedTab.size.width, _kTabIndicatorHeight);
final _TabBarParentData selectedTabParentData = selectedTab.parentData;
final Point point = new Point(
selectedTabParentData.offset.dx,
_tabBarHeight - _kTabIndicatorHeight
);
canvas.drawRect((point + offset) & size, new Paint()..color = indicatorColor);
}
@override
void paint(PaintingContext context, Offset offset) {
int index = 0;
RenderBox child = firstChild;
while (child != null) {
final _TabBarParentData childParentData = child.parentData;
context.paintChild(child, childParentData.offset + offset);
if (index++ == selectedIndex)
_paintIndicator(context.canvas, child, offset);
child = childParentData.nextSibling;
}
}
}
class _TabBarWrapper extends MultiChildRenderObjectWidget {
_TabBarWrapper({
Key key, Key key,
List<Widget> children, this.text,
this.selectedIndex, this.icon,
this.indicatorColor, }) : super(key: key) {
this.indicatorRect, assert(text != null || icon != null);
this.textAndIcons,
this.isScrollable: false,
this.onLayoutChanged
}) : super(key: key, children: children);
final int selectedIndex;
final Color indicatorColor;
final Rect indicatorRect;
final bool textAndIcons;
final bool isScrollable;
final _TabLayoutChanged onLayoutChanged;
@override
_RenderTabBar createRenderObject(BuildContext context) {
_RenderTabBar result = new _RenderTabBar(onLayoutChanged);
updateRenderObject(context, result);
return result;
}
@override
void updateRenderObject(BuildContext context, _RenderTabBar renderObject) {
renderObject
..selectedIndex = selectedIndex
..indicatorColor = indicatorColor
..indicatorRect = indicatorRect
..textAndIcons = textAndIcons
..isScrollable = isScrollable
..onLayoutChanged = onLayoutChanged;
} }
}
/// Signature for building icons for [TabLabel]s. /// The text to display as the tab's label.
///
/// Used by [TabLabel.iconBuilder].
typedef Widget TabLabelIconBuilder(BuildContext context, Color color);
/// Each TabBar tab can display either a title [text], an icon, or both. An icon
/// can be specified by either the [icon] or [iconBuilder] parameters. In either
/// case the icon will occupy a 24x24 box above the title text. If iconBuilder
/// is specified its color parameter is the color that an ordinary icon would
/// have been drawn with. The color reflects that tab's selection state.
class TabLabel {
/// Creates a tab label description.
///
/// At least one of [text], [icon], or [iconBuilder] must be non-null.
const TabLabel({ this.text, this.icon, this.iconBuilder });
/// The text to display as the label of the tab.
final String text; final String text;
/// The icon to display as the label of the tab. /// An icon to display as the tab's label.
///
/// The size and color of the icon is configured automatically using an
/// [IconTheme] and therefore does not need to be explicitly given in the
/// icon widget.
///
/// See [Icon], [ImageIcon].
final Widget icon; final Widget icon;
/// Called if [icon] is null to build an icon as a label for this tab.
///
/// The color argument to this builder is the color that an ordinary icon
/// would have been drawn with. The color reflects that tab's selection state.
///
/// Return value must be non-null.
final TabLabelIconBuilder iconBuilder;
/// Whether this label has any text (specified using [text]).
bool get hasText => text != null;
/// Whether this label has an icon (specified either using [icon] or [iconBuilder]).
bool get hasIcon => icon != null || iconBuilder != null;
}
class _Tab extends StatelessWidget {
_Tab({
Key key,
this.onSelected,
this.label,
this.color
}) : super(key: key) {
assert(label.hasText || label.hasIcon);
}
final VoidCallback onSelected;
final TabLabel label;
final Color color;
Widget _buildLabelText() { Widget _buildLabelText() {
assert(label.text != null); return new Text(text, softWrap: false, overflow: TextOverflow.fade);
TextStyle style = new TextStyle(color: color);
return new Text(
label.text,
style: style,
softWrap: false,
overflow: TextOverflow.fade
);
}
Widget _buildLabelIcon(BuildContext context) {
assert(label.hasIcon);
if (label.icon != null) {
return new IconTheme.merge(
context: context,
data: new IconThemeData(
color: color,
size: 24.0
),
child: label.icon
);
} else {
return new SizedBox(
width: 24.0,
height: 24.0,
child: label.iconBuilder(context, color)
);
}
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context)); assert(debugCheckHasMaterial(context));
Widget labelContent;
if (!label.hasIcon) { double height;
labelContent = _buildLabelText(); Widget label;
} else if (!label.hasText) { if (icon == null) {
labelContent = _buildLabelIcon(context); height = _kTabHeight;
label = _buildLabelText();
} else if (text == null) {
height = _kTabHeight;
label = icon;
} else { } else {
labelContent = new Column( height = _kTextAndIconTabHeight;
label = new Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[ children: <Widget>[
new Container( new Container(
child: _buildLabelIcon(context), child: icon,
margin: const EdgeInsets.only(bottom: 10.0) margin: const EdgeInsets.only(bottom: 10.0)
), ),
_buildLabelText() _buildLabelText()
...@@ -400,349 +84,260 @@ class _Tab extends StatelessWidget { ...@@ -400,349 +84,260 @@ class _Tab extends StatelessWidget {
); );
} }
Container centeredLabel = new Container( return new Container(
child: new Center(child: labelContent, widthFactor: 1.0, heightFactor: 1.0), padding: _kTabLabelPadding,
height: height,
constraints: const BoxConstraints(minWidth: _kMinTabWidth), constraints: const BoxConstraints(minWidth: _kMinTabWidth),
padding: _kTabLabelPadding child: new Center(child: label),
);
return new InkWell(
onTap: onSelected,
child: centeredLabel
); );
} }
@override @override
void debugFillDescription(List<String> description) { void debugFillDescription(List<String> description) {
super.debugFillDescription(description); super.debugFillDescription(description);
description.add('$label'); if (text != null)
description.add('text: $text');
if (icon != null)
description.add('icon: $icon');
} }
} }
class _TabsScrollBehavior extends BoundedBehavior { class _TabStyle extends AnimatedWidget {
_TabsScrollBehavior(); _TabStyle({
Key key,
Animation<double> animation,
this.selected,
this.labelColor,
this.child
}) : super(key: key, animation: animation);
@override final bool selected;
bool isScrollable = true; final Color labelColor;
final Widget child;
@override @override
Simulation createScrollSimulation(double position, double velocity) { Widget build(BuildContext context) {
if (!isScrollable) final ThemeData themeData = Theme.of(context);
return null; final TextStyle textStyle = themeData.primaryTextTheme.body2;
final Color selectedColor = labelColor ?? themeData.primaryTextTheme.body2.color;
return new BoundedFrictionSimulation( final Color unselectedColor = selectedColor.withAlpha(0xB2); // 70% alpha
_kTabBarScrollDrag, position, velocity, minScrollOffset, maxScrollOffset final Color color = selected
? Color.lerp(unselectedColor, selectedColor, animation.value)
: Color.lerp(selectedColor, unselectedColor, animation.value);
return new DefaultTextStyle(
style: textStyle.copyWith(color: color),
child: new IconTheme.merge(
context: context,
data: new IconThemeData(
size: 24.0,
color: color,
),
child: child,
),
); );
} }
@override
double applyCurve(double scrollOffset, double scrollDelta) {
return (isScrollable) ? super.applyCurve(scrollOffset, scrollDelta) : 0.0;
}
} }
/// An abstract interface through which [TabBarSelection] reports changes. class _TabLabelBarRenderer extends RenderFlex {
abstract class TabBarSelectionAnimationListener { _TabLabelBarRenderer({
/// Called when the status of the [TabBarSelection] animation changes. List<RenderBox> children,
void handleStatusChange(AnimationStatus status); Axis direction,
MainAxisSize mainAxisSize,
/// Called on each animation frame when the [TabBarSelection] animation ticks. MainAxisAlignment mainAxisAlignment,
void handleProgressChange(); CrossAxisAlignment crossAxisAlignment,
TextBaseline textBaseline,
/// Called when the [TabBarSelection] is deactivated. this.onPerformLayout,
/// }) : super(
/// Implementations typically drop their reference to the [TabBarSelection] children: children,
/// during this callback. direction: direction,
void handleSelectionDeactivate(); mainAxisSize: mainAxisSize,
} mainAxisAlignment: mainAxisAlignment,
crossAxisAlignment: crossAxisAlignment,
/// Coordinates the tab selection between a [TabBar] and a [TabBarView]. textBaseline: textBaseline,
/// ) {
/// Place a [TabBarSelection] widget in the tree such that it is a common assert(onPerformLayout != null);
/// ancestor of both the [TabBar] and the [TabBarView]. Both the [TabBar] and
/// the [TabBarView] can alter which tab is selected. They coodinate by
/// listening to the selection value stored in a common ancestor
/// [TabBarSelection] selection widget.
class TabBarSelection<T> extends StatefulWidget {
/// Creates a tab bar selection.
///
/// The values argument must be non-null, non-empty, and each value must be
/// unique. The value argument must either be null or contained in the values
/// argument. The child argument must be non-null.
TabBarSelection({
Key key,
this.value,
@required this.values,
this.onChanged,
@required this.child
}) : super(key: key) {
assert(values != null && values.isNotEmpty);
assert(new Set<T>.from(values).length == values.length);
assert(value == null ? true : values.where((T e) => e == value).length == 1);
assert(child != null);
} }
/// The current value of the selection. ValueChanged<List<double>> onPerformLayout;
final T value;
/// The list of possible values that the selection can obtain.
List<T> values;
/// Called when the value of the selection should change.
///
/// The tab bar selection passes the new value to the callback but does not
/// actually change state until the parent widget rebuilds the tab bar
/// selection with the new value.
///
/// If null, the tab bar selection cannot change value.
final ValueChanged<T> onChanged;
/// The widget below this widget in the tree.
final Widget child;
@override
TabBarSelectionState<T> createState() => new TabBarSelectionState<T>();
/// The state from the closest instance of this class that encloses the given context.
///
/// Typical usage is as follows:
///
/// ```dart
/// TabBarSelectionState<Foo> tabState = TabBarSelection.of/*<Foo>*/(context);
/// ```
static TabBarSelectionState<dynamic/*=T*/> of/*<T>*/(BuildContext context) {
return context.ancestorStateOfType(const TypeMatcher<TabBarSelectionState<dynamic/*=T*/>>());
}
@override @override
void debugFillDescription(List<String> description) { void performLayout() {
super.debugFillDescription(description); super.performLayout();
description.add('current tab: $value'); RenderBox child = firstChild;
description.add('available tabs: $values'); final List<double> xOffsets = <double>[];
while (child != null) {
final FlexParentData childParentData = child.parentData;
xOffsets.add(childParentData.offset.dx);
assert(child.parentData == childParentData);
child = childParentData.nextSibling;
}
xOffsets.add(size.width); // So xOffsets[lastTabIndex + 1] is valid.
onPerformLayout(xOffsets);
} }
} }
/// State for a [TabBarSelection] widget. // This class and its renderer class only exist to report the widths of the tabs
/// // upon layout. The tab widths are only used at paint time (see _IndicatorPainter)
/// Subclasses of [TabBarSelection] typically use [State] objects that extend // or in response to input.
/// this class. class _TabLabelBar extends Flex {
class TabBarSelectionState<T> extends State<TabBarSelection<T>> with SingleTickerProviderStateMixin { _TabLabelBar({
Key key,
// Both the TabBar and TabBarView classes access _controller because they MainAxisAlignment mainAxisAlignment,
// alternately drive selection progress between tabs. CrossAxisAlignment crossAxisAlignment,
AnimationController _controller; List<Widget> children: const <Widget>[],
this.onPerformLayout,
/// An animation that updates as the selected tab changes. }) : super(
Animation<double> get animation => _controller.view; key: key,
children: children,
final Map<T, int> _valueToIndex = new Map<T, int>(); direction: Axis.horizontal,
mainAxisSize: MainAxisSize.max,
@override mainAxisAlignment: MainAxisAlignment.start,
void initState() { crossAxisAlignment: CrossAxisAlignment.center,
super.initState(); );
_controller = new AnimationController( final ValueChanged<List<double>> onPerformLayout;
duration: _kTabBarScroll,
value: 1.0, @override
vsync: this, RenderFlex createRenderObject(BuildContext context) {
return new _TabLabelBarRenderer(
direction: direction,
mainAxisAlignment: mainAxisAlignment,
mainAxisSize: mainAxisSize,
crossAxisAlignment: crossAxisAlignment,
textBaseline: textBaseline,
onPerformLayout: onPerformLayout,
); );
_value = config.value ?? PageStorage.of(context)?.readState(context) ?? values.first;
// If the selection's values have changed since the selected value was saved with
// PageStorage.writeState() then use the default.
if (!values.contains(_value))
_value = values.first;
_previousValue = _value;
_initValueToIndex();
} }
@override @override
void didUpdateConfig(TabBarSelection<T> oldConfig) { void updateRenderObject(BuildContext context, _TabLabelBarRenderer renderObject) {
super.didUpdateConfig(oldConfig); super.updateRenderObject(context, renderObject);
if (values != oldConfig.values) renderObject.onPerformLayout = onPerformLayout;
_initValueToIndex();
}
void _initValueToIndex() {
_valueToIndex.clear();
int index = 0;
for (T value in values)
_valueToIndex[value] = index++;
}
void _writeValue() {
PageStorage.of(context)?.writeState(context, _value);
} }
}
/// The list of possible values that the selection can obtain. double _indexChangeProgress(TabController controller) {
List<T> get values => config.values; if (!controller.indexIsChanging)
return 1.0;
final double controllerValue = controller.animation.value;
final double previousIndex = controller.previousIndex.toDouble();
final double currentIndex = controller.index.toDouble();
if (controllerValue == previousIndex)
return 0.0;
else if (controllerValue == currentIndex)
return 1.0;
else
return (controllerValue - previousIndex).abs() / (currentIndex - previousIndex).abs();
}
/// The previously selected value. class _IndicatorPainter extends CustomPainter {
/// _IndicatorPainter(this.controller) : super(repaint: controller.animation);
/// When the tab selection changes, the tab selection animates from the
/// previously selected value to the new value.
T get previousValue => _previousValue;
T _previousValue;
/// Whether the tab selection is in the process of animating from one value to
/// another.
// TODO(abarth): Try computing this value from _controller.state so we don't
// need to keep a separate bool in sync.
bool get valueIsChanging => _valueIsChanging;
bool _valueIsChanging = false;
/// The index of a given value in [values].
///
/// Runs in constant time.
int indexOf(T tabValue) => _valueToIndex[tabValue];
/// The index of the currently selected value. TabController controller;
int get index => _valueToIndex[value]; List<double> tabOffsets;
Color color;
Animatable<Rect> indicatorTween;
Rect currentRect;
/// The index of the previoulsy selected value. // tabOffsets[index] is the offset of the left edge of the tab at index, and
int get previousIndex => indexOf(_previousValue); // tabOffsets[tabOffsets.length] is the right edge of the last tab.
int get maxTabIndex => tabOffsets.length - 2;
/// The currently selected value. Rect indicatorRect(Size tabBarSize, int tabIndex) {
/// assert(tabOffsets != null && tabIndex >= 0 && tabIndex <= maxTabIndex);
/// Writing to this field will cause the tab selection to animate from the final double tabLeft = tabOffsets[tabIndex];
/// previous value to the new value. final double tabRight = tabOffsets[tabIndex + 1];
T get value => _value; final double tabTop = tabBarSize.height - _kTabIndicatorHeight;
T _value; return new Rect.fromLTWH(tabLeft, tabTop, tabRight - tabLeft, _kTabIndicatorHeight);
set value(T newValue) {
if (newValue == _value)
return;
_previousValue = _value;
_value = newValue;
_writeValue();
_valueIsChanging = true;
// If the selected value change was triggered by a drag gesture, the current
// value of _controller.value will reflect where the gesture ended. While
// the drag was underway the controller's value indicates where the indicator
// and TabBarView scrollPositions 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 index of the selected value was 0
// or values.length - 1. In those cases the controller's value just moves between
// the selected tab and the adjacent one. So: convert the controller's value
// here to reflect the fact that we're now moving between (just) the previous
// and current selection index.
double value;
if (_controller.status == AnimationStatus.completed)
value = 0.0;
else if (_previousValue == values.first)
value = _controller.value;
else if (_previousValue == values.last)
value = 1.0 - _controller.value;
else if (previousIndex < index)
value = (_controller.value - 0.5) * 2.0;
else
value = 1.0 - _controller.value * 2.0;
_controller
..value = value
..forward().then((_) {
// TODO(abarth): Consider using a status listener and checking for
// AnimationStatus.completed.
if (_controller.value == 1.0) {
if (config.onChanged != null)
config.onChanged(_value);
_valueIsChanging = false;
}
});
} }
final List<TabBarSelectionAnimationListener> _animationListeners = <TabBarSelectionAnimationListener>[]; @override
void paint(Canvas canvas, Size size) {
/// Calls listener methods every time the value or status of the selection animation changes. if (controller.indexIsChanging) {
/// final Rect targetRect = indicatorRect(size, controller.index);
/// Listeners can be removed with [removeAnimationListener]. currentRect = Rect.lerp(currentRect ?? targetRect, targetRect, _indexChangeProgress(controller));
void addAnimationListener(TabBarSelectionAnimationListener listener) { } else {
_animationListeners.add(listener); final int currentIndex = controller.index;
_controller final Rect left = currentIndex > 0 ? indicatorRect(size, currentIndex - 1) : null;
..addStatusListener(listener.handleStatusChange) final Rect middle = indicatorRect(size, currentIndex);
..addListener(listener.handleProgressChange); final Rect right = currentIndex < maxTabIndex ? indicatorRect(size, currentIndex + 1) : null;
}
final double index = controller.index.toDouble();
/// Stop calling listener methods every time the value or status of the animation changes. final double value = controller.animation.value;
/// if (value == index - 1.0)
/// Listeners can be added with [addAnimationListener]. currentRect = left ?? middle;
void removeAnimationListener(TabBarSelectionAnimationListener listener) { else if (value == index + 1.0)
_animationListeners.remove(listener); currentRect = right ?? middle;
_controller else if (value == index)
..removeStatusListener(listener.handleStatusChange) currentRect = middle;
..removeListener(listener.handleProgressChange); else if (value < index)
currentRect = left == null ? middle : Rect.lerp(middle, left, index - value);
else
currentRect = right == null ? middle : Rect.lerp(middle, right, value - index);
}
assert(currentRect != null);
canvas.drawRect(currentRect, new Paint()..color = color);
} }
@override static bool tabOffsetsNotEqual(List<double> a, List<double> b) {
void deactivate() { assert(a != null && b != null && a.length == b.length);
_controller.stop(); for(int i = 0; i < a.length; i++) {
for (TabBarSelectionAnimationListener listener in _animationListeners.toList()) { if (a[i] != b[i])
listener.handleSelectionDeactivate(); return true;
removeAnimationListener(listener);
} }
assert(_animationListeners.isEmpty); return false;
_writeValue();
super.deactivate();
} }
@override @override
Widget build(BuildContext context) { bool shouldRepaint(_IndicatorPainter old) {
return config.child; return controller != old.controller ||
tabOffsets?.length != old.tabOffsets?.length ||
tabOffsetsNotEqual(tabOffsets, old.tabOffsets);
} }
} }
// Used when the user is dragging the TabBar or the TabBarView left or right. class _ChangeAnimation extends Animation<double> with AnimationWithParentMixin<double> {
// Dragging from the selected tab to the left varies t between 0.5 and 0.0. _ChangeAnimation(this.controller);
// Dragging towards the tab on the right varies t between 0.5 and 1.0.
class _TabIndicatorTween extends Tween<Rect> {
_TabIndicatorTween({ Rect begin, this.middle, Rect end }) : super(begin: begin, end: end);
final Rect middle; final TabController controller;
@override @override
Rect lerp(double t) { Animation<double> get parent => controller.animation;
return t <= 0.5
? Rect.lerp(begin, middle, t * 2.0) @override
: Rect.lerp(middle, end, (t - 0.5) * 2.0); double get value => _indexChangeProgress(controller);
}
} }
/// A widget that displays a horizontal row of tabs, one per label. /// A material design widget that displays a horizontal row of tabs. Typically
/// /// created as part of an [AppBar] and in conjuction with a [TabBarView].
/// Requires one of its ancestors to be a [TabBarSelection] widget to enable
/// saving and monitoring the selected tab.
///
/// Requires one of its ancestors to be a [Material] widget.
/// ///
/// See also: /// If a [TabController] is not provided, then there must be a [DefaultTabController]
/// ancestor.
/// ///
/// * [TabBarSelection] /// Requires one of its ancestors to be a [Material] widget
/// * [TabBarView] class TabBar extends StatefulWidget implements AppBarBottomWidget {
/// * [AppBar.tabBar]
/// * <https://material.google.com/components/tabs.html>
class TabBar<T> extends Scrollable implements AppBarBottomWidget {
/// Creates a widget that displays a horizontal row of tabs, one per label.
///
/// The [labels] argument must not be null.
TabBar({ TabBar({
Key key, Key key,
@required this.labels, @required this.tabs,
this.controller,
this.isScrollable: false, this.isScrollable: false,
this.indicatorColor, this.indicatorColor,
this.labelColor this.labelColor,
}) : super(key: key, scrollDirection: Axis.horizontal) { }) : super(key: key) {
assert(labels != null); assert(tabs != null && tabs.length > 1);
assert(isScrollable != null);
} }
/// The labels to display in the tabs. /// Typically a list of [Tab] widgets.
final List<Widget> tabs;
/// This widget's selection and animation state.
/// ///
/// The [TabBarSelection.values] are used as keys for this map to determine /// If [TabController] is not provided, then the value of [DefaultTabController.of]
/// which tab label is selected. /// will be used.
final Map<T, TabLabel> labels; final TabController controller;
/// Whether this tab bar can be scrolled horizontally. /// Whether this tab bar can be scrolled horizontally.
/// ///
...@@ -760,543 +355,498 @@ class TabBar<T> extends Scrollable implements AppBarBottomWidget { ...@@ -760,543 +355,498 @@ class TabBar<T> extends Scrollable implements AppBarBottomWidget {
/// the color of the theme's body2 text color is used. /// the color of the theme's body2 text color is used.
final Color labelColor; final Color labelColor;
/// The height of the tab labels and indicator.
@override @override
double get bottomHeight { double get bottomHeight {
for (TabLabel label in labels.values) { for (Widget widget in tabs) {
if (label.hasText && label.hasIcon) if (widget is Tab) {
return _kTextAndIconTabHeight + _kTabIndicatorHeight; final Tab tab = widget;
if (tab.text != null && tab.icon != null)
return _kTextAndIconTabHeight + _kTabIndicatorHeight;
}
} }
return _kTabHeight + _kTabIndicatorHeight; return _kTabHeight + _kTabIndicatorHeight;
} }
@override @override
_TabBarState<T> createState() => new _TabBarState<T>(); _TabBarState createState() => new _TabBarState();
} }
class _TabBarState<T> extends ScrollableState<TabBar<T>> implements TabBarSelectionAnimationListener { class _TabBarState extends State<TabBar> {
TabBarSelectionState<T> _selection; final GlobalKey<ScrollableState> viewportKey = new GlobalKey<ScrollableState>();
bool _valueIsChanging = false;
int _lastSelectedIndex = -1;
void _initSelection(TabBarSelectionState<T> newSelection) { TabController _controller;
if (_selection == newSelection) _ChangeAnimation _changeAnimation;
_IndicatorPainter _indicatorPainter;
int _currentIndex;
void _updateTabController() {
TabController newController = config.controller ?? DefaultTabController.of(context);
if (newController == _controller)
return; return;
_selection?.removeAnimationListener(this);
_selection = newSelection;
_selection?.addAnimationListener(this);
if (_selection != null)
_lastSelectedIndex = _selection.index;
}
@override if (_controller != null)
void didUpdateConfig(TabBar<T> oldConfig) { _controller.animation.removeListener(_handleTick);
super.didUpdateConfig(oldConfig); _controller = newController;
if (config.isScrollable != oldConfig.isScrollable) { if (_controller != null) {
scrollBehavior.isScrollable = config.isScrollable; _controller.animation.addListener(_handleTick);
if (!config.isScrollable) _changeAnimation = new _ChangeAnimation(_controller);
scrollTo(0.0); _currentIndex = _controller.index;
final List<double> offsets = _indicatorPainter?.tabOffsets;
_indicatorPainter = new _IndicatorPainter(_controller)..tabOffsets = offsets;
} }
} }
@override @override
void dispose() { void dependenciesChanged() {
_selection?.removeAnimationListener(this); super.dependenciesChanged();
super.dispose(); _updateTabController();
} }
@override @override
void handleSelectionDeactivate() { void didUpdateConfig(TabBar oldConfig) {
_selection = null; super.didUpdateConfig(oldConfig);
} if (config.controller != oldConfig.controller)
_updateTabController();
// Initialize _indicatorTween for interactive dragging between the tab on the left
// and the tab on the right. In this case _selection.animation.value is 0.5 when
// the indicator is below the selected tab, 0.0 when it's under the left tab, and 1.0
// when it's under the tab on the right.
void _initIndicatorTweenForDrag() {
assert(!_valueIsChanging);
int index = _selection.index;
int beginIndex = math.max(0, index - 1);
int endIndex = math.min(config.labels.length - 1, index + 1);
if (beginIndex == index || endIndex == index) {
_indicatorTween = new RectTween(
begin: _tabIndicatorRect(beginIndex),
end: _tabIndicatorRect(endIndex)
);
} else {
_indicatorTween = new _TabIndicatorTween(
begin: _tabIndicatorRect(beginIndex),
middle: _tabIndicatorRect(index),
end: _tabIndicatorRect(endIndex)
);
}
}
// Initialize _indicatorTween for animating the selected tab indicator from the
// previously selected tab to the newly selected one. In this case
// _selection.animation.value is 0.0 when the indicator is below the previously
// selected tab, and 1.0 when it's under the newly selected one.
void _initIndicatorTweenForAnimation() {
assert(_valueIsChanging);
_indicatorTween = new RectTween(
begin: _indicatorRect ?? _tabIndicatorRect(_selection.previousIndex),
end: _tabIndicatorRect(_selection.index)
);
} }
@override @override
void handleStatusChange(AnimationStatus status) { void dispose() {
if (config.labels.isEmpty) if (_controller != null)
return; _controller.animation.removeListener(_handleTick);
// We don't own the _controller Animation, so it's not disposed here.
if (_valueIsChanging && status == AnimationStatus.completed) { super.dispose();
_valueIsChanging = false;
setState(() {
_initIndicatorTweenForDrag();
_indicatorRect = _tabIndicatorRect(_selection.index);
});
}
} }
@override // tabOffsets[index] is the offset of the left edge of the tab at index, and
void handleProgressChange() { // tabOffsets[tabOffsets.length] is the right edge of the last tab.
if (config.labels.isEmpty || _selection == null) int get maxTabIndex => _indicatorPainter.tabOffsets.length - 2;
return;
if (_lastSelectedIndex != _selection.index) {
_valueIsChanging = true;
if (config.isScrollable)
scrollTo(_centeredTabScrollOffset(_selection.index), duration: _kTabBarScroll);
_initIndicatorTweenForAnimation();
_lastSelectedIndex = _selection.index;
} else if (_indicatorTween == null) {
_initIndicatorTweenForDrag();
}
Rect oldRect = _indicatorRect; double _tabCenteredScrollOffset(ScrollableState viewport, int tabIndex) {
double t = _selection.animation.value; final List<double> tabOffsets = _indicatorPainter.tabOffsets;
assert(tabOffsets != null && tabIndex >= 0 && tabIndex <= maxTabIndex);
// When _valueIsChanging is false, we're animating based on drag gesture and final ExtentScrollBehavior scrollBehavior = viewport.scrollBehavior;
// want linear selected tab indicator motion. When _valueIsChanging is true, final double viewportWidth = scrollBehavior.containerExtent;
// a ticker is driving the selection change and we want to curve the animation. final double tabCenter = (tabOffsets[tabIndex] + tabOffsets[tabIndex + 1]) / 2.0;
// In this case the leading and trailing edges of the move at different rates. return (tabCenter - viewportWidth / 2.0)
// The easiest way to do this is to lerp 2 rects, and piece them together into 1. .clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset);
if (!_valueIsChanging) {
_indicatorRect = _indicatorTween.lerp(t);
} else {
Rect leftRect, rightRect;
if (_selection.index > _selection.previousIndex) {
// Moving to the right - right edge is leading.
rightRect = _indicatorTween.lerp(_kTabIndicatorLeadingCurve.transform(t));
leftRect = _indicatorTween.lerp(_kTabIndicatorTrailingCurve.transform(t));
} else {
// Moving to the left - left edge is leading.
leftRect = _indicatorTween.lerp(_kTabIndicatorLeadingCurve.transform(t));
rightRect = _indicatorTween.lerp(_kTabIndicatorTrailingCurve.transform(t));
}
_indicatorRect = new Rect.fromLTRB(
leftRect.left, leftRect.top, rightRect.right, rightRect.bottom
);
}
if (oldRect != _indicatorRect)
setState(() { /* The indicator rect has changed. */ });
}
Size _viewportSize = Size.zero;
Size _tabBarSize;
List<double> _tabWidths;
Rect _indicatorRect;
Tween<Rect> _indicatorTween;
Rect _tabRect(int tabIndex) {
assert(_tabBarSize != null);
assert(_tabWidths != null);
assert(tabIndex >= 0 && tabIndex < _tabWidths.length);
double tabLeft = 0.0;
if (tabIndex > 0)
tabLeft = _tabWidths.take(tabIndex).reduce((double sum, double width) => sum + width);
final double tabTop = 0.0;
final double tabBottom = _tabBarSize.height - _kTabIndicatorHeight;
final double tabRight = tabLeft + _tabWidths[tabIndex];
return new Rect.fromLTRB(tabLeft, tabTop, tabRight, tabBottom);
} }
Rect _tabIndicatorRect(int tabIndex) { void _scrollToCurrentIndex() {
Rect r = _tabRect(tabIndex); final ScrollableState viewport = viewportKey.currentState;
return new Rect.fromLTRB(r.left, r.bottom, r.right, r.bottom + _kTabIndicatorHeight); final double offset = _tabCenteredScrollOffset(viewport, _currentIndex);
} viewport.scrollTo(offset, duration: kTabScrollDuration);
}
void _scrollToControllerValue() {
final ScrollableState viewport = viewportKey.currentState;
final double left = _currentIndex > 0 ? _tabCenteredScrollOffset(viewport, _currentIndex - 1) : null;
final double middle = _tabCenteredScrollOffset(viewport, _currentIndex);
final double right = _currentIndex < maxTabIndex ? _tabCenteredScrollOffset(viewport, _currentIndex + 1) : null;
final double index = _controller.index.toDouble();
final double value = _controller.animation.value;
double offset;
if (value == index - 1.0)
offset = left ?? middle;
else if (value == index + 1.0)
offset = right ?? middle;
else if (value == index)
offset = middle;
else if (value < index)
offset = left == null ? middle : lerpDouble(middle, left, index - value);
else
offset = right == null ? middle : lerpDouble(middle, right, value - index);
@override viewport.scrollTo(offset);
ExtentScrollBehavior createScrollBehavior() {
return new _TabsScrollBehavior()
..isScrollable = config.isScrollable;
} }
@override void _handleTick() {
_TabsScrollBehavior get scrollBehavior => super.scrollBehavior; assert(mounted);
double _centeredTabScrollOffset(int tabIndex) { if (_controller.indexIsChanging) {
double viewportWidth = scrollBehavior.containerExtent;
Rect tabRect = _tabRect(tabIndex);
return (tabRect.left + tabRect.width / 2.0 - viewportWidth / 2.0)
.clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset);
}
void _handleTabSelected(int tabIndex) {
if (_selection != null && tabIndex != _selection.index)
setState(() { setState(() {
_selection.value = _selection.values[tabIndex]; // Rebuild so that the tab label colors reflect the selected tab index.
// The first build for a new _controller.index value will also trigger
// a scroll to center the selected tab.
}); });
} else if (config.isScrollable) {
_scrollToControllerValue();
}
} }
Widget _toTab(TabLabel label, int tabIndex, Color color, Color selectedColor) { void _saveTabOffsets(List<double> tabOffsets) {
Color labelColor = color; _indicatorPainter?.tabOffsets = tabOffsets;
if (_selection != null) {
final bool isSelectedTab = tabIndex == _selection.index;
final bool isPreviouslySelectedTab = tabIndex == _selection.previousIndex;
labelColor = isSelectedTab ? selectedColor : color;
if (_selection.valueIsChanging) {
if (isSelectedTab)
labelColor = Color.lerp(color, selectedColor, _selection.animation.value);
else if (isPreviouslySelectedTab)
labelColor = Color.lerp(selectedColor, color, _selection.animation.value);
}
}
return new _Tab(
onSelected: () { _handleTabSelected(tabIndex); },
label: label,
color: labelColor
);
} }
void _updateScrollBehavior() { void _handleTap(int index) {
didUpdateScrollBehavior(scrollBehavior.updateExtents( assert(index >= 0 && index < config.tabs.length);
containerExtent: config.scrollDirection == Axis.vertical ? _viewportSize.height : _viewportSize.width, _controller.animateTo(index);
contentExtent: _tabWidths.reduce((double sum, double width) => sum + width),
scrollOffset: scrollOffset
));
} }
void _layoutChanged(Size tabBarSize, List<double> tabWidths) { @override
// This is bad. We should use a LayoutBuilder or CustomMultiChildLayout or some such. Widget build(BuildContext context) {
// As designed today, tabs are always lagging one frame behind, taking two frames final List<Widget> wrappedTabs = new List<Widget>.from(config.tabs, growable: false);
// to handle a layout change.
_tabBarSize = tabBarSize; // If the controller was provided by DefaultTabController and we're part
_tabWidths = tabWidths; // of a Hero (typically the AppBar), then we will not be able to find the
_indicatorRect = _selection != null ? _tabIndicatorRect(_selection.index) : Rect.zero; // controller during a Hero transition. See https://github.com/flutter/flutter/issues/213.
_updateScrollBehavior(); if (_controller != null) {
SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) { _indicatorPainter.color = config.indicatorColor ?? Theme.of(context).indicatorColor;
if (mounted) { if (_indicatorPainter.color == Material.of(context).color) {
setState(() { // ThemeData tries to avoid this by having indicatorColor avoid being the
// the changes were made at layout time // primaryColor. However, it's possible that the tab bar is on a
// TODO(ianh): remove this setState: https://github.com/flutter/flutter/issues/5749 // Material that isn't the primaryColor. In that case, if the indicator
}); // color ends up clashing, then this overrides it. When that happens,
// automatic transitions of the theme will likely look ugly as the
// indicator color suddenly snaps to white at one end, but it's not clear
// how to avoid that any further.
_indicatorPainter.color = Colors.white;
} }
});
}
Offset _handlePaintOffsetUpdateNeeded(ViewportDimensions dimensions) { if (_controller.index != _currentIndex) {
// We make various state changes here but don't have to do so in a _currentIndex = _controller.index;
// setState() callback because we are called during layout and all if (config.isScrollable)
// we're updating is the new offset, which we are providing to the _scrollToCurrentIndex();
// render object via our return value. }
_viewportSize = dimensions.containerSize;
_updateScrollBehavior();
if (config.isScrollable && _selection != null)
scrollTo(_centeredTabScrollOffset(_selection.index), duration: _kTabBarScroll);
return scrollOffsetToPixelDelta(scrollOffset);
}
@override final int previousIndex = _controller.previousIndex;
Widget buildContent(BuildContext context) {
TabBarSelectionState<T> newSelection = TabBarSelection.of(context);
_initSelection(newSelection);
assert(config.labels.isNotEmpty);
assert(Material.of(context) != null);
ThemeData themeData = Theme.of(context);
Color backgroundColor = Material.of(context).color;
Color indicatorColor = config.indicatorColor ?? themeData.indicatorColor;
if (indicatorColor == backgroundColor) {
// ThemeData tries to avoid this by having indicatorColor avoid being the
// primaryColor. However, it's possible that the tab bar is on a
// Material that isn't the primaryColor. In that case, if the indicator
// color ends up clashing, then this overrides it. When that happens,
// automatic transitions of the theme will likely look ugly as the
// indicator color suddenly snaps to white at one end, but it's not clear
// how to avoid that any further.
indicatorColor = Colors.white;
}
final TextStyle textStyle = themeData.primaryTextTheme.body2; if (_controller.indexIsChanging) {
final Color selectedLabelColor = config.labelColor ?? themeData.primaryTextTheme.body2.color; assert(_currentIndex != previousIndex);
final Color labelColor = selectedLabelColor.withAlpha(0xB2); // 70% alpha wrappedTabs[_currentIndex] = new _TabStyle(
animation: _changeAnimation,
List<Widget> tabs = <Widget>[]; selected: true,
bool textAndIcons = false; labelColor: config.labelColor,
int tabIndex = 0; child: wrappedTabs[_currentIndex],
for (TabLabel label in config.labels.values) { );
tabs.add(_toTab(label, tabIndex++, labelColor, selectedLabelColor)); wrappedTabs[previousIndex] = new _TabStyle(
if (label.hasText && label.hasIcon) animation: _changeAnimation,
textAndIcons = true; selected: false,
labelColor: config.labelColor,
child: wrappedTabs[previousIndex],
);
} else {
wrappedTabs[_currentIndex] = new _TabStyle(
animation: kAlwaysCompleteAnimation,
selected: true,
labelColor: config.labelColor,
child: wrappedTabs[_currentIndex],
);
}
} }
Widget contents = new DefaultTextStyle( // Add the tap handler to each tab. If the tab bar is scrollable
style: textStyle, // then give all of the tabs equal flexibility so that their widths
child: new _TabBarWrapper( // reflect the intrinsic width of their labels.
children: tabs, for (int index = 0; index < config.tabs.length; index++) {
selectedIndex: _selection?.index, wrappedTabs[index] = new InkWell(
indicatorColor: indicatorColor, onTap: () { _handleTap(index); },
indicatorRect: _indicatorRect, child: wrappedTabs[index],
textAndIcons: textAndIcons, );
isScrollable: config.isScrollable, if (!config.isScrollable)
onLayoutChanged: _layoutChanged wrappedTabs[index] = new Flexible(child: wrappedTabs[index]);
) }
Widget tabBar = new CustomPaint(
painter: _indicatorPainter,
child: new Padding(
padding: const EdgeInsets.only(bottom: _kTabIndicatorHeight),
child: new _TabStyle(
animation: kAlwaysCompleteAnimation,
selected: false,
labelColor: config.labelColor,
child: new _TabLabelBar(
onPerformLayout: _saveTabOffsets,
children: wrappedTabs,
),
),
),
); );
if (config.isScrollable) { if (config.isScrollable) {
return new Viewport( tabBar = new ScrollableViewport(
mainAxis: Axis.horizontal, scrollableKey: viewportKey,
paintOffset: scrollOffsetToPixelDelta(scrollOffset), scrollDirection: Axis.horizontal,
onPaintOffsetUpdateNeeded: _handlePaintOffsetUpdateNeeded, child: tabBar
child: contents
); );
} }
return contents; return tabBar;
} }
} }
/// A widget that displays the contents of a tab. class _PageableTabBarView extends PageableList {
/// _PageableTabBarView({
/// Requires one of its ancestors to be a [TabBarSelection] widget to enable
/// saving and monitoring the selected tab.
///
/// See also:
///
/// * [TabBarSelection]
/// * [TabBar]
/// * <https://material.google.com/components/tabs.html>
class TabBarView<T> extends PageableList {
/// Creates a widget that displays the contents of a tab.
///
/// The [children] argument must not be null and must not be empty.
TabBarView({
Key key, Key key,
@required List<Widget> children List<Widget> children,
double initialScrollOffset: 0.0,
}) : super( }) : super(
key: key, key: key,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
children: children children: children,
) { initialScrollOffset: initialScrollOffset,
assert(children != null); );
assert(children.length > 1);
}
@override @override
_TabBarViewState<T> createState() => new _TabBarViewState<T>(); _PageableTabBarViewState createState() => new _PageableTabBarViewState();
} }
class _TabBarViewState<T> extends PageableListState<TabBarView<T>> implements TabBarSelectionAnimationListener { class _PageableTabBarViewState extends PageableListState<_PageableTabBarView> {
TabBarSelectionState<T> _selection;
List<Widget> _items;
int get _tabCount => config.children.length;
BoundedBehavior _boundedBehavior; BoundedBehavior _boundedBehavior;
@override @override
ExtentScrollBehavior get scrollBehavior { ExtentScrollBehavior get scrollBehavior {
_boundedBehavior ??= new BoundedBehavior(platform: platform); _boundedBehavior ??= new BoundedBehavior(
platform: platform,
containerExtent: 1.0,
contentExtent: config.children.length.toDouble(),
);
return _boundedBehavior; return _boundedBehavior;
} }
@override @override
TargetPlatform get platform => Theme.of(context).platform; TargetPlatform get platform => Theme.of(context).platform;
void _initSelection(TabBarSelectionState<T> newSelection) { @override
if (_selection == newSelection) Future<Null> fling(double scrollVelocity) {
return; final double newScrollOffset = snapScrollOffset(scrollOffset + scrollVelocity.sign)
_selection?.removeAnimationListener(this); .clamp(snapScrollOffset(scrollOffset - 0.5), snapScrollOffset(scrollOffset + 0.5))
_selection = newSelection; .clamp(0.0, (config.children.length - 1).toDouble());
_selection?.addAnimationListener(this); return scrollTo(newScrollOffset, duration: config.duration, curve: config.curve);
if (_selection != null)
_updateItemsAndScrollBehavior();
} }
@override @override
void didUpdateConfig(TabBarView<T> oldConfig) { Widget buildContent(BuildContext context) {
super.didUpdateConfig(oldConfig); return new PageViewport(
if (_selection != null && config.children != oldConfig.children) mainAxis: config.scrollDirection,
_updateItemsForSelectedIndex(_selection.index); startOffset: scrollOffset,
children: config.children,
);
} }
}
@override /// A pageable list that displays the widget which corresponds to the currently
void dispose() { /// selected tab. Typically used in conjuction with a [TabBar].
_selection?.removeAnimationListener(this); ///
super.dispose(); /// If a [TabController] is not provided, then there must be a [DefaultTabController]
/// ancestor.
class TabBarView extends StatefulWidget {
/// Creates a pageable list with one child per tab.
///
/// The length of [children] must be the same as the [controller]'s length.
TabBarView({
Key key,
@required this.children,
this.controller,
}) : super(key: key) {
assert(children != null && children.length > 1);
} }
/// This widget's selection and animation state.
///
/// If [TabController] is not provided, then the value of [DefaultTabController.of]
/// will be used.
final TabController controller;
/// One widget per tab.
final List<Widget> children;
@override @override
void handleSelectionDeactivate() { _TabBarViewState createState() => new _TabBarViewState();
_selection = null; }
}
void _updateItemsFromChildren(int first, int second, [int third]) { class _TabBarViewState extends State<TabBarView> {
List<Widget> widgets = config.children; final GlobalKey<ScrollableState> viewportKey = new GlobalKey<ScrollableState>();
_items = <Widget>[
new KeyedSubtree.wrap(widgets[first], first),
new KeyedSubtree.wrap(widgets[second], second),
];
if (third != null)
_items.add(new KeyedSubtree.wrap(widgets[third], third));
}
void _updateItemsForSelectedIndex(int selectedIndex) { TabController _controller;
if (selectedIndex == 0) { List<Widget> _children;
_updateItemsFromChildren(0, 1); double _offsetAnchor;
} else if (selectedIndex == _tabCount - 1) { double _offsetBias = 0.0;
_updateItemsFromChildren(selectedIndex - 1, selectedIndex); int _currentIndex;
} else { int _warpUnderwayCount = 0;
_updateItemsFromChildren(selectedIndex - 1, selectedIndex, selectedIndex + 1);
} void _updateTabController() {
TabController newController = config.controller ?? DefaultTabController.of(context);
if (newController == _controller)
return;
if (_controller != null)
_controller.animation.removeListener(_handleTick);
_controller = newController;
if (_controller != null)
_controller.animation.addListener(_handleTick);
} }
void _updateScrollBehaviorForSelectedIndex(int selectedIndex) { @override
if (selectedIndex == 0) { void initState() {
didUpdateScrollBehavior(scrollBehavior.updateExtents(contentExtent: 2.0, containerExtent: 1.0, scrollOffset: 0.0)); super.initState();
} else if (selectedIndex == _tabCount - 1) { _children = config.children;
didUpdateScrollBehavior(scrollBehavior.updateExtents(contentExtent: 2.0, containerExtent: 1.0, scrollOffset: 1.0));
} else {
didUpdateScrollBehavior(scrollBehavior.updateExtents(contentExtent: 3.0, containerExtent: 1.0, scrollOffset: 1.0));
}
} }
void _updateItemsAndScrollBehavior() { @override
assert(_selection != null); void dependenciesChanged() {
final int selectedIndex = _selection.index; super.dependenciesChanged();
assert(selectedIndex != null); _updateTabController();
_updateItemsForSelectedIndex(selectedIndex); _currentIndex = _controller?.index;
_updateScrollBehaviorForSelectedIndex(selectedIndex);
} }
@override @override
void handleStatusChange(AnimationStatus status) { void didUpdateConfig(TabBarView oldConfig) {
super.didUpdateConfig(oldConfig);
if (config.controller != oldConfig.controller)
_updateTabController();
if (config.children != oldConfig.children && _warpUnderwayCount == 0)
_children = config.children;
} }
@override @override
void handleProgressChange() { void dispose() {
if (_selection == null || !_selection.valueIsChanging) if (_controller != null)
return; _controller.animation.removeListener(_handleTick);
// The TabBar is driving the TabBarSelection animation. // We don't own the _controller Animation, so it's not disposed here.
super.dispose();
}
final Animation<double> animation = _selection.animation; void _handleTick() {
if (!_controller.indexIsChanging)
return; // This widget is driving the controller's animation.
if (animation.status == AnimationStatus.completed) { if (_controller.index != _currentIndex) {
_updateItemsAndScrollBehavior(); _currentIndex = _controller.index;
return; _warpToCurrentIndex();
} }
}
if (animation.status != AnimationStatus.forward) Future<Null> _warpToCurrentIndex() async {
return; if (!mounted)
return new Future<Null>.value();
final int selectedIndex = _selection.index; final ScrollableState viewport = viewportKey.currentState;
final int previousSelectedIndex = _selection.previousIndex; if (viewport.scrollOffset == _currentIndex.toDouble())
return new Future<Null>.value();
if (selectedIndex < previousSelectedIndex) { final int previousIndex = _controller.previousIndex;
_updateItemsFromChildren(selectedIndex, previousSelectedIndex); if ((_currentIndex - previousIndex).abs() == 1)
scrollTo(new CurveTween(curve: Curves.fastOutSlowIn.flipped).evaluate(new ReverseAnimation(animation))); return viewport.scrollTo(_currentIndex.toDouble(), duration: kTabScrollDuration);
} else {
_updateItemsFromChildren(previousSelectedIndex, selectedIndex); assert((_currentIndex - previousIndex).abs() > 1);
scrollTo(new CurveTween(curve: Curves.fastOutSlowIn).evaluate(animation)); double initialScroll;
} setState(() {
} _warpUnderwayCount += 1;
_children = new List<Widget>.from(config.children, growable: false);
if (_currentIndex > previousIndex) {
_children[_currentIndex - 1] = _children[previousIndex];
initialScroll = (_currentIndex - 1).toDouble();
} else {
_children[_currentIndex + 1] = _children[previousIndex];
initialScroll = (_currentIndex + 1).toDouble();
}
});
@override await viewport.scrollTo(initialScroll);
void dispatchOnScroll() { if (!mounted)
if (_selection == null || _selection.valueIsChanging) return new Future<Null>.value();
return;
// This class is driving the TabBarSelection's animation.
final AnimationController controller = _selection._controller; await viewport.scrollTo(_currentIndex.toDouble(), duration: kTabScrollDuration);
if (!mounted)
return new Future<Null>.value();
if (_selection.index == 0 || _selection.index == _tabCount - 1) setState(() {
controller.value = scrollOffset; _warpUnderwayCount -= 1;
else _children = config.children;
controller.value = scrollOffset / 2.0; });
} }
@override // Called when the _PageableTabBarView scrolls
Future<Null> fling(double scrollVelocity) { bool _handleScrollNotification(ScrollNotification notification) {
if (_selection == null || _selection.valueIsChanging) if (_warpUnderwayCount > 0)
return new Future<Null>.value(); return false;
if (scrollVelocity.abs() > _kMinFlingVelocity) { final ScrollableState scrollable = notification.scrollable;
final int selectionDelta = scrollVelocity.sign.truncate(); if (scrollable.config.key != viewportKey)
final int targetIndex = (_selection.index + selectionDelta).clamp(0, _tabCount - 1); return false;
if (_selection.index != targetIndex) {
_selection.value = _selection.values[targetIndex];
return new Future<Null>.value();
}
}
final int selectionIndex = _selection.index; switch(notification.kind) {
final int settleIndex = snapScrollOffset(scrollOffset).toInt(); case ScrollNotificationKind.started:
if (selectionIndex > 0 && settleIndex != 1) { _offsetAnchor = null;
final int targetIndex = (selectionIndex + (settleIndex == 2 ? 1 : -1)).clamp(0, _tabCount - 1); break;
_selection.value = _selection.values[targetIndex];
return new Future<Null>.value(); case ScrollNotificationKind.updated:
} else if (selectionIndex == 0 && settleIndex == 1) { if (!_controller.indexIsChanging) {
_selection.value = _selection.values[1]; _offsetAnchor ??= scrollable.scrollOffset;
return new Future<Null>.value(); _controller.offset = (_offsetBias + scrollable.scrollOffset - _offsetAnchor).clamp(-1.0, 1.0);
}
break;
// Either the the animation that follows a fling has completed and we've landed
// on a new tab view, or a new pointer gesture has interrupted the fling
// animation before it has completed.
case ScrollNotificationKind.ended:
final double integralScrollOffset = scrollable.scrollOffset.floorToDouble();
if (integralScrollOffset == scrollable.scrollOffset) {
_offsetBias = 0.0;
// The animation duration is short since the tab indicator and this
// pageable list have already moved.
_controller.animateTo(
integralScrollOffset.floor(),
duration: const Duration(milliseconds: 30)
);
} else {
// The fling scroll animation was interrupted.
_offsetBias = _controller.offset;
}
break;
} }
return settleScrollOffset();
return false;
} }
@override @override
Widget buildContent(BuildContext context) { Widget build(BuildContext context) {
TabBarSelectionState<T> newSelection = TabBarSelection.of(context); return new NotificationListener<ScrollNotification>(
_initSelection(newSelection); onNotification: _handleScrollNotification,
return new PageViewport( child: new _PageableTabBarView(
itemsWrap: config.itemsWrap, key: viewportKey,
mainAxis: config.scrollDirection, children: _children,
startOffset: scrollOffset, initialScrollOffset: (_controller?.index ?? 0).toDouble(),
children: _items ),
); );
} }
} }
/// A widget that displays a visual indicator of which tab is selected. /// Displays a row of small circular indicators, one per tab. The selected
/// /// tab's indicator is highlighted. Often used in conjuction with a [TabBarView].
/// Requires one of its ancestors to be a [TabBarSelection] widget to enable
/// saving and monitoring the selected tab.
///
/// See also:
/// ///
/// * [TabBarSelection] /// If a [TabController] is not provided, then there must be a [DefaultTabController]
/// * [TabBarView] /// ancestor.
class TabPageSelector<T> extends StatelessWidget { class TabPageSelector extends StatelessWidget {
/// Creates a widget that displays a visual indicator of which tab is selected. /// Creates a compact widget that indicates which tab has been selected.
/// TabPageSelector({ Key key, this.controller }) : super(key: key);
/// Requires one of its ancestors to be a [TabBarSelection] widget to enable
/// saving and monitoring the selected tab.
const TabPageSelector({ Key key }) : super(key: key);
Widget _buildTabIndicator(TabBarSelectionState<T> selection, T tab, Animation<double> animation, ColorTween selectedColor, ColorTween previousColor) { /// This widget's selection and animation state.
///
/// If [TabController] is not provided, then the value of [DefaultTabController.of]
/// will be used.
final TabController controller;
Widget _buildTabIndicator(
int tabIndex,
TabController tabController,
ColorTween selectedColor,
ColorTween previousColor,
) {
Color background; Color background;
if (selection.valueIsChanging) { if (tabController.indexIsChanging) {
// The selection's animation is animating from previousValue to value. // The selection's animation is animating from previousValue to value.
if (selection.value == tab) if (tabController.index == tabIndex)
background = selectedColor.evaluate(animation); background = selectedColor.lerp(_indexChangeProgress(tabController));
else if (selection.previousValue == tab) else if (tabController.previousIndex == tabIndex)
background = previousColor.evaluate(animation); background = previousColor.lerp(_indexChangeProgress(tabController));
else else
background = selectedColor.begin; background = selectedColor.begin;
} else { } else {
background = selection.value == tab ? selectedColor.end : selectedColor.begin; background = tabController.index == tabIndex ? selectedColor.end : selectedColor.begin;
} }
return new Container( return new Container(
width: 12.0, width: 12.0,
...@@ -1312,20 +862,25 @@ class TabPageSelector<T> extends StatelessWidget { ...@@ -1312,20 +862,25 @@ class TabPageSelector<T> extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final TabBarSelectionState<T> selection = TabBarSelection.of(context);
final Color color = Theme.of(context).accentColor; final Color color = Theme.of(context).accentColor;
final ColorTween selectedColor = new ColorTween(begin: Colors.transparent, end: color); final ColorTween selectedColor = new ColorTween(begin: Colors.transparent, end: color);
final ColorTween previousColor = new ColorTween(begin: color, end: Colors.transparent); final ColorTween previousColor = new ColorTween(begin: color, end: Colors.transparent);
Animation<double> animation = new CurvedAnimation(parent: selection.animation, curve: Curves.fastOutSlowIn); final TabController tabController = controller ?? DefaultTabController.of(context);
final Animation<double> animation = new CurvedAnimation(
parent: tabController.animation,
curve: Curves.fastOutSlowIn,
);
return new AnimatedBuilder( return new AnimatedBuilder(
animation: animation, animation: animation,
builder: (BuildContext context, Widget child) { builder: (BuildContext context, Widget child) {
return new Semantics( return new Semantics(
label: 'Page ${selection.index + 1} of ${selection.values.length}', label: 'Page ${controller.index + 1} of ${controller.length}',
child: new Row( child: new Row(
children: selection.values.map((T tab) => _buildTabIndicator(selection, tab, animation, selectedColor, previousColor)).toList(), mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min children: new List<Widget>.generate(controller.length, (int tabIndex) {
) return _buildTabIndicator(tabIndex, controller, selectedColor, previousColor);
}).toList(),
),
); );
} }
); );
......
...@@ -28,33 +28,70 @@ class StateMarkerState extends State<StateMarker> { ...@@ -28,33 +28,70 @@ class StateMarkerState extends State<StateMarker> {
Widget buildFrame({ List<String> tabs, String value, bool isScrollable: false, Key tabBarKey }) { Widget buildFrame({ List<String> tabs, String value, bool isScrollable: false, Key tabBarKey }) {
return new Material( return new Material(
child: new TabBarSelection<String>( child: new DefaultTabController(
value: value, initialIndex: tabs.indexOf(value),
values: tabs, length: tabs.length,
child: new TabBar<String>( child: new TabBar(
key: tabBarKey, key: tabBarKey,
labels: new Map<String, TabLabel>.fromIterable(tabs, value: (String tab) => new TabLabel(text: tab)), tabs: tabs.map((String tab) => new Tab(text: tab)).toList(),
isScrollable: isScrollable isScrollable: isScrollable,
) ),
) ),
); );
} }
typedef Widget TabControllerFrameBuilder(BuildContext context, TabController controller);
class TabControllerFrame extends StatefulWidget {
TabControllerFrame({ this.length, this.initialIndex: 0, this.builder });
final int length;
final int initialIndex;
final TabControllerFrameBuilder builder;
@override
TabControllerFrameState createState() => new TabControllerFrameState();
}
class TabControllerFrameState extends State<TabControllerFrame> with SingleTickerProviderStateMixin {
TabController _controller;
@override
void initState() {
super.initState();
_controller = new TabController(
vsync: this,
length: config.length,
initialIndex: config.initialIndex,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return config.builder(context, _controller);
}
}
Widget buildLeftRightApp({ List<String> tabs, String value }) { Widget buildLeftRightApp({ List<String> tabs, String value }) {
return new MaterialApp( return new MaterialApp(
theme: new ThemeData(platform: TargetPlatform.android), theme: new ThemeData(platform: TargetPlatform.android),
home: new TabBarSelection<String>( home: new DefaultTabController(
value: value, initialIndex: tabs.indexOf(value),
values: tabs, length: tabs.length,
child: new Scaffold( child: new Scaffold(
appBar: new AppBar( appBar: new AppBar(
title: new Text('tabs'), title: new Text('tabs'),
bottom: new TabBar<String>( bottom: new TabBar(
labels: new Map<String, TabLabel>.fromIterable(tabs, value: (String tab) => new TabLabel(text: tab)), tabs: tabs.map((String tab) => new Tab(text: tab)).toList(),
) ),
), ),
body: new TabBarView<String>( body: new TabBarView(
children: <Widget>[ children: <Widget>[
new Center(child: new Text('LEFT CHILD')), new Center(child: new Text('LEFT CHILD')),
new Center(child: new Text('RIGHT CHILD')) new Center(child: new Text('RIGHT CHILD'))
...@@ -70,83 +107,72 @@ void main() { ...@@ -70,83 +107,72 @@ void main() {
List<String> tabs = <String>['A', 'B', 'C']; List<String> tabs = <String>['A', 'B', 'C'];
await tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: false)); await tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: false));
TabBarSelectionState<String> selection = TabBarSelection.of(tester.element(find.text('A')));
expect(selection, isNotNull);
expect(selection.indexOf('A'), equals(0));
expect(selection.indexOf('B'), equals(1));
expect(selection.indexOf('C'), equals(2));
expect(find.text('A'), findsOneWidget); expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsOneWidget); expect(find.text('B'), findsOneWidget);
expect(find.text('C'), findsOneWidget); expect(find.text('C'), findsOneWidget);
expect(selection.index, equals(2)); TabController controller = DefaultTabController.of(tester.element(find.text('A')));
expect(selection.previousIndex, equals(2)); expect(controller, isNotNull);
expect(selection.value, equals('C')); expect(controller.index, 2);
expect(selection.previousValue, equals('C')); expect(controller.previousIndex, 2);
await tester.pumpWidget(buildFrame(tabs: tabs, value: 'C' ,isScrollable: false)); await tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: false));
await tester.tap(find.text('B')); await tester.tap(find.text('B'));
await tester.pump(); await tester.pump();
expect(selection.valueIsChanging, true); expect(controller.indexIsChanging, true);
await tester.pump(const Duration(seconds: 1)); // finish the animation await tester.pump(const Duration(seconds: 1)); // finish the animation
expect(selection.valueIsChanging, false); expect(controller.index, 1);
expect(selection.value, equals('B')); expect(controller.previousIndex, 2);
expect(selection.previousValue, equals('C')); expect(controller.indexIsChanging, false);
expect(selection.index, equals(1));
expect(selection.previousIndex, equals(2));
await tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: false)); await tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: false));
await tester.tap(find.text('C')); await tester.tap(find.text('C'));
await tester.pump(); await tester.pump();
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
expect(selection.value, equals('C')); expect(controller.index, 2);
expect(selection.previousValue, equals('B')); expect(controller.previousIndex, 1);
expect(selection.index, equals(2));
expect(selection.previousIndex, equals(1));
await tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: false)); await tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: false));
await tester.tap(find.text('A')); await tester.tap(find.text('A'));
await tester.pump(); await tester.pump();
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
expect(selection.value, equals('A')); expect(controller.index, 0);
expect(selection.previousValue, equals('C')); expect(controller.previousIndex, 2);
expect(selection.index, equals(0));
expect(selection.previousIndex, equals(2));
}); });
testWidgets('Scrollable TabBar tap selects tab', (WidgetTester tester) async { testWidgets('Scrollable TabBar tap selects tab', (WidgetTester tester) async {
List<String> tabs = <String>['A', 'B', 'C']; List<String> tabs = <String>['A', 'B', 'C'];
await tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: true)); await tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: true));
TabBarSelectionState<String> selection = TabBarSelection.of(tester.element(find.text('A')));
expect(selection, isNotNull);
expect(find.text('A'), findsOneWidget); expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsOneWidget); expect(find.text('B'), findsOneWidget);
expect(find.text('C'), findsOneWidget); expect(find.text('C'), findsOneWidget);
expect(selection.value, equals('C')); TabController controller = DefaultTabController.of(tester.element(find.text('A')));
expect(controller.index, 2);
expect(controller.previousIndex, 2);
await tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: true)); await tester.tap(find.text('C'));
await tester.tap(find.text('B'));
await tester.pump(); await tester.pump();
expect(selection.value, equals('B')); await tester.pump(const Duration(seconds: 1));
expect(controller.index, 2);
await tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: true)); await tester.tap(find.text('B'));
await tester.tap(find.text('C'));
await tester.pump(); await tester.pump();
expect(selection.value, equals('C')); await tester.pump(const Duration(seconds: 1));
expect(controller.index, 1);
await tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: true));
await tester.tap(find.text('A')); await tester.tap(find.text('A'));
await tester.pump(); await tester.pump();
expect(selection.value, equals('A')); await tester.pump(const Duration(seconds: 1));
expect(controller.index, 0);
}); });
testWidgets('Scrollable TabBar tap centers selected tab', (WidgetTester tester) async { testWidgets('Scrollable TabBar tap centers selected tab', (WidgetTester tester) async {
List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE', 'FFFFFF', 'GGGGGG', 'HHHHHH', 'IIIIII', 'JJJJJJ', 'KKKKKK', 'LLLLLL']; List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE', 'FFFFFF', 'GGGGGG', 'HHHHHH', 'IIIIII', 'JJJJJJ', 'KKKKKK', 'LLLLLL'];
Key tabBarKey = new Key('TabBar'); Key tabBarKey = new Key('TabBar');
await tester.pumpWidget(buildFrame(tabs: tabs, value: 'AAAAAA', isScrollable: true, tabBarKey: tabBarKey)); await tester.pumpWidget(buildFrame(tabs: tabs, value: 'AAAAAA', isScrollable: true, tabBarKey: tabBarKey));
TabBarSelectionState<String> selection = TabBarSelection.of(tester.element(find.text('AAAAAA'))); TabController controller = DefaultTabController.of(tester.element(find.text('AAAAAA')));
expect(selection, isNotNull); expect(controller, isNotNull);
expect(selection.value, equals('AAAAAA')); expect(controller.index, 0);
expect(tester.getSize(find.byKey(tabBarKey)).width, equals(800.0)); expect(tester.getSize(find.byKey(tabBarKey)).width, equals(800.0));
// The center of the FFFFFF item is to the right of the TabBar's center // The center of the FFFFFF item is to the right of the TabBar's center
...@@ -155,7 +181,7 @@ void main() { ...@@ -155,7 +181,7 @@ void main() {
await tester.tap(find.text('FFFFFF')); await tester.tap(find.text('FFFFFF'));
await tester.pump(); await tester.pump();
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
expect(selection.value, equals('FFFFFF')); expect(controller.index, 5);
// The center of the FFFFFF item is now at the TabBar's center // The center of the FFFFFF item is now at the TabBar's center
expect(tester.getCenter(find.text('FFFFFF')).x, closeTo(400.0, 1.0)); expect(tester.getCenter(find.text('FFFFFF')).x, closeTo(400.0, 1.0));
}); });
...@@ -165,9 +191,9 @@ void main() { ...@@ -165,9 +191,9 @@ void main() {
List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE', 'FFFFFF', 'GGGGGG', 'HHHHHH', 'IIIIII', 'JJJJJJ', 'KKKKKK', 'LLLLLL']; List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE', 'FFFFFF', 'GGGGGG', 'HHHHHH', 'IIIIII', 'JJJJJJ', 'KKKKKK', 'LLLLLL'];
Key tabBarKey = new Key('TabBar'); Key tabBarKey = new Key('TabBar');
await tester.pumpWidget(buildFrame(tabs: tabs, value: 'AAAAAA', isScrollable: true, tabBarKey: tabBarKey)); await tester.pumpWidget(buildFrame(tabs: tabs, value: 'AAAAAA', isScrollable: true, tabBarKey: tabBarKey));
TabBarSelectionState<String> selection = TabBarSelection.of(tester.element(find.text('AAAAAA'))); TabController controller = DefaultTabController.of(tester.element(find.text('AAAAAA')));
expect(selection, isNotNull); expect(controller, isNotNull);
expect(selection.value, equals('AAAAAA')); expect(controller.index, 0);
// Fling-scroll the TabBar to the left // Fling-scroll the TabBar to the left
expect(tester.getCenter(find.text('HHHHHH')).x, lessThan(700.0)); expect(tester.getCenter(find.text('HHHHHH')).x, lessThan(700.0));
...@@ -177,31 +203,26 @@ void main() { ...@@ -177,31 +203,26 @@ void main() {
expect(tester.getCenter(find.text('HHHHHH')).x, lessThan(500.0)); expect(tester.getCenter(find.text('HHHHHH')).x, lessThan(500.0));
// Scrolling the TabBar doesn't change the selection // Scrolling the TabBar doesn't change the selection
expect(selection.value, equals('AAAAAA')); expect(controller.index, 0);
}); });
testWidgets('TabView maintains state', (WidgetTester tester) async { testWidgets('TabBarView maintains state', (WidgetTester tester) async {
List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE']; List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE'];
String value = tabs[0]; String value = tabs[0];
void onTabSelectionChanged(String newValue) {
value = newValue;
}
Widget builder() { Widget builder() {
return new Material( return new Material(
child: new TabBarSelection<String>( child: new DefaultTabController(
value: value, initialIndex: tabs.indexOf(value),
values: tabs, length: tabs.length,
onChanged: onTabSelectionChanged, child: new TabBarView(
child: new TabBarView<String>(
children: tabs.map((String name) { children: tabs.map((String name) {
return new StateMarker( return new StateMarker(
child: new Text(name) child: new Text(name)
); );
}).toList() }).toList()
) ),
) ),
); );
} }
...@@ -210,6 +231,8 @@ void main() { ...@@ -210,6 +231,8 @@ void main() {
} }
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
TabController controller = DefaultTabController.of(tester.element(find.text('AAAAAA')));
TestGesture gesture = await tester.startGesture(tester.getCenter(find.text(tabs[0]))); TestGesture gesture = await tester.startGesture(tester.getCenter(find.text(tabs[0])));
await gesture.moveBy(const Offset(-600.0, 0.0)); await gesture.moveBy(const Offset(-600.0, 0.0));
await tester.pump(); await tester.pump();
...@@ -218,6 +241,7 @@ void main() { ...@@ -218,6 +241,7 @@ void main() {
await gesture.up(); await gesture.up();
await tester.pump(); await tester.pump();
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
value = tabs[controller.index];
expect(value, equals(tabs[1])); expect(value, equals(tabs[1]));
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
expect(findStateMarkerState(tabs[1]).marker, equals('marked')); expect(findStateMarkerState(tabs[1]).marker, equals('marked'));
...@@ -230,6 +254,7 @@ void main() { ...@@ -230,6 +254,7 @@ void main() {
await tester.pump(); await tester.pump();
expect(findStateMarkerState(tabs[1]).marker, equals('marked')); expect(findStateMarkerState(tabs[1]).marker, equals('marked'));
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
value = tabs[controller.index];
expect(value, equals(tabs[2])); expect(value, equals(tabs[2]));
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
...@@ -248,6 +273,7 @@ void main() { ...@@ -248,6 +273,7 @@ void main() {
await gesture.up(); await gesture.up();
await tester.pump(); await tester.pump();
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
value = tabs[controller.index];
expect(value, equals(tabs[1])); expect(value, equals(tabs[1]));
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
expect(findStateMarkerState(tabs[1]).marker, equals('marked')); expect(findStateMarkerState(tabs[1]).marker, equals('marked'));
...@@ -262,15 +288,15 @@ void main() { ...@@ -262,15 +288,15 @@ void main() {
expect(find.text('LEFT CHILD'), findsOneWidget); expect(find.text('LEFT CHILD'), findsOneWidget);
expect(find.text('RIGHT CHILD'), findsNothing); expect(find.text('RIGHT CHILD'), findsNothing);
TabBarSelectionState<String> selection = TabBarSelection.of(tester.element(find.text('LEFT'))); TabController controller = DefaultTabController.of(tester.element(find.text('LEFT')));
expect(selection.value, equals('LEFT')); expect(controller.index, 0);
// Fling to the left, switch from the 'LEFT' tab to the 'RIGHT' // Fling to the left, switch from the 'LEFT' tab to the 'RIGHT'
Point flingStart = tester.getCenter(find.text('LEFT CHILD')); Point flingStart = tester.getCenter(find.text('LEFT CHILD'));
await tester.flingFrom(flingStart, const Offset(-200.0, 0.0), 10000.0); await tester.flingFrom(flingStart, const Offset(-200.0, 0.0), 10000.0);
await tester.pump(); await tester.pump();
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
expect(selection.value, equals('RIGHT')); expect(controller.index, 1);
expect(find.text('LEFT CHILD'), findsNothing); expect(find.text('LEFT CHILD'), findsNothing);
expect(find.text('RIGHT CHILD'), findsOneWidget); expect(find.text('RIGHT CHILD'), findsOneWidget);
...@@ -279,7 +305,7 @@ void main() { ...@@ -279,7 +305,7 @@ void main() {
await tester.flingFrom(flingStart, const Offset(200.0, 0.0), 10000.0); await tester.flingFrom(flingStart, const Offset(200.0, 0.0), 10000.0);
await tester.pump(); await tester.pump();
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
expect(selection.value, equals('LEFT')); expect(controller.index, 0);
expect(find.text('LEFT CHILD'), findsOneWidget); expect(find.text('LEFT CHILD'), findsOneWidget);
expect(find.text('RIGHT CHILD'), findsNothing); expect(find.text('RIGHT CHILD'), findsNothing);
}); });
...@@ -294,8 +320,8 @@ void main() { ...@@ -294,8 +320,8 @@ void main() {
expect(find.text('LEFT CHILD'), findsOneWidget); expect(find.text('LEFT CHILD'), findsOneWidget);
expect(find.text('RIGHT CHILD'), findsNothing); expect(find.text('RIGHT CHILD'), findsNothing);
TabBarSelectionState<String> selection = TabBarSelection.of(tester.element(find.text('LEFT'))); TabController controller = DefaultTabController.of(tester.element(find.text('LEFT')));
expect(selection.value, equals('LEFT')); expect(controller.index, 0);
// End the fling by reversing direction. This should cause not cause // End the fling by reversing direction. This should cause not cause
// a change to the selected tab, everything should just settle back to // a change to the selected tab, everything should just settle back to
...@@ -304,7 +330,7 @@ void main() { ...@@ -304,7 +330,7 @@ void main() {
await tester.flingFrom(flingStart, const Offset(-200.0, 0.0), -10000.0); await tester.flingFrom(flingStart, const Offset(-200.0, 0.0), -10000.0);
await tester.pump(); await tester.pump();
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
expect(selection.value, equals('LEFT')); expect(controller.index, 0);
expect(find.text('LEFT CHILD'), findsOneWidget); expect(find.text('LEFT CHILD'), findsOneWidget);
expect(find.text('RIGHT CHILD'), findsNothing); expect(find.text('RIGHT CHILD'), findsNothing);
}); });
...@@ -321,17 +347,17 @@ void main() { ...@@ -321,17 +347,17 @@ void main() {
child: new SizedBox( child: new SizedBox(
width: 300.0, width: 300.0,
height: 200.0, height: 200.0,
child: new TabBarSelection<String>( child: new DefaultTabController(
values: tabs, length: tabs.length,
child: new Scaffold( child: new Scaffold(
appBar: new AppBar( appBar: new AppBar(
title: new Text('tabs'), title: new Text('tabs'),
bottom: new TabBar<String>( bottom: new TabBar(
isScrollable: true, isScrollable: true,
labels: new Map<String, TabLabel>.fromIterable(tabs, value: (String tab) => new TabLabel(text: tab)), tabs: tabs.map((String tab) => new Tab(text: tab)).toList(),
), ),
), ),
body: new TabBarView<String>( body: new TabBarView(
children: tabs.map((String name) => new Text('${index++}')).toList(), children: tabs.map((String name) => new Text('${index++}')).toList(),
), ),
), ),
...@@ -348,4 +374,175 @@ void main() { ...@@ -348,4 +374,175 @@ void main() {
final RenderBox box = tester.renderObject(find.text('BBBBBB')); final RenderBox box = tester.renderObject(find.text('BBBBBB'));
expect(box.localToGlobal(Point.origin).x, greaterThan(0.0)); expect(box.localToGlobal(Point.origin).x, greaterThan(0.0));
}); });
testWidgets('TabController change notification', (WidgetTester tester) async {
List<String> tabs = <String>['LEFT', 'RIGHT'];
await tester.pumpWidget(buildLeftRightApp(tabs: tabs, value: 'LEFT'));
TabController controller = DefaultTabController.of(tester.element(find.text('LEFT')));
expect(controller, isNotNull);
expect(controller.index, 0);
String value;
controller.addListener(() {
value = tabs[controller.index];
});
// TODO(hixie) - the new scrolling framework should eliminate most of the pump
// calls that follow. Currently they exist to complete chains of future.then
// in the implementation.
await tester.tap(find.text('RIGHT'));
await tester.pump(); // start the animation
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 500));
expect(value, 'RIGHT');
await tester.tap(find.text('LEFT'));
await tester.pump(); // start the animation
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 500));
expect(value, 'LEFT');
Point leftFlingStart = tester.getCenter(find.text('LEFT CHILD'));
await tester.flingFrom(leftFlingStart, const Offset(-200.0, 0.0), 10000.0);
await tester.pump(); // start the animation
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 500));
expect(value, 'RIGHT');
Point rightFlingStart = tester.getCenter(find.text('RIGHT CHILD'));
await tester.flingFrom(rightFlingStart, const Offset(200.0, 0.0), 10000.0);
await tester.pump(); // start the animation
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 500));
expect(value, 'LEFT');
});
testWidgets('Explicit TabController', (WidgetTester tester) async {
List<String> tabs = <String>['LEFT', 'RIGHT'];
TabController tabController;
Widget buildTabControllerFrame(BuildContext context, TabController controller) {
tabController = controller;
return new MaterialApp(
theme: new ThemeData(platform: TargetPlatform.android),
home: new Scaffold(
appBar: new AppBar(
title: new Text('tabs'),
bottom: new TabBar(
controller: controller,
tabs: tabs.map((String tab) => new Tab(text: tab)).toList(),
),
),
body: new TabBarView(
controller: controller,
children: <Widget>[
new Center(child: new Text('LEFT CHILD')),
new Center(child: new Text('RIGHT CHILD'))
]
),
),
);
}
await tester.pumpWidget(new TabControllerFrame(
builder: buildTabControllerFrame,
length: tabs.length,
initialIndex: 1,
));
expect(find.text('LEFT'), findsOneWidget);
expect(find.text('RIGHT'), findsOneWidget);
expect(find.text('LEFT CHILD'), findsNothing);
expect(find.text('RIGHT CHILD'), findsOneWidget);
expect(tabController.index, 1);
expect(tabController.previousIndex, 1);
expect(tabController.indexIsChanging, false);
expect(tabController.animation.value, 1.0);
expect(tabController.animation.status, AnimationStatus.completed);
tabController.index = 0;
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 500));
expect(find.text('LEFT CHILD'), findsOneWidget);
expect(find.text('RIGHT CHILD'), findsNothing);
tabController.index = 1;
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 500));
expect(find.text('LEFT CHILD'), findsNothing);
expect(find.text('RIGHT CHILD'), findsOneWidget);
});
testWidgets('TabController listener resets index', (WidgetTester tester) async {
// This is a regression test for the scenario brought up here
// https://github.com/flutter/flutter/pull/7387#pullrequestreview-15630946
List<String> tabs = <String>['A', 'B', 'C'];
TabController tabController;
Widget buildTabControllerFrame(BuildContext context, TabController controller) {
tabController = controller;
return new MaterialApp(
theme: new ThemeData(platform: TargetPlatform.android),
home: new Scaffold(
appBar: new AppBar(
title: new Text('tabs'),
bottom: new TabBar(
controller: controller,
tabs: tabs.map((String tab) => new Tab(text: tab)).toList(),
),
),
body: new TabBarView(
controller: controller,
children: <Widget>[
new Center(child: new Text('CHILD A')),
new Center(child: new Text('CHILD B')),
new Center(child: new Text('CHILD C')),
]
),
),
);
}
await tester.pumpWidget(new TabControllerFrame(
builder: buildTabControllerFrame,
length: tabs.length,
));
tabController.animation.addListener(() {
if (tabController.animation.status == AnimationStatus.forward)
tabController.index = 2;
expect(tabController.indexIsChanging, true);
});
expect(tabController.index, 0);
expect(tabController.indexIsChanging, false);
tabController.animateTo(1, duration: const Duration(milliseconds: 200), curve: Curves.linear);
await tester.pump();
await tester.pump(const Duration(milliseconds: 300));
expect(tabController.index, 2);
expect(tabController.indexIsChanging, false);
});
testWidgets('TabBarView child disposed during animation', (WidgetTester tester) async {
// This is a regression test for the scenario brought up here
// https://github.com/flutter/flutter/pull/7387#discussion_r95089191x
List<String> tabs = <String>['LEFT', 'RIGHT'];
await tester.pumpWidget(buildLeftRightApp(tabs: tabs, value: 'LEFT'));
// Fling to the left, switch from the 'LEFT' tab to the 'RIGHT'
Point flingStart = tester.getCenter(find.text('LEFT CHILD'));
await tester.flingFrom(flingStart, const Offset(-200.0, 0.0), 10000.0);
await tester.pump();
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
});
} }
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