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