Skip to content

Commit 378668d

Browse files
authored
added MaterialStateColor support to TabBarTheme.labelColor (#109541)
1 parent d29668d commit 378668d

File tree

3 files changed

+290
-54
lines changed

3 files changed

+290
-54
lines changed

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ class TabBarTheme with Diagnosticable {
5555
final Color? dividerColor;
5656

5757
/// Overrides the default value for [TabBar.labelColor].
58+
///
59+
/// If [labelColor] is a [MaterialStateColor], then the effective color will
60+
/// depend on the [MaterialState.selected] state, i.e. if the [Tab] is
61+
/// selected or not. In case of unselected state, this [MaterialStateColor]'s
62+
/// resolved color will be used even if [TabBar.unselectedLabelColor] or
63+
/// [unselectedLabelColor] is non-null.
5864
final Color? labelColor;
5965

6066
/// Overrides the default value for [TabBar.labelPadding].

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

Lines changed: 96 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ class Tab extends StatelessWidget implements PreferredSizeWidget {
166166
class _TabStyle extends AnimatedWidget {
167167
const _TabStyle({
168168
required Animation<double> animation,
169-
required this.selected,
169+
required this.isSelected,
170170
required this.labelColor,
171171
required this.unselectedLabelColor,
172172
required this.labelStyle,
@@ -176,18 +176,58 @@ class _TabStyle extends AnimatedWidget {
176176

177177
final TextStyle? labelStyle;
178178
final TextStyle? unselectedLabelStyle;
179-
final bool selected;
179+
final bool isSelected;
180180
final Color? labelColor;
181181
final Color? unselectedLabelColor;
182182
final Widget child;
183183

184+
MaterialStateColor _resolveWithLabelColor(BuildContext context) {
185+
final ThemeData themeData = Theme.of(context);
186+
final TabBarTheme tabBarTheme = TabBarTheme.of(context);
187+
final TabBarTheme defaults = themeData.useMaterial3 ? _TabsDefaultsM3(context) : _TabsDefaultsM2(context);
188+
final Animation<double> animation = listenable as Animation<double>;
189+
190+
// labelStyle.color (and tabBarTheme.labelStyle.color) is not considered
191+
// as it'll be a breaking change without a possible migration plan. for
192+
// details: https://github.com/flutter/flutter/pull/109541#issuecomment-1294241417
193+
Color selectedColor = labelColor
194+
?? tabBarTheme.labelColor
195+
?? defaults.labelColor!;
196+
197+
final Color unselectedColor;
198+
199+
if (selectedColor is MaterialStateColor) {
200+
unselectedColor = selectedColor.resolve(const <MaterialState>{});
201+
selectedColor = selectedColor.resolve(const <MaterialState>{MaterialState.selected});
202+
} else {
203+
// unselectedLabelColor and tabBarTheme.unselectedLabelColor are ignored
204+
// when labelColor is a MaterialStateColor.
205+
unselectedColor = unselectedLabelColor
206+
?? tabBarTheme.unselectedLabelColor
207+
?? (themeData.useMaterial3
208+
? defaults.unselectedLabelColor!
209+
: selectedColor.withAlpha(0xB2)); // 70% alpha
210+
}
211+
212+
return MaterialStateColor.resolveWith((Set<MaterialState> states) {
213+
if (states.contains(MaterialState.selected)) {
214+
return Color.lerp(selectedColor, unselectedColor, animation.value)!;
215+
}
216+
return Color.lerp(unselectedColor, selectedColor, animation.value)!;
217+
});
218+
}
219+
184220
@override
185221
Widget build(BuildContext context) {
186222
final ThemeData themeData = Theme.of(context);
187223
final TabBarTheme tabBarTheme = TabBarTheme.of(context);
188224
final TabBarTheme defaults = themeData.useMaterial3 ? _TabsDefaultsM3(context) : _TabsDefaultsM2(context);
189225
final Animation<double> animation = listenable as Animation<double>;
190226

227+
final Set<MaterialState> states = isSelected
228+
? const <MaterialState>{MaterialState.selected}
229+
: const <MaterialState>{};
230+
191231
// To enable TextStyle.lerp(style1, style2, value), both styles must have
192232
// the same value of inherit. Force that to be inherit=true here.
193233
final TextStyle defaultStyle = (labelStyle
@@ -199,21 +239,10 @@ class _TabStyle extends AnimatedWidget {
199239
?? labelStyle
200240
?? defaults.unselectedLabelStyle!
201241
).copyWith(inherit: true);
202-
final TextStyle textStyle = selected
242+
final TextStyle textStyle = isSelected
203243
? TextStyle.lerp(defaultStyle, defaultUnselectedStyle, animation.value)!
204244
: TextStyle.lerp(defaultUnselectedStyle, defaultStyle, animation.value)!;
205-
206-
final Color selectedColor = labelColor
207-
?? tabBarTheme.labelColor
208-
?? defaults.labelColor!;
209-
final Color unselectedColor = unselectedLabelColor
210-
?? tabBarTheme.unselectedLabelColor
211-
?? (themeData.useMaterial3
212-
? defaults.unselectedLabelColor!
213-
: selectedColor.withAlpha(0xB2)); // 70% alpha
214-
final Color color = selected
215-
? Color.lerp(selectedColor, unselectedColor, animation.value)!
216-
: Color.lerp(unselectedColor, selectedColor, animation.value)!;
245+
final Color color = _resolveWithLabelColor(context).resolve(states);
217246

218247
return DefaultTextStyle(
219248
style: textStyle.copyWith(color: color),
@@ -738,7 +767,8 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
738767
///
739768
/// If [automaticIndicatorColorAdjustment] is true,
740769
/// then the [indicatorColor] will be automatically adjusted to [Colors.white]
741-
/// when the [indicatorColor] is same as [Material.color] of the [Material] parent widget.
770+
/// when the [indicatorColor] is same as [Material.color] of the [Material]
771+
/// parent widget.
742772
final bool automaticIndicatorColorAdjustment;
743773

744774
/// Defines how the selected tab indicator's size is computed.
@@ -762,23 +792,50 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
762792

763793
/// The color of selected tab labels.
764794
///
765-
/// If [ThemeData.useMaterial3] is false, unselected tab labels are rendered with
766-
/// the same color with 70% opacity unless [unselectedLabelColor] is non-null.
767-
///
768-
/// If this property is null and [ThemeData.useMaterial3] is true, [ColorScheme.primary]
769-
/// will be used, otherwise the color of the [ThemeData.primaryTextTheme]'s
795+
/// If null, then [TabBarTheme.labelColor] is used. If that is also null and
796+
/// [ThemeData.useMaterial3] is true, [ColorScheme.primary] will be used,
797+
/// otherwise the color of the [ThemeData.primaryTextTheme]'s
770798
/// [TextTheme.bodyLarge] text color is used.
799+
///
800+
/// If [labelColor] (or, if null, [TabBarTheme.labelColor]) is a
801+
/// [MaterialStateColor], then the effective tab color will depend on the
802+
/// [MaterialState.selected] state, i.e. if the [Tab] is selected or not,
803+
/// ignoring [unselectedLabelColor] even if it's non-null.
804+
///
805+
/// Note: [labelStyle]'s color and [TabBarTheme.labelStyle]'s color do not
806+
/// affect the effective [labelColor].
807+
///
808+
/// See also:
809+
///
810+
/// * [unselectedLabelColor], for color of unselected tab labels.
771811
final Color? labelColor;
772812

773813
/// The color of unselected tab labels.
774814
///
775-
/// If this property is null and [ThemeData.useMaterial3] is true, [ColorScheme.onSurfaceVariant]
776-
/// will be used, otherwise unselected tab labels are rendered with the
777-
/// [labelColor] with 70% opacity.
815+
/// If [labelColor] (or, if null, [TabBarTheme.labelColor]) is a
816+
/// [MaterialStateColor], then the unselected tabs are rendered with
817+
/// that [MaterialStateColor]'s resolved color for unselected state, even if
818+
/// [unselectedLabelColor] is non-null.
819+
///
820+
/// If null, then [TabBarTheme.unselectedLabelColor] is used. If that is also
821+
/// null and [ThemeData.useMaterial3] is true, [ColorScheme.onSurfaceVariant]
822+
/// will be used, otherwise unselected tab labels are rendered with
823+
/// [labelColor] at 70% opacity.
824+
///
825+
/// Note: [unselectedLabelStyle]'s color and
826+
/// [TabBarTheme.unselectedLabelStyle]'s color are ignored in
827+
/// [unselectedLabelColor]'s precedence calculation.
828+
///
829+
/// See also:
830+
///
831+
/// * [labelColor], for color of selected tab labels.
778832
final Color? unselectedLabelColor;
779833

780834
/// The text style of the selected tab labels.
781835
///
836+
/// This does not influence color of the tab labels even if [TextStyle.color]
837+
/// is non-null. Refer [labelColor] to color selected tab labels instead.
838+
///
782839
/// If [unselectedLabelStyle] is null, then this text style will be used for
783840
/// both selected and unselected label styles.
784841
///
@@ -787,6 +844,18 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
787844
/// [TextTheme.bodyLarge] definition is used.
788845
final TextStyle? labelStyle;
789846

847+
/// The text style of the unselected tab labels.
848+
///
849+
/// This does not influence color of the tab labels even if [TextStyle.color]
850+
/// is non-null. Refer [unselectedLabelColor] to color unselected tab labels
851+
/// instead.
852+
///
853+
/// If this property is null and [ThemeData.useMaterial3] is true,
854+
/// [TextTheme.titleSmall] will be used, otherwise then the [labelStyle] value
855+
/// is used. If [labelStyle] is null, the text style of the
856+
/// [ThemeData.primaryTextTheme]'s [TextTheme.bodyLarge] definition is used.
857+
final TextStyle? unselectedLabelStyle;
858+
790859
/// The padding added to each of the tab labels.
791860
///
792861
/// If there are few tabs with both icon and text and few
@@ -796,14 +865,6 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
796865
/// If this property is null, then kTabLabelPadding is used.
797866
final EdgeInsetsGeometry? labelPadding;
798867

799-
/// The text style of the unselected tab labels.
800-
///
801-
/// If this property is null and [ThemeData.useMaterial3] is true, [TextTheme.titleSmall]
802-
/// will be used, otherwise then the [labelStyle] value is used. If [labelStyle]
803-
/// is null, the text style of the [ThemeData.primaryTextTheme]'s
804-
/// [TextTheme.bodyLarge] definition is used.
805-
final TextStyle? unselectedLabelStyle;
806-
807868
/// Defines the ink response focus, hover, and splash colors.
808869
///
809870
/// If non-null, it is resolved against one of [MaterialState.focused],
@@ -1209,10 +1270,10 @@ class _TabBarState extends State<TabBar> {
12091270
widget.onTap?.call(index);
12101271
}
12111272

1212-
Widget _buildStyledTab(Widget child, bool selected, Animation<double> animation) {
1273+
Widget _buildStyledTab(Widget child, bool isSelected, Animation<double> animation) {
12131274
return _TabStyle(
12141275
animation: animation,
1215-
selected: selected,
1276+
isSelected: isSelected,
12161277
labelColor: widget.labelColor,
12171278
unselectedLabelColor: widget.unselectedLabelColor,
12181279
labelStyle: widget.labelStyle,
@@ -1368,7 +1429,7 @@ class _TabBarState extends State<TabBar> {
13681429
painter: _indicatorPainter,
13691430
child: _TabStyle(
13701431
animation: kAlwaysDismissedAnimation,
1371-
selected: false,
1432+
isSelected: false,
13721433
labelColor: widget.labelColor,
13731434
unselectedLabelColor: widget.unselectedLabelColor,
13741435
labelStyle: widget.labelStyle,

0 commit comments

Comments
 (0)