Skip to content

Commit 32fde13

Browse files
authored
Fix Material 3 Scrollable TabBar (flutter#125974)
fix flutter#117722 ### Description 1. Fix the divider doesn't stretch to take all the available width in the scrollable tab bar in M3 2. Add `dividerHeight` property. 3. Update the default tab alignment for the scrollable tab bar to match the specs (this is backward compatible for M2 with the new `tabAlignment` property). ### Bug (default tab alignment) ![Screenshot 2023-05-05 at 19 04 40](https://user-images.githubusercontent.com/48603081/236509483-1d03af21-a764-4776-acef-2126560f0d51.png) ### Fix (default tab alignment) ![Screenshot 2023-05-05 at 19 04 15](https://user-images.githubusercontent.com/48603081/236509513-2426d456-c54f-42bd-9545-a14dc6ee7e69.png) ### Code sample <details> <summary>code sample</summary> ```dart import 'package:flutter/material.dart'; /// Flutter code sample for [TabBar]. void main() => runApp(const TabBarApp()); class TabBarApp extends StatelessWidget { const TabBarApp({super.key}); @OverRide Widget build(BuildContext context) { return MaterialApp( theme: ThemeData( // tabBarTheme: const TabBarTheme(tabAlignment: TabAlignment.start), useMaterial3: true, ), home: const TabBarExample(), ); } } class TabBarExample extends StatefulWidget { const TabBarExample({super.key}); @OverRide State<TabBarExample> createState() => _TabBarExampleState(); } class _TabBarExampleState extends State<TabBarExample> { bool rtl = false; @OverRide Widget build(BuildContext context) { return DefaultTabController( initialIndex: 1, length: 3, child: Directionality( textDirection: rtl ? TextDirection.rtl : TextDirection.ltr, child: Scaffold( appBar: AppBar( title: const Text('TabBar Sample'), ), body: const Column( children: <Widget>[ Text('Scrollable-TabAlignment.start'), TabBar( isScrollable: true, tabAlignment: TabAlignment.start, tabs: <Widget>[ Tab( icon: Icon(Icons.cloud_outlined), ), Tab( icon: Icon(Icons.beach_access_sharp), ), Tab( icon: Icon(Icons.brightness_5_sharp), ), ], ), Text('Scrollable-TabAlignment.startOffset'), TabBar( isScrollable: true, tabAlignment: TabAlignment.startOffset, tabs: <Widget>[ Tab( icon: Icon(Icons.cloud_outlined), ), Tab( icon: Icon(Icons.beach_access_sharp), ), Tab( icon: Icon(Icons.brightness_5_sharp), ), ], ), Text('Scrollable-TabAlignment.center'), TabBar( isScrollable: true, tabAlignment: TabAlignment.center, tabs: <Widget>[ Tab( icon: Icon(Icons.cloud_outlined), ), Tab( icon: Icon(Icons.beach_access_sharp), ), Tab( icon: Icon(Icons.brightness_5_sharp), ), ], ), Spacer(), Text('Non-scrollable-TabAlignment.fill'), TabBar( tabAlignment: TabAlignment.fill, tabs: <Widget>[ Tab( icon: Icon(Icons.cloud_outlined), ), Tab( icon: Icon(Icons.beach_access_sharp), ), Tab( icon: Icon(Icons.brightness_5_sharp), ), ], ), Text('Non-scrollable-TabAlignment.center'), TabBar( tabAlignment: TabAlignment.center, tabs: <Widget>[ Tab( icon: Icon(Icons.cloud_outlined), ), Tab( icon: Icon(Icons.beach_access_sharp), ), Tab( icon: Icon(Icons.brightness_5_sharp), ), ], ), Spacer(), ], ), floatingActionButton: FloatingActionButton.extended( onPressed: () { setState(() { rtl = !rtl; }); }, label: const Text('Switch Direction'), icon: const Icon(Icons.swap_horiz), ), ), ), ); } } ``` </details> ![Screenshot 2023-06-06 at 18 06 12](https://github.com/flutter/flutter/assets/48603081/5ee5386d-cc64-4025-a020-ed2222cb6031)
1 parent 0da8012 commit 32fde13

File tree

6 files changed

+624
-43
lines changed

6 files changed

+624
-43
lines changed

dev/tools/gen_defaults/generated/used_tokens.csv

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,7 @@ md.comp.primary-navigation-tab.active.hover.state-layer.opacity,
529529
md.comp.primary-navigation-tab.active.pressed.state-layer.color,
530530
md.comp.primary-navigation-tab.active.pressed.state-layer.opacity,
531531
md.comp.primary-navigation-tab.divider.color,
532+
md.comp.primary-navigation-tab.divider.height,
532533
md.comp.primary-navigation-tab.inactive.focus.state-layer.color,
533534
md.comp.primary-navigation-tab.inactive.focus.state-layer.opacity,
534535
md.comp.primary-navigation-tab.inactive.hover.state-layer.color,
@@ -588,6 +589,7 @@ md.comp.search-view.header.supporting-text.color,
588589
md.comp.search-view.header.supporting-text.text-style,
589590
md.comp.secondary-navigation-tab.active.label-text.color,
590591
md.comp.secondary-navigation-tab.divider.color,
592+
md.comp.secondary-navigation-tab.divider.height,
591593
md.comp.secondary-navigation-tab.focus.state-layer.color,
592594
md.comp.secondary-navigation-tab.focus.state-layer.opacity,
593595
md.comp.secondary-navigation-tab.hover.state-layer.color,

dev/tools/gen_defaults/lib/tabs_template.dart

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ class _${blockName}PrimaryDefaultsM3 extends TabBarTheme {
2424
@override
2525
Color? get dividerColor => ${componentColor("md.comp.primary-navigation-tab.divider")};
2626
27+
@override
28+
double? get dividerHeight => ${getToken('md.comp.primary-navigation-tab.divider.height')};
29+
2730
@override
2831
Color? get indicatorColor => ${componentColor("md.comp.primary-navigation-tab.active-indicator")};
2932
@@ -71,7 +74,7 @@ class _${blockName}PrimaryDefaultsM3 extends TabBarTheme {
7174
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
7275
7376
@override
74-
TabAlignment? get tabAlignment => isScrollable ? TabAlignment.start : TabAlignment.fill;
77+
TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill;
7578
7679
static double indicatorWeight = ${getToken('md.comp.primary-navigation-tab.active-indicator.height')};
7780
}
@@ -88,6 +91,9 @@ class _${blockName}SecondaryDefaultsM3 extends TabBarTheme {
8891
@override
8992
Color? get dividerColor => ${componentColor("md.comp.secondary-navigation-tab.divider")};
9093
94+
@override
95+
double? get dividerHeight => ${getToken('md.comp.secondary-navigation-tab.divider.height')};
96+
9197
@override
9298
Color? get indicatorColor => ${componentColor("md.comp.primary-navigation-tab.active-indicator")};
9399
@@ -135,7 +141,7 @@ class _${blockName}SecondaryDefaultsM3 extends TabBarTheme {
135141
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
136142
137143
@override
138-
TabAlignment? get tabAlignment => isScrollable ? TabAlignment.start : TabAlignment.fill;
144+
TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill;
139145
}
140146
''';
141147

packages/flutter/lib/src/material/tab_bar_theme.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class TabBarTheme with Diagnosticable {
3232
this.indicatorColor,
3333
this.indicatorSize,
3434
this.dividerColor,
35+
this.dividerHeight,
3536
this.labelColor,
3637
this.labelPadding,
3738
this.labelStyle,
@@ -55,6 +56,9 @@ class TabBarTheme with Diagnosticable {
5556
/// Overrides the default value for [TabBar.dividerColor].
5657
final Color? dividerColor;
5758

59+
/// Overrides the default value for [TabBar.dividerHeight].
60+
final double? dividerHeight;
61+
5862
/// Overrides the default value for [TabBar.labelColor].
5963
///
6064
/// If [labelColor] is a [MaterialStateColor], then the effective color will
@@ -101,6 +105,7 @@ class TabBarTheme with Diagnosticable {
101105
Color? indicatorColor,
102106
TabBarIndicatorSize? indicatorSize,
103107
Color? dividerColor,
108+
double? dividerHeight,
104109
Color? labelColor,
105110
EdgeInsetsGeometry? labelPadding,
106111
TextStyle? labelStyle,
@@ -116,6 +121,7 @@ class TabBarTheme with Diagnosticable {
116121
indicatorColor: indicatorColor ?? this.indicatorColor,
117122
indicatorSize: indicatorSize ?? this.indicatorSize,
118123
dividerColor: dividerColor ?? this.dividerColor,
124+
dividerHeight: dividerHeight ?? this.dividerHeight,
119125
labelColor: labelColor ?? this.labelColor,
120126
labelPadding: labelPadding ?? this.labelPadding,
121127
labelStyle: labelStyle ?? this.labelStyle,
@@ -147,6 +153,7 @@ class TabBarTheme with Diagnosticable {
147153
indicatorColor: Color.lerp(a.indicatorColor, b.indicatorColor, t),
148154
indicatorSize: t < 0.5 ? a.indicatorSize : b.indicatorSize,
149155
dividerColor: Color.lerp(a.dividerColor, b.dividerColor, t),
156+
dividerHeight: t < 0.5 ? a.dividerHeight : b.dividerHeight,
150157
labelColor: Color.lerp(a.labelColor, b.labelColor, t),
151158
labelPadding: EdgeInsetsGeometry.lerp(a.labelPadding, b.labelPadding, t),
152159
labelStyle: TextStyle.lerp(a.labelStyle, b.labelStyle, t),
@@ -165,6 +172,7 @@ class TabBarTheme with Diagnosticable {
165172
indicatorColor,
166173
indicatorSize,
167174
dividerColor,
175+
dividerHeight,
168176
labelColor,
169177
labelPadding,
170178
labelStyle,
@@ -189,6 +197,7 @@ class TabBarTheme with Diagnosticable {
189197
&& other.indicatorColor == indicatorColor
190198
&& other.indicatorSize == indicatorSize
191199
&& other.dividerColor == dividerColor
200+
&& other.dividerHeight == dividerHeight
192201
&& other.labelColor == labelColor
193202
&& other.labelPadding == labelPadding
194203
&& other.labelStyle == labelStyle

packages/flutter/lib/src/material/tabs.dart

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,8 @@ class _IndicatorPainter extends CustomPainter {
397397
required this.indicatorPadding,
398398
required this.labelPaddings,
399399
this.dividerColor,
400+
this.dividerHeight,
401+
required this.width,
400402
}) : super(repaint: controller.animation) {
401403
if (old != null) {
402404
saveTabOffsets(old._currentTabOffsets, old._currentTextDirection);
@@ -408,8 +410,10 @@ class _IndicatorPainter extends CustomPainter {
408410
final TabBarIndicatorSize? indicatorSize;
409411
final EdgeInsetsGeometry indicatorPadding;
410412
final List<GlobalKey> tabKeys;
411-
final Color? dividerColor;
412413
final List<EdgeInsetsGeometry> labelPaddings;
414+
final Color? dividerColor;
415+
final double? dividerHeight;
416+
final double width;
413417

414418
// _currentTabOffsets and _currentTextDirection are set each time TabBar
415419
// layout is completed. These values can be null when TabBar contains no
@@ -502,8 +506,10 @@ class _IndicatorPainter extends CustomPainter {
502506
textDirection: _currentTextDirection,
503507
);
504508
if (dividerColor != null) {
505-
final Paint dividerPaint = Paint()..color = dividerColor!..strokeWidth = 1;
506-
canvas.drawLine(Offset(0, size.height), Offset(size.width, size.height), dividerPaint);
509+
final Paint dividerPaint = Paint()..color = dividerColor!..strokeWidth = dividerHeight!;
510+
final Offset dividerP1 = Offset(-width, size.height - (dividerPaint.strokeWidth / 2));
511+
final Offset dividerP2 = Offset(width, size.height - (dividerPaint.strokeWidth / 2));
512+
canvas.drawLine(dividerP1, dividerP2, dividerPaint);
507513
}
508514
_painter!.paint(canvas, _currentRect!.topLeft, configuration);
509515
}
@@ -718,6 +724,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
718724
this.indicator,
719725
this.indicatorSize,
720726
this.dividerColor,
727+
this.dividerHeight,
721728
this.labelColor,
722729
this.labelStyle,
723730
this.labelPadding,
@@ -768,6 +775,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
768775
this.indicator,
769776
this.indicatorSize,
770777
this.dividerColor,
778+
this.dividerHeight,
771779
this.labelColor,
772780
this.labelStyle,
773781
this.labelPadding,
@@ -895,6 +903,13 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
895903
/// [ColorScheme.surfaceVariant] will be used, otherwise divider will not be drawn.
896904
final Color? dividerColor;
897905

906+
/// The height of the divider.
907+
///
908+
/// If null and [ThemeData.useMaterial3] is true, [TabBarTheme.dividerHeight] is used.
909+
/// If that is also null and [ThemeData.useMaterial3] is true, 1.0 will be used.
910+
/// Otherwise divider will not be drawn.
911+
final double? dividerHeight;
912+
898913
/// The color of selected tab labels.
899914
///
900915
/// If null, then [TabBarTheme.labelColor] is used. If that is also null and
@@ -1154,8 +1169,8 @@ class _TabBarState extends State<TabBar> {
11541169
TabBarTheme get _defaults {
11551170
if (Theme.of(context).useMaterial3) {
11561171
return widget._isPrimary
1157-
? _TabsPrimaryDefaultsM3(context, widget.isScrollable)
1158-
: _TabsSecondaryDefaultsM3(context, widget.isScrollable);
1172+
? _TabsPrimaryDefaultsM3(context, widget.isScrollable)
1173+
: _TabsSecondaryDefaultsM3(context, widget.isScrollable);
11591174
} else {
11601175
return _TabsDefaultsM2(context, widget.isScrollable);
11611176
}
@@ -1269,8 +1284,10 @@ class _TabBarState extends State<TabBar> {
12691284
indicatorPadding: widget.indicatorPadding,
12701285
tabKeys: _tabKeys,
12711286
old: _indicatorPainter,
1272-
dividerColor: theme.useMaterial3 ? widget.dividerColor ?? tabBarTheme.dividerColor ?? _defaults.dividerColor : null,
12731287
labelPaddings: _labelPaddings,
1288+
dividerColor: theme.useMaterial3 ? widget.dividerColor ?? tabBarTheme.dividerColor ?? _defaults.dividerColor : null,
1289+
dividerHeight: theme.useMaterial3 ? widget.dividerHeight ?? tabBarTheme.dividerHeight ?? _defaults.dividerHeight : null,
1290+
width: MediaQuery.sizeOf(context).width,
12741291
);
12751292
}
12761293

@@ -1475,6 +1492,7 @@ class _TabBarState extends State<TabBar> {
14751492
Widget build(BuildContext context) {
14761493
assert(debugCheckHasMaterialLocalizations(context));
14771494
assert(_debugScheduleCheckHasValidTabsCount());
1495+
final ThemeData theme = Theme.of(context);
14781496
final TabBarTheme tabBarTheme = TabBarTheme.of(context);
14791497
final TabAlignment effectiveTabAlignment = widget.tabAlignment ?? tabBarTheme.tabAlignment ?? _defaults.tabAlignment!;
14801498
assert(_debugTabAlignmentIsValid(effectiveTabAlignment));
@@ -1627,6 +1645,17 @@ class _TabBarState extends State<TabBar> {
16271645
child: tabBar,
16281646
),
16291647
);
1648+
if (theme.useMaterial3) {
1649+
final AlignmentGeometry effectiveAlignment = switch (effectiveTabAlignment) {
1650+
TabAlignment.center => Alignment.center,
1651+
TabAlignment.start || TabAlignment.startOffset || TabAlignment.fill => AlignmentDirectional.centerStart,
1652+
};
1653+
tabBar = Align(
1654+
heightFactor: 1.0,
1655+
alignment: effectiveAlignment,
1656+
child: tabBar,
1657+
);
1658+
}
16301659
} else if (widget.padding != null) {
16311660
tabBar = Padding(
16321661
padding: widget.padding!,
@@ -2177,6 +2206,9 @@ class _TabsPrimaryDefaultsM3 extends TabBarTheme {
21772206
@override
21782207
Color? get dividerColor => _colors.surfaceVariant;
21792208

2209+
@override
2210+
double? get dividerHeight => 1.0;
2211+
21802212
@override
21812213
Color? get indicatorColor => _colors.primary;
21822214

@@ -2224,7 +2256,7 @@ class _TabsPrimaryDefaultsM3 extends TabBarTheme {
22242256
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
22252257

22262258
@override
2227-
TabAlignment? get tabAlignment => isScrollable ? TabAlignment.start : TabAlignment.fill;
2259+
TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill;
22282260

22292261
static double indicatorWeight = 3.0;
22302262
}
@@ -2241,6 +2273,9 @@ class _TabsSecondaryDefaultsM3 extends TabBarTheme {
22412273
@override
22422274
Color? get dividerColor => _colors.surfaceVariant;
22432275

2276+
@override
2277+
double? get dividerHeight => 1.0;
2278+
22442279
@override
22452280
Color? get indicatorColor => _colors.primary;
22462281

@@ -2288,7 +2323,7 @@ class _TabsSecondaryDefaultsM3 extends TabBarTheme {
22882323
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
22892324

22902325
@override
2291-
TabAlignment? get tabAlignment => isScrollable ? TabAlignment.start : TabAlignment.fill;
2326+
TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill;
22922327
}
22932328

22942329
// END GENERATED TOKEN PROPERTIES - Tabs

0 commit comments

Comments
 (0)