mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
New Tabs API (#7387)
This commit is contained in:
parent
e82b18d47b
commit
b23aed7a86
@ -470,12 +470,12 @@ class ItemGalleryBox extends StatelessWidget {
|
||||
|
||||
return new SizedBox(
|
||||
height: 200.0,
|
||||
child: new TabBarSelection<String>(
|
||||
values: tabNames,
|
||||
child: new DefaultTabController(
|
||||
length: tabNames.length,
|
||||
child: new Column(
|
||||
children: <Widget>[
|
||||
new Expanded(
|
||||
child: new TabBarView<String>(
|
||||
child: new TabBarView(
|
||||
children: tabNames.map((String tabName) {
|
||||
return new Container(
|
||||
key: new Key('Tab $index - $tabName'),
|
||||
@ -521,7 +521,7 @@ class ItemGalleryBox extends StatelessWidget {
|
||||
)
|
||||
),
|
||||
new Container(
|
||||
child: new TabPageSelector<String>()
|
||||
child: new TabPageSelector()
|
||||
)
|
||||
]
|
||||
)
|
||||
|
@ -412,8 +412,6 @@ class AnimationDemo extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AnimationDemoState extends State<AnimationDemo> with TickerProviderStateMixin {
|
||||
static final GlobalKey<TabBarSelectionState<_ArcDemo>> _tabsKey = new GlobalKey<TabBarSelectionState<_ArcDemo>>();
|
||||
|
||||
List<_ArcDemo> _allDemos;
|
||||
|
||||
@override
|
||||
@ -435,8 +433,7 @@ class _AnimationDemoState extends State<AnimationDemo> with TickerProviderStateM
|
||||
];
|
||||
}
|
||||
|
||||
Future<Null> _play() async {
|
||||
_ArcDemo demo = _tabsKey.currentState.value;
|
||||
Future<Null> _play(_ArcDemo demo) async {
|
||||
await demo.controller.forward();
|
||||
if (demo.key.currentState != null && demo.key.currentState.mounted)
|
||||
demo.controller.reverse();
|
||||
@ -444,23 +441,26 @@ class _AnimationDemoState extends State<AnimationDemo> with TickerProviderStateM
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return new TabBarSelection<_ArcDemo>(
|
||||
key: _tabsKey,
|
||||
values: _allDemos,
|
||||
return new DefaultTabController(
|
||||
length: _allDemos.length,
|
||||
child: new Scaffold(
|
||||
appBar: new AppBar(
|
||||
title: new Text('Animation'),
|
||||
bottom: new TabBar<_ArcDemo>(
|
||||
labels: new Map<_ArcDemo, TabLabel>.fromIterable(_allDemos, value: (_ArcDemo demo) {
|
||||
return new TabLabel(text: demo.title);
|
||||
})
|
||||
)
|
||||
bottom: new TabBar(
|
||||
tabs: _allDemos.map((_ArcDemo demo) => new Tab(text: demo.title)).toList(),
|
||||
),
|
||||
),
|
||||
floatingActionButton: new FloatingActionButton(
|
||||
onPressed: _play,
|
||||
child: new Icon(Icons.refresh)
|
||||
floatingActionButton: new Builder(
|
||||
builder: (BuildContext context) {
|
||||
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()
|
||||
)
|
||||
)
|
||||
|
@ -107,38 +107,28 @@ class ColorSwatchTabView extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class ColorsDemo extends StatefulWidget {
|
||||
ColorsDemo({ Key key }) : super(key: key);
|
||||
|
||||
class ColorsDemo extends StatelessWidget {
|
||||
static const String routeName = '/colors';
|
||||
|
||||
@override
|
||||
_ColorsDemoState createState() => new _ColorsDemoState();
|
||||
}
|
||||
|
||||
class _ColorsDemoState extends State<ColorsDemo> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return new TabBarSelection<ColorSwatch>(
|
||||
values: colorSwatches,
|
||||
return new DefaultTabController(
|
||||
length: colorSwatches.length,
|
||||
child: new Scaffold(
|
||||
appBar: new AppBar(
|
||||
elevation: 0,
|
||||
title: new Text('Colors'),
|
||||
bottom: new TabBar<ColorSwatch>(
|
||||
bottom: new TabBar(
|
||||
isScrollable: true,
|
||||
labels: new Map<ColorSwatch, TabLabel>.fromIterable(colorSwatches, value: (ColorSwatch swatch) {
|
||||
return new TabLabel(text: swatch.name);
|
||||
})
|
||||
tabs: colorSwatches.map((ColorSwatch swatch) => new Tab(text: swatch.name)).toList(),
|
||||
)
|
||||
),
|
||||
body: new TabBarView<ColorSwatch>(
|
||||
body: new TabBarView(
|
||||
children: colorSwatches.map((ColorSwatch swatch) {
|
||||
return new ColorSwatchTabView(swatch: swatch);
|
||||
})
|
||||
.toList()
|
||||
)
|
||||
)
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -4,78 +4,83 @@
|
||||
|
||||
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) {
|
||||
final TabBarSelectionState<IconData> selection = TabBarSelection.of/*<IconData>*/(context);
|
||||
if (!selection.valueIsChanging)
|
||||
selection.value = selection.values[(selection.index + delta).clamp(0, selection.values.length - 1)];
|
||||
TabController controller = DefaultTabController.of(context);
|
||||
if (!controller.indexIsChanging)
|
||||
controller.animateTo(controller.index + delta);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext notUsed) { // Can't find the TabBarSelection from this context.
|
||||
final List<IconData> icons = <IconData>[
|
||||
Icons.event,
|
||||
Icons.home,
|
||||
Icons.android,
|
||||
Icons.alarm,
|
||||
Icons.face,
|
||||
Icons.language,
|
||||
];
|
||||
|
||||
return new Scaffold(
|
||||
appBar: new AppBar(title: new Text('Page selector')),
|
||||
body: new TabBarSelection<IconData>(
|
||||
values: icons,
|
||||
child: new Builder(
|
||||
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
|
||||
)
|
||||
Widget build(BuildContext context) {
|
||||
final TabController controller = DefaultTabController.of(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(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)
|
||||
),
|
||||
),
|
||||
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()
|
||||
)
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
}).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(
|
||||
appBar: new AppBar(title: new Text('Page selector')),
|
||||
body: new DefaultTabController(
|
||||
length: icons.length,
|
||||
child: new _PageSelector(icons: icons),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,21 @@ enum TabsDemoStyle {
|
||||
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 {
|
||||
static const String routeName = '/scrollable-tabs';
|
||||
|
||||
@ -17,27 +32,22 @@ class ScrollableTabsDemo extends StatefulWidget {
|
||||
ScrollableTabsDemoState createState() => new ScrollableTabsDemoState();
|
||||
}
|
||||
|
||||
class ScrollableTabsDemoState extends State<ScrollableTabsDemo> {
|
||||
final List<IconData> icons = <IconData>[
|
||||
Icons.event,
|
||||
Icons.home,
|
||||
Icons.android,
|
||||
Icons.alarm,
|
||||
Icons.face,
|
||||
Icons.language,
|
||||
];
|
||||
|
||||
final Map<IconData, String> labels = <IconData, String>{
|
||||
Icons.event: 'EVENT',
|
||||
Icons.home: 'HOME',
|
||||
Icons.android: 'ANDROID',
|
||||
Icons.alarm: 'ALARM',
|
||||
Icons.face: 'FACE',
|
||||
Icons.language: 'LANGUAGE',
|
||||
};
|
||||
|
||||
class ScrollableTabsDemoState extends State<ScrollableTabsDemo> with SingleTickerProviderStateMixin {
|
||||
TabController _controller;
|
||||
TabsDemoStyle _demoStyle = TabsDemoStyle.iconsAndText;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = new TabController(vsync: this, length: _allPages.length);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void changeDemoStyle(TabsDemoStyle style) {
|
||||
setState(() {
|
||||
_demoStyle = style;
|
||||
@ -47,65 +57,61 @@ class ScrollableTabsDemoState extends State<ScrollableTabsDemo> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color iconColor = Theme.of(context).accentColor;
|
||||
return new TabBarSelection<IconData>(
|
||||
values: icons,
|
||||
child: new Scaffold(
|
||||
appBar: new AppBar(
|
||||
title: new Text('Scrollable tabs'),
|
||||
actions: <Widget>[
|
||||
new PopupMenuButton<TabsDemoStyle>(
|
||||
onSelected: changeDemoStyle,
|
||||
itemBuilder: (BuildContext context) => <PopupMenuItem<TabsDemoStyle>>[
|
||||
new PopupMenuItem<TabsDemoStyle>(
|
||||
value: TabsDemoStyle.iconsAndText,
|
||||
child: new Text('Icons and text')
|
||||
),
|
||||
new PopupMenuItem<TabsDemoStyle>(
|
||||
value: TabsDemoStyle.iconsOnly,
|
||||
child: new Text('Icons only')
|
||||
),
|
||||
new PopupMenuItem<TabsDemoStyle>(
|
||||
value: TabsDemoStyle.textOnly,
|
||||
child: new Text('Text only')
|
||||
),
|
||||
]
|
||||
)
|
||||
],
|
||||
bottom: new TabBar<IconData>(
|
||||
isScrollable: true,
|
||||
labels: new Map<IconData, TabLabel>.fromIterable(
|
||||
icons,
|
||||
value: (IconData icon) {
|
||||
switch(_demoStyle) {
|
||||
case TabsDemoStyle.iconsAndText:
|
||||
return new TabLabel(text: labels[icon], icon: new Icon(icon));
|
||||
case TabsDemoStyle.iconsOnly:
|
||||
return new TabLabel(icon: new Icon(icon));
|
||||
case TabsDemoStyle.textOnly:
|
||||
return new TabLabel(text: labels[icon]);
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
return new Scaffold(
|
||||
appBar: new AppBar(
|
||||
title: new Text('Scrollable tabs'),
|
||||
actions: <Widget>[
|
||||
new PopupMenuButton<TabsDemoStyle>(
|
||||
onSelected: changeDemoStyle,
|
||||
itemBuilder: (BuildContext context) => <PopupMenuItem<TabsDemoStyle>>[
|
||||
new PopupMenuItem<TabsDemoStyle>(
|
||||
value: TabsDemoStyle.iconsAndText,
|
||||
child: new Text('Icons and text')
|
||||
),
|
||||
new PopupMenuItem<TabsDemoStyle>(
|
||||
value: TabsDemoStyle.iconsOnly,
|
||||
child: new Text('Icons only')
|
||||
),
|
||||
new PopupMenuItem<TabsDemoStyle>(
|
||||
value: TabsDemoStyle.textOnly,
|
||||
child: new Text('Text only')
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
bottom: new TabBar(
|
||||
controller: _controller,
|
||||
isScrollable: true,
|
||||
tabs: _allPages.map((_Page page) {
|
||||
switch(_demoStyle) {
|
||||
case TabsDemoStyle.iconsAndText:
|
||||
return new Tab(text: page.text, icon: new Icon(page.icon));
|
||||
case TabsDemoStyle.iconsOnly:
|
||||
return new Tab(icon: new Icon(page.icon));
|
||||
case TabsDemoStyle.textOnly:
|
||||
return new Tab(text: page.text);
|
||||
}
|
||||
}).toList(),
|
||||
),
|
||||
body: 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,
|
||||
color: iconColor,
|
||||
size: 128.0
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}).toList()
|
||||
)
|
||||
)
|
||||
),
|
||||
body: new TabBarView(
|
||||
controller: _controller,
|
||||
children: _allPages.map((_Page page) {
|
||||
return new Container(
|
||||
key: new ObjectKey(page.icon),
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child:new Card(
|
||||
child: new Center(
|
||||
child: new Icon(
|
||||
page.icon,
|
||||
color: iconColor,
|
||||
size: 128.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList()
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -111,30 +111,21 @@ class _CardDataItem extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class TabsDemo extends StatefulWidget {
|
||||
TabsDemo({ Key key }) : super(key: key);
|
||||
|
||||
class TabsDemo extends StatelessWidget {
|
||||
static const String routeName = '/tabs';
|
||||
|
||||
@override
|
||||
_TabsDemoState createState() => new _TabsDemoState();
|
||||
}
|
||||
|
||||
class _TabsDemoState extends State<TabsDemo> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return new TabBarSelection<_Page>(
|
||||
values: _allPages.keys.toList(),
|
||||
return new DefaultTabController(
|
||||
length: _allPages.length,
|
||||
child: new Scaffold(
|
||||
appBar: new AppBar(
|
||||
title: new Text('Tabs and scrolling'),
|
||||
bottom: new TabBar<_Page>(
|
||||
labels: new Map<_Page, TabLabel>.fromIterable(_allPages.keys, value: (_Page page) {
|
||||
return new TabLabel(text: page.label);
|
||||
})
|
||||
)
|
||||
bottom: new TabBar(
|
||||
tabs: _allPages.keys.map((_Page page) => new Tab(text: page.label)).toList(),
|
||||
),
|
||||
),
|
||||
body: new TabBarView<_Page>(
|
||||
body: new TabBarView(
|
||||
children: _allPages.keys.map((_Page page) {
|
||||
return new ScrollableList(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
|
||||
@ -144,11 +135,11 @@ class _TabsDemoState extends State<TabsDemo> {
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: new _CardDataItem(page: page, data: data)
|
||||
);
|
||||
}).toList()
|
||||
}).toList(),
|
||||
);
|
||||
}).toList()
|
||||
)
|
||||
)
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,12 @@
|
||||
|
||||
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 {
|
||||
_Page({ this.label, this.colors, this.icon });
|
||||
|
||||
@ -11,7 +17,6 @@ class _Page {
|
||||
final Map<int, Color> colors;
|
||||
final IconData icon;
|
||||
|
||||
TabLabel get tabLabel => new TabLabel(text: label.toUpperCase());
|
||||
Color get labelColor => colors != null ? colors[300] : Colors.grey[300];
|
||||
bool get fabDefined => colors != null && icon != null;
|
||||
Color get fabColor => colors[400];
|
||||
@ -19,11 +24,13 @@ class _Page {
|
||||
Key get fabKey => new ValueKey<Color>(fabColor);
|
||||
}
|
||||
|
||||
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.";
|
||||
final List<_Page> _allPages = <_Page>[
|
||||
new _Page(label: 'Blue', colors: Colors.indigo, icon: Icons.add),
|
||||
new _Page(label: 'Eco', colors: Colors.green, icon: Icons.create),
|
||||
new _Page(label: 'No'),
|
||||
new _Page(label: 'Teal', colors: Colors.teal, icon: Icons.add),
|
||||
new _Page(label: 'Red', colors: Colors.red, icon: Icons.create),
|
||||
];
|
||||
|
||||
class TabsFabDemo extends StatefulWidget {
|
||||
static const String routeName = '/tabs-fab';
|
||||
@ -32,31 +39,34 @@ class TabsFabDemo extends StatefulWidget {
|
||||
_TabsFabDemoState createState() => new _TabsFabDemoState();
|
||||
}
|
||||
|
||||
class _TabsFabDemoState extends State<TabsFabDemo> {
|
||||
final GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>();
|
||||
final List<_Page> pages = <_Page>[
|
||||
new _Page(label: 'Blue', colors: Colors.indigo, icon: Icons.add),
|
||||
new _Page(label: 'Eco', colors: Colors.green, icon: Icons.create),
|
||||
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;
|
||||
class _TabsFabDemoState extends State<TabsFabDemo> with SingleTickerProviderStateMixin {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
|
||||
|
||||
TabController _controller;
|
||||
_Page _selectedPage;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
selectedPage = pages[0];
|
||||
_controller = new TabController(vsync: this, length: _allPages.length);
|
||||
_controller.addListener(_handleTabSelection);
|
||||
_selectedPage = _allPages[0];
|
||||
}
|
||||
|
||||
void _handleTabSelection(_Page page) {
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleTabSelection() {
|
||||
setState(() {
|
||||
selectedPage = page;
|
||||
_selectedPage = _allPages[_controller.index];
|
||||
});
|
||||
}
|
||||
|
||||
void _showExplanatoryText() {
|
||||
scaffoldKey.currentState.showBottomSheet((BuildContext context) {
|
||||
_scaffoldKey.currentState.showBottomSheet((BuildContext context) {
|
||||
return new Container(
|
||||
decoration: new BoxDecoration(
|
||||
border: new Border(top: new BorderSide(color: Theme.of(context).dividerColor))
|
||||
@ -93,26 +103,26 @@ class _TabsFabDemoState extends State<TabsFabDemo> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return new TabBarSelection<_Page>(
|
||||
values: pages,
|
||||
onChanged: _handleTabSelection,
|
||||
child: new Scaffold(
|
||||
key: scaffoldKey,
|
||||
appBar: new AppBar(
|
||||
title: new Text('FAB per tab'),
|
||||
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,
|
||||
tooltip: 'Show explanation',
|
||||
backgroundColor: selectedPage.fabColor,
|
||||
child: selectedPage.fabIcon,
|
||||
onPressed: _showExplanatoryText
|
||||
),
|
||||
body: new TabBarView<_Page>(children: pages.map(buildTabView).toList())
|
||||
)
|
||||
return new Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: new AppBar(
|
||||
title: new Text('FAB per tab'),
|
||||
bottom: new TabBar(
|
||||
controller: _controller,
|
||||
tabs: _allPages.map((_Page page) => new Tab(text: page.label.toUpperCase())).toList(),
|
||||
)
|
||||
),
|
||||
floatingActionButton: !_selectedPage.fabDefined ? null : new FloatingActionButton(
|
||||
key: _selectedPage.fabKey,
|
||||
tooltip: 'Show explanation',
|
||||
backgroundColor: _selectedPage.fabColor,
|
||||
child: _selectedPage.fabIcon,
|
||||
onPressed: _showExplanatoryText
|
||||
),
|
||||
body: new TabBarView(
|
||||
controller: _controller,
|
||||
children: _allPages.map(buildTabView).toList()
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -20,13 +20,6 @@ class ComponentDemoTabData {
|
||||
final String description;
|
||||
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
|
||||
bool operator==(Object other) {
|
||||
if (other.runtimeType != runtimeType)
|
||||
@ -49,8 +42,7 @@ class TabbedComponentDemoScaffold extends StatelessWidget {
|
||||
final String title;
|
||||
|
||||
void _showExampleCode(BuildContext context) {
|
||||
TabBarSelectionState<ComponentDemoTabData> selection = TabBarSelection.of(context);
|
||||
String tag = selection.value?.exampleCodeTag;
|
||||
String tag = demos[DefaultTabController.of(context).index].exampleCodeTag;
|
||||
if (tag != null) {
|
||||
Navigator.push(context, new MaterialPageRoute<FullScreenCodeDialog>(
|
||||
builder: (BuildContext context) => new FullScreenCodeDialog(exampleCodeTag: tag)
|
||||
@ -60,8 +52,8 @@ class TabbedComponentDemoScaffold extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return new TabBarSelection<ComponentDemoTabData>(
|
||||
values: demos,
|
||||
return new DefaultTabController(
|
||||
length: demos.length,
|
||||
child: new Scaffold(
|
||||
appBar: new AppBar(
|
||||
title: new Text(title),
|
||||
@ -71,17 +63,19 @@ class TabbedComponentDemoScaffold extends StatelessWidget {
|
||||
return new IconButton(
|
||||
icon: new Icon(Icons.description),
|
||||
tooltip: 'Show example code',
|
||||
onPressed: () { _showExampleCode(context); }
|
||||
onPressed: () {
|
||||
_showExampleCode(context);
|
||||
},
|
||||
);
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
],
|
||||
bottom: new TabBar<ComponentDemoTabData>(
|
||||
bottom: new TabBar(
|
||||
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) {
|
||||
return new Column(
|
||||
children: <Widget>[
|
||||
@ -92,11 +86,11 @@ class TabbedComponentDemoScaffold extends StatelessWidget {
|
||||
)
|
||||
),
|
||||
new Expanded(child: demo.widget)
|
||||
]
|
||||
],
|
||||
);
|
||||
}).toList()
|
||||
)
|
||||
)
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -222,11 +222,11 @@ class StockHomeState extends State<StockHome> {
|
||||
]
|
||||
)
|
||||
],
|
||||
bottom: new TabBar<StockHomeTab>(
|
||||
labels: <StockHomeTab, TabLabel>{
|
||||
StockHomeTab.market: new TabLabel(text: StockStrings.of(context).market()),
|
||||
StockHomeTab.portfolio: new TabLabel(text: StockStrings.of(context).portfolio())
|
||||
}
|
||||
bottom: new TabBar(
|
||||
tabs: <Widget>[
|
||||
new Tab(text: StockStrings.of(context).market()),
|
||||
new Tab(text: StockStrings.of(context).portfolio()),
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -318,14 +318,14 @@ class StockHomeState extends State<StockHome> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return new TabBarSelection<StockHomeTab>(
|
||||
values: <StockHomeTab>[StockHomeTab.market, StockHomeTab.portfolio],
|
||||
return new DefaultTabController(
|
||||
length: 2,
|
||||
child: new Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: _isSearching ? buildSearchBar() : buildAppBar(),
|
||||
floatingActionButton: buildFloatingActionButton(),
|
||||
drawer: _buildDrawer(context),
|
||||
body: new TabBarView<StockHomeTab>(
|
||||
body: new TabBarView(
|
||||
children: <Widget>[
|
||||
_buildStockTab(context, StockHomeTab.market, config.symbols),
|
||||
_buildStockTab(context, StockHomeTab.portfolio, portfolioSymbols),
|
||||
|
@ -71,6 +71,7 @@ export 'src/material/snack_bar.dart';
|
||||
export 'src/material/stepper.dart';
|
||||
export 'src/material/switch.dart';
|
||||
export 'src/material/tabs.dart';
|
||||
export 'src/material/tab_controller.dart';
|
||||
export 'src/material/theme.dart';
|
||||
export 'src/material/theme_data.dart';
|
||||
export 'src/material/time_picker.dart';
|
||||
|
@ -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.
|
||||
const int kRadialReactionAlpha = 0x33;
|
||||
|
||||
/// The duration
|
||||
const Duration kTabScrollDuration = const Duration(milliseconds: 200);
|
||||
|
202
packages/flutter/lib/src/material/tab_controller.dart
Normal file
202
packages/flutter/lib/src/material/tab_controller.dart
Normal file
@ -0,0 +1,202 @@
|
||||
// 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,
|
||||
);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -28,33 +28,70 @@ class StateMarkerState extends State<StateMarker> {
|
||||
|
||||
Widget buildFrame({ List<String> tabs, String value, bool isScrollable: false, Key tabBarKey }) {
|
||||
return new Material(
|
||||
child: new TabBarSelection<String>(
|
||||
value: value,
|
||||
values: tabs,
|
||||
child: new TabBar<String>(
|
||||
child: new DefaultTabController(
|
||||
initialIndex: tabs.indexOf(value),
|
||||
length: tabs.length,
|
||||
child: new TabBar(
|
||||
key: tabBarKey,
|
||||
labels: new Map<String, TabLabel>.fromIterable(tabs, value: (String tab) => new TabLabel(text: tab)),
|
||||
isScrollable: isScrollable
|
||||
)
|
||||
)
|
||||
tabs: tabs.map((String tab) => new Tab(text: tab)).toList(),
|
||||
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 }) {
|
||||
return new MaterialApp(
|
||||
theme: new ThemeData(platform: TargetPlatform.android),
|
||||
home: new TabBarSelection<String>(
|
||||
value: value,
|
||||
values: tabs,
|
||||
home: new DefaultTabController(
|
||||
initialIndex: tabs.indexOf(value),
|
||||
length: tabs.length,
|
||||
child: new Scaffold(
|
||||
appBar: new AppBar(
|
||||
title: new Text('tabs'),
|
||||
bottom: new TabBar<String>(
|
||||
labels: new Map<String, TabLabel>.fromIterable(tabs, value: (String tab) => new TabLabel(text: tab)),
|
||||
)
|
||||
bottom: new TabBar(
|
||||
tabs: tabs.map((String tab) => new Tab(text: tab)).toList(),
|
||||
),
|
||||
),
|
||||
body: new TabBarView<String>(
|
||||
body: new TabBarView(
|
||||
children: <Widget>[
|
||||
new Center(child: new Text('LEFT CHILD')),
|
||||
new Center(child: new Text('RIGHT CHILD'))
|
||||
@ -70,83 +107,72 @@ void main() {
|
||||
List<String> tabs = <String>['A', 'B', 'C'];
|
||||
|
||||
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('B'), findsOneWidget);
|
||||
expect(find.text('C'), findsOneWidget);
|
||||
expect(selection.index, equals(2));
|
||||
expect(selection.previousIndex, equals(2));
|
||||
expect(selection.value, equals('C'));
|
||||
expect(selection.previousValue, equals('C'));
|
||||
TabController controller = DefaultTabController.of(tester.element(find.text('A')));
|
||||
expect(controller, isNotNull);
|
||||
expect(controller.index, 2);
|
||||
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.pump();
|
||||
expect(selection.valueIsChanging, true);
|
||||
expect(controller.indexIsChanging, true);
|
||||
await tester.pump(const Duration(seconds: 1)); // finish the animation
|
||||
expect(selection.valueIsChanging, false);
|
||||
expect(selection.value, equals('B'));
|
||||
expect(selection.previousValue, equals('C'));
|
||||
expect(selection.index, equals(1));
|
||||
expect(selection.previousIndex, equals(2));
|
||||
expect(controller.index, 1);
|
||||
expect(controller.previousIndex, 2);
|
||||
expect(controller.indexIsChanging, false);
|
||||
|
||||
await tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: false));
|
||||
await tester.tap(find.text('C'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
expect(selection.value, equals('C'));
|
||||
expect(selection.previousValue, equals('B'));
|
||||
expect(selection.index, equals(2));
|
||||
expect(selection.previousIndex, equals(1));
|
||||
expect(controller.index, 2);
|
||||
expect(controller.previousIndex, 1);
|
||||
|
||||
await tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: false));
|
||||
await tester.tap(find.text('A'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
expect(selection.value, equals('A'));
|
||||
expect(selection.previousValue, equals('C'));
|
||||
expect(selection.index, equals(0));
|
||||
expect(selection.previousIndex, equals(2));
|
||||
expect(controller.index, 0);
|
||||
expect(controller.previousIndex, 2);
|
||||
});
|
||||
|
||||
testWidgets('Scrollable TabBar tap selects tab', (WidgetTester tester) async {
|
||||
List<String> tabs = <String>['A', 'B', 'C'];
|
||||
|
||||
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('B'), 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('B'));
|
||||
await tester.pump();
|
||||
expect(selection.value, equals('B'));
|
||||
|
||||
await tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: true));
|
||||
await tester.tap(find.text('C'));
|
||||
await tester.pump();
|
||||
expect(selection.value, equals('C'));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
expect(controller.index, 2);
|
||||
|
||||
await tester.tap(find.text('B'));
|
||||
await tester.pump();
|
||||
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.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 {
|
||||
List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE', 'FFFFFF', 'GGGGGG', 'HHHHHH', 'IIIIII', 'JJJJJJ', 'KKKKKK', 'LLLLLL'];
|
||||
Key tabBarKey = new Key('TabBar');
|
||||
await tester.pumpWidget(buildFrame(tabs: tabs, value: 'AAAAAA', isScrollable: true, tabBarKey: tabBarKey));
|
||||
TabBarSelectionState<String> selection = TabBarSelection.of(tester.element(find.text('AAAAAA')));
|
||||
expect(selection, isNotNull);
|
||||
expect(selection.value, equals('AAAAAA'));
|
||||
TabController controller = DefaultTabController.of(tester.element(find.text('AAAAAA')));
|
||||
expect(controller, isNotNull);
|
||||
expect(controller.index, 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
|
||||
@ -155,7 +181,7 @@ void main() {
|
||||
await tester.tap(find.text('FFFFFF'));
|
||||
await tester.pump();
|
||||
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
|
||||
expect(tester.getCenter(find.text('FFFFFF')).x, closeTo(400.0, 1.0));
|
||||
});
|
||||
@ -165,9 +191,9 @@ void main() {
|
||||
List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE', 'FFFFFF', 'GGGGGG', 'HHHHHH', 'IIIIII', 'JJJJJJ', 'KKKKKK', 'LLLLLL'];
|
||||
Key tabBarKey = new Key('TabBar');
|
||||
await tester.pumpWidget(buildFrame(tabs: tabs, value: 'AAAAAA', isScrollable: true, tabBarKey: tabBarKey));
|
||||
TabBarSelectionState<String> selection = TabBarSelection.of(tester.element(find.text('AAAAAA')));
|
||||
expect(selection, isNotNull);
|
||||
expect(selection.value, equals('AAAAAA'));
|
||||
TabController controller = DefaultTabController.of(tester.element(find.text('AAAAAA')));
|
||||
expect(controller, isNotNull);
|
||||
expect(controller.index, 0);
|
||||
|
||||
// Fling-scroll the TabBar to the left
|
||||
expect(tester.getCenter(find.text('HHHHHH')).x, lessThan(700.0));
|
||||
@ -177,31 +203,26 @@ void main() {
|
||||
expect(tester.getCenter(find.text('HHHHHH')).x, lessThan(500.0));
|
||||
|
||||
// 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'];
|
||||
String value = tabs[0];
|
||||
|
||||
void onTabSelectionChanged(String newValue) {
|
||||
value = newValue;
|
||||
}
|
||||
|
||||
Widget builder() {
|
||||
return new Material(
|
||||
child: new TabBarSelection<String>(
|
||||
value: value,
|
||||
values: tabs,
|
||||
onChanged: onTabSelectionChanged,
|
||||
child: new TabBarView<String>(
|
||||
child: new DefaultTabController(
|
||||
initialIndex: tabs.indexOf(value),
|
||||
length: tabs.length,
|
||||
child: new TabBarView(
|
||||
children: tabs.map((String name) {
|
||||
return new StateMarker(
|
||||
child: new Text(name)
|
||||
);
|
||||
}).toList()
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -210,6 +231,8 @@ void main() {
|
||||
}
|
||||
|
||||
await tester.pumpWidget(builder());
|
||||
TabController controller = DefaultTabController.of(tester.element(find.text('AAAAAA')));
|
||||
|
||||
TestGesture gesture = await tester.startGesture(tester.getCenter(find.text(tabs[0])));
|
||||
await gesture.moveBy(const Offset(-600.0, 0.0));
|
||||
await tester.pump();
|
||||
@ -218,6 +241,7 @@ void main() {
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
value = tabs[controller.index];
|
||||
expect(value, equals(tabs[1]));
|
||||
await tester.pumpWidget(builder());
|
||||
expect(findStateMarkerState(tabs[1]).marker, equals('marked'));
|
||||
@ -230,6 +254,7 @@ void main() {
|
||||
await tester.pump();
|
||||
expect(findStateMarkerState(tabs[1]).marker, equals('marked'));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
value = tabs[controller.index];
|
||||
expect(value, equals(tabs[2]));
|
||||
await tester.pumpWidget(builder());
|
||||
|
||||
@ -248,6 +273,7 @@ void main() {
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
value = tabs[controller.index];
|
||||
expect(value, equals(tabs[1]));
|
||||
await tester.pumpWidget(builder());
|
||||
expect(findStateMarkerState(tabs[1]).marker, equals('marked'));
|
||||
@ -262,15 +288,15 @@ void main() {
|
||||
expect(find.text('LEFT CHILD'), findsOneWidget);
|
||||
expect(find.text('RIGHT CHILD'), findsNothing);
|
||||
|
||||
TabBarSelectionState<String> selection = TabBarSelection.of(tester.element(find.text('LEFT')));
|
||||
expect(selection.value, equals('LEFT'));
|
||||
TabController controller = DefaultTabController.of(tester.element(find.text('LEFT')));
|
||||
expect(controller.index, 0);
|
||||
|
||||
// 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
|
||||
expect(selection.value, equals('RIGHT'));
|
||||
expect(controller.index, 1);
|
||||
expect(find.text('LEFT CHILD'), findsNothing);
|
||||
expect(find.text('RIGHT CHILD'), findsOneWidget);
|
||||
|
||||
@ -279,7 +305,7 @@ void main() {
|
||||
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
|
||||
expect(selection.value, equals('LEFT'));
|
||||
expect(controller.index, 0);
|
||||
expect(find.text('LEFT CHILD'), findsOneWidget);
|
||||
expect(find.text('RIGHT CHILD'), findsNothing);
|
||||
});
|
||||
@ -294,8 +320,8 @@ void main() {
|
||||
expect(find.text('LEFT CHILD'), findsOneWidget);
|
||||
expect(find.text('RIGHT CHILD'), findsNothing);
|
||||
|
||||
TabBarSelectionState<String> selection = TabBarSelection.of(tester.element(find.text('LEFT')));
|
||||
expect(selection.value, equals('LEFT'));
|
||||
TabController controller = DefaultTabController.of(tester.element(find.text('LEFT')));
|
||||
expect(controller.index, 0);
|
||||
|
||||
// End the fling by reversing direction. This should cause not cause
|
||||
// a change to the selected tab, everything should just settle back to
|
||||
@ -304,7 +330,7 @@ void main() {
|
||||
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
|
||||
expect(selection.value, equals('LEFT'));
|
||||
expect(controller.index, 0);
|
||||
expect(find.text('LEFT CHILD'), findsOneWidget);
|
||||
expect(find.text('RIGHT CHILD'), findsNothing);
|
||||
});
|
||||
@ -321,17 +347,17 @@ void main() {
|
||||
child: new SizedBox(
|
||||
width: 300.0,
|
||||
height: 200.0,
|
||||
child: new TabBarSelection<String>(
|
||||
values: tabs,
|
||||
child: new DefaultTabController(
|
||||
length: tabs.length,
|
||||
child: new Scaffold(
|
||||
appBar: new AppBar(
|
||||
title: new Text('tabs'),
|
||||
bottom: new TabBar<String>(
|
||||
bottom: new TabBar(
|
||||
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(),
|
||||
),
|
||||
),
|
||||
@ -348,4 +374,175 @@ void main() {
|
||||
final RenderBox box = tester.renderObject(find.text('BBBBBB'));
|
||||
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
|
||||
});
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user