diff --git a/dev/tools/gen_defaults/lib/tabs_template.dart b/dev/tools/gen_defaults/lib/tabs_template.dart index 22ed0e3eb1..fb37601066 100644 --- a/dev/tools/gen_defaults/lib/tabs_template.dart +++ b/dev/tools/gen_defaults/lib/tabs_template.dart @@ -20,6 +20,9 @@ class _${blockName}PrimaryDefaultsM3 extends TabBarTheme { late final ColorScheme _colors = Theme.of(context).colorScheme; late final TextTheme _textTheme = Theme.of(context).textTheme; + @override + double? get dividerHeight => ${tokens['md.comp.primary-navigation-tab.divider.height']}; + @override Color? get dividerColor => ${componentColor("md.comp.primary-navigation-tab.divider")}; @@ -81,6 +84,9 @@ class _${blockName}SecondaryDefaultsM3 extends TabBarTheme { @override Color? get dividerColor => ${componentColor("md.comp.secondary-navigation-tab.divider")}; + @override + double? get dividerHeight => ${tokens['md.comp.primary-navigation-tab.divider.height']}; + @override Color? get indicatorColor => ${componentColor("md.comp.primary-navigation-tab.active-indicator")}; diff --git a/packages/flutter/lib/src/material/tab_bar_theme.dart b/packages/flutter/lib/src/material/tab_bar_theme.dart index 3c4b9e0c72..8a9f8e4714 100644 --- a/packages/flutter/lib/src/material/tab_bar_theme.dart +++ b/packages/flutter/lib/src/material/tab_bar_theme.dart @@ -32,6 +32,7 @@ class TabBarTheme with Diagnosticable { this.indicatorColor, this.indicatorSize, this.dividerColor, + this.dividerHeight, this.labelColor, this.labelPadding, this.labelStyle, @@ -54,6 +55,9 @@ class TabBarTheme with Diagnosticable { /// Overrides the default value for [TabBar.dividerColor]. final Color? dividerColor; + /// Overrides the default value for [TabBar.dividerHeight]. + final double? dividerHeight; + /// Overrides the default value for [TabBar.labelColor]. /// /// If [labelColor] is a [MaterialStateColor], then the effective color will @@ -97,6 +101,7 @@ class TabBarTheme with Diagnosticable { Color? indicatorColor, TabBarIndicatorSize? indicatorSize, Color? dividerColor, + double? dividerHeight, Color? labelColor, EdgeInsetsGeometry? labelPadding, TextStyle? labelStyle, @@ -111,6 +116,7 @@ class TabBarTheme with Diagnosticable { indicatorColor: indicatorColor ?? this.indicatorColor, indicatorSize: indicatorSize ?? this.indicatorSize, dividerColor: dividerColor ?? this.dividerColor, + dividerHeight: dividerHeight ?? this.dividerHeight, labelColor: labelColor ?? this.labelColor, labelPadding: labelPadding ?? this.labelPadding, labelStyle: labelStyle ?? this.labelStyle, @@ -141,6 +147,7 @@ class TabBarTheme with Diagnosticable { indicatorColor: Color.lerp(a.indicatorColor, b.indicatorColor, t), indicatorSize: t < 0.5 ? a.indicatorSize : b.indicatorSize, dividerColor: Color.lerp(a.dividerColor, b.dividerColor, t), + dividerHeight: t < 0.5 ? a.dividerHeight : b.dividerHeight, labelColor: Color.lerp(a.labelColor, b.labelColor, t), labelPadding: EdgeInsetsGeometry.lerp(a.labelPadding, b.labelPadding, t), labelStyle: TextStyle.lerp(a.labelStyle, b.labelStyle, t), @@ -158,6 +165,7 @@ class TabBarTheme with Diagnosticable { indicatorColor, indicatorSize, dividerColor, + dividerHeight, labelColor, labelPadding, labelStyle, @@ -181,6 +189,7 @@ class TabBarTheme with Diagnosticable { && other.indicatorColor == indicatorColor && other.indicatorSize == indicatorSize && other.dividerColor == dividerColor + && other.dividerHeight == dividerHeight && other.labelColor == labelColor && other.labelPadding == labelPadding && other.labelStyle == labelStyle diff --git a/packages/flutter/lib/src/material/tab_indicator.dart b/packages/flutter/lib/src/material/tab_indicator.dart index e94995654e..603bde91b4 100644 --- a/packages/flutter/lib/src/material/tab_indicator.dart +++ b/packages/flutter/lib/src/material/tab_indicator.dart @@ -109,7 +109,8 @@ class _UnderlinePainter extends BoxPainter { if (borderRadius != null) { paint = Paint()..color = decoration.borderSide.color; final Rect indicator = decoration._indicatorRectFor(rect, textDirection) - .inflate(decoration.borderSide.width / 4.0); + .inflate(decoration.borderSide.width / 4.0) + .shift(Offset(0.0, -decoration.borderSide.width / 2.0)); final RRect rrect = RRect.fromRectAndCorners( indicator, topLeft: borderRadius!.topLeft, diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart index a326e2b116..e76c3919ae 100644 --- a/packages/flutter/lib/src/material/tabs.dart +++ b/packages/flutter/lib/src/material/tabs.dart @@ -15,6 +15,7 @@ import 'color_scheme.dart'; import 'colors.dart'; import 'constants.dart'; import 'debug.dart'; +import 'divider.dart'; import 'ink_well.dart'; import 'material.dart'; import 'material_localizations.dart'; @@ -360,7 +361,6 @@ class _IndicatorPainter extends CustomPainter { required _IndicatorPainter? old, required this.indicatorPadding, required this.labelPaddings, - this.dividerColor, }) : super(repaint: controller.animation) { if (old != null) { saveTabOffsets(old._currentTabOffsets, old._currentTextDirection); @@ -372,7 +372,6 @@ class _IndicatorPainter extends CustomPainter { final TabBarIndicatorSize? indicatorSize; final EdgeInsetsGeometry indicatorPadding; final List tabKeys; - final Color? dividerColor; final List labelPaddings; // _currentTabOffsets and _currentTextDirection are set each time TabBar @@ -465,10 +464,6 @@ class _IndicatorPainter extends CustomPainter { size: _currentRect!.size, textDirection: _currentTextDirection, ); - if (dividerColor != null) { - final Paint dividerPaint = Paint()..color = dividerColor!..strokeWidth = 1; - canvas.drawLine(Offset(0, size.height), Offset(size.width, size.height), dividerPaint); - } _painter!.paint(canvas, _currentRect!.topLeft, configuration); } @@ -682,6 +677,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { this.indicator, this.indicatorSize, this.dividerColor, + this.dividerHeight, this.labelColor, this.labelStyle, this.labelPadding, @@ -731,6 +727,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { this.indicator, this.indicatorSize, this.dividerColor, + this.dividerHeight, this.labelColor, this.labelStyle, this.labelPadding, @@ -849,6 +846,13 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { /// [ColorScheme.surfaceVariant] will be used, otherwise divider will not be drawn. final Color? dividerColor; + /// The height of the divider. + /// + /// If null and [ThemeData.useMaterial3] is true, [TabBarTheme.dividerHeight] + /// is used. If that is null and [ThemeData.useMaterial3] is true, 1.0 will be used. + /// Otherwise divider will not be drawn. + final double? dividerHeight; + /// The color of selected tab labels. /// /// If null, then [TabBarTheme.labelColor] is used. If that is also null and @@ -1096,7 +1100,7 @@ class _TabBarState extends State { } } - Decoration _getIndicator() { + Decoration _getIndicator(TabBarIndicatorSize indicatorSize) { final ThemeData theme = Theme.of(context); final TabBarTheme tabBarTheme = TabBarTheme.of(context); @@ -1130,17 +1134,24 @@ class _TabBarState extends State { color = Colors.white; } - return UnderlineTabIndicator( - borderRadius: theme.useMaterial3 && widget._isPrimary + if (theme.useMaterial3 && widget._isPrimary && indicatorSize == TabBarIndicatorSize.label) { + return UnderlineTabIndicator( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(3.0), + topRight: Radius.circular(3.0), + ), + borderSide: BorderSide( // TODO(tahatesser): Make sure this value matches Material 3 Tabs spec // when `preferredSize`and `indicatorWeight` are updated to support Material 3 // https://m3.material.io/components/tabs/specs#149a189f-9039-4195-99da-15c205d20e30, // https://github.com/flutter/flutter/issues/116136 - ? const BorderRadius.only( - topLeft: Radius.circular(3.0), - topRight: Radius.circular(3.0), - ) - : null, + width: widget.indicatorWeight, + color: color, + ), + ); + } + + return UnderlineTabIndicator( borderSide: BorderSide( width: widget.indicatorWeight, color: color, @@ -1185,17 +1196,18 @@ class _TabBarState extends State { } void _initIndicatorPainter() { - final ThemeData theme = Theme.of(context); final TabBarTheme tabBarTheme = TabBarTheme.of(context); + final TabBarIndicatorSize indicatorSize = widget.indicatorSize + ?? tabBarTheme.indicatorSize + ?? _defaults.indicatorSize!; _indicatorPainter = !_controllerIsValid ? null : _IndicatorPainter( controller: _controller!, - indicator: _getIndicator(), - indicatorSize: widget.indicatorSize ?? tabBarTheme.indicatorSize ?? _defaults.indicatorSize!, + indicator: _getIndicator(indicatorSize), + indicatorSize: indicatorSize, indicatorPadding: widget.indicatorPadding, tabKeys: _tabKeys, old: _indicatorPainter, - dividerColor: theme.useMaterial3 ? widget.dividerColor ?? tabBarTheme.dividerColor ?? _defaults.dividerColor : null, labelPaddings: _labelPaddings, ); } @@ -1390,6 +1402,7 @@ class _TabBarState extends State { ); } + final ThemeData theme = Theme.of(context); final TabBarTheme tabBarTheme = TabBarTheme.of(context); final List wrappedTabs = List.generate(widget.tabs.length, (int index) { @@ -1535,6 +1548,24 @@ class _TabBarState extends State { ); } + if (theme.useMaterial3) { + tabBar = Stack( + alignment: Alignment.center, + children: [ + Container( + height: widget.preferredSize.height, + alignment: Alignment.bottomCenter, + child: Divider( + height: 0, + thickness: widget.dividerHeight ?? tabBarTheme.dividerHeight ?? _defaults.dividerHeight, + color: widget.dividerColor ?? tabBarTheme.dividerColor ?? _defaults.dividerColor, + ), + ), + tabBar, + ], + ); + } + return tabBar; } } @@ -2068,6 +2099,9 @@ class _TabsPrimaryDefaultsM3 extends TabBarTheme { late final ColorScheme _colors = Theme.of(context).colorScheme; late final TextTheme _textTheme = Theme.of(context).textTheme; + @override + double? get dividerHeight => 1.0; + @override Color? get dividerColor => _colors.surfaceVariant; @@ -2129,6 +2163,9 @@ class _TabsSecondaryDefaultsM3 extends TabBarTheme { @override Color? get dividerColor => _colors.surfaceVariant; + @override + double? get dividerHeight => 1.0; + @override Color? get indicatorColor => _colors.primary; diff --git a/packages/flutter/test/material/tab_bar_theme_test.dart b/packages/flutter/test/material/tab_bar_theme_test.dart index eb5176f66d..1a9db6364e 100644 --- a/packages/flutter/test/material/tab_bar_theme_test.dart +++ b/packages/flutter/test/material/tab_bar_theme_test.dart @@ -126,27 +126,30 @@ void main() { final Rect tabTwoRect = tester.getRect(find.byKey(_sizedTabs[1].key!)); // Verify tabOne coordinates. - expect(tabOneRect.left, equals(kTabLabelPadding.left)); + final double tabOneLeft = (tabBar.width + - (tabOneRect.width + tabTwoRect.width) - kTabLabelPadding.horizontal) / 2; + expect(tabOneRect.left, equals(tabOneLeft)); expect(tabOneRect.top, equals(kTabLabelPadding.top)); expect(tabOneRect.bottom, equals(tabBar.bottom - kTabLabelPadding.bottom - indicatorWeight)); // Verify tabTwo coordinates. - expect(tabTwoRect.right, equals(tabBar.width - kTabLabelPadding.right)); + final double tabTwoRight = tabBar.width + - (tabBar.width - (tabOneRect.width + tabTwoRect.width) - kTabLabelPadding.horizontal) / 2; + expect(tabTwoRect.right, equals(tabTwoRight)); expect(tabTwoRect.top, equals(kTabLabelPadding.top)); expect(tabTwoRect.bottom, equals(tabBar.bottom - kTabLabelPadding.bottom - indicatorWeight)); // Verify tabOne and tabTwo is separated by right padding of tabOne and left padding of tabTwo. expect(tabOneRect.right, equals(tabTwoRect.left - kTabLabelPadding.left - kTabLabelPadding.right)); - // Verify divider color and indicator color. + // Test default divider color. + final Divider divider = tester.widget(find.byType(Divider)); + expect(divider.color, equals(theme.colorScheme.surfaceVariant)); + expect(divider.thickness, 1.0); + + // Test default indicator color. final RenderBox tabBarBox = tester.firstRenderObject(find.byType(TabBar)); - expect( - tabBarBox, - paints - ..line(color: theme.colorScheme.surfaceVariant) - // Indicator is a rrect in the primary tab bar. - ..rrect(color: theme.colorScheme.primary), - ); + expect(tabBarBox, paints..rrect(color: theme.colorScheme.primary)); }); testWidgets('Tab bar defaults (secondary)', (WidgetTester tester) async { @@ -177,27 +180,30 @@ void main() { final Rect tabTwoRect = tester.getRect(find.byKey(_sizedTabs[1].key!)); // Verify tabOne coordinates. - expect(tabOneRect.left, equals(kTabLabelPadding.left)); + final double tabOneLeft = (tabBar.width + - (tabOneRect.width + tabTwoRect.width) - kTabLabelPadding.horizontal) / 2; + expect(tabOneRect.left, equals(tabOneLeft)); expect(tabOneRect.top, equals(kTabLabelPadding.top)); expect(tabOneRect.bottom, equals(tabBar.bottom - kTabLabelPadding.bottom - indicatorWeight)); // Verify tabTwo coordinates. - expect(tabTwoRect.right, equals(tabBar.width - kTabLabelPadding.right)); + final double tabTwoRight = tabBar.width + - (tabBar.width - (tabOneRect.width + tabTwoRect.width) - kTabLabelPadding.horizontal) / 2; + expect(tabTwoRect.right, equals(tabTwoRight)); expect(tabTwoRect.top, equals(kTabLabelPadding.top)); expect(tabTwoRect.bottom, equals(tabBar.bottom - kTabLabelPadding.bottom - indicatorWeight)); // Verify tabOne and tabTwo is separated by right padding of tabOne and left padding of tabTwo. expect(tabOneRect.right, equals(tabTwoRect.left - kTabLabelPadding.left - kTabLabelPadding.right)); - // Verify divider color and indicator color. + // Test default divider color. + final Divider divider = tester.widget(find.byType(Divider)); + expect(divider.color, equals(theme.colorScheme.surfaceVariant)); + expect(divider.thickness, 1.0); + + // Test default indicator color. final RenderBox tabBarBox = tester.firstRenderObject(find.byType(TabBar)); - expect( - tabBarBox, - paints - ..line(color: theme.colorScheme.surfaceVariant) - // Indicator is a line in the secondary tab bar. - ..line(color: theme.colorScheme.primary), - ); + expect(tabBarBox, paints..line(color: theme.colorScheme.primary)); }); testWidgets('Tab bar theme overrides label color (selected)', (WidgetTester tester) async { @@ -378,21 +384,21 @@ void main() { expect(iconRenderObject.text.style!.color, equals(unselectedLabelColor)); }); - testWidgets('Tab bar default tab indicator size', (WidgetTester tester) async { + testWidgets('Tab bar default tab indicator size (primary)', (WidgetTester tester) async { await tester.pumpWidget(buildTabBar(useMaterial3: true, isScrollable: true)); await expectLater( find.byKey(_painterKey), - matchesGoldenFile('tab_bar.default.tab_indicator_size.png'), + matchesGoldenFile('tab_bar_primary.default.tab_indicator_size.png'), ); }); - testWidgets('Tab bar default tab indicator size', (WidgetTester tester) async { - await tester.pumpWidget(buildTabBar(useMaterial3: true, isScrollable: true)); + testWidgets('Tab bar default tab indicator size (secondary)', (WidgetTester tester) async { + await tester.pumpWidget(buildTabBar(secondaryTabBar: true, useMaterial3: true, isScrollable: true)); await expectLater( find.byKey(_painterKey), - matchesGoldenFile('tab_bar.default.tab_indicator_size.png'), + matchesGoldenFile('tab_bar_secondary.default.tab_indicator_size.png'), ); }); @@ -464,6 +470,74 @@ void main() { ); }); + testWidgets('TabBar divider can use TabBarTheme.dividerColor & TabBarTheme.dividerHeight', (WidgetTester tester) async { + const Color dividerColor = Colors.yellow; + const double dividerHeight = 10.0; + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + useMaterial3: true, + tabBarTheme: const TabBarTheme( + dividerColor: dividerColor, + dividerHeight: dividerHeight, + ), + ), + home: Scaffold( + appBar: AppBar( + bottom: TabBar( + controller: TabController(length: 3, vsync: const TestVSync()), + tabs: const [ + Tab(text: 'Tab 1'), + Tab(text: 'Tab 2'), + Tab(text: 'Tab 3'), + ], + ), + ), + ), + ), + ); + + final Divider divider = tester.widget(find.byType(Divider)); + expect(divider.color, equals(dividerColor)); + expect(divider.thickness, dividerHeight); + }); + + testWidgets('dividerColor & dividerHeight overrides TabBarTheme.dividerColor', (WidgetTester tester) async { + const Color dividerColor = Colors.amber; + const double dividerHeight = 8.0; + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + useMaterial3: true, + tabBarTheme: const TabBarTheme( + dividerColor: Colors.pink, + dividerHeight: 5.0, + ), + ), + home: Scaffold( + appBar: AppBar( + bottom: TabBar( + dividerColor: dividerColor, + dividerHeight: dividerHeight, + controller: TabController(length: 3, vsync: const TestVSync()), + tabs: const [ + Tab(text: 'Tab 1'), + Tab(text: 'Tab 2'), + Tab(text: 'Tab 3'), + ], + ), + ), + ), + ), + ); + + final Divider divider = tester.widget(find.byType(Divider)); + expect(divider.color, equals(dividerColor)); + expect(divider.thickness, dividerHeight); + }); + group('Material 2', () { // Tests that are only relevant for Material 2. Once ThemeData.useMaterial3 // is turned on by default, these tests can be removed. diff --git a/packages/flutter/test/material/tabs_test.dart b/packages/flutter/test/material/tabs_test.dart index b1ad1b5bdc..ed183cf434 100644 --- a/packages/flutter/test/material/tabs_test.dart +++ b/packages/flutter/test/material/tabs_test.dart @@ -5778,36 +5778,6 @@ void main() { labelColor.withAlpha(0xB2) // 70% alpha, ); }); - - testWidgets('Material3 - TabBar inherits the dividerColor of TabBarTheme', (WidgetTester tester) async { - const Color dividerColor = Colors.yellow; - - await tester.pumpWidget( - MaterialApp( - theme: ThemeData( - useMaterial3: true, - tabBarTheme: const TabBarTheme(dividerColor: dividerColor), - ), - home: Scaffold( - appBar: AppBar( - bottom: TabBar( - controller: TabController(length: 3, vsync: const TestVSync()), - tabs: const [ - Tab(text: 'Tab 1'), - Tab(text: 'Tab 2'), - Tab(text: 'Tab 3'), - ], - ), - ), - ), - ), - ); - - // Test painter's divider color. - final CustomPaint paint = tester.widget(find.byType(CustomPaint).last); - // ignore: avoid_dynamic_calls - expect((paint.painter as dynamic).dividerColor, dividerColor); - }); }); }