@@ -49,6 +49,41 @@ enum TabBarIndicatorSize {
49
49
label,
50
50
}
51
51
52
+ /// Defines how tabs are aligned horizontally in a [TabBar] .
53
+ ///
54
+ /// See also:
55
+ ///
56
+ /// * [TabBar] , which displays a row of tabs.
57
+ /// * [TabBarView] , which displays a widget for the currently selected tab.
58
+ /// * [TabBar.tabAlignment] , which defines the horizontal alignment of the
59
+ /// tabs within the [TabBar].
60
+ enum TabAlignment {
61
+ // TODO(tahatesser): Add a link to the Material Design spec for
62
+ // horizontal offset when it is available.
63
+ // It's currently sourced from androidx/compose/material3/TabRow.kt.
64
+ /// If [TabBar.isScrollable] is true, tabs are aligned to the
65
+ /// start of the [TabBar] . Otherwise throws an exception.
66
+ ///
67
+ /// It is not recommended to set [TabAlignment.start] when
68
+ /// [ThemeData.useMaterial3] is false.
69
+ start,
70
+
71
+ /// If [TabBar.isScrollable] is true, tabs are aligned to the
72
+ /// start of the [TabBar] with an offset of 52.0 pixels.
73
+ /// Otherwise throws an exception.
74
+ ///
75
+ /// It is not recommended to set [TabAlignment.startOffset] when
76
+ /// [ThemeData.useMaterial3] is false.
77
+ startOffset,
78
+
79
+ /// If [TabBar.isScrollable] is false, tabs are stretched to fill the
80
+ /// [TabBar] . Otherwise throws an exception.
81
+ fill,
82
+
83
+ /// Tabs are aligned to the center of the [TabBar] .
84
+ center,
85
+ }
86
+
52
87
/// A Material Design [TabBar] tab.
53
88
///
54
89
/// If both [icon] and [text] are provided, the text is displayed below
@@ -306,9 +341,9 @@ class _TabLabelBar extends Flex {
306
341
const _TabLabelBar ({
307
342
super .children,
308
343
required this .onPerformLayout,
344
+ required super .mainAxisSize,
309
345
}) : super (
310
346
direction: Axis .horizontal,
311
- mainAxisSize: MainAxisSize .max,
312
347
mainAxisAlignment: MainAxisAlignment .start,
313
348
crossAxisAlignment: CrossAxisAlignment .center,
314
349
verticalDirection: VerticalDirection .down,
@@ -695,6 +730,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
695
730
this .physics,
696
731
this .splashFactory,
697
732
this .splashBorderRadius,
733
+ this .tabAlignment,
698
734
}) : _isPrimary = true ,
699
735
assert (indicator != null || (indicatorWeight > 0.0 ));
700
736
@@ -744,6 +780,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
744
780
this .physics,
745
781
this .splashFactory,
746
782
this .splashBorderRadius,
783
+ this .tabAlignment,
747
784
}) : _isPrimary = false ,
748
785
assert (indicator != null || (indicatorWeight > 0.0 ));
749
786
@@ -1027,6 +1064,25 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
1027
1064
/// If this property is null, it is interpreted as [BorderRadius.zero] .
1028
1065
final BorderRadius ? splashBorderRadius;
1029
1066
1067
+ /// Specifies the horizontal alignment of the tabs within a [TabBar] .
1068
+ ///
1069
+ /// If [TabBar.isScrollable] is false, only [TabAlignment.fill] and
1070
+ /// [TabAlignment.center] are supported. Otherwise an exception is thrown.
1071
+ ///
1072
+ /// If [TabBar.isScrollable] is true, only [TabAlignment.start] , [TabAlignment.startOffset] ,
1073
+ /// and [TabAlignment.center] are supported. Otherwise an exception is thrown.
1074
+ ///
1075
+ /// If this is null, then the value of [TabBarTheme.tabAlignment] is used.
1076
+ ///
1077
+ /// If [TabBarTheme.tabAlignment] is null and [ThemeData.useMaterial3] is true,
1078
+ /// then [TabAlignment.startOffset] is used if [isScrollable] is true,
1079
+ /// otherwise [TabAlignment.fill] is used.
1080
+ ///
1081
+ /// If [TabBarTheme.tabAlignment] is null and [ThemeData.useMaterial3] is false,
1082
+ /// then [TabAlignment.center] is used if [isScrollable] is true,
1083
+ /// otherwise [TabAlignment.fill] is used.
1084
+ final TabAlignment ? tabAlignment;
1085
+
1030
1086
/// A size whose height depends on if the tabs have both icons and text.
1031
1087
///
1032
1088
/// [AppBar] uses this size to compute its own preferred size.
@@ -1089,10 +1145,10 @@ class _TabBarState extends State<TabBar> {
1089
1145
TabBarTheme get _defaults {
1090
1146
if (Theme .of (context).useMaterial3) {
1091
1147
return widget._isPrimary
1092
- ? _TabsPrimaryDefaultsM3 (context)
1093
- : _TabsSecondaryDefaultsM3 (context);
1148
+ ? _TabsPrimaryDefaultsM3 (context, widget.isScrollable )
1149
+ : _TabsSecondaryDefaultsM3 (context, widget.isScrollable );
1094
1150
} else {
1095
- return _TabsDefaultsM2 (context);
1151
+ return _TabsDefaultsM2 (context, widget.isScrollable );
1096
1152
}
1097
1153
}
1098
1154
@@ -1378,10 +1434,32 @@ class _TabBarState extends State<TabBar> {
1378
1434
return true ;
1379
1435
}
1380
1436
1437
+ bool _debugTabAlignmentIsValid (TabAlignment tabAlignment) {
1438
+ assert (() {
1439
+ if (widget.isScrollable && tabAlignment == TabAlignment .fill) {
1440
+ throw FlutterError (
1441
+ '$tabAlignment is only valid for non-scrollable tab bars.' ,
1442
+ );
1443
+ }
1444
+ if (! widget.isScrollable
1445
+ && (tabAlignment == TabAlignment .start
1446
+ || tabAlignment == TabAlignment .startOffset)) {
1447
+ throw FlutterError (
1448
+ '$tabAlignment is only valid for scrollable tab bars.' ,
1449
+ );
1450
+ }
1451
+ return true ;
1452
+ }());
1453
+ return true ;
1454
+ }
1455
+
1381
1456
@override
1382
1457
Widget build (BuildContext context) {
1383
1458
assert (debugCheckHasMaterialLocalizations (context));
1384
1459
assert (_debugScheduleCheckHasValidTabsCount ());
1460
+ final TabBarTheme tabBarTheme = TabBarTheme .of (context);
1461
+ final TabAlignment effectiveTabAlignment = widget.tabAlignment ?? tabBarTheme.tabAlignment ?? _defaults.tabAlignment! ;
1462
+ assert (_debugTabAlignmentIsValid (effectiveTabAlignment));
1385
1463
1386
1464
final MaterialLocalizations localizations = MaterialLocalizations .of (context);
1387
1465
if (_controller! .length == 0 ) {
@@ -1390,7 +1468,6 @@ class _TabBarState extends State<TabBar> {
1390
1468
);
1391
1469
}
1392
1470
1393
- final TabBarTheme tabBarTheme = TabBarTheme .of (context);
1394
1471
1395
1472
final List <Widget > wrappedTabs = List <Widget >.generate (widget.tabs.length, (int index) {
1396
1473
const double verticalAdjustment = (_kTextAndIconTabHeight - _kTabHeight)/ 2.0 ;
@@ -1491,7 +1568,7 @@ class _TabBarState extends State<TabBar> {
1491
1568
),
1492
1569
),
1493
1570
);
1494
- if (! widget.isScrollable) {
1571
+ if (! widget.isScrollable && effectiveTabAlignment == TabAlignment .fill ) {
1495
1572
wrappedTabs[index] = Expanded (child: wrappedTabs[index]);
1496
1573
}
1497
1574
}
@@ -1509,12 +1586,16 @@ class _TabBarState extends State<TabBar> {
1509
1586
defaults: _defaults,
1510
1587
child: _TabLabelBar (
1511
1588
onPerformLayout: _saveTabOffsets,
1589
+ mainAxisSize: effectiveTabAlignment == TabAlignment .fill ? MainAxisSize .max : MainAxisSize .min,
1512
1590
children: wrappedTabs,
1513
1591
),
1514
1592
),
1515
1593
);
1516
1594
1517
1595
if (widget.isScrollable) {
1596
+ final EdgeInsetsGeometry ? effectivePadding = effectiveTabAlignment == TabAlignment .startOffset
1597
+ ? const EdgeInsetsDirectional .only (start: 56.0 ).add (widget.padding ?? EdgeInsets .zero)
1598
+ : widget.padding;
1518
1599
_scrollController ?? = _TabBarScrollController (this );
1519
1600
tabBar = ScrollConfiguration (
1520
1601
// The scrolling tabs should not show an overscroll indicator.
@@ -1523,7 +1604,7 @@ class _TabBarState extends State<TabBar> {
1523
1604
dragStartBehavior: widget.dragStartBehavior,
1524
1605
scrollDirection: Axis .horizontal,
1525
1606
controller: _scrollController,
1526
- padding: widget.padding ,
1607
+ padding: effectivePadding ,
1527
1608
physics: widget.physics,
1528
1609
child: tabBar,
1529
1610
),
@@ -2030,10 +2111,11 @@ class TabPageSelector extends StatelessWidget {
2030
2111
2031
2112
// Hand coded defaults based on Material Design 2.
2032
2113
class _TabsDefaultsM2 extends TabBarTheme {
2033
- const _TabsDefaultsM2 (this .context)
2114
+ const _TabsDefaultsM2 (this .context, this .isScrollable )
2034
2115
: super (indicatorSize: TabBarIndicatorSize .tab);
2035
2116
2036
2117
final BuildContext context;
2118
+ final bool isScrollable;
2037
2119
2038
2120
@override
2039
2121
Color ? get indicatorColor => Theme .of (context).indicatorColor;
@@ -2049,6 +2131,9 @@ class _TabsDefaultsM2 extends TabBarTheme {
2049
2131
2050
2132
@override
2051
2133
InteractiveInkFeatureFactory ? get splashFactory => Theme .of (context).splashFactory;
2134
+
2135
+ @override
2136
+ TabAlignment ? get tabAlignment => isScrollable ? TabAlignment .start : TabAlignment .fill;
2052
2137
}
2053
2138
2054
2139
// BEGIN GENERATED TOKEN PROPERTIES - Tabs
@@ -2061,12 +2146,13 @@ class _TabsDefaultsM2 extends TabBarTheme {
2061
2146
// Token database version: v0_162
2062
2147
2063
2148
class _TabsPrimaryDefaultsM3 extends TabBarTheme {
2064
- _TabsPrimaryDefaultsM3 (this .context)
2149
+ _TabsPrimaryDefaultsM3 (this .context, this .isScrollable )
2065
2150
: super (indicatorSize: TabBarIndicatorSize .label);
2066
2151
2067
2152
final BuildContext context;
2068
2153
late final ColorScheme _colors = Theme .of (context).colorScheme;
2069
2154
late final TextTheme _textTheme = Theme .of (context).textTheme;
2155
+ final bool isScrollable;
2070
2156
2071
2157
@override
2072
2158
Color ? get dividerColor => _colors.surfaceVariant;
@@ -2116,15 +2202,19 @@ class _TabsPrimaryDefaultsM3 extends TabBarTheme {
2116
2202
2117
2203
@override
2118
2204
InteractiveInkFeatureFactory ? get splashFactory => Theme .of (context).splashFactory;
2205
+
2206
+ @override
2207
+ TabAlignment ? get tabAlignment => isScrollable ? TabAlignment .start : TabAlignment .fill;
2119
2208
}
2120
2209
2121
2210
class _TabsSecondaryDefaultsM3 extends TabBarTheme {
2122
- _TabsSecondaryDefaultsM3 (this .context)
2211
+ _TabsSecondaryDefaultsM3 (this .context, this .isScrollable )
2123
2212
: super (indicatorSize: TabBarIndicatorSize .tab);
2124
2213
2125
2214
final BuildContext context;
2126
2215
late final ColorScheme _colors = Theme .of (context).colorScheme;
2127
2216
late final TextTheme _textTheme = Theme .of (context).textTheme;
2217
+ final bool isScrollable;
2128
2218
2129
2219
@override
2130
2220
Color ? get dividerColor => _colors.surfaceVariant;
@@ -2174,6 +2264,9 @@ class _TabsSecondaryDefaultsM3 extends TabBarTheme {
2174
2264
2175
2265
@override
2176
2266
InteractiveInkFeatureFactory ? get splashFactory => Theme .of (context).splashFactory;
2267
+
2268
+ @override
2269
+ TabAlignment ? get tabAlignment => isScrollable ? TabAlignment .start : TabAlignment .fill;
2177
2270
}
2178
2271
2179
2272
// END GENERATED TOKEN PROPERTIES - Tabs
0 commit comments