Skip to content

Commit

Permalink
Introduce TabBar.tabAlignment (flutter#125036)
Browse files Browse the repository at this point in the history
fixes flutter#124195

This introduces `TabBar.tabAlignment` while keeping the default alignment for both M2 and M3.
  • Loading branch information
TahaTesser authored May 1, 2023
1 parent e2ddf56 commit a732a74
Show file tree
Hide file tree
Showing 5 changed files with 355 additions and 24 deletions.
12 changes: 10 additions & 2 deletions dev/tools/gen_defaults/lib/tabs_template.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ class TabsTemplate extends TokenTemplate {
@override
String generate() => '''
class _${blockName}PrimaryDefaultsM3 extends TabBarTheme {
_${blockName}PrimaryDefaultsM3(this.context)
_${blockName}PrimaryDefaultsM3(this.context, this.isScrollable)
: super(indicatorSize: TabBarIndicatorSize.label);
final BuildContext context;
late final ColorScheme _colors = Theme.of(context).colorScheme;
late final TextTheme _textTheme = Theme.of(context).textTheme;
final bool isScrollable;
@override
Color? get dividerColor => ${componentColor("md.comp.primary-navigation-tab.divider")};
Expand Down Expand Up @@ -68,15 +69,19 @@ class _${blockName}PrimaryDefaultsM3 extends TabBarTheme {
@override
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
@override
TabAlignment? get tabAlignment => isScrollable ? TabAlignment.start : TabAlignment.fill;
}
class _${blockName}SecondaryDefaultsM3 extends TabBarTheme {
_${blockName}SecondaryDefaultsM3(this.context)
_${blockName}SecondaryDefaultsM3(this.context, this.isScrollable)
: super(indicatorSize: TabBarIndicatorSize.tab);
final BuildContext context;
late final ColorScheme _colors = Theme.of(context).colorScheme;
late final TextTheme _textTheme = Theme.of(context).textTheme;
final bool isScrollable;
@override
Color? get dividerColor => ${componentColor("md.comp.secondary-navigation-tab.divider")};
Expand Down Expand Up @@ -126,6 +131,9 @@ class _${blockName}SecondaryDefaultsM3 extends TabBarTheme {
@override
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
@override
TabAlignment? get tabAlignment => isScrollable ? TabAlignment.start : TabAlignment.fill;
}
''';

Expand Down
11 changes: 10 additions & 1 deletion packages/flutter/lib/src/material/tab_bar_theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class TabBarTheme with Diagnosticable {
this.overlayColor,
this.splashFactory,
this.mouseCursor,
this.tabAlignment,
});

/// Overrides the default value for [TabBar.indicator].
Expand Down Expand Up @@ -90,6 +91,9 @@ class TabBarTheme with Diagnosticable {
/// If specified, overrides the default value of [TabBar.mouseCursor].
final MaterialStateProperty<MouseCursor?>? mouseCursor;

/// Overrides the default value for [TabBar.tabAlignment].
final TabAlignment? tabAlignment;

/// Creates a copy of this object but with the given fields replaced with the
/// new values.
TabBarTheme copyWith({
Expand All @@ -105,6 +109,7 @@ class TabBarTheme with Diagnosticable {
MaterialStateProperty<Color?>? overlayColor,
InteractiveInkFeatureFactory? splashFactory,
MaterialStateProperty<MouseCursor?>? mouseCursor,
TabAlignment? tabAlignment,
}) {
return TabBarTheme(
indicator: indicator ?? this.indicator,
Expand All @@ -119,6 +124,7 @@ class TabBarTheme with Diagnosticable {
overlayColor: overlayColor ?? this.overlayColor,
splashFactory: splashFactory ?? this.splashFactory,
mouseCursor: mouseCursor ?? this.mouseCursor,
tabAlignment: tabAlignment ?? this.tabAlignment,
);
}

Expand Down Expand Up @@ -149,6 +155,7 @@ class TabBarTheme with Diagnosticable {
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,
tabAlignment: t < 0.5 ? a.tabAlignment : b.tabAlignment,
);
}

Expand All @@ -166,6 +173,7 @@ class TabBarTheme with Diagnosticable {
overlayColor,
splashFactory,
mouseCursor,
tabAlignment,
);

@override
Expand All @@ -188,6 +196,7 @@ class TabBarTheme with Diagnosticable {
&& other.unselectedLabelStyle == unselectedLabelStyle
&& other.overlayColor == overlayColor
&& other.splashFactory == splashFactory
&& other.mouseCursor == mouseCursor;
&& other.mouseCursor == mouseCursor
&& other.tabAlignment == tabAlignment;
}
}
113 changes: 103 additions & 10 deletions packages/flutter/lib/src/material/tabs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,41 @@ enum TabBarIndicatorSize {
label,
}

/// Defines how tabs are aligned horizontally in a [TabBar].
///
/// See also:
///
/// * [TabBar], which displays a row of tabs.
/// * [TabBarView], which displays a widget for the currently selected tab.
/// * [TabBar.tabAlignment], which defines the horizontal alignment of the
/// tabs within the [TabBar].
enum TabAlignment {
// TODO(tahatesser): Add a link to the Material Design spec for
// horizontal offset when it is available.
// It's currently sourced from androidx/compose/material3/TabRow.kt.
/// If [TabBar.isScrollable] is true, tabs are aligned to the
/// start of the [TabBar]. Otherwise throws an exception.
///
/// It is not recommended to set [TabAlignment.start] when
/// [ThemeData.useMaterial3] is false.
start,

/// If [TabBar.isScrollable] is true, tabs are aligned to the
/// start of the [TabBar] with an offset of 52.0 pixels.
/// Otherwise throws an exception.
///
/// It is not recommended to set [TabAlignment.startOffset] when
/// [ThemeData.useMaterial3] is false.
startOffset,

/// If [TabBar.isScrollable] is false, tabs are stretched to fill the
/// [TabBar]. Otherwise throws an exception.
fill,

/// Tabs are aligned to the center of the [TabBar].
center,
}

/// A Material Design [TabBar] tab.
///
/// If both [icon] and [text] are provided, the text is displayed below
Expand Down Expand Up @@ -306,9 +341,9 @@ class _TabLabelBar extends Flex {
const _TabLabelBar({
super.children,
required this.onPerformLayout,
required super.mainAxisSize,
}) : super(
direction: Axis.horizontal,
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
verticalDirection: VerticalDirection.down,
Expand Down Expand Up @@ -695,6 +730,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
this.physics,
this.splashFactory,
this.splashBorderRadius,
this.tabAlignment,
}) : _isPrimary = true,
assert(indicator != null || (indicatorWeight > 0.0));

Expand Down Expand Up @@ -744,6 +780,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
this.physics,
this.splashFactory,
this.splashBorderRadius,
this.tabAlignment,
}) : _isPrimary = false,
assert(indicator != null || (indicatorWeight > 0.0));

Expand Down Expand Up @@ -1027,6 +1064,25 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
/// If this property is null, it is interpreted as [BorderRadius.zero].
final BorderRadius? splashBorderRadius;

/// Specifies the horizontal alignment of the tabs within a [TabBar].
///
/// If [TabBar.isScrollable] is false, only [TabAlignment.fill] and
/// [TabAlignment.center] are supported. Otherwise an exception is thrown.
///
/// If [TabBar.isScrollable] is true, only [TabAlignment.start], [TabAlignment.startOffset],
/// and [TabAlignment.center] are supported. Otherwise an exception is thrown.
///
/// If this is null, then the value of [TabBarTheme.tabAlignment] is used.
///
/// If [TabBarTheme.tabAlignment] is null and [ThemeData.useMaterial3] is true,
/// then [TabAlignment.startOffset] is used if [isScrollable] is true,
/// otherwise [TabAlignment.fill] is used.
///
/// If [TabBarTheme.tabAlignment] is null and [ThemeData.useMaterial3] is false,
/// then [TabAlignment.center] is used if [isScrollable] is true,
/// otherwise [TabAlignment.fill] is used.
final TabAlignment? tabAlignment;

/// A size whose height depends on if the tabs have both icons and text.
///
/// [AppBar] uses this size to compute its own preferred size.
Expand Down Expand Up @@ -1089,10 +1145,10 @@ class _TabBarState extends State<TabBar> {
TabBarTheme get _defaults {
if (Theme.of(context).useMaterial3) {
return widget._isPrimary
? _TabsPrimaryDefaultsM3(context)
: _TabsSecondaryDefaultsM3(context);
? _TabsPrimaryDefaultsM3(context, widget.isScrollable)
: _TabsSecondaryDefaultsM3(context, widget.isScrollable);
} else {
return _TabsDefaultsM2(context);
return _TabsDefaultsM2(context, widget.isScrollable);
}
}

Expand Down Expand Up @@ -1378,10 +1434,32 @@ class _TabBarState extends State<TabBar> {
return true;
}

bool _debugTabAlignmentIsValid(TabAlignment tabAlignment) {
assert(() {
if (widget.isScrollable && tabAlignment == TabAlignment.fill) {
throw FlutterError(
'$tabAlignment is only valid for non-scrollable tab bars.',
);
}
if (!widget.isScrollable
&& (tabAlignment == TabAlignment.start
|| tabAlignment == TabAlignment.startOffset)) {
throw FlutterError(
'$tabAlignment is only valid for scrollable tab bars.',
);
}
return true;
}());
return true;
}

@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context));
assert(_debugScheduleCheckHasValidTabsCount());
final TabBarTheme tabBarTheme = TabBarTheme.of(context);
final TabAlignment effectiveTabAlignment = widget.tabAlignment ?? tabBarTheme.tabAlignment ?? _defaults.tabAlignment!;
assert(_debugTabAlignmentIsValid(effectiveTabAlignment));

final MaterialLocalizations localizations = MaterialLocalizations.of(context);
if (_controller!.length == 0) {
Expand All @@ -1390,7 +1468,6 @@ class _TabBarState extends State<TabBar> {
);
}

final TabBarTheme tabBarTheme = TabBarTheme.of(context);

final List<Widget> wrappedTabs = List<Widget>.generate(widget.tabs.length, (int index) {
const double verticalAdjustment = (_kTextAndIconTabHeight - _kTabHeight)/2.0;
Expand Down Expand Up @@ -1491,7 +1568,7 @@ class _TabBarState extends State<TabBar> {
),
),
);
if (!widget.isScrollable) {
if (!widget.isScrollable && effectiveTabAlignment == TabAlignment.fill) {
wrappedTabs[index] = Expanded(child: wrappedTabs[index]);
}
}
Expand All @@ -1509,12 +1586,16 @@ class _TabBarState extends State<TabBar> {
defaults: _defaults,
child: _TabLabelBar(
onPerformLayout: _saveTabOffsets,
mainAxisSize: effectiveTabAlignment == TabAlignment.fill ? MainAxisSize.max : MainAxisSize.min,
children: wrappedTabs,
),
),
);

if (widget.isScrollable) {
final EdgeInsetsGeometry? effectivePadding = effectiveTabAlignment == TabAlignment.startOffset
? const EdgeInsetsDirectional.only(start: 56.0).add(widget.padding ?? EdgeInsets.zero)
: widget.padding;
_scrollController ??= _TabBarScrollController(this);
tabBar = ScrollConfiguration(
// The scrolling tabs should not show an overscroll indicator.
Expand All @@ -1523,7 +1604,7 @@ class _TabBarState extends State<TabBar> {
dragStartBehavior: widget.dragStartBehavior,
scrollDirection: Axis.horizontal,
controller: _scrollController,
padding: widget.padding,
padding: effectivePadding,
physics: widget.physics,
child: tabBar,
),
Expand Down Expand Up @@ -2030,10 +2111,11 @@ class TabPageSelector extends StatelessWidget {

// Hand coded defaults based on Material Design 2.
class _TabsDefaultsM2 extends TabBarTheme {
const _TabsDefaultsM2(this.context)
const _TabsDefaultsM2(this.context, this.isScrollable)
: super(indicatorSize: TabBarIndicatorSize.tab);

final BuildContext context;
final bool isScrollable;

@override
Color? get indicatorColor => Theme.of(context).indicatorColor;
Expand All @@ -2049,6 +2131,9 @@ class _TabsDefaultsM2 extends TabBarTheme {

@override
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;

@override
TabAlignment? get tabAlignment => isScrollable ? TabAlignment.start : TabAlignment.fill;
}

// BEGIN GENERATED TOKEN PROPERTIES - Tabs
Expand All @@ -2061,12 +2146,13 @@ class _TabsDefaultsM2 extends TabBarTheme {
// Token database version: v0_162

class _TabsPrimaryDefaultsM3 extends TabBarTheme {
_TabsPrimaryDefaultsM3(this.context)
_TabsPrimaryDefaultsM3(this.context, this.isScrollable)
: super(indicatorSize: TabBarIndicatorSize.label);

final BuildContext context;
late final ColorScheme _colors = Theme.of(context).colorScheme;
late final TextTheme _textTheme = Theme.of(context).textTheme;
final bool isScrollable;

@override
Color? get dividerColor => _colors.surfaceVariant;
Expand Down Expand Up @@ -2116,15 +2202,19 @@ class _TabsPrimaryDefaultsM3 extends TabBarTheme {

@override
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;

@override
TabAlignment? get tabAlignment => isScrollable ? TabAlignment.start : TabAlignment.fill;
}

class _TabsSecondaryDefaultsM3 extends TabBarTheme {
_TabsSecondaryDefaultsM3(this.context)
_TabsSecondaryDefaultsM3(this.context, this.isScrollable)
: super(indicatorSize: TabBarIndicatorSize.tab);

final BuildContext context;
late final ColorScheme _colors = Theme.of(context).colorScheme;
late final TextTheme _textTheme = Theme.of(context).textTheme;
final bool isScrollable;

@override
Color? get dividerColor => _colors.surfaceVariant;
Expand Down Expand Up @@ -2174,6 +2264,9 @@ class _TabsSecondaryDefaultsM3 extends TabBarTheme {

@override
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;

@override
TabAlignment? get tabAlignment => isScrollable ? TabAlignment.start : TabAlignment.fill;
}

// END GENERATED TOKEN PROPERTIES - Tabs
Loading

0 comments on commit a732a74

Please sign in to comment.