[Reland] Add Material 3 support for TabBar (#116283)

* Add Material 3 support for `TabBar`

* M3 `TabBar` revert fix and tests
This commit is contained in:
Taha Tesser 2022-11-30 19:58:07 +02:00 committed by GitHub
parent ef999051d1
commit a52293843c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 629 additions and 72 deletions

View file

@ -46,6 +46,7 @@ import 'package:gen_defaults/segmented_button_template.dart';
import 'package:gen_defaults/slider_template.dart';
import 'package:gen_defaults/surface_tint.dart';
import 'package:gen_defaults/switch_template.dart';
import 'package:gen_defaults/tabs_template.dart';
import 'package:gen_defaults/text_field_template.dart';
import 'package:gen_defaults/typography_template.dart';
@ -165,5 +166,6 @@ Future<void> main(List<String> args) async {
SurfaceTintTemplate('SurfaceTint', '$materialLib/elevation_overlay.dart', tokens).updateFile();
SwitchTemplate('Switch', '$materialLib/switch.dart', tokens).updateFile();
TextFieldTemplate('TextField', '$materialLib/text_field.dart', tokens).updateFile();
TabsTemplate('Tabs', '$materialLib/tabs.dart', tokens).updateFile();
TypographyTemplate('Typography', '$materialLib/typography.dart', tokens).updateFile();
}

View file

@ -0,0 +1,73 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'template.dart';
class TabsTemplate extends TokenTemplate {
const TabsTemplate(super.blockName, super.fileName, super.tokens, {
super.colorSchemePrefix = '_colors.',
super.textThemePrefix = '_textTheme.',
});
@override
String generate() => '''
class _${blockName}DefaultsM3 extends TabBarTheme {
_${blockName}DefaultsM3(this.context)
: super(indicatorSize: TabBarIndicatorSize.label);
final BuildContext context;
late final ColorScheme _colors = Theme.of(context).colorScheme;
late final TextTheme _textTheme = Theme.of(context).textTheme;
@override
Color? get dividerColor => ${componentColor("md.comp.primary-navigation-tab.divider")};
@override
Color? get indicatorColor => ${componentColor("md.comp.primary-navigation-tab.active-indicator")};
@override
Color? get labelColor => ${componentColor("md.comp.primary-navigation-tab.with-label-text.active.label-text")};
@override
TextStyle? get labelStyle => ${textStyle("md.comp.primary-navigation-tab.with-label-text.label-text")};
@override
Color? get unselectedLabelColor => ${componentColor("md.comp.primary-navigation-tab.with-label-text.inactive.label-text")};
@override
TextStyle? get unselectedLabelStyle => ${textStyle("md.comp.primary-navigation-tab.with-label-text.label-text")};
@override
MaterialStateProperty<Color?> get overlayColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.hovered)) {
return ${componentColor('md.comp.primary-navigation-tab.active.hover.state-layer')};
}
if (states.contains(MaterialState.focused)) {
return ${componentColor('md.comp.primary-navigation-tab.active.focus.state-layer')};
}
if (states.contains(MaterialState.pressed)) {
return ${componentColor('md.comp.primary-navigation-tab.active.pressed.state-layer')};
}
return null;
}
if (states.contains(MaterialState.hovered)) {
return ${componentColor('md.comp.primary-navigation-tab.inactive.hover.state-layer')};
}
if (states.contains(MaterialState.focused)) {
return ${componentColor('md.comp.primary-navigation-tab.inactive.focus.state-layer')};
}
if (states.contains(MaterialState.pressed)) {
return ${componentColor('md.comp.primary-navigation-tab.inactive.pressed.state-layer')};
}
return null;
});
}
@override
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
}
''';
}

View file

@ -29,7 +29,9 @@ class TabBarTheme with Diagnosticable {
/// Creates a tab bar theme that can be used with [ThemeData.tabBarTheme].
const TabBarTheme({
this.indicator,
this.indicatorColor,
this.indicatorSize,
this.dividerColor,
this.labelColor,
this.labelPadding,
this.labelStyle,
@ -43,9 +45,15 @@ class TabBarTheme with Diagnosticable {
/// Overrides the default value for [TabBar.indicator].
final Decoration? indicator;
/// Overrides the default value for [TabBar.indicatorColor].
final Color? indicatorColor;
/// Overrides the default value for [TabBar.indicatorSize].
final TabBarIndicatorSize? indicatorSize;
/// Overrides the default value for [TabBar.dividerColor].
final Color? dividerColor;
/// Overrides the default value for [TabBar.labelColor].
final Color? labelColor;
@ -80,7 +88,9 @@ class TabBarTheme with Diagnosticable {
/// new values.
TabBarTheme copyWith({
Decoration? indicator,
Color? indicatorColor,
TabBarIndicatorSize? indicatorSize,
Color? dividerColor,
Color? labelColor,
EdgeInsetsGeometry? labelPadding,
TextStyle? labelStyle,
@ -92,7 +102,9 @@ class TabBarTheme with Diagnosticable {
}) {
return TabBarTheme(
indicator: indicator ?? this.indicator,
indicatorColor: indicatorColor ?? this.indicatorColor,
indicatorSize: indicatorSize ?? this.indicatorSize,
dividerColor: dividerColor ?? this.dividerColor,
labelColor: labelColor ?? this.labelColor,
labelPadding: labelPadding ?? this.labelPadding,
labelStyle: labelStyle ?? this.labelStyle,
@ -120,13 +132,15 @@ class TabBarTheme with Diagnosticable {
assert(t != null);
return TabBarTheme(
indicator: Decoration.lerp(a.indicator, b.indicator, t),
indicatorColor: Color.lerp(a.indicatorColor, b.indicatorColor, t),
indicatorSize: t < 0.5 ? a.indicatorSize : b.indicatorSize,
dividerColor: Color.lerp(a.dividerColor, b.dividerColor, t),
labelColor: Color.lerp(a.labelColor, b.labelColor, t),
labelPadding: EdgeInsetsGeometry.lerp(a.labelPadding, b.labelPadding, t),
labelStyle: TextStyle.lerp(a.labelStyle, b.labelStyle, t),
unselectedLabelColor: Color.lerp(a.unselectedLabelColor, b.unselectedLabelColor, t),
unselectedLabelStyle: TextStyle.lerp(a.unselectedLabelStyle, b.unselectedLabelStyle, t),
overlayColor: _LerpColors(a.overlayColor, b.overlayColor, t),
overlayColor: MaterialStateProperty.lerp<Color?>(a.overlayColor, b.overlayColor, t, Color.lerp),
splashFactory: t < 0.5 ? a.splashFactory : b.splashFactory,
mouseCursor: t < 0.5 ? a.mouseCursor : b.mouseCursor,
);
@ -135,7 +149,9 @@ class TabBarTheme with Diagnosticable {
@override
int get hashCode => Object.hash(
indicator,
indicatorColor,
indicatorSize,
dividerColor,
labelColor,
labelPadding,
labelStyle,
@ -156,7 +172,9 @@ class TabBarTheme with Diagnosticable {
}
return other is TabBarTheme
&& other.indicator == indicator
&& other.indicatorColor == indicatorColor
&& other.indicatorSize == indicatorSize
&& other.dividerColor == dividerColor
&& other.labelColor == labelColor
&& other.labelPadding == labelPadding
&& other.labelStyle == labelStyle
@ -167,39 +185,3 @@ class TabBarTheme with Diagnosticable {
&& other.mouseCursor == mouseCursor;
}
}
@immutable
class _LerpColors implements MaterialStateProperty<Color?> {
const _LerpColors(this.a, this.b, this.t);
final MaterialStateProperty<Color?>? a;
final MaterialStateProperty<Color?>? b;
final double t;
@override
Color? resolve(Set<MaterialState> states) {
final Color? resolvedA = a?.resolve(states);
final Color? resolvedB = b?.resolve(states);
return Color.lerp(resolvedA, resolvedB, t);
}
@override
int get hashCode {
return Object.hash(a, b, t);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
return other is _LerpColors
&& other.a == a
&& other.b == b
&& other.t == t;
}
}

View file

@ -20,11 +20,18 @@ class UnderlineTabIndicator extends Decoration {
///
/// The [borderSide] and [insets] arguments must not be null.
const UnderlineTabIndicator({
this.borderRadius,
this.borderSide = const BorderSide(width: 2.0, color: Colors.white),
this.insets = EdgeInsets.zero,
}) : assert(borderSide != null),
assert(insets != null);
/// The radius of the indicator's corners.
///
/// If this value is non-null, rounded rectangular tab indicator is
/// drawn, otherwise rectangular tab indictor is drawn.
final BorderRadius? borderRadius;
/// The color and weight of the horizontal line drawn below the selected tab.
final BorderSide borderSide;
@ -60,7 +67,7 @@ class UnderlineTabIndicator extends Decoration {
@override
BoxPainter createBoxPainter([ VoidCallback? onChanged ]) {
return _UnderlinePainter(this, onChanged);
return _UnderlinePainter(this, borderRadius, onChanged);
}
Rect _indicatorRectFor(Rect rect, TextDirection textDirection) {
@ -77,15 +84,25 @@ class UnderlineTabIndicator extends Decoration {
@override
Path getClipPath(Rect rect, TextDirection textDirection) {
if (borderRadius != null) {
return Path()..addRRect(
borderRadius!.toRRect(_indicatorRectFor(rect, textDirection))
);
}
return Path()..addRect(_indicatorRectFor(rect, textDirection));
}
}
class _UnderlinePainter extends BoxPainter {
_UnderlinePainter(this.decoration, super.onChanged)
_UnderlinePainter(
this.decoration,
this.borderRadius,
super.onChanged,
)
: assert(decoration != null);
final UnderlineTabIndicator decoration;
final BorderRadius? borderRadius;
@override
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
@ -93,8 +110,24 @@ class _UnderlinePainter extends BoxPainter {
assert(configuration.size != null);
final Rect rect = offset & configuration.size!;
final TextDirection textDirection = configuration.textDirection!;
final Rect indicator = decoration._indicatorRectFor(rect, textDirection).deflate(decoration.borderSide.width / 2.0);
final Paint paint = decoration.borderSide.toPaint()..strokeCap = StrokeCap.square;
canvas.drawLine(indicator.bottomLeft, indicator.bottomRight, paint);
final Paint paint;
if (borderRadius != null) {
paint = Paint()..color = decoration.borderSide.color;
final Rect indicator = decoration._indicatorRectFor(rect, textDirection)
.inflate(decoration.borderSide.width / 4.0);
final RRect rrect = RRect.fromRectAndCorners(
indicator,
topLeft: borderRadius!.topLeft,
topRight: borderRadius!.topRight,
bottomRight: borderRadius!.bottomRight,
bottomLeft: borderRadius!.bottomLeft,
);
canvas.drawRRect(rrect, paint);
} else {
paint = decoration.borderSide.toPaint()..strokeCap = StrokeCap.square;
final Rect indicator = decoration._indicatorRectFor(rect, textDirection)
.deflate(decoration.borderSide.width / 2.0);
canvas.drawLine(indicator.bottomLeft, indicator.bottomRight, paint);
}
}
}

View file

@ -11,6 +11,7 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'app_bar.dart';
import 'color_scheme.dart';
import 'colors.dart';
import 'constants.dart';
import 'debug.dart';
@ -21,6 +22,7 @@ import 'material_state.dart';
import 'tab_bar_theme.dart';
import 'tab_controller.dart';
import 'tab_indicator.dart';
import 'text_theme.dart';
import 'theme.dart';
const double _kTabHeight = 46.0;
@ -183,18 +185,19 @@ class _TabStyle extends AnimatedWidget {
Widget build(BuildContext context) {
final ThemeData themeData = Theme.of(context);
final TabBarTheme tabBarTheme = TabBarTheme.of(context);
final TabBarTheme defaults = themeData.useMaterial3 ? _TabsDefaultsM3(context) : _TabsDefaultsM2(context);
final Animation<double> animation = listenable as Animation<double>;
// To enable TextStyle.lerp(style1, style2, value), both styles must have
// the same value of inherit. Force that to be inherit=true here.
final TextStyle defaultStyle = (labelStyle
?? tabBarTheme.labelStyle
?? themeData.primaryTextTheme.bodyLarge!
?? defaults.labelStyle!
).copyWith(inherit: true);
final TextStyle defaultUnselectedStyle = (unselectedLabelStyle
?? tabBarTheme.unselectedLabelStyle
?? labelStyle
?? themeData.primaryTextTheme.bodyLarge!
?? defaults.unselectedLabelStyle!
).copyWith(inherit: true);
final TextStyle textStyle = selected
? TextStyle.lerp(defaultStyle, defaultUnselectedStyle, animation.value)!
@ -202,10 +205,12 @@ class _TabStyle extends AnimatedWidget {
final Color selectedColor = labelColor
?? tabBarTheme.labelColor
?? themeData.primaryTextTheme.bodyLarge!.color!;
?? defaults.labelColor!;
final Color unselectedColor = unselectedLabelColor
?? tabBarTheme.unselectedLabelColor
?? selectedColor.withAlpha(0xB2); // 70% alpha
?? (themeData.useMaterial3
? defaults.unselectedLabelColor!
: selectedColor.withAlpha(0xB2)); // 70% alpha
final Color color = selected
? Color.lerp(selectedColor, unselectedColor, animation.value)!
: Color.lerp(unselectedColor, selectedColor, animation.value)!;
@ -327,6 +332,7 @@ class _IndicatorPainter extends CustomPainter {
required this.tabKeys,
required _IndicatorPainter? old,
required this.indicatorPadding,
this.dividerColor,
}) : assert(controller != null),
assert(indicator != null),
super(repaint: controller.animation) {
@ -340,6 +346,7 @@ class _IndicatorPainter extends CustomPainter {
final TabBarIndicatorSize? indicatorSize;
final EdgeInsetsGeometry indicatorPadding;
final List<GlobalKey> tabKeys;
final Color? dividerColor;
// _currentTabOffsets and _currentTextDirection are set each time TabBar
// layout is completed. These values can be null when TabBar contains no
@ -431,6 +438,10 @@ 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);
}
@ -630,6 +641,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
this.indicatorPadding = EdgeInsets.zero,
this.indicator,
this.indicatorSize,
this.dividerColor,
this.labelColor,
this.labelStyle,
this.labelPadding,
@ -744,18 +756,27 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
/// [indicator] properties.
final TabBarIndicatorSize? indicatorSize;
/// The color of the divider.
///
/// If null and [ThemeData.useMaterial3] is true, [TabBarTheme.dividerColor]
/// color is used. If that is null and [ThemeData.useMaterial3] is true,
/// [ColorScheme.surfaceVariant] will be used, otherwise divider will not be drawn.
final Color? dividerColor;
/// The color of selected tab labels.
///
/// Unselected tab labels are rendered with the same color rendered at 70%
/// opacity unless [unselectedLabelColor] is non-null.
/// If [ThemeData.useMaterial3] is false, unselected tab labels are rendered with
/// the same color with 70% opacity unless [unselectedLabelColor] is non-null.
///
/// If this parameter is null, then the color of the [ThemeData.primaryTextTheme]'s
/// If this property is null and [ThemeData.useMaterial3] is true, [ColorScheme.primary]
/// will be used, otherwise the color of the [ThemeData.primaryTextTheme]'s
/// [TextTheme.bodyLarge] text color is used.
final Color? labelColor;
/// The color of unselected tab labels.
///
/// If this property is null, unselected tab labels are rendered with the
/// If this property is null and [ThemeData.useMaterial3] is true, [ColorScheme.onSurfaceVariant]
/// will be used, otherwise unselected tab labels are rendered with the
/// [labelColor] with 70% opacity.
final Color? unselectedLabelColor;
@ -764,8 +785,9 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
/// If [unselectedLabelStyle] is null, then this text style will be used for
/// both selected and unselected label styles.
///
/// If this property is null, then the text style of the
/// [ThemeData.primaryTextTheme]'s [TextTheme.bodyLarge] definition is used.
/// If this property is null and [ThemeData.useMaterial3] is true, [TextTheme.titleSmall]
/// will be used, otherwise the text style of the [ThemeData.primaryTextTheme]'s
/// [TextTheme.bodyLarge] definition is used.
final TextStyle? labelStyle;
/// The padding added to each of the tab labels.
@ -779,8 +801,9 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
/// The text style of the unselected tab labels.
///
/// If this property is null, then the [labelStyle] value is used. If [labelStyle]
/// is null, then the text style of the [ThemeData.primaryTextTheme]'s
/// If this property is null and [ThemeData.useMaterial3] is true, [TextTheme.titleSmall]
/// will be used, otherwise then the [labelStyle] value is used. If [labelStyle]
/// is null, the text style of the [ThemeData.primaryTextTheme]'s
/// [TextTheme.bodyLarge] definition is used.
final TextStyle? unselectedLabelStyle;
@ -939,16 +962,22 @@ class _TabBarState extends State<TabBar> {
_tabKeys = widget.tabs.map((Widget tab) => GlobalKey()).toList();
}
Decoration get _indicator {
Decoration _getIndicator() {
final ThemeData theme = Theme.of(context);
final TabBarTheme tabBarTheme = TabBarTheme.of(context);
final TabBarTheme defaults = theme.useMaterial3 ? _TabsDefaultsM3(context) : _TabsDefaultsM2(context);
if (widget.indicator != null) {
return widget.indicator!;
}
final TabBarTheme tabBarTheme = TabBarTheme.of(context);
if (tabBarTheme.indicator != null) {
return tabBarTheme.indicator!;
}
Color color = widget.indicatorColor ?? Theme.of(context).indicatorColor;
Color color = widget.indicatorColor
?? (theme.useMaterial3
? tabBarTheme.indicatorColor ?? defaults.indicatorColor!
: Theme.of(context).indicatorColor);
// ThemeData tries to avoid this by having indicatorColor avoid being the
// primaryColor. However, it's possible that the tab bar is on a
// Material that isn't the primaryColor. In that case, if the indicator
@ -968,6 +997,16 @@ class _TabBarState extends State<TabBar> {
}
return UnderlineTabIndicator(
borderRadius: theme.useMaterial3
// 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,
borderSide: BorderSide(
width: widget.indicatorWeight,
color: color,
@ -1012,13 +1051,18 @@ class _TabBarState extends State<TabBar> {
}
void _initIndicatorPainter() {
final ThemeData theme = Theme.of(context);
final TabBarTheme tabBarTheme = TabBarTheme.of(context);
final TabBarTheme defaults = theme.useMaterial3 ? _TabsDefaultsM3(context) : _TabsDefaultsM2(context);
_indicatorPainter = !_controllerIsValid ? null : _IndicatorPainter(
controller: _controller!,
indicator: _indicator,
indicatorSize: widget.indicatorSize ?? TabBarTheme.of(context).indicatorSize,
indicator: _getIndicator(),
indicatorSize: widget.indicatorSize ?? tabBarTheme.indicatorSize ?? defaults.indicatorSize!,
indicatorPadding: widget.indicatorPadding,
tabKeys: _tabKeys,
old: _indicatorPainter,
dividerColor: theme.useMaterial3 ? widget.dividerColor ?? defaults.dividerColor : null,
);
}
@ -1210,7 +1254,9 @@ class _TabBarState extends State<TabBar> {
);
}
final ThemeData theme = Theme.of(context);
final TabBarTheme tabBarTheme = TabBarTheme.of(context);
final TabBarTheme defaults = theme.useMaterial3 ? _TabsDefaultsM3(context) : _TabsDefaultsM2(context);
final List<Widget> wrappedTabs = List<Widget>.generate(widget.tabs.length, (int index) {
const double verticalAdjustment = (_kTextAndIconTabHeight - _kTabHeight)/2.0;
@ -1275,20 +1321,26 @@ class _TabBarState extends State<TabBar> {
// the same share of the tab bar's overall width.
final int tabCount = widget.tabs.length;
for (int index = 0; index < tabCount; index += 1) {
final Set<MaterialState> states = <MaterialState>{
final Set<MaterialState> selectedState = <MaterialState>{
if (index == _currentIndex) MaterialState.selected,
};
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states)
?? tabBarTheme.mouseCursor?.resolve(states)
?? MaterialStateMouseCursor.clickable.resolve(states);
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, selectedState)
?? tabBarTheme.mouseCursor?.resolve(selectedState)
?? MaterialStateMouseCursor.clickable.resolve(selectedState);
final MaterialStateProperty<Color?> defaultOverlay = MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
final Set<MaterialState> effectiveStates = selectedState..addAll(states);
return defaults.overlayColor?.resolve(effectiveStates);
},
);
wrappedTabs[index] = InkWell(
mouseCursor: effectiveMouseCursor,
onTap: () { _handleTap(index); },
enableFeedback: widget.enableFeedback ?? true,
overlayColor: widget.overlayColor ?? tabBarTheme.overlayColor,
splashFactory: widget.splashFactory ?? tabBarTheme.splashFactory,
overlayColor: widget.overlayColor ?? tabBarTheme.overlayColor ?? defaultOverlay,
splashFactory: widget.splashFactory ?? tabBarTheme.splashFactory ?? defaults.splashFactory,
borderRadius: widget.splashBorderRadius,
child: Padding(
padding: EdgeInsets.only(bottom: widget.indicatorWeight),
@ -1818,3 +1870,95 @@ class TabPageSelector extends StatelessWidget {
);
}
}
// Hand coded defaults based on Material Design 2.
class _TabsDefaultsM2 extends TabBarTheme {
const _TabsDefaultsM2(this.context)
: super(indicatorSize: TabBarIndicatorSize.tab);
final BuildContext context;
@override
Color? get indicatorColor => Theme.of(context).indicatorColor;
@override
Color? get labelColor => Theme.of(context).primaryTextTheme.bodyLarge!.color!;
@override
TextStyle? get labelStyle => Theme.of(context).primaryTextTheme.bodyLarge;
@override
TextStyle? get unselectedLabelStyle => Theme.of(context).primaryTextTheme.bodyLarge;
@override
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
}
// BEGIN GENERATED TOKEN PROPERTIES - Tabs
// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
// Token database version: v0_143
class _TabsDefaultsM3 extends TabBarTheme {
_TabsDefaultsM3(this.context)
: super(indicatorSize: TabBarIndicatorSize.label);
final BuildContext context;
late final ColorScheme _colors = Theme.of(context).colorScheme;
late final TextTheme _textTheme = Theme.of(context).textTheme;
@override
Color? get dividerColor => _colors.surfaceVariant;
@override
Color? get indicatorColor => _colors.primary;
@override
Color? get labelColor => _colors.primary;
@override
TextStyle? get labelStyle => _textTheme.titleSmall;
@override
Color? get unselectedLabelColor => _colors.onSurfaceVariant;
@override
TextStyle? get unselectedLabelStyle => _textTheme.titleSmall;
@override
MaterialStateProperty<Color?> get overlayColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.hovered)) {
return _colors.primary.withOpacity(0.08);
}
if (states.contains(MaterialState.focused)) {
return _colors.primary.withOpacity(0.12);
}
if (states.contains(MaterialState.pressed)) {
return _colors.primary.withOpacity(0.12);
}
return null;
}
if (states.contains(MaterialState.hovered)) {
return _colors.onSurface.withOpacity(0.08);
}
if (states.contains(MaterialState.focused)) {
return _colors.onSurface.withOpacity(0.12);
}
if (states.contains(MaterialState.pressed)) {
return _colors.primary.withOpacity(0.12);
}
return null;
});
}
@override
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
}
// END GENERATED TOKEN PROPERTIES - Tabs

View file

@ -11,6 +11,8 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
const String _tab1Text = 'tab 1';
const String _tab2Text = 'tab 2';
const String _tab3Text = 'tab 3';
@ -32,9 +34,10 @@ Widget _withTheme(
TabBarTheme? theme, {
List<Widget> tabs = _tabs,
bool isScrollable = false,
bool useMaterial3 = false,
}) {
return MaterialApp(
theme: ThemeData(tabBarTheme: theme),
theme: ThemeData(tabBarTheme: theme, useMaterial3: useMaterial3),
home: Scaffold(
body: RepaintBoundary(
key: _painterKey,
@ -60,7 +63,9 @@ void main() {
expect(const TabBarTheme().hashCode, const TabBarTheme().copyWith().hashCode);
expect(const TabBarTheme().indicator, null);
expect(const TabBarTheme().indicatorColor, null);
expect(const TabBarTheme().indicatorSize, null);
expect(const TabBarTheme().dividerColor, null);
expect(const TabBarTheme().labelColor, null);
expect(const TabBarTheme().labelPadding, null);
expect(const TabBarTheme().labelStyle, null);
@ -71,18 +76,19 @@ void main() {
expect(const TabBarTheme().mouseCursor, null);
});
testWidgets('Tab bar defaults - label style and selected/unselected label colors', (WidgetTester tester) async {
testWidgets('Tab bar defaults', (WidgetTester tester) async {
// tests for the default label color and label styles when tabBarTheme and tabBar do not provide any
await tester.pumpWidget(_withTheme(null));
await tester.pumpWidget(_withTheme(null, useMaterial3: true));
final ThemeData theme = ThemeData(useMaterial3: true);
final RenderParagraph selectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab1Text));
expect(selectedRenderObject.text.style!.fontFamily, equals('Roboto'));
expect(selectedRenderObject.text.style!.fontFamily, equals(theme.textTheme.titleSmall!.fontFamily));
expect(selectedRenderObject.text.style!.fontSize, equals(14.0));
expect(selectedRenderObject.text.style!.color, equals(Colors.white));
expect(selectedRenderObject.text.style!.color, equals(theme.colorScheme.primary));
final RenderParagraph unselectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab2Text));
expect(unselectedRenderObject.text.style!.fontFamily, equals('Roboto'));
expect(unselectedRenderObject.text.style!.fontFamily, equals(theme.textTheme.titleSmall!.fontFamily));
expect(unselectedRenderObject.text.style!.fontSize, equals(14.0));
expect(unselectedRenderObject.text.style!.color, equals(Colors.white.withAlpha(0xB2)));
expect(unselectedRenderObject.text.style!.color, equals(theme.colorScheme.onSurfaceVariant));
// tests for the default value of labelPadding when tabBarTheme and tabBar do not provide one
await tester.pumpWidget(_withTheme(null, tabs: _sizedTabs, isScrollable: true));
@ -104,7 +110,16 @@ void main() {
// 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));
final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
expect(
tabBarBox,
paints
..line(color: theme.colorScheme.surfaceVariant)
..rrect(color: theme.colorScheme.primary),
);
});
testWidgets('Tab bar theme overrides label color (selected)', (WidgetTester tester) async {
const Color labelColor = Colors.black;
const TabBarTheme tabBarTheme = TabBarTheme(labelColor: labelColor);
@ -282,6 +297,15 @@ void main() {
expect(iconRenderObject.text.style!.color, equals(unselectedLabelColor));
});
testWidgets('Tab bar default tab indicator size', (WidgetTester tester) async {
await tester.pumpWidget(_withTheme(null, useMaterial3: true, isScrollable: true));
await expectLater(
find.byKey(_painterKey),
matchesGoldenFile('tab_bar.default.tab_indicator_size.png'),
);
});
testWidgets('Tab bar theme overrides tab indicator size (tab)', (WidgetTester tester) async {
const TabBarTheme tabBarTheme = TabBarTheme(indicatorSize: TabBarIndicatorSize.tab);
@ -349,4 +373,56 @@ void main() {
matchesGoldenFile('tab_bar_theme.beveled_rect_indicator.png'),
);
});
group('Material 2', () {
// Tests that are only relevant for Material 2. Once ThemeData.useMaterial3
// is turned on by default, these tests can be removed.
testWidgets('Tab bar defaults', (WidgetTester tester) async {
// tests for the default label color and label styles when tabBarTheme and tabBar do not provide any
await tester.pumpWidget(_withTheme(null));
final RenderParagraph selectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab1Text));
expect(selectedRenderObject.text.style!.fontFamily, equals('Roboto'));
expect(selectedRenderObject.text.style!.fontSize, equals(14.0));
expect(selectedRenderObject.text.style!.color, equals(Colors.white));
final RenderParagraph unselectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab2Text));
expect(unselectedRenderObject.text.style!.fontFamily, equals('Roboto'));
expect(unselectedRenderObject.text.style!.fontSize, equals(14.0));
expect(unselectedRenderObject.text.style!.color, equals(Colors.white.withAlpha(0xB2)));
// tests for the default value of labelPadding when tabBarTheme and tabBar do not provide one
await tester.pumpWidget(_withTheme(null, tabs: _sizedTabs, isScrollable: true));
const double indicatorWeight = 2.0;
final Rect tabBar = tester.getRect(find.byType(TabBar));
final Rect tabOneRect = tester.getRect(find.byKey(_sizedTabs[0].key!));
final Rect tabTwoRect = tester.getRect(find.byKey(_sizedTabs[1].key!));
// verify coordinates of tabOne
expect(tabOneRect.left, equals(kTabLabelPadding.left));
expect(tabOneRect.top, equals(kTabLabelPadding.top));
expect(tabOneRect.bottom, equals(tabBar.bottom - kTabLabelPadding.bottom - indicatorWeight));
// verify coordinates of tabTwo
expect(tabTwoRect.right, equals(tabBar.width - kTabLabelPadding.right));
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));
final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
expect(tabBarBox, paints..line(color: const Color(0xff2196f3)));
});
testWidgets('Tab bar default tab indicator size', (WidgetTester tester) async {
await tester.pumpWidget(_withTheme(null));
await expectLater(
find.byKey(_painterKey),
matchesGoldenFile('tab_bar.m2.default.tab_indicator_size.png'),
);
});
});
}

View file

@ -5,6 +5,7 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
@ -357,6 +358,39 @@ void main() {
expect(find.byType(TabBar), paints..line(color: Colors.blue[500]));
});
testWidgets('TabBar default selected/unselected text style', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true);
final List<String> tabs = <String>['A', 'B', 'C'];
const String selectedValue = 'A';
const String unSelectedValue = 'C';
await tester.pumpWidget(
Theme(
data: theme,
child: buildFrame(tabs: tabs, value: selectedValue),
),
);
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsOneWidget);
expect(find.text('C'), findsOneWidget);
// Test selected label text style.
expect(tester.renderObject<RenderParagraph>(find.text(selectedValue)).text.style!.fontFamily, 'Roboto');
expect(tester.renderObject<RenderParagraph>(find.text(selectedValue)).text.style!.fontSize, 14.0);
expect(tester.renderObject<RenderParagraph>(
find.text(selectedValue)).text.style!.color,
theme.colorScheme.primary,
);
// Test unselected label text style.
expect(tester.renderObject<RenderParagraph>(find.text(unSelectedValue)).text.style!.fontFamily, 'Roboto');
expect(tester.renderObject<RenderParagraph>(find.text(unSelectedValue)).text.style!.fontSize, 14.0);
expect(tester.renderObject<RenderParagraph>(
find.text(unSelectedValue)).text.style!.color,
theme.colorScheme.onSurfaceVariant,
);
});
testWidgets('TabBar tap selects tab', (WidgetTester tester) async {
final List<String> tabs = <String>['A', 'B', 'C'];
@ -5088,6 +5122,219 @@ void main() {
expect(tester.takeException(), isAssertionError);
});
testWidgets('Tab has correct selected/unselected hover color', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
final ThemeData theme = ThemeData(useMaterial3: true);
final List<String> tabs = <String>['A', 'B', 'C'];
await tester.pumpWidget(Theme(
data: theme,
child: buildFrame(tabs: tabs, value: 'C')),
);
await tester.pumpAndSettle();
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(
inkFeatures,
isNot(paints
..rect(
color: theme.colorScheme.onSurface.withOpacity(0.08),
))
);
expect(
inkFeatures,
isNot(paints
..rect(
color: theme.colorScheme.primary.withOpacity(0.08),
))
);
// Start hovering unselected tab.
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byType(Tab).first));
await tester.pumpAndSettle();
expect(
inkFeatures,
paints
..rect(
color: theme.colorScheme.onSurface.withOpacity(0.08),
)
);
// Start hovering selected tab.
await gesture.moveTo(tester.getCenter(find.byType(Tab).last));
await tester.pumpAndSettle();
expect(
inkFeatures,
paints
..rect(
color: theme.colorScheme.primary.withOpacity(0.08),
),
);
});
testWidgets('Tab has correct selected/unselected focus color', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
final ThemeData theme = ThemeData(useMaterial3: true);
final List<String> tabs = <String>['A', 'B', 'C'];
await tester.pumpWidget(MaterialApp(
theme: theme,
home: buildFrame(tabs: tabs, value: 'B'),
),
);
await tester.pumpAndSettle();
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(
inkFeatures,
isNot(paints
..rect(
color: theme.colorScheme.onSurface.withOpacity(0.12),
))
);
expect(
inkFeatures,
isNot(paints
..rect(
color: theme.colorScheme.primary.withOpacity(0.12),
))
);
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pumpAndSettle();
expect(tester.binding.focusManager.primaryFocus?.hasPrimaryFocus, isTrue);
expect(
inkFeatures,
paints
..rect(
color: theme.colorScheme.onSurface.withOpacity(0.12),
),
);
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pumpAndSettle();
expect(tester.binding.focusManager.primaryFocus?.hasPrimaryFocus, isTrue);
expect(
inkFeatures,
paints
..rect(
color: theme.colorScheme.primary.withOpacity(0.12),
),
);
});
testWidgets('Tab has correct selected/unselected pressed color', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true);
final List<String> tabs = <String>['A', 'B', 'C'];
await tester.pumpWidget(MaterialApp(
theme: theme,
home: buildFrame(tabs: tabs, value: 'B'),
),
);
await tester.pumpAndSettle();
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(
inkFeatures,
isNot(paints
..rect(
color: theme.colorScheme.primary.withOpacity(0.12),
))
);
// Press unselected tab.
TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('A')));
await tester.pumpAndSettle(); // Let the press highlight animation finish.
expect(
inkFeatures,
paints
..rect(
color: theme.colorScheme.primary.withOpacity(0.12),
),
);
// Release pressed gesture.
await gesture.up();
await tester.pumpAndSettle();
// Press selected tab.
gesture = await tester.startGesture(tester.getCenter(find.text('B')));
await tester.pumpAndSettle(); // Let the press highlight animation finish.
expect(
inkFeatures,
paints
..rect(
color: theme.colorScheme.primary.withOpacity(0.12),
),
);
});
group('Material 2', () {
// Tests that are only relevant for Material 2. Once ThemeData.useMaterial3
// is turned on by default, these tests can be removed.
testWidgets('TabBar default selected/unselected text style', (WidgetTester tester) async {
final ThemeData theme = ThemeData();
final List<String> tabs = <String>['A', 'B', 'C'];
const String selectedValue = 'A';
const String unSelectedValue = 'C';
await tester.pumpWidget(buildFrame(tabs: tabs, value: selectedValue));
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsOneWidget);
expect(find.text('C'), findsOneWidget);
// Test selected label text style.
expect(tester.renderObject<RenderParagraph>(find.text(selectedValue)).text.style!.fontFamily, 'Roboto');
expect(tester.renderObject<RenderParagraph>(find.text(selectedValue)).text.style!.fontSize, 14.0);
expect(tester.renderObject<RenderParagraph>(
find.text(selectedValue)).text.style!.color,
theme.primaryTextTheme.bodyLarge!.color,
);
// Test unselected label text style.
expect(tester.renderObject<RenderParagraph>(find.text(unSelectedValue)).text.style!.fontFamily, 'Roboto');
expect(tester.renderObject<RenderParagraph>(find.text(unSelectedValue)).text.style!.fontSize, 14.0);
expect(tester.renderObject<RenderParagraph>(
find.text(unSelectedValue)).text.style!.color,
theme.primaryTextTheme.bodyLarge!.color!.withAlpha(0xB2) // 70% alpha,
);
});
testWidgets('TabBar default unselectedLabelColor inherits labelColor with 70% opacity', (WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/pull/116273
final List<String> tabs = <String>['A', 'B', 'C'];
const String selectedValue = 'A';
const String unSelectedValue = 'C';
const Color labelColor = Color(0xff0000ff);
await tester.pumpWidget(
Theme(
data: ThemeData(tabBarTheme: const TabBarTheme(labelColor: labelColor)),
child: buildFrame(tabs: tabs, value: selectedValue),
),
);
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsOneWidget);
expect(find.text('C'), findsOneWidget);
// Test selected label color.
expect(tester.renderObject<RenderParagraph>(
find.text(selectedValue)).text.style!.color,
labelColor,
);
// Test unselected label color.
expect(tester.renderObject<RenderParagraph>(
find.text(unSelectedValue)).text.style!.color,
labelColor.withAlpha(0xB2) // 70% alpha,
);
});
});
}
class KeepAliveInk extends StatefulWidget {