diff --git a/dev/tools/gen_defaults/generated/used_tokens.csv b/dev/tools/gen_defaults/generated/used_tokens.csv index 12446e1df3a..260505f6870 100644 --- a/dev/tools/gen_defaults/generated/used_tokens.csv +++ b/dev/tools/gen_defaults/generated/used_tokens.csv @@ -599,6 +599,8 @@ md.comp.search-view.header.input-text.color, md.comp.search-view.header.input-text.text-style, md.comp.search-view.header.supporting-text.color, md.comp.search-view.header.supporting-text.text-style, +md.comp.secondary-navigation-tab.active-indicator.color, +md.comp.secondary-navigation-tab.active-indicator.height, md.comp.secondary-navigation-tab.active.label-text.color, md.comp.secondary-navigation-tab.focus.state-layer.color, md.comp.secondary-navigation-tab.focus.state-layer.opacity, diff --git a/dev/tools/gen_defaults/lib/tabs_template.dart b/dev/tools/gen_defaults/lib/tabs_template.dart index 0dca79409f3..fbe7c6f7664 100644 --- a/dev/tools/gen_defaults/lib/tabs_template.dart +++ b/dev/tools/gen_defaults/lib/tabs_template.dart @@ -78,7 +78,12 @@ class _${blockName}PrimaryDefaultsM3 extends TabBarTheme { @override TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill; - static double indicatorWeight = ${getToken('md.comp.primary-navigation-tab.active-indicator.height')}; + static double indicatorWeight(TabBarIndicatorSize indicatorSize) { + return switch (indicatorSize) { + TabBarIndicatorSize.label => ${getToken('md.comp.primary-navigation-tab.active-indicator.height')}, + TabBarIndicatorSize.tab => ${getToken('md.comp.secondary-navigation-tab.active-indicator.height')}, + }; + } // TODO(davidmartos96): This value doesn't currently exist in // https://m3.material.io/components/tabs/specs @@ -104,7 +109,7 @@ class _${blockName}SecondaryDefaultsM3 extends TabBarTheme { double? get dividerHeight => ${getToken("md.comp.divider.thickness")}; @override - Color? get indicatorColor => ${componentColor("md.comp.primary-navigation-tab.active-indicator")}; + Color? get indicatorColor => ${componentColor("md.comp.secondary-navigation-tab.active-indicator")}; @override Color? get labelColor => ${componentColor("md.comp.secondary-navigation-tab.active.label-text")}; @@ -151,6 +156,8 @@ class _${blockName}SecondaryDefaultsM3 extends TabBarTheme { @override TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill; + + static double indicatorWeight = ${getToken('md.comp.secondary-navigation-tab.active-indicator.height')}; } '''; diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart index 776e6393188..bfcdb9550b6 100644 --- a/packages/flutter/lib/src/material/tabs.dart +++ b/packages/flutter/lib/src/material/tabs.dart @@ -1343,11 +1343,20 @@ class _TabBarState extends State { color = Colors.white; } - final bool primaryWithLabelIndicator = widget._isPrimary && indicatorSize == TabBarIndicatorSize.label; - final double effectiveIndicatorWeight = theme.useMaterial3 && primaryWithLabelIndicator - ? math.max(widget.indicatorWeight, _TabsPrimaryDefaultsM3.indicatorWeight) + final double effectiveIndicatorWeight = theme.useMaterial3 + ? math.max( + widget.indicatorWeight, + switch (widget._isPrimary) { + true => _TabsPrimaryDefaultsM3.indicatorWeight(indicatorSize), + false => _TabsSecondaryDefaultsM3.indicatorWeight, + }, + ) : widget.indicatorWeight; // Only Material 3 primary TabBar with label indicatorSize should be rounded. + final bool primaryWithLabelIndicator = switch (indicatorSize) { + TabBarIndicatorSize.label => widget._isPrimary, + TabBarIndicatorSize.tab => false, + }; final BorderRadius? effectiveBorderRadius = theme.useMaterial3 && primaryWithLabelIndicator ? BorderRadius.only( topLeft: Radius.circular(effectiveIndicatorWeight), @@ -2429,7 +2438,12 @@ class _TabsPrimaryDefaultsM3 extends TabBarTheme { @override TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill; - static double indicatorWeight = 3.0; + static double indicatorWeight(TabBarIndicatorSize indicatorSize) { + return switch (indicatorSize) { + TabBarIndicatorSize.label => 3.0, + TabBarIndicatorSize.tab => 2.0, + }; + } // TODO(davidmartos96): This value doesn't currently exist in // https://m3.material.io/components/tabs/specs @@ -2502,6 +2516,8 @@ class _TabsSecondaryDefaultsM3 extends TabBarTheme { @override TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill; + + static double indicatorWeight = 2.0; } // END GENERATED TOKEN PROPERTIES - Tabs diff --git a/packages/flutter/test/material/tabs_test.dart b/packages/flutter/test/material/tabs_test.dart index 8a1d25aa409..d7e6a653ae3 100644 --- a/packages/flutter/test/material/tabs_test.dart +++ b/packages/flutter/test/material/tabs_test.dart @@ -13,9 +13,15 @@ import '../widgets/semantics_tester.dart'; import 'feedback_tester.dart'; import 'tabs_utils.dart'; -Widget boilerplate({ Widget? child, TextDirection textDirection = TextDirection.ltr, bool? useMaterial3, TabBarTheme? tabBarTheme }) { +Widget boilerplate({ + Widget? child, + TextDirection textDirection = TextDirection.ltr, + ThemeData? theme, + TabBarTheme? tabBarTheme, + bool? useMaterial3, +}) { return Theme( - data: ThemeData(useMaterial3: useMaterial3, tabBarTheme: tabBarTheme), + data: theme ?? ThemeData(useMaterial3: useMaterial3, tabBarTheme: tabBarTheme), child: Localizations( locale: const Locale('en', 'US'), delegates: const >[ @@ -346,45 +352,48 @@ void main() { }); testWidgets('TabBar default tab indicator (primary)', (WidgetTester tester) async { - final ThemeData theme = ThemeData(useMaterial3: true); + final ThemeData theme = ThemeData(); final List tabs = List.generate(4, (int index) { return Tab(text: 'Tab $index'); }); - final TabController controller = createTabController( vsync: const TestVSync(), length: tabs.length, ); + const double indicatorWeightLabel = 3.0; + const double indicatorWeightTab = 2.0; - await tester.pumpWidget( - MaterialApp( + Widget buildTab({ TabBarIndicatorSize? indicatorSize }) { + return MaterialApp( home: boilerplate( - useMaterial3: theme.useMaterial3, + theme: theme, child: Container( alignment: Alignment.topLeft, child: TabBar( + indicatorSize: indicatorSize, controller: controller, tabs: tabs, ), ), ), - ), - ); + ); + } - final RenderBox tabBarBox = tester.firstRenderObject(find.byType(TabBar)); + // Test default tab indicator (TabBarIndicatorSize.label). + await tester.pumpWidget(buildTab()); + + RenderBox tabBarBox = tester.firstRenderObject(find.byType(TabBar)); expect(tabBarBox.size.height, 48.0); - const double indicatorWeight = 3.0; - + // Check tab indicator size and color. final RRect rrect = RRect.fromLTRBAndCorners( 64.75, - tabBarBox.size.height - indicatorWeight, + tabBarBox.size.height - indicatorWeightLabel, 135.25, tabBarBox.size.height, topLeft: const Radius.circular(3.0), topRight: const Radius.circular(3.0), ); - expect( tabBarBox, paints @@ -392,23 +401,52 @@ void main() { color: theme.colorScheme.primary, rrect: rrect, )); + + // Test default tab indicator (TabBarIndicatorSize.tab). + await tester.pumpWidget(buildTab(indicatorSize: TabBarIndicatorSize.tab)); + await tester.pumpAndSettle(); + + tabBarBox = tester.firstRenderObject(find.byType(TabBar)); + expect(tabBarBox.size.height, 48.0); + + const double indicatorY = 48 - (indicatorWeightTab / 2.0); + const double indicatorLeft = indicatorWeightTab / 2.0; + const double indicatorRight = 200.0 - (indicatorWeightTab / 2.0); + + // Check tab indicator size and color. + expect( + tabBarBox, + paints + // Divider. + ..line( + color: theme.colorScheme.outlineVariant, + ) + // Tab indicator. + ..line( + color: theme.colorScheme.primary, + strokeWidth: indicatorWeightTab, + p1: const Offset(indicatorLeft, indicatorY), + p2: const Offset(indicatorRight, indicatorY), + ), + ); }); testWidgets('TabBar default tab indicator (secondary)', (WidgetTester tester) async { - final ThemeData theme = ThemeData(useMaterial3: true); + final ThemeData theme = ThemeData(); final List tabs = List.generate(4, (int index) { return Tab(text: 'Tab $index'); }); - final TabController controller = createTabController( vsync: const TestVSync(), length: tabs.length, ); + const double indicatorWeight = 2.0; + // Test default tab indicator. await tester.pumpWidget( MaterialApp( home: boilerplate( - useMaterial3: theme.useMaterial3, + theme: theme, child: Container( alignment: Alignment.topLeft, child: TabBar.secondary( @@ -423,26 +461,26 @@ void main() { final RenderBox tabBarBox = tester.firstRenderObject(find.byType(TabBar)); expect(tabBarBox.size.height, 48.0); - const double indicatorWeight = 2.0; const double indicatorY = 48 - (indicatorWeight / 2.0); const double indicatorLeft = indicatorWeight / 2.0; const double indicatorRight = 200.0 - (indicatorWeight / 2.0); + // Check tab indicator size and color. expect( tabBarBox, paints - // Divider + // Divider. ..line( color: theme.colorScheme.outlineVariant, ) - // Tab indicator + // Tab indicator. ..line( color: theme.colorScheme.primary, strokeWidth: indicatorWeight, p1: const Offset(indicatorLeft, indicatorY), p2: const Offset(indicatorRight, indicatorY), ), - ); + ); }); testWidgets('TabBar default overlay (primary)', (WidgetTester tester) async {