Skip to content

Add Material 3 support for TabBar #116110

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions dev/tools/gen_defaults/bin/gen_defaults.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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();
}
73 changes: 73 additions & 0 deletions dev/tools/gen_defaults/lib/tabs_template.dart
Original file line number Diff line number Diff line change
@@ -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;
}
''';
}
56 changes: 19 additions & 37 deletions packages/flutter/lib/src/material/tab_bar_theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;

Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
);
Expand All @@ -135,7 +149,9 @@ class TabBarTheme with Diagnosticable {
@override
int get hashCode => Object.hash(
indicator,
indicatorColor,
indicatorSize,
dividerColor,
labelColor,
labelPadding,
labelStyle,
Expand All @@ -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
Expand All @@ -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;
}
}
43 changes: 38 additions & 5 deletions packages/flutter/lib/src/material/tab_indicator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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) {
Expand All @@ -77,24 +84,50 @@ 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) {
assert(configuration != null);
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);
}
}
}
Loading