mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Update TabBar
and TabBar.secondary
to use indicator height/color M3 tokens (#145753)
fixes [Secondary `TabBar` indicator height token is missing ](https://github.com/flutter/flutter/issues/124965) ### Code sample <details> <summary>expand to view the code sample</summary> ```dart import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: DefaultTabController( length: 2, child: Scaffold( appBar: AppBar( title: const Text('Sample'), bottom: const TabBar.secondary( tabs: <Widget>[ Tab(icon: Icon(Icons.directions_car)), Tab(icon: Icon(Icons.directions_transit)), ], ), ), floatingActionButton: FloatingActionButton( onPressed: () {}, child: const Icon(Icons.add), ), ), ), ); } } ``` </details>
This commit is contained in:
parent
c311d42c8c
commit
0bcd228e67
@ -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.input-text.text-style,
|
||||||
md.comp.search-view.header.supporting-text.color,
|
md.comp.search-view.header.supporting-text.color,
|
||||||
md.comp.search-view.header.supporting-text.text-style,
|
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.active.label-text.color,
|
||||||
md.comp.secondary-navigation-tab.focus.state-layer.color,
|
md.comp.secondary-navigation-tab.focus.state-layer.color,
|
||||||
md.comp.secondary-navigation-tab.focus.state-layer.opacity,
|
md.comp.secondary-navigation-tab.focus.state-layer.opacity,
|
||||||
|
|
@ -78,7 +78,12 @@ class _${blockName}PrimaryDefaultsM3 extends TabBarTheme {
|
|||||||
@override
|
@override
|
||||||
TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill;
|
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
|
// TODO(davidmartos96): This value doesn't currently exist in
|
||||||
// https://m3.material.io/components/tabs/specs
|
// https://m3.material.io/components/tabs/specs
|
||||||
@ -104,7 +109,7 @@ class _${blockName}SecondaryDefaultsM3 extends TabBarTheme {
|
|||||||
double? get dividerHeight => ${getToken("md.comp.divider.thickness")};
|
double? get dividerHeight => ${getToken("md.comp.divider.thickness")};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Color? get indicatorColor => ${componentColor("md.comp.primary-navigation-tab.active-indicator")};
|
Color? get indicatorColor => ${componentColor("md.comp.secondary-navigation-tab.active-indicator")};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Color? get labelColor => ${componentColor("md.comp.secondary-navigation-tab.active.label-text")};
|
Color? get labelColor => ${componentColor("md.comp.secondary-navigation-tab.active.label-text")};
|
||||||
@ -151,6 +156,8 @@ class _${blockName}SecondaryDefaultsM3 extends TabBarTheme {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill;
|
TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill;
|
||||||
|
|
||||||
|
static double indicatorWeight = ${getToken('md.comp.secondary-navigation-tab.active-indicator.height')};
|
||||||
}
|
}
|
||||||
''';
|
''';
|
||||||
|
|
||||||
|
@ -1343,11 +1343,20 @@ class _TabBarState extends State<TabBar> {
|
|||||||
color = Colors.white;
|
color = Colors.white;
|
||||||
}
|
}
|
||||||
|
|
||||||
final bool primaryWithLabelIndicator = widget._isPrimary && indicatorSize == TabBarIndicatorSize.label;
|
final double effectiveIndicatorWeight = theme.useMaterial3
|
||||||
final double effectiveIndicatorWeight = theme.useMaterial3 && primaryWithLabelIndicator
|
? math.max(
|
||||||
? math.max(widget.indicatorWeight, _TabsPrimaryDefaultsM3.indicatorWeight)
|
widget.indicatorWeight,
|
||||||
|
switch (widget._isPrimary) {
|
||||||
|
true => _TabsPrimaryDefaultsM3.indicatorWeight(indicatorSize),
|
||||||
|
false => _TabsSecondaryDefaultsM3.indicatorWeight,
|
||||||
|
},
|
||||||
|
)
|
||||||
: widget.indicatorWeight;
|
: widget.indicatorWeight;
|
||||||
// Only Material 3 primary TabBar with label indicatorSize should be rounded.
|
// 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
|
final BorderRadius? effectiveBorderRadius = theme.useMaterial3 && primaryWithLabelIndicator
|
||||||
? BorderRadius.only(
|
? BorderRadius.only(
|
||||||
topLeft: Radius.circular(effectiveIndicatorWeight),
|
topLeft: Radius.circular(effectiveIndicatorWeight),
|
||||||
@ -2429,7 +2438,12 @@ class _TabsPrimaryDefaultsM3 extends TabBarTheme {
|
|||||||
@override
|
@override
|
||||||
TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill;
|
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
|
// TODO(davidmartos96): This value doesn't currently exist in
|
||||||
// https://m3.material.io/components/tabs/specs
|
// https://m3.material.io/components/tabs/specs
|
||||||
@ -2502,6 +2516,8 @@ class _TabsSecondaryDefaultsM3 extends TabBarTheme {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill;
|
TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill;
|
||||||
|
|
||||||
|
static double indicatorWeight = 2.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// END GENERATED TOKEN PROPERTIES - Tabs
|
// END GENERATED TOKEN PROPERTIES - Tabs
|
||||||
|
@ -13,9 +13,15 @@ import '../widgets/semantics_tester.dart';
|
|||||||
import 'feedback_tester.dart';
|
import 'feedback_tester.dart';
|
||||||
import 'tabs_utils.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(
|
return Theme(
|
||||||
data: ThemeData(useMaterial3: useMaterial3, tabBarTheme: tabBarTheme),
|
data: theme ?? ThemeData(useMaterial3: useMaterial3, tabBarTheme: tabBarTheme),
|
||||||
child: Localizations(
|
child: Localizations(
|
||||||
locale: const Locale('en', 'US'),
|
locale: const Locale('en', 'US'),
|
||||||
delegates: const <LocalizationsDelegate<dynamic>>[
|
delegates: const <LocalizationsDelegate<dynamic>>[
|
||||||
@ -346,45 +352,48 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('TabBar default tab indicator (primary)', (WidgetTester tester) async {
|
testWidgets('TabBar default tab indicator (primary)', (WidgetTester tester) async {
|
||||||
final ThemeData theme = ThemeData(useMaterial3: true);
|
final ThemeData theme = ThemeData();
|
||||||
final List<Widget> tabs = List<Widget>.generate(4, (int index) {
|
final List<Widget> tabs = List<Widget>.generate(4, (int index) {
|
||||||
return Tab(text: 'Tab $index');
|
return Tab(text: 'Tab $index');
|
||||||
});
|
});
|
||||||
|
|
||||||
final TabController controller = createTabController(
|
final TabController controller = createTabController(
|
||||||
vsync: const TestVSync(),
|
vsync: const TestVSync(),
|
||||||
length: tabs.length,
|
length: tabs.length,
|
||||||
);
|
);
|
||||||
|
const double indicatorWeightLabel = 3.0;
|
||||||
|
const double indicatorWeightTab = 2.0;
|
||||||
|
|
||||||
await tester.pumpWidget(
|
Widget buildTab({ TabBarIndicatorSize? indicatorSize }) {
|
||||||
MaterialApp(
|
return MaterialApp(
|
||||||
home: boilerplate(
|
home: boilerplate(
|
||||||
useMaterial3: theme.useMaterial3,
|
theme: theme,
|
||||||
child: Container(
|
child: Container(
|
||||||
alignment: Alignment.topLeft,
|
alignment: Alignment.topLeft,
|
||||||
child: TabBar(
|
child: TabBar(
|
||||||
|
indicatorSize: indicatorSize,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
tabs: tabs,
|
tabs: tabs,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
}
|
||||||
|
|
||||||
final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
|
// Test default tab indicator (TabBarIndicatorSize.label).
|
||||||
|
await tester.pumpWidget(buildTab());
|
||||||
|
|
||||||
|
RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
|
||||||
expect(tabBarBox.size.height, 48.0);
|
expect(tabBarBox.size.height, 48.0);
|
||||||
|
|
||||||
const double indicatorWeight = 3.0;
|
// Check tab indicator size and color.
|
||||||
|
|
||||||
final RRect rrect = RRect.fromLTRBAndCorners(
|
final RRect rrect = RRect.fromLTRBAndCorners(
|
||||||
64.75,
|
64.75,
|
||||||
tabBarBox.size.height - indicatorWeight,
|
tabBarBox.size.height - indicatorWeightLabel,
|
||||||
135.25,
|
135.25,
|
||||||
tabBarBox.size.height,
|
tabBarBox.size.height,
|
||||||
topLeft: const Radius.circular(3.0),
|
topLeft: const Radius.circular(3.0),
|
||||||
topRight: const Radius.circular(3.0),
|
topRight: const Radius.circular(3.0),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
tabBarBox,
|
tabBarBox,
|
||||||
paints
|
paints
|
||||||
@ -392,23 +401,52 @@ void main() {
|
|||||||
color: theme.colorScheme.primary,
|
color: theme.colorScheme.primary,
|
||||||
rrect: rrect,
|
rrect: rrect,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// Test default tab indicator (TabBarIndicatorSize.tab).
|
||||||
|
await tester.pumpWidget(buildTab(indicatorSize: TabBarIndicatorSize.tab));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
tabBarBox = tester.firstRenderObject<RenderBox>(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 {
|
testWidgets('TabBar default tab indicator (secondary)', (WidgetTester tester) async {
|
||||||
final ThemeData theme = ThemeData(useMaterial3: true);
|
final ThemeData theme = ThemeData();
|
||||||
final List<Widget> tabs = List<Widget>.generate(4, (int index) {
|
final List<Widget> tabs = List<Widget>.generate(4, (int index) {
|
||||||
return Tab(text: 'Tab $index');
|
return Tab(text: 'Tab $index');
|
||||||
});
|
});
|
||||||
|
|
||||||
final TabController controller = createTabController(
|
final TabController controller = createTabController(
|
||||||
vsync: const TestVSync(),
|
vsync: const TestVSync(),
|
||||||
length: tabs.length,
|
length: tabs.length,
|
||||||
);
|
);
|
||||||
|
const double indicatorWeight = 2.0;
|
||||||
|
|
||||||
|
// Test default tab indicator.
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: boilerplate(
|
home: boilerplate(
|
||||||
useMaterial3: theme.useMaterial3,
|
theme: theme,
|
||||||
child: Container(
|
child: Container(
|
||||||
alignment: Alignment.topLeft,
|
alignment: Alignment.topLeft,
|
||||||
child: TabBar.secondary(
|
child: TabBar.secondary(
|
||||||
@ -423,26 +461,26 @@ void main() {
|
|||||||
final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
|
final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
|
||||||
expect(tabBarBox.size.height, 48.0);
|
expect(tabBarBox.size.height, 48.0);
|
||||||
|
|
||||||
const double indicatorWeight = 2.0;
|
|
||||||
const double indicatorY = 48 - (indicatorWeight / 2.0);
|
const double indicatorY = 48 - (indicatorWeight / 2.0);
|
||||||
const double indicatorLeft = indicatorWeight / 2.0;
|
const double indicatorLeft = indicatorWeight / 2.0;
|
||||||
const double indicatorRight = 200.0 - (indicatorWeight / 2.0);
|
const double indicatorRight = 200.0 - (indicatorWeight / 2.0);
|
||||||
|
|
||||||
|
// Check tab indicator size and color.
|
||||||
expect(
|
expect(
|
||||||
tabBarBox,
|
tabBarBox,
|
||||||
paints
|
paints
|
||||||
// Divider
|
// Divider.
|
||||||
..line(
|
..line(
|
||||||
color: theme.colorScheme.outlineVariant,
|
color: theme.colorScheme.outlineVariant,
|
||||||
)
|
)
|
||||||
// Tab indicator
|
// Tab indicator.
|
||||||
..line(
|
..line(
|
||||||
color: theme.colorScheme.primary,
|
color: theme.colorScheme.primary,
|
||||||
strokeWidth: indicatorWeight,
|
strokeWidth: indicatorWeight,
|
||||||
p1: const Offset(indicatorLeft, indicatorY),
|
p1: const Offset(indicatorLeft, indicatorY),
|
||||||
p2: const Offset(indicatorRight, indicatorY),
|
p2: const Offset(indicatorRight, indicatorY),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('TabBar default overlay (primary)', (WidgetTester tester) async {
|
testWidgets('TabBar default overlay (primary)', (WidgetTester tester) async {
|
||||||
|
Loading…
Reference in New Issue
Block a user