mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
TabNavigator animates selected TabView
The TabBar's selection is now represented by a TabBarSelection object which encapsulates both the previous and currently selected indices and the Performance used to animate the selection indicator. Added a TabBarView class which displays a tab's contents. It uses a shared TabBarSelection to stay in sync with a TabBar. The TabBarView scrolls in sync with the TabBar when the selection changes. Eventually it will allow one to fling the selection forward or backwards. Added a tabBar property to ToolBar. Typically the corresponding TabBarView will be the body of the toolbar's Scaffold. Removed TabNavigatorView and TabNavigator. Added a widget gallery tabs demo page. Removed the old tabs demo.
This commit is contained in:
parent
40b3cf3f0f
commit
9de4df1e8b
@ -3,3 +3,9 @@ material-design-icons:
|
||||
- name: navigation/arrow_drop_down
|
||||
- name: navigation/cancel
|
||||
- name: navigation/menu
|
||||
- name: action/event
|
||||
- name: action/home
|
||||
- name: action/android
|
||||
- name: action/alarm
|
||||
- name: action/face
|
||||
- name: action/language
|
||||
|
59
examples/material_gallery/lib/demo/tabs_demo.dart
Normal file
59
examples/material_gallery/lib/demo/tabs_demo.dart
Normal file
@ -0,0 +1,59 @@
|
||||
// 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/material.dart';
|
||||
|
||||
import 'widget_demo.dart';
|
||||
|
||||
final TabBarSelection _selection = new TabBarSelection();
|
||||
final List<String> _iconNames = <String>["event", "home", "android", "alarm", "face", "language"];
|
||||
|
||||
Widget buildTabBar(_) {
|
||||
return new TabBar(
|
||||
selection: _selection,
|
||||
isScrollable: true,
|
||||
labels: _iconNames.map((String iconName) => new TabLabel(text: iconName, icon: "action/$iconName")).toList()
|
||||
);
|
||||
}
|
||||
|
||||
class TabsDemo extends StatefulComponent {
|
||||
_TabsDemoState createState() => new _TabsDemoState();
|
||||
}
|
||||
|
||||
class _TabsDemoState extends State<TabsDemo> {
|
||||
double _viewWidth = 100.0;
|
||||
|
||||
void _handleSizeChanged(Size newSize) {
|
||||
setState(() {
|
||||
_viewWidth = newSize.width;
|
||||
});
|
||||
}
|
||||
|
||||
Widget build(_) {
|
||||
return new SizeObserver(
|
||||
onSizeChanged: _handleSizeChanged,
|
||||
child: new TabBarView<String>(
|
||||
selection: _selection,
|
||||
items: _iconNames,
|
||||
itemExtent: _viewWidth,
|
||||
itemBuilder: (BuildContext context, String iconName, int index) {
|
||||
return new Container(
|
||||
key: new ValueKey<String>(iconName),
|
||||
padding: const EdgeDims.all(12.0),
|
||||
child: new Card(
|
||||
child: new Center(child: new Icon(icon: "action/$iconName", size:IconSize.s48))
|
||||
)
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final WidgetDemo kTabsDemo = new WidgetDemo(
|
||||
title: 'Tabs',
|
||||
routeName: '/tabs',
|
||||
tabBarBuilder: buildTabBar,
|
||||
builder: (_) => new TabsDemo()
|
||||
);
|
@ -5,9 +5,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class WidgetDemo {
|
||||
WidgetDemo({ this.title, this.routeName, this.builder });
|
||||
WidgetDemo({ this.title, this.routeName, this.tabBarBuilder, this.builder });
|
||||
|
||||
final String title;
|
||||
final String routeName;
|
||||
final WidgetBuilder tabBarBuilder;
|
||||
final WidgetBuilder builder;
|
||||
}
|
||||
|
@ -40,6 +40,11 @@ class GalleryPage extends StatelessComponent {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _tabBar(BuildContext context) {
|
||||
final WidgetBuilder builder = active?.tabBarBuilder;
|
||||
return builder != null ? builder(context) : null;
|
||||
}
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
return new Scaffold(
|
||||
toolBar: new ToolBar(
|
||||
@ -47,7 +52,8 @@ class GalleryPage extends StatelessComponent {
|
||||
icon: 'navigation/menu',
|
||||
onPressed: () { _showDrawer(context); }
|
||||
),
|
||||
center: new Text(active?.title ?? 'Material gallery')
|
||||
center: new Text(active?.title ?? 'Material gallery'),
|
||||
tabBar: _tabBar(context)
|
||||
),
|
||||
body: _body(context)
|
||||
);
|
||||
|
@ -9,6 +9,7 @@ import 'demo/date_picker_demo.dart';
|
||||
import 'demo/drop_down_demo.dart';
|
||||
import 'demo/selection_controls_demo.dart';
|
||||
import 'demo/slider_demo.dart';
|
||||
import 'demo/tabs_demo.dart';
|
||||
import 'demo/time_picker_demo.dart';
|
||||
import 'demo/widget_demo.dart';
|
||||
import 'gallery_page.dart';
|
||||
@ -18,6 +19,7 @@ final List<WidgetDemo> _kDemos = <WidgetDemo>[
|
||||
kSelectionControlsDemo,
|
||||
kSliderDemo,
|
||||
kDatePickerDemo,
|
||||
kTabsDemo,
|
||||
kTimePickerDemo,
|
||||
kDropDownDemo,
|
||||
];
|
||||
|
@ -6,6 +6,8 @@ part of stocks;
|
||||
|
||||
typedef void ModeUpdater(StockMode mode);
|
||||
|
||||
enum StockHomeTab { market, portfolio }
|
||||
|
||||
class StockHome extends StatefulComponent {
|
||||
StockHome(this.stocks, this.symbols, this.stockMode, this.modeUpdater);
|
||||
|
||||
@ -20,6 +22,7 @@ class StockHome extends StatefulComponent {
|
||||
class StockHomeState extends State<StockHome> {
|
||||
|
||||
final GlobalKey scaffoldKey = new GlobalKey();
|
||||
final TabBarSelection _tabBarSelection = new TabBarSelection();
|
||||
bool _isSearching = false;
|
||||
String _searchQuery;
|
||||
|
||||
@ -160,7 +163,13 @@ class StockHomeState extends State<StockHome> {
|
||||
icon: "navigation/more_vert",
|
||||
onPressed: _handleMenuShow
|
||||
)
|
||||
]
|
||||
],
|
||||
tabBar: new TabBar(
|
||||
selection: _tabBarSelection,
|
||||
labels: <TabLabel>[
|
||||
const TabLabel(text: 'MARKET'),
|
||||
const TabLabel(text: 'PORTFOLIO')]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -208,25 +217,6 @@ class StockHomeState extends State<StockHome> {
|
||||
|
||||
static const List<String> portfolioSymbols = const <String>["AAPL","FIZZ", "FIVE", "FLAT", "ZINC", "ZNGA"];
|
||||
|
||||
Widget buildTabNavigator() {
|
||||
return new TabNavigator(
|
||||
views: <TabNavigatorView>[
|
||||
new TabNavigatorView(
|
||||
label: const TabLabel(text: 'MARKET'),
|
||||
builder: (BuildContext context) => buildStockList(context, _filterBySearchQuery(_getStockList(config.symbols)).toList())
|
||||
),
|
||||
new TabNavigatorView(
|
||||
label: const TabLabel(text: 'PORTFOLIO'),
|
||||
builder: (BuildContext context) => buildStockList(context, _filterBySearchQuery(_getStockList(portfolioSymbols)).toList())
|
||||
)
|
||||
],
|
||||
selectedIndex: selectedTabIndex,
|
||||
onChanged: (int tabIndex) {
|
||||
setState(() { selectedTabIndex = tabIndex; } );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
static GlobalKey searchFieldKey = new GlobalKey();
|
||||
static GlobalKey companyNameKey = new GlobalKey();
|
||||
|
||||
@ -270,12 +260,43 @@ class StockHomeState extends State<StockHome> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildStockTab(BuildContext context, StockHomeTab tab, List<String> stockSymbols) {
|
||||
return new Container(
|
||||
key: new ValueKey<StockHomeTab>(tab),
|
||||
child: buildStockList(context, _filterBySearchQuery(_getStockList(stockSymbols)).toList())
|
||||
);
|
||||
}
|
||||
|
||||
double _viewWidth = 100.0;
|
||||
void _handleSizeChanged(Size newSize) {
|
||||
setState(() {
|
||||
_viewWidth = newSize.width;
|
||||
});
|
||||
}
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
return new Scaffold(
|
||||
key: scaffoldKey,
|
||||
toolBar: _isSearching ? buildSearchBar() : buildToolBar(),
|
||||
body: buildTabNavigator(),
|
||||
floatingActionButton: buildFloatingActionButton()
|
||||
floatingActionButton: buildFloatingActionButton(),
|
||||
body: new SizeObserver(
|
||||
onSizeChanged: _handleSizeChanged,
|
||||
child: new TabBarView<StockHomeTab>(
|
||||
selection: _tabBarSelection,
|
||||
items: [StockHomeTab.market, StockHomeTab.portfolio],
|
||||
itemExtent: _viewWidth,
|
||||
itemBuilder: (BuildContext context, StockHomeTab tab, _) {
|
||||
switch (tab) {
|
||||
case StockHomeTab.market:
|
||||
return buildStockTab(context, tab, config.symbols);
|
||||
case StockHomeTab.portfolio:
|
||||
return buildStockTab(context, tab, portfolioSymbols);
|
||||
default:
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,146 +0,0 @@
|
||||
// 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/material.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
|
||||
class TabbedNavigatorApp extends StatefulComponent {
|
||||
TabbedNavigatorAppState createState() => new TabbedNavigatorAppState();
|
||||
}
|
||||
|
||||
class TabbedNavigatorAppState extends State<TabbedNavigatorApp> {
|
||||
// The index of the selected tab for each of the TabNavigators constructed below.
|
||||
List<int> selectedIndices = new List<int>.filled(5, 0);
|
||||
|
||||
TabNavigator _buildTabNavigator(int n, List<TabNavigatorView> views, Key key, {isScrollable: false}) {
|
||||
return new TabNavigator(
|
||||
key: key,
|
||||
views: views,
|
||||
selectedIndex: selectedIndices[n],
|
||||
isScrollable: isScrollable,
|
||||
onChanged: (int tabIndex) {
|
||||
setState(() { selectedIndices[n] = tabIndex; } );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(String label) {
|
||||
return new Center(
|
||||
child: new Text(label, style: const TextStyle(fontSize: 48.0, fontWeight: FontWeight.w800))
|
||||
);
|
||||
}
|
||||
|
||||
TabNavigator _buildTextLabelsTabNavigator(int n) {
|
||||
Iterable<TabNavigatorView> views = ["ONE", "TWO", "FREE", "FOUR"]
|
||||
.map((text) {
|
||||
return new TabNavigatorView(
|
||||
label: new TabLabel(text: text),
|
||||
builder: (BuildContext context) => _buildContent(text)
|
||||
);
|
||||
});
|
||||
return _buildTabNavigator(n, views.toList(), const ValueKey<String>('textLabelsTabNavigator'));
|
||||
}
|
||||
|
||||
TabNavigator _buildIconLabelsTabNavigator(int n) {
|
||||
Iterable<TabNavigatorView> views = ["event", "home", "android", "alarm", "face", "language"]
|
||||
.map((String iconName) {
|
||||
return new TabNavigatorView(
|
||||
label: new TabLabel(icon: "action/$iconName"),
|
||||
builder: (BuildContext context) => _buildContent(iconName)
|
||||
);
|
||||
});
|
||||
return _buildTabNavigator(n, views.toList(), const ValueKey<String>('iconLabelsTabNavigator'));
|
||||
}
|
||||
|
||||
TabNavigator _buildTextAndIconLabelsTabNavigator(int n) {
|
||||
List<TabNavigatorView> views = <TabNavigatorView>[
|
||||
new TabNavigatorView(
|
||||
label: const TabLabel(text: 'STOCKS', icon: 'action/list'),
|
||||
builder: (BuildContext context) => _buildContent("Stocks")
|
||||
),
|
||||
new TabNavigatorView(
|
||||
label: const TabLabel(text: 'PORTFOLIO', icon: 'action/account_circle'),
|
||||
builder: (BuildContext context) => _buildContent("Portfolio")
|
||||
),
|
||||
new TabNavigatorView(
|
||||
label: const TabLabel(text: 'SUMMARY', icon: 'action/assessment'),
|
||||
builder: (BuildContext context) => _buildContent("Summary")
|
||||
)
|
||||
];
|
||||
return _buildTabNavigator(n, views, const ValueKey<String>('textAndIconLabelsTabNavigator'));
|
||||
}
|
||||
|
||||
TabNavigator _buildScrollableTabNavigator(int n) {
|
||||
Iterable<TabNavigatorView> views = [
|
||||
"MIN WIDTH",
|
||||
"THIS TAB LABEL IS SO WIDE THAT IT OCCUPIES TWO LINES",
|
||||
"THIS TAB IS PRETTY WIDE TOO",
|
||||
"MORE",
|
||||
"TABS",
|
||||
"TO",
|
||||
"STRETCH",
|
||||
"OUT",
|
||||
"THE",
|
||||
"TAB BAR"
|
||||
]
|
||||
.map((text) {
|
||||
return new TabNavigatorView(
|
||||
label: new TabLabel(text: text),
|
||||
builder: (BuildContext context) => _buildContent(text)
|
||||
);
|
||||
});
|
||||
return _buildTabNavigator(n, views.toList(), const ValueKey<String>('scrollableTabNavigator'), isScrollable: true);
|
||||
}
|
||||
|
||||
|
||||
Container _buildCard(BuildContext context, TabNavigator tabNavigator) {
|
||||
return new Container(
|
||||
padding: const EdgeDims.all(12.0),
|
||||
child: new Card(child: new Padding(child: tabNavigator, padding: const EdgeDims.all(8.0)))
|
||||
);
|
||||
}
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
List<TabNavigatorView> views = <TabNavigatorView>[
|
||||
new TabNavigatorView(
|
||||
label: const TabLabel(text: 'TEXT'),
|
||||
builder: (BuildContext context) => _buildCard(context, _buildTextLabelsTabNavigator(0))
|
||||
),
|
||||
new TabNavigatorView(
|
||||
label: const TabLabel(text: 'ICONS'),
|
||||
builder: (BuildContext context) => _buildCard(context, _buildIconLabelsTabNavigator(1))
|
||||
),
|
||||
new TabNavigatorView(
|
||||
label: const TabLabel(text: 'BOTH'),
|
||||
builder: (BuildContext context) => _buildCard(context, _buildTextAndIconLabelsTabNavigator(2))
|
||||
),
|
||||
new TabNavigatorView(
|
||||
label: const TabLabel(text: 'SCROLL'),
|
||||
builder: (BuildContext context) => _buildCard(context, _buildScrollableTabNavigator(3))
|
||||
)
|
||||
];
|
||||
|
||||
TabNavigator tabNavigator = _buildTabNavigator(4, views, const ValueKey<String>('tabs'));
|
||||
assert(selectedIndices.length == 5);
|
||||
|
||||
ToolBar toolbar = new ToolBar(
|
||||
center: new Text('Tabbed Navigator', style: Typography.white.title),
|
||||
elevation: 0
|
||||
);
|
||||
|
||||
return new Scaffold(
|
||||
toolBar: toolbar,
|
||||
body: tabNavigator
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
runApp(new MaterialApp(
|
||||
title: 'Tabs',
|
||||
routes: <String, RouteBuilder>{
|
||||
'/': (RouteArguments args) => new TabbedNavigatorApp(),
|
||||
}
|
||||
));
|
||||
}
|
@ -292,14 +292,12 @@ class TabLabel {
|
||||
}
|
||||
}
|
||||
|
||||
class Tab extends StatelessComponent {
|
||||
Tab({
|
||||
class _Tab extends StatelessComponent {
|
||||
_Tab({
|
||||
Key key,
|
||||
this.onSelected,
|
||||
this.label,
|
||||
this.color,
|
||||
this.selected: false,
|
||||
this.selectedColor
|
||||
this.color
|
||||
}) : super(key: key) {
|
||||
assert(label.text != null || label.icon != null);
|
||||
}
|
||||
@ -307,19 +305,16 @@ class Tab extends StatelessComponent {
|
||||
final VoidCallback onSelected;
|
||||
final TabLabel label;
|
||||
final Color color;
|
||||
final bool selected;
|
||||
final Color selectedColor;
|
||||
|
||||
Widget _buildLabelText() {
|
||||
assert(label.text != null);
|
||||
TextStyle style = new TextStyle(color: selected ? selectedColor : color);
|
||||
TextStyle style = new TextStyle(color: color);
|
||||
return new Text(label.text, style: style);
|
||||
}
|
||||
|
||||
Widget _buildLabelIcon() {
|
||||
assert(label.icon != null);
|
||||
Color iconColor = selected ? selectedColor : color;
|
||||
ColorFilter filter = new ColorFilter.mode(iconColor, TransferMode.srcATop);
|
||||
ColorFilter filter = new ColorFilter.mode(color, TransferMode.srcATop);
|
||||
return new Icon(icon: label.icon, colorFilter: filter);
|
||||
}
|
||||
|
||||
@ -381,18 +376,46 @@ class _TabsScrollBehavior extends BoundedBehavior {
|
||||
}
|
||||
}
|
||||
|
||||
class TabBarSelection {
|
||||
TabBarSelection({ int index: 0, this.onChanged }) : _index = index;
|
||||
|
||||
final VoidCallback onChanged;
|
||||
|
||||
PerformanceView get performance => _performance.view;
|
||||
final _performance = new Performance(duration: _kTabBarScroll, progress: 1.0);
|
||||
|
||||
int get index => _index;
|
||||
int _index;
|
||||
void set index(int value) {
|
||||
if (value == _index)
|
||||
return;
|
||||
_previousIndex = _index;
|
||||
_index = value;
|
||||
_performance
|
||||
..progress = 0.0
|
||||
..play().then((_) {
|
||||
if (onChanged != null)
|
||||
onChanged();
|
||||
});
|
||||
}
|
||||
|
||||
int get previousIndex => _previousIndex;
|
||||
int _previousIndex = 0;
|
||||
}
|
||||
|
||||
class TabBar extends Scrollable {
|
||||
TabBar({
|
||||
Key key,
|
||||
this.labels,
|
||||
this.selectedIndex: 0,
|
||||
this.onChanged,
|
||||
this.selection,
|
||||
this.isScrollable: false
|
||||
}) : super(key: key, scrollDirection: ScrollDirection.horizontal);
|
||||
}) : super(key: key, scrollDirection: ScrollDirection.horizontal) {
|
||||
assert(labels != null);
|
||||
assert(selection != null);
|
||||
}
|
||||
|
||||
final Iterable<TabLabel> labels;
|
||||
final int selectedIndex;
|
||||
final TabSelectedIndexChanged onChanged;
|
||||
final TabBarSelection selection;
|
||||
final bool isScrollable;
|
||||
|
||||
_TabBarState createState() => new _TabBarState();
|
||||
@ -401,36 +424,53 @@ class TabBar extends Scrollable {
|
||||
class _TabBarState extends ScrollableState<TabBar> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_indicatorAnimation = new ValuePerformance<Rect>()
|
||||
..duration = _kTabBarScroll
|
||||
..variable = new AnimatedRectValue(null, curve: Curves.ease);
|
||||
scrollBehavior.isScrollable = config.isScrollable;
|
||||
config.selection._performance
|
||||
..addStatusListener(_handleStatusChange)
|
||||
..addListener(_handleProgressChange);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
config.selection._performance
|
||||
..removeStatusListener(_handleStatusChange)
|
||||
..removeListener(_handleProgressChange)
|
||||
..stop();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Performance get _performance => config.selection._performance;
|
||||
|
||||
bool _indicatorRectIsValid = false;
|
||||
|
||||
// The performance's status change is our indication that the selection index has
|
||||
// changed. We don't start animating the _indicatorRect until after we've reset
|
||||
// _indicatorRect here.
|
||||
void _handleStatusChange(PerformanceStatus status) {
|
||||
_indicatorRectIsValid = status == PerformanceStatus.forward;
|
||||
if (status == PerformanceStatus.forward) {
|
||||
if (config.isScrollable)
|
||||
scrollTo(_centeredTabScrollOffset(config.selection.index), duration: _kTabBarScroll);
|
||||
setState(() {
|
||||
_indicatorRect
|
||||
..begin = _indicatorRect.value ?? _tabIndicatorRect(config.selection.previousIndex)
|
||||
..end = _tabIndicatorRect(config.selection.index);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _handleProgressChange() {
|
||||
// Performance listeners are notified before statusListeners.
|
||||
if (_indicatorRectIsValid && _performance.status == PerformanceStatus.forward) {
|
||||
setState(() {
|
||||
_indicatorRect.setProgress(_performance.progress, AnimationDirection.forward);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Size _tabBarSize;
|
||||
Size _viewportSize = Size.zero;
|
||||
Size _tabBarSize;
|
||||
List<double> _tabWidths;
|
||||
ValuePerformance<Rect> _indicatorAnimation;
|
||||
|
||||
void didUpdateConfig(TabBar oldConfig) {
|
||||
super.didUpdateConfig(oldConfig);
|
||||
if (!config.isScrollable)
|
||||
scrollTo(0.0);
|
||||
}
|
||||
|
||||
AnimatedRectValue get _indicatorRect => _indicatorAnimation.variable;
|
||||
|
||||
void _startIndicatorAnimation(int fromTabIndex, int toTabIndex) {
|
||||
_indicatorRect
|
||||
..begin = (_indicatorRect.value == null ? _tabIndicatorRect(fromTabIndex) : _indicatorRect.value)
|
||||
..end = _tabIndicatorRect(toTabIndex);
|
||||
_indicatorAnimation
|
||||
..progress = 0.0
|
||||
..play();
|
||||
}
|
||||
|
||||
ScrollBehavior createScrollBehavior() => new _TabsScrollBehavior();
|
||||
_TabsScrollBehavior get scrollBehavior => super.scrollBehavior;
|
||||
AnimatedRectValue _indicatorRect = new AnimatedRectValue(null, curve: Curves.ease);
|
||||
|
||||
Rect _tabRect(int tabIndex) {
|
||||
assert(_tabBarSize != null);
|
||||
@ -450,31 +490,40 @@ class _TabBarState extends ScrollableState<TabBar> {
|
||||
return new Rect.fromLTRB(r.left, r.bottom, r.right, r.bottom + _kTabIndicatorHeight);
|
||||
}
|
||||
|
||||
void didUpdateConfig(TabBar oldConfig) {
|
||||
super.didUpdateConfig(oldConfig);
|
||||
if (!config.isScrollable)
|
||||
scrollTo(0.0);
|
||||
}
|
||||
|
||||
ScrollBehavior createScrollBehavior() => new _TabsScrollBehavior();
|
||||
_TabsScrollBehavior get scrollBehavior => super.scrollBehavior;
|
||||
|
||||
double _centeredTabScrollOffset(int tabIndex) {
|
||||
double viewportWidth = scrollBehavior.containerExtent;
|
||||
return (_tabRect(tabIndex).left + _tabWidths[tabIndex] / 2.0 - viewportWidth / 2.0)
|
||||
Rect tabRect = _tabRect(tabIndex);
|
||||
return (tabRect.left + tabRect.width / 2.0 - viewportWidth / 2.0)
|
||||
.clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset);
|
||||
}
|
||||
|
||||
void _handleTabSelected(int tabIndex) {
|
||||
if (tabIndex != config.selectedIndex) {
|
||||
if (_tabWidths != null) {
|
||||
if (config.isScrollable)
|
||||
scrollTo(_centeredTabScrollOffset(tabIndex), duration: _kTabBarScroll);
|
||||
_startIndicatorAnimation(config.selectedIndex, tabIndex);
|
||||
}
|
||||
if (config.onChanged != null)
|
||||
config.onChanged(tabIndex);
|
||||
}
|
||||
if (tabIndex != config.selection.index)
|
||||
setState(() {
|
||||
config.selection.index = tabIndex;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _toTab(TabLabel label, int tabIndex, Color color, Color selectedColor) {
|
||||
return new Tab(
|
||||
onSelected: () => _handleTabSelected(tabIndex),
|
||||
Color labelColor = color;
|
||||
if (tabIndex == config.selection.index)
|
||||
labelColor = Color.lerp(color, selectedColor, _performance.progress);
|
||||
else if (tabIndex == config.selection.previousIndex)
|
||||
labelColor = Color.lerp(selectedColor, color, _performance.progress);
|
||||
|
||||
return new _Tab(
|
||||
onSelected: () { _handleTabSelected(tabIndex); },
|
||||
label: label,
|
||||
color: color,
|
||||
selected: tabIndex == config.selectedIndex,
|
||||
selectedColor: selectedColor
|
||||
color: labelColor
|
||||
);
|
||||
}
|
||||
|
||||
@ -496,6 +545,8 @@ class _TabBarState extends ScrollableState<TabBar> {
|
||||
void _handleViewportSizeChanged(Size newSize) {
|
||||
_viewportSize = newSize;
|
||||
_updateScrollBehavior();
|
||||
if (config.isScrollable)
|
||||
scrollTo(_centeredTabScrollOffset(config.selection.index), duration: _kTabBarScroll);
|
||||
}
|
||||
|
||||
Widget buildContent(BuildContext context) {
|
||||
@ -526,11 +577,11 @@ class _TabBarState extends ScrollableState<TabBar> {
|
||||
style: textStyle,
|
||||
child: new BuilderTransition(
|
||||
variables: <AnimatedValue<Rect>>[_indicatorRect],
|
||||
performance: _indicatorAnimation.view,
|
||||
performance: config.selection.performance,
|
||||
builder: (BuildContext context) {
|
||||
return new _TabBarWrapper(
|
||||
children: tabs,
|
||||
selectedIndex: config.selectedIndex,
|
||||
selectedIndex: config.selection.index,
|
||||
indicatorColor: indicatorColor,
|
||||
indicatorRect: _indicatorRect.value,
|
||||
textAndIcons: textAndIcons,
|
||||
@ -563,46 +614,111 @@ class _TabBarState extends ScrollableState<TabBar> {
|
||||
}
|
||||
}
|
||||
|
||||
class TabNavigatorView {
|
||||
TabNavigatorView({ this.label, this.builder }) {
|
||||
assert(builder != null);
|
||||
}
|
||||
|
||||
// this uses a builder for the contents, rather than a raw Widget child,
|
||||
// because there might be many, many tabs and some might be relatively
|
||||
// expensive to create up front. This way, the view is only created lazily.
|
||||
|
||||
final TabLabel label;
|
||||
final WidgetBuilder builder;
|
||||
}
|
||||
|
||||
class TabNavigator extends StatelessComponent {
|
||||
TabNavigator({
|
||||
class TabBarView<T> extends ScrollableList<T> {
|
||||
TabBarView({
|
||||
Key key,
|
||||
this.views,
|
||||
this.selectedIndex: 0,
|
||||
this.onChanged,
|
||||
this.isScrollable: false
|
||||
}) : super(key: key);
|
||||
this.selection,
|
||||
List<T> items,
|
||||
ItemBuilder<T> itemBuilder,
|
||||
double itemExtent
|
||||
}) : super(
|
||||
key: key,
|
||||
scrollDirection: ScrollDirection.horizontal,
|
||||
items: items,
|
||||
itemBuilder: itemBuilder,
|
||||
itemExtent: itemExtent,
|
||||
itemsWrap: false
|
||||
) {
|
||||
assert(selection != null);
|
||||
}
|
||||
|
||||
final List<TabNavigatorView> views;
|
||||
final int selectedIndex;
|
||||
final TabSelectedIndexChanged onChanged;
|
||||
final bool isScrollable;
|
||||
final TabBarSelection selection;
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
assert(views != null && views.isNotEmpty);
|
||||
assert(selectedIndex >= 0 && selectedIndex < views.length);
|
||||
return new Column(<Widget>[
|
||||
new TabBar(
|
||||
labels: views.map((TabNavigatorView view) => view.label),
|
||||
onChanged: onChanged,
|
||||
selectedIndex: selectedIndex,
|
||||
isScrollable: isScrollable
|
||||
),
|
||||
new Flexible(child: views[selectedIndex].builder(context))
|
||||
],
|
||||
alignItems: FlexAlignItems.stretch
|
||||
);
|
||||
_TabBarViewState createState() => new _TabBarViewState<T>();
|
||||
}
|
||||
|
||||
// TODO(hansmuller): horizontal scrolling should drive the TabSelection's performance.
|
||||
class _NotScrollable extends BoundedBehavior {
|
||||
bool get isScrollable => false;
|
||||
}
|
||||
|
||||
class _TabBarViewState<T> extends ScrollableListState<T, TabBarView<T>> {
|
||||
|
||||
ScrollBehavior createScrollBehavior() => new _NotScrollable();
|
||||
|
||||
List<int> _itemIndices = [0, 1];
|
||||
AnimationDirection _scrollDirection = AnimationDirection.forward;
|
||||
|
||||
void _initItemIndicesAndScrollPosition() {
|
||||
final int selectedIndex = config.selection.index;
|
||||
|
||||
if (selectedIndex == 0) {
|
||||
_itemIndices = <int>[0, 1];
|
||||
scrollTo(0.0);
|
||||
} else if (selectedIndex == config.items.length - 1) {
|
||||
_itemIndices = <int>[selectedIndex - 1, selectedIndex];
|
||||
scrollTo(config.itemExtent);
|
||||
} else {
|
||||
_itemIndices = <int>[selectedIndex - 1, selectedIndex, selectedIndex + 1];
|
||||
scrollTo(config.itemExtent);
|
||||
}
|
||||
}
|
||||
|
||||
Performance get _performance => config.selection._performance;
|
||||
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initItemIndicesAndScrollPosition();
|
||||
_performance
|
||||
..addStatusListener(_handleStatusChange)
|
||||
..addListener(_handleProgressChange);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_performance
|
||||
..removeStatusListener(_handleStatusChange)
|
||||
..removeListener(_handleProgressChange)
|
||||
..stop();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void didUpdateConfig(TabBarView oldConfig) {
|
||||
super.didUpdateConfig(oldConfig);
|
||||
if (oldConfig.itemExtent != config.itemExtent && !_performance.isAnimating)
|
||||
_initItemIndicesAndScrollPosition();
|
||||
}
|
||||
|
||||
void _handleStatusChange(PerformanceStatus status) {
|
||||
final int selectedIndex = config.selection.index;
|
||||
final int previousSelectedIndex = config.selection.previousIndex;
|
||||
|
||||
if (status == PerformanceStatus.forward) {
|
||||
if (selectedIndex < previousSelectedIndex) {
|
||||
_itemIndices = <int>[selectedIndex, previousSelectedIndex];
|
||||
_scrollDirection = AnimationDirection.reverse;
|
||||
} else {
|
||||
_itemIndices = <int>[previousSelectedIndex, selectedIndex];
|
||||
_scrollDirection = AnimationDirection.forward;
|
||||
}
|
||||
} else if (status == PerformanceStatus.completed) {
|
||||
_initItemIndicesAndScrollPosition();
|
||||
}
|
||||
}
|
||||
|
||||
void _handleProgressChange() {
|
||||
if (_scrollDirection == AnimationDirection.forward)
|
||||
scrollTo(config.itemExtent * _performance.progress);
|
||||
else
|
||||
scrollTo(config.itemExtent * (1.0 - _performance.progress));
|
||||
}
|
||||
|
||||
int get itemCount => _itemIndices.length;
|
||||
|
||||
List<Widget> buildItems(BuildContext context, int start, int count) {
|
||||
return _itemIndices
|
||||
.skip(start)
|
||||
.take(count)
|
||||
.map((int i) => config.itemBuilder(context, config.items[i], i))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import 'constants.dart';
|
||||
import 'icon_theme.dart';
|
||||
import 'icon_theme_data.dart';
|
||||
import 'shadows.dart';
|
||||
import 'tabs.dart';
|
||||
import 'theme.dart';
|
||||
import 'typography.dart';
|
||||
|
||||
@ -18,6 +19,7 @@ class ToolBar extends StatelessComponent {
|
||||
this.center,
|
||||
this.right,
|
||||
this.bottom,
|
||||
this.tabBar,
|
||||
this.elevation: 4,
|
||||
this.backgroundColor,
|
||||
this.textTheme,
|
||||
@ -28,6 +30,7 @@ class ToolBar extends StatelessComponent {
|
||||
final Widget center;
|
||||
final List<Widget> right;
|
||||
final Widget bottom;
|
||||
final TabBar tabBar;
|
||||
final int elevation;
|
||||
final Color backgroundColor;
|
||||
final TextTheme textTheme;
|
||||
@ -40,6 +43,7 @@ class ToolBar extends StatelessComponent {
|
||||
center: center,
|
||||
right: right,
|
||||
bottom: bottom,
|
||||
tabBar: tabBar,
|
||||
elevation: elevation,
|
||||
backgroundColor: backgroundColor,
|
||||
textTheme: textTheme,
|
||||
@ -90,6 +94,9 @@ class ToolBar extends StatelessComponent {
|
||||
child: new Container(height: kExtendedToolBarHeight - kToolBarHeight, child: bottom)
|
||||
));
|
||||
|
||||
if (tabBar != null)
|
||||
columnChildren.add(tabBar);
|
||||
|
||||
Widget content = new AnimatedContainer(
|
||||
duration: kThemeChangeDuration,
|
||||
padding: new EdgeDims.symmetric(horizontal: 8.0),
|
||||
|
@ -7,16 +7,13 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
int selectedIndex = 2;
|
||||
TabBarSelection selection;
|
||||
|
||||
Widget buildFrame({ List<String> tabs, bool isScrollable: false }) {
|
||||
return new TabBar(
|
||||
labels: tabs.map((String tab) => new TabLabel(text: tab)).toList(),
|
||||
selectedIndex: selectedIndex,
|
||||
isScrollable: isScrollable,
|
||||
onChanged: (int tabIndex) {
|
||||
selectedIndex = tabIndex;
|
||||
}
|
||||
selection: selection,
|
||||
isScrollable: isScrollable
|
||||
);
|
||||
}
|
||||
|
||||
@ -24,56 +21,56 @@ void main() {
|
||||
test('TabBar tap selects tab', () {
|
||||
testWidgets((WidgetTester tester) {
|
||||
List<String> tabs = <String>['A', 'B', 'C'];
|
||||
selectedIndex = 2;
|
||||
selection = new TabBarSelection(index: 2);
|
||||
|
||||
tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: false));
|
||||
expect(tester.findText('A'), isNotNull);
|
||||
expect(tester.findText('B'), isNotNull);
|
||||
expect(tester.findText('C'), isNotNull);
|
||||
expect(selectedIndex, equals(2));
|
||||
expect(selection.index, equals(2));
|
||||
|
||||
tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: false));
|
||||
tester.tap(tester.findText('B'));
|
||||
tester.pump();
|
||||
expect(selectedIndex, equals(1));
|
||||
expect(selection.index, equals(1));
|
||||
|
||||
tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: false));
|
||||
tester.tap(tester.findText('C'));
|
||||
tester.pump();
|
||||
expect(selectedIndex, equals(2));
|
||||
expect(selection.index, equals(2));
|
||||
|
||||
tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: false));
|
||||
tester.tap(tester.findText('A'));
|
||||
tester.pump();
|
||||
expect(selectedIndex, equals(0));
|
||||
expect(selection.index, equals(0));
|
||||
});
|
||||
});
|
||||
|
||||
test('Scrollable TabBar tap selects tab', () {
|
||||
testWidgets((WidgetTester tester) {
|
||||
List<String> tabs = <String>['A', 'B', 'C'];
|
||||
selectedIndex = 2;
|
||||
selection = new TabBarSelection(index: 2);
|
||||
|
||||
tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: true));
|
||||
expect(tester.findText('A'), isNotNull);
|
||||
expect(tester.findText('B'), isNotNull);
|
||||
expect(tester.findText('C'), isNotNull);
|
||||
expect(selectedIndex, equals(2));
|
||||
expect(selection.index, equals(2));
|
||||
|
||||
tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: true));
|
||||
tester.tap(tester.findText('B'));
|
||||
tester.pump();
|
||||
expect(selectedIndex, equals(1));
|
||||
expect(selection.index, equals(1));
|
||||
|
||||
tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: true));
|
||||
tester.tap(tester.findText('C'));
|
||||
tester.pump();
|
||||
expect(selectedIndex, equals(2));
|
||||
expect(selection.index, equals(2));
|
||||
|
||||
tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: true));
|
||||
tester.tap(tester.findText('A'));
|
||||
tester.pump();
|
||||
expect(selectedIndex, equals(0));
|
||||
expect(selection.index, equals(0));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user