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(
|
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 {
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
new Container(
|
new Container(
|
||||||
child: new TabPageSelector<String>()
|
child: new TabPageSelector()
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
@ -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
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|||||||
|
|
||||||
@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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 @@
|
|||||||
|
|
||||||
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(
|
||||||
return new Scaffold(
|
icon: new Icon(Icons.chevron_left),
|
||||||
appBar: new AppBar(title: new Text('Page selector')),
|
color: color,
|
||||||
body: new TabBarSelection<IconData>(
|
onPressed: () { _handleArrowButtonPress(context, -1); },
|
||||||
values: icons,
|
tooltip: 'Page back'
|
||||||
child: new Builder(
|
),
|
||||||
builder: (BuildContext context) {
|
new TabPageSelector(controller: controller),
|
||||||
final Color color = Theme.of(context).accentColor;
|
new IconButton(
|
||||||
return new Column(
|
icon: new Icon(Icons.chevron_right),
|
||||||
children: <Widget>[
|
color: color,
|
||||||
new Container(
|
onPressed: () { _handleArrowButtonPress(context, 1); },
|
||||||
margin: const EdgeInsets.only(top: 16.0),
|
tooltip: 'Page forward'
|
||||||
child: new Row(
|
)
|
||||||
children: <Widget>[
|
],
|
||||||
new IconButton(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween
|
||||||
icon: new Icon(Icons.chevron_left),
|
)
|
||||||
color: color,
|
),
|
||||||
onPressed: () { _handleArrowButtonPress(context, -1); },
|
new Expanded(
|
||||||
tooltip: 'Page back'
|
child: new TabBarView(
|
||||||
),
|
children: icons.map((IconData icon) {
|
||||||
new TabPageSelector<IconData>(),
|
return new Container(
|
||||||
new IconButton(
|
key: new ObjectKey(icon),
|
||||||
icon: new Icon(Icons.chevron_right),
|
padding: const EdgeInsets.all(12.0),
|
||||||
color: color,
|
child: new Card(
|
||||||
onPressed: () { _handleArrowButtonPress(context, 1); },
|
child: new Center(
|
||||||
tooltip: 'Page forward'
|
child: new Icon(icon, size: 128.0, color: color)
|
||||||
)
|
),
|
||||||
],
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
new Expanded(
|
);
|
||||||
child: new TabBarView<IconData>(
|
}).toList()
|
||||||
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)
|
|
||||||
)
|
class PageSelectorDemo extends StatelessWidget {
|
||||||
)
|
static const String routeName = '/page-selector';
|
||||||
);
|
static final List<IconData> icons = <IconData>[
|
||||||
})
|
Icons.event,
|
||||||
.toList()
|
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
|
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,27 +32,22 @@ 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,
|
|
||||||
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',
|
|
||||||
};
|
|
||||||
|
|
||||||
TabsDemoStyle _demoStyle = TabsDemoStyle.iconsAndText;
|
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) {
|
void changeDemoStyle(TabsDemoStyle style) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_demoStyle = style;
|
_demoStyle = style;
|
||||||
@ -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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
|||||||
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 @@
|
|||||||
|
|
||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
_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];
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleTabSelection(_Page page) {
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
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> {
|
|||||||
|
|
||||||
@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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
|
|
||||||
@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 {
|
|||||||
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 {
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
new Expanded(child: demo.widget)
|
new Expanded(child: demo.widget)
|
||||||
]
|
],
|
||||||
);
|
);
|
||||||
}).toList()
|
}).toList(),
|
||||||
)
|
),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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> {
|
|||||||
|
|
||||||
@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';
|
|||||||
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);
|
|||||||
|
|
||||||
/// 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);
|
||||||
|
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 }) {
|
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() {
|
|||||||
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('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.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, 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.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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user