Skip to content

Commit a522938

Browse files
authored
[Reland] Add Material 3 support for TabBar (#116283)
* Add Material 3 support for `TabBar` * M3 `TabBar` revert fix and tests
1 parent ef99905 commit a522938

File tree

7 files changed

+629
-72
lines changed

7 files changed

+629
-72
lines changed

dev/tools/gen_defaults/bin/gen_defaults.dart

+2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import 'package:gen_defaults/segmented_button_template.dart';
4646
import 'package:gen_defaults/slider_template.dart';
4747
import 'package:gen_defaults/surface_tint.dart';
4848
import 'package:gen_defaults/switch_template.dart';
49+
import 'package:gen_defaults/tabs_template.dart';
4950
import 'package:gen_defaults/text_field_template.dart';
5051
import 'package:gen_defaults/typography_template.dart';
5152

@@ -165,5 +166,6 @@ Future<void> main(List<String> args) async {
165166
SurfaceTintTemplate('SurfaceTint', '$materialLib/elevation_overlay.dart', tokens).updateFile();
166167
SwitchTemplate('Switch', '$materialLib/switch.dart', tokens).updateFile();
167168
TextFieldTemplate('TextField', '$materialLib/text_field.dart', tokens).updateFile();
169+
TabsTemplate('Tabs', '$materialLib/tabs.dart', tokens).updateFile();
168170
TypographyTemplate('Typography', '$materialLib/typography.dart', tokens).updateFile();
169171
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'template.dart';
6+
7+
class TabsTemplate extends TokenTemplate {
8+
const TabsTemplate(super.blockName, super.fileName, super.tokens, {
9+
super.colorSchemePrefix = '_colors.',
10+
super.textThemePrefix = '_textTheme.',
11+
});
12+
13+
@override
14+
String generate() => '''
15+
class _${blockName}DefaultsM3 extends TabBarTheme {
16+
_${blockName}DefaultsM3(this.context)
17+
: super(indicatorSize: TabBarIndicatorSize.label);
18+
19+
final BuildContext context;
20+
late final ColorScheme _colors = Theme.of(context).colorScheme;
21+
late final TextTheme _textTheme = Theme.of(context).textTheme;
22+
23+
@override
24+
Color? get dividerColor => ${componentColor("md.comp.primary-navigation-tab.divider")};
25+
26+
@override
27+
Color? get indicatorColor => ${componentColor("md.comp.primary-navigation-tab.active-indicator")};
28+
29+
@override
30+
Color? get labelColor => ${componentColor("md.comp.primary-navigation-tab.with-label-text.active.label-text")};
31+
32+
@override
33+
TextStyle? get labelStyle => ${textStyle("md.comp.primary-navigation-tab.with-label-text.label-text")};
34+
35+
@override
36+
Color? get unselectedLabelColor => ${componentColor("md.comp.primary-navigation-tab.with-label-text.inactive.label-text")};
37+
38+
@override
39+
TextStyle? get unselectedLabelStyle => ${textStyle("md.comp.primary-navigation-tab.with-label-text.label-text")};
40+
41+
@override
42+
MaterialStateProperty<Color?> get overlayColor {
43+
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
44+
if (states.contains(MaterialState.selected)) {
45+
if (states.contains(MaterialState.hovered)) {
46+
return ${componentColor('md.comp.primary-navigation-tab.active.hover.state-layer')};
47+
}
48+
if (states.contains(MaterialState.focused)) {
49+
return ${componentColor('md.comp.primary-navigation-tab.active.focus.state-layer')};
50+
}
51+
if (states.contains(MaterialState.pressed)) {
52+
return ${componentColor('md.comp.primary-navigation-tab.active.pressed.state-layer')};
53+
}
54+
return null;
55+
}
56+
if (states.contains(MaterialState.hovered)) {
57+
return ${componentColor('md.comp.primary-navigation-tab.inactive.hover.state-layer')};
58+
}
59+
if (states.contains(MaterialState.focused)) {
60+
return ${componentColor('md.comp.primary-navigation-tab.inactive.focus.state-layer')};
61+
}
62+
if (states.contains(MaterialState.pressed)) {
63+
return ${componentColor('md.comp.primary-navigation-tab.inactive.pressed.state-layer')};
64+
}
65+
return null;
66+
});
67+
}
68+
69+
@override
70+
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
71+
}
72+
''';
73+
}

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

+19-37
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ class TabBarTheme with Diagnosticable {
2929
/// Creates a tab bar theme that can be used with [ThemeData.tabBarTheme].
3030
const TabBarTheme({
3131
this.indicator,
32+
this.indicatorColor,
3233
this.indicatorSize,
34+
this.dividerColor,
3335
this.labelColor,
3436
this.labelPadding,
3537
this.labelStyle,
@@ -43,9 +45,15 @@ class TabBarTheme with Diagnosticable {
4345
/// Overrides the default value for [TabBar.indicator].
4446
final Decoration? indicator;
4547

48+
/// Overrides the default value for [TabBar.indicatorColor].
49+
final Color? indicatorColor;
50+
4651
/// Overrides the default value for [TabBar.indicatorSize].
4752
final TabBarIndicatorSize? indicatorSize;
4853

54+
/// Overrides the default value for [TabBar.dividerColor].
55+
final Color? dividerColor;
56+
4957
/// Overrides the default value for [TabBar.labelColor].
5058
final Color? labelColor;
5159

@@ -80,7 +88,9 @@ class TabBarTheme with Diagnosticable {
8088
/// new values.
8189
TabBarTheme copyWith({
8290
Decoration? indicator,
91+
Color? indicatorColor,
8392
TabBarIndicatorSize? indicatorSize,
93+
Color? dividerColor,
8494
Color? labelColor,
8595
EdgeInsetsGeometry? labelPadding,
8696
TextStyle? labelStyle,
@@ -92,7 +102,9 @@ class TabBarTheme with Diagnosticable {
92102
}) {
93103
return TabBarTheme(
94104
indicator: indicator ?? this.indicator,
105+
indicatorColor: indicatorColor ?? this.indicatorColor,
95106
indicatorSize: indicatorSize ?? this.indicatorSize,
107+
dividerColor: dividerColor ?? this.dividerColor,
96108
labelColor: labelColor ?? this.labelColor,
97109
labelPadding: labelPadding ?? this.labelPadding,
98110
labelStyle: labelStyle ?? this.labelStyle,
@@ -120,13 +132,15 @@ class TabBarTheme with Diagnosticable {
120132
assert(t != null);
121133
return TabBarTheme(
122134
indicator: Decoration.lerp(a.indicator, b.indicator, t),
135+
indicatorColor: Color.lerp(a.indicatorColor, b.indicatorColor, t),
123136
indicatorSize: t < 0.5 ? a.indicatorSize : b.indicatorSize,
137+
dividerColor: Color.lerp(a.dividerColor, b.dividerColor, t),
124138
labelColor: Color.lerp(a.labelColor, b.labelColor, t),
125139
labelPadding: EdgeInsetsGeometry.lerp(a.labelPadding, b.labelPadding, t),
126140
labelStyle: TextStyle.lerp(a.labelStyle, b.labelStyle, t),
127141
unselectedLabelColor: Color.lerp(a.unselectedLabelColor, b.unselectedLabelColor, t),
128142
unselectedLabelStyle: TextStyle.lerp(a.unselectedLabelStyle, b.unselectedLabelStyle, t),
129-
overlayColor: _LerpColors(a.overlayColor, b.overlayColor, t),
143+
overlayColor: MaterialStateProperty.lerp<Color?>(a.overlayColor, b.overlayColor, t, Color.lerp),
130144
splashFactory: t < 0.5 ? a.splashFactory : b.splashFactory,
131145
mouseCursor: t < 0.5 ? a.mouseCursor : b.mouseCursor,
132146
);
@@ -135,7 +149,9 @@ class TabBarTheme with Diagnosticable {
135149
@override
136150
int get hashCode => Object.hash(
137151
indicator,
152+
indicatorColor,
138153
indicatorSize,
154+
dividerColor,
139155
labelColor,
140156
labelPadding,
141157
labelStyle,
@@ -156,7 +172,9 @@ class TabBarTheme with Diagnosticable {
156172
}
157173
return other is TabBarTheme
158174
&& other.indicator == indicator
175+
&& other.indicatorColor == indicatorColor
159176
&& other.indicatorSize == indicatorSize
177+
&& other.dividerColor == dividerColor
160178
&& other.labelColor == labelColor
161179
&& other.labelPadding == labelPadding
162180
&& other.labelStyle == labelStyle
@@ -167,39 +185,3 @@ class TabBarTheme with Diagnosticable {
167185
&& other.mouseCursor == mouseCursor;
168186
}
169187
}
170-
171-
172-
@immutable
173-
class _LerpColors implements MaterialStateProperty<Color?> {
174-
const _LerpColors(this.a, this.b, this.t);
175-
176-
final MaterialStateProperty<Color?>? a;
177-
final MaterialStateProperty<Color?>? b;
178-
final double t;
179-
180-
@override
181-
Color? resolve(Set<MaterialState> states) {
182-
final Color? resolvedA = a?.resolve(states);
183-
final Color? resolvedB = b?.resolve(states);
184-
return Color.lerp(resolvedA, resolvedB, t);
185-
}
186-
187-
@override
188-
int get hashCode {
189-
return Object.hash(a, b, t);
190-
}
191-
192-
@override
193-
bool operator ==(Object other) {
194-
if (identical(this, other)) {
195-
return true;
196-
}
197-
if (other.runtimeType != runtimeType) {
198-
return false;
199-
}
200-
return other is _LerpColors
201-
&& other.a == a
202-
&& other.b == b
203-
&& other.t == t;
204-
}
205-
}

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

+38-5
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,18 @@ class UnderlineTabIndicator extends Decoration {
2020
///
2121
/// The [borderSide] and [insets] arguments must not be null.
2222
const UnderlineTabIndicator({
23+
this.borderRadius,
2324
this.borderSide = const BorderSide(width: 2.0, color: Colors.white),
2425
this.insets = EdgeInsets.zero,
2526
}) : assert(borderSide != null),
2627
assert(insets != null);
2728

29+
/// The radius of the indicator's corners.
30+
///
31+
/// If this value is non-null, rounded rectangular tab indicator is
32+
/// drawn, otherwise rectangular tab indictor is drawn.
33+
final BorderRadius? borderRadius;
34+
2835
/// The color and weight of the horizontal line drawn below the selected tab.
2936
final BorderSide borderSide;
3037

@@ -60,7 +67,7 @@ class UnderlineTabIndicator extends Decoration {
6067

6168
@override
6269
BoxPainter createBoxPainter([ VoidCallback? onChanged ]) {
63-
return _UnderlinePainter(this, onChanged);
70+
return _UnderlinePainter(this, borderRadius, onChanged);
6471
}
6572

6673
Rect _indicatorRectFor(Rect rect, TextDirection textDirection) {
@@ -77,24 +84,50 @@ class UnderlineTabIndicator extends Decoration {
7784

7885
@override
7986
Path getClipPath(Rect rect, TextDirection textDirection) {
87+
if (borderRadius != null) {
88+
return Path()..addRRect(
89+
borderRadius!.toRRect(_indicatorRectFor(rect, textDirection))
90+
);
91+
}
8092
return Path()..addRect(_indicatorRectFor(rect, textDirection));
8193
}
8294
}
8395

8496
class _UnderlinePainter extends BoxPainter {
85-
_UnderlinePainter(this.decoration, super.onChanged)
97+
_UnderlinePainter(
98+
this.decoration,
99+
this.borderRadius,
100+
super.onChanged,
101+
)
86102
: assert(decoration != null);
87103

88104
final UnderlineTabIndicator decoration;
105+
final BorderRadius? borderRadius;
89106

90107
@override
91108
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
92109
assert(configuration != null);
93110
assert(configuration.size != null);
94111
final Rect rect = offset & configuration.size!;
95112
final TextDirection textDirection = configuration.textDirection!;
96-
final Rect indicator = decoration._indicatorRectFor(rect, textDirection).deflate(decoration.borderSide.width / 2.0);
97-
final Paint paint = decoration.borderSide.toPaint()..strokeCap = StrokeCap.square;
98-
canvas.drawLine(indicator.bottomLeft, indicator.bottomRight, paint);
113+
final Paint paint;
114+
if (borderRadius != null) {
115+
paint = Paint()..color = decoration.borderSide.color;
116+
final Rect indicator = decoration._indicatorRectFor(rect, textDirection)
117+
.inflate(decoration.borderSide.width / 4.0);
118+
final RRect rrect = RRect.fromRectAndCorners(
119+
indicator,
120+
topLeft: borderRadius!.topLeft,
121+
topRight: borderRadius!.topRight,
122+
bottomRight: borderRadius!.bottomRight,
123+
bottomLeft: borderRadius!.bottomLeft,
124+
);
125+
canvas.drawRRect(rrect, paint);
126+
} else {
127+
paint = decoration.borderSide.toPaint()..strokeCap = StrokeCap.square;
128+
final Rect indicator = decoration._indicatorRectFor(rect, textDirection)
129+
.deflate(decoration.borderSide.width / 2.0);
130+
canvas.drawLine(indicator.bottomLeft, indicator.bottomRight, paint);
131+
}
99132
}
100133
}

0 commit comments

Comments
 (0)