@@ -49,6 +49,41 @@ enum TabBarIndicatorSize {
4949 label,
5050}
5151
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+
5287/// A Material Design [TabBar] tab.
5388///
5489/// If both [icon] and [text] are provided, the text is displayed below
@@ -306,9 +341,9 @@ class _TabLabelBar extends Flex {
306341 const _TabLabelBar ({
307342 super .children,
308343 required this .onPerformLayout,
344+ required super .mainAxisSize,
309345 }) : super (
310346 direction: Axis .horizontal,
311- mainAxisSize: MainAxisSize .max,
312347 mainAxisAlignment: MainAxisAlignment .start,
313348 crossAxisAlignment: CrossAxisAlignment .center,
314349 verticalDirection: VerticalDirection .down,
@@ -695,6 +730,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
695730 this .physics,
696731 this .splashFactory,
697732 this .splashBorderRadius,
733+ this .tabAlignment,
698734 }) : _isPrimary = true ,
699735 assert (indicator != null || (indicatorWeight > 0.0 ));
700736
@@ -744,6 +780,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
744780 this .physics,
745781 this .splashFactory,
746782 this .splashBorderRadius,
783+ this .tabAlignment,
747784 }) : _isPrimary = false ,
748785 assert (indicator != null || (indicatorWeight > 0.0 ));
749786
@@ -1027,6 +1064,25 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
10271064 /// If this property is null, it is interpreted as [BorderRadius.zero] .
10281065 final BorderRadius ? splashBorderRadius;
10291066
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+
10301086 /// A size whose height depends on if the tabs have both icons and text.
10311087 ///
10321088 /// [AppBar] uses this size to compute its own preferred size.
@@ -1089,10 +1145,10 @@ class _TabBarState extends State<TabBar> {
10891145 TabBarTheme get _defaults {
10901146 if (Theme .of (context).useMaterial3) {
10911147 return widget._isPrimary
1092- ? _TabsPrimaryDefaultsM3 (context)
1093- : _TabsSecondaryDefaultsM3 (context);
1148+ ? _TabsPrimaryDefaultsM3 (context, widget.isScrollable )
1149+ : _TabsSecondaryDefaultsM3 (context, widget.isScrollable );
10941150 } else {
1095- return _TabsDefaultsM2 (context);
1151+ return _TabsDefaultsM2 (context, widget.isScrollable );
10961152 }
10971153 }
10981154
@@ -1378,10 +1434,32 @@ class _TabBarState extends State<TabBar> {
13781434 return true ;
13791435 }
13801436
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+
13811456 @override
13821457 Widget build (BuildContext context) {
13831458 assert (debugCheckHasMaterialLocalizations (context));
13841459 assert (_debugScheduleCheckHasValidTabsCount ());
1460+ final TabBarTheme tabBarTheme = TabBarTheme .of (context);
1461+ final TabAlignment effectiveTabAlignment = widget.tabAlignment ?? tabBarTheme.tabAlignment ?? _defaults.tabAlignment! ;
1462+ assert (_debugTabAlignmentIsValid (effectiveTabAlignment));
13851463
13861464 final MaterialLocalizations localizations = MaterialLocalizations .of (context);
13871465 if (_controller! .length == 0 ) {
@@ -1390,7 +1468,6 @@ class _TabBarState extends State<TabBar> {
13901468 );
13911469 }
13921470
1393- final TabBarTheme tabBarTheme = TabBarTheme .of (context);
13941471
13951472 final List <Widget > wrappedTabs = List <Widget >.generate (widget.tabs.length, (int index) {
13961473 const double verticalAdjustment = (_kTextAndIconTabHeight - _kTabHeight)/ 2.0 ;
@@ -1491,7 +1568,7 @@ class _TabBarState extends State<TabBar> {
14911568 ),
14921569 ),
14931570 );
1494- if (! widget.isScrollable) {
1571+ if (! widget.isScrollable && effectiveTabAlignment == TabAlignment .fill ) {
14951572 wrappedTabs[index] = Expanded (child: wrappedTabs[index]);
14961573 }
14971574 }
@@ -1509,12 +1586,16 @@ class _TabBarState extends State<TabBar> {
15091586 defaults: _defaults,
15101587 child: _TabLabelBar (
15111588 onPerformLayout: _saveTabOffsets,
1589+ mainAxisSize: effectiveTabAlignment == TabAlignment .fill ? MainAxisSize .max : MainAxisSize .min,
15121590 children: wrappedTabs,
15131591 ),
15141592 ),
15151593 );
15161594
15171595 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;
15181599 _scrollController ?? = _TabBarScrollController (this );
15191600 tabBar = ScrollConfiguration (
15201601 // The scrolling tabs should not show an overscroll indicator.
@@ -1523,7 +1604,7 @@ class _TabBarState extends State<TabBar> {
15231604 dragStartBehavior: widget.dragStartBehavior,
15241605 scrollDirection: Axis .horizontal,
15251606 controller: _scrollController,
1526- padding: widget.padding ,
1607+ padding: effectivePadding ,
15271608 physics: widget.physics,
15281609 child: tabBar,
15291610 ),
@@ -2030,10 +2111,11 @@ class TabPageSelector extends StatelessWidget {
20302111
20312112// Hand coded defaults based on Material Design 2.
20322113class _TabsDefaultsM2 extends TabBarTheme {
2033- const _TabsDefaultsM2 (this .context)
2114+ const _TabsDefaultsM2 (this .context, this .isScrollable )
20342115 : super (indicatorSize: TabBarIndicatorSize .tab);
20352116
20362117 final BuildContext context;
2118+ final bool isScrollable;
20372119
20382120 @override
20392121 Color ? get indicatorColor => Theme .of (context).indicatorColor;
@@ -2049,6 +2131,9 @@ class _TabsDefaultsM2 extends TabBarTheme {
20492131
20502132 @override
20512133 InteractiveInkFeatureFactory ? get splashFactory => Theme .of (context).splashFactory;
2134+
2135+ @override
2136+ TabAlignment ? get tabAlignment => isScrollable ? TabAlignment .start : TabAlignment .fill;
20522137}
20532138
20542139// BEGIN GENERATED TOKEN PROPERTIES - Tabs
@@ -2061,12 +2146,13 @@ class _TabsDefaultsM2 extends TabBarTheme {
20612146// Token database version: v0_162
20622147
20632148class _TabsPrimaryDefaultsM3 extends TabBarTheme {
2064- _TabsPrimaryDefaultsM3 (this .context)
2149+ _TabsPrimaryDefaultsM3 (this .context, this .isScrollable )
20652150 : super (indicatorSize: TabBarIndicatorSize .label);
20662151
20672152 final BuildContext context;
20682153 late final ColorScheme _colors = Theme .of (context).colorScheme;
20692154 late final TextTheme _textTheme = Theme .of (context).textTheme;
2155+ final bool isScrollable;
20702156
20712157 @override
20722158 Color ? get dividerColor => _colors.surfaceVariant;
@@ -2116,15 +2202,19 @@ class _TabsPrimaryDefaultsM3 extends TabBarTheme {
21162202
21172203 @override
21182204 InteractiveInkFeatureFactory ? get splashFactory => Theme .of (context).splashFactory;
2205+
2206+ @override
2207+ TabAlignment ? get tabAlignment => isScrollable ? TabAlignment .start : TabAlignment .fill;
21192208}
21202209
21212210class _TabsSecondaryDefaultsM3 extends TabBarTheme {
2122- _TabsSecondaryDefaultsM3 (this .context)
2211+ _TabsSecondaryDefaultsM3 (this .context, this .isScrollable )
21232212 : super (indicatorSize: TabBarIndicatorSize .tab);
21242213
21252214 final BuildContext context;
21262215 late final ColorScheme _colors = Theme .of (context).colorScheme;
21272216 late final TextTheme _textTheme = Theme .of (context).textTheme;
2217+ final bool isScrollable;
21282218
21292219 @override
21302220 Color ? get dividerColor => _colors.surfaceVariant;
@@ -2174,6 +2264,9 @@ class _TabsSecondaryDefaultsM3 extends TabBarTheme {
21742264
21752265 @override
21762266 InteractiveInkFeatureFactory ? get splashFactory => Theme .of (context).splashFactory;
2267+
2268+ @override
2269+ TabAlignment ? get tabAlignment => isScrollable ? TabAlignment .start : TabAlignment .fill;
21772270}
21782271
21792272// END GENERATED TOKEN PROPERTIES - Tabs
0 commit comments