Introduce TabBar.tabAlignment
(#125036)
fixes https://github.com/flutter/flutter/issues/124195 This introduces `TabBar.tabAlignment` while keeping the default alignment for both M2 and M3.
This commit is contained in:
parent
e2ddf5630d
commit
a732a74888
@ -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")};
|
||||
@ -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")};
|
||||
@ -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;
|
||||
}
|
||||
''';
|
||||
|
||||
|
@ -40,6 +40,7 @@ class TabBarTheme with Diagnosticable {
|
||||
this.overlayColor,
|
||||
this.splashFactory,
|
||||
this.mouseCursor,
|
||||
this.tabAlignment,
|
||||
});
|
||||
|
||||
/// Overrides the default value for [TabBar.indicator].
|
||||
@ -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({
|
||||
@ -105,6 +109,7 @@ class TabBarTheme with Diagnosticable {
|
||||
MaterialStateProperty<Color?>? overlayColor,
|
||||
InteractiveInkFeatureFactory? splashFactory,
|
||||
MaterialStateProperty<MouseCursor?>? mouseCursor,
|
||||
TabAlignment? tabAlignment,
|
||||
}) {
|
||||
return TabBarTheme(
|
||||
indicator: indicator ?? this.indicator,
|
||||
@ -119,6 +124,7 @@ class TabBarTheme with Diagnosticable {
|
||||
overlayColor: overlayColor ?? this.overlayColor,
|
||||
splashFactory: splashFactory ?? this.splashFactory,
|
||||
mouseCursor: mouseCursor ?? this.mouseCursor,
|
||||
tabAlignment: tabAlignment ?? this.tabAlignment,
|
||||
);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@ -166,6 +173,7 @@ class TabBarTheme with Diagnosticable {
|
||||
overlayColor,
|
||||
splashFactory,
|
||||
mouseCursor,
|
||||
tabAlignment,
|
||||
);
|
||||
|
||||
@override
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
@ -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,
|
||||
@ -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));
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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.
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
@ -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;
|
||||
@ -1491,7 +1568,7 @@ class _TabBarState extends State<TabBar> {
|
||||
),
|
||||
),
|
||||
);
|
||||
if (!widget.isScrollable) {
|
||||
if (!widget.isScrollable && effectiveTabAlignment == TabAlignment.fill) {
|
||||
wrappedTabs[index] = Expanded(child: wrappedTabs[index]);
|
||||
}
|
||||
}
|
||||
@ -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.
|
||||
@ -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,
|
||||
),
|
||||
@ -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;
|
||||
@ -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
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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
|
||||
|
@ -96,6 +96,7 @@ void main() {
|
||||
expect(const TabBarTheme().overlayColor, null);
|
||||
expect(const TabBarTheme().splashFactory, null);
|
||||
expect(const TabBarTheme().mouseCursor, null);
|
||||
expect(const TabBarTheme().tabAlignment, null);
|
||||
});
|
||||
|
||||
test('TabBarTheme lerp special cases', () {
|
||||
@ -138,7 +139,7 @@ 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));
|
||||
|
||||
// Verify divider color and indicator color.
|
||||
// Test default indicator color and divider color.
|
||||
final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
|
||||
expect(
|
||||
tabBarBox,
|
||||
@ -189,7 +190,7 @@ 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));
|
||||
|
||||
// Verify divider color and indicator color.
|
||||
// Test default indicator color and divider color.
|
||||
final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
|
||||
expect(
|
||||
tabBarBox,
|
||||
@ -464,14 +465,57 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('TabAlignment.fill from TabBarTheme only supports non-scrollable tab bar', (WidgetTester tester) async {
|
||||
const TabBarTheme tabBarTheme = TabBarTheme(tabAlignment: TabAlignment.fill);
|
||||
|
||||
// Test TabAlignment.fill from TabBarTheme with non-scrollable tab bar.
|
||||
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
|
||||
// Test TabAlignment.fill from TabBarTheme with scrollable tab bar.
|
||||
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme, isScrollable: true));
|
||||
|
||||
expect(tester.takeException(), isAssertionError);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'TabAlignment.start & TabAlignment.startOffset from TabBarTheme only supports scrollable tab bar',
|
||||
(WidgetTester tester) async {
|
||||
TabBarTheme tabBarTheme = const TabBarTheme(tabAlignment: TabAlignment.start);
|
||||
|
||||
// Test TabAlignment.start from TabBarTheme with scrollable tab bar.
|
||||
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme, isScrollable: true));
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
|
||||
// Test TabAlignment.start from TabBarTheme with non-scrollable tab bar.
|
||||
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
|
||||
|
||||
expect(tester.takeException(), isAssertionError);
|
||||
|
||||
tabBarTheme = const TabBarTheme(tabAlignment: TabAlignment.startOffset);
|
||||
|
||||
// Test TabAlignment.startOffset from TabBarTheme with scrollable tab bar.
|
||||
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme, isScrollable: true));
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
|
||||
// Test TabAlignment.startOffset from TabBarTheme with non-scrollable tab bar.
|
||||
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
|
||||
|
||||
expect(tester.takeException(), isAssertionError);
|
||||
});
|
||||
|
||||
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
|
||||
testWidgets('Tab bar defaults (primary)', (WidgetTester tester) async {
|
||||
// Test default label color and label styles.
|
||||
await tester.pumpWidget(buildTabBar());
|
||||
|
||||
final ThemeData theme = ThemeData(useMaterial3: false);
|
||||
final RenderParagraph selectedLabel = _getText(tester, _tab1Text);
|
||||
expect(selectedLabel.text.style!.fontFamily, equals('Roboto'));
|
||||
expect(selectedLabel.text.style!.fontSize, equals(14.0));
|
||||
@ -481,7 +525,7 @@ void main() {
|
||||
expect(unselectedLabel.text.style!.fontSize, equals(14.0));
|
||||
expect(unselectedLabel.text.style!.color, equals(Colors.white.withAlpha(0xB2)));
|
||||
|
||||
// tests for the default value of labelPadding when tabBarTheme and tabBar do not provide one
|
||||
// Test default labelPadding.
|
||||
await tester.pumpWidget(buildTabBar(tabs: _sizedTabs, isScrollable: true));
|
||||
|
||||
const double indicatorWeight = 2.0;
|
||||
@ -489,21 +533,62 @@ void main() {
|
||||
final Rect tabOneRect = tester.getRect(find.byKey(_sizedTabs[0].key!));
|
||||
final Rect tabTwoRect = tester.getRect(find.byKey(_sizedTabs[1].key!));
|
||||
|
||||
// verify coordinates of tabOne
|
||||
// Verify tabOne coordinates.
|
||||
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
|
||||
// Verify tabTwo coordinates.
|
||||
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
|
||||
// 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));
|
||||
|
||||
// Test default indicator color.
|
||||
final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
|
||||
expect(tabBarBox, paints..line(color: const Color(0xff2196f3)));
|
||||
expect(tabBarBox, paints..line(color: theme.indicatorColor));
|
||||
});
|
||||
|
||||
testWidgets('Tab bar defaults (secondary)', (WidgetTester tester) async {
|
||||
// Test default label color and label styles.
|
||||
await tester.pumpWidget(buildTabBar(secondaryTabBar: true));
|
||||
|
||||
final ThemeData theme = ThemeData(useMaterial3: false);
|
||||
final RenderParagraph selectedLabel = _getText(tester, _tab1Text);
|
||||
expect(selectedLabel.text.style!.fontFamily, equals('Roboto'));
|
||||
expect(selectedLabel.text.style!.fontSize, equals(14.0));
|
||||
expect(selectedLabel.text.style!.color, equals(Colors.white));
|
||||
final RenderParagraph unselectedLabel = _getText(tester, _tab2Text);
|
||||
expect(unselectedLabel.text.style!.fontFamily, equals('Roboto'));
|
||||
expect(unselectedLabel.text.style!.fontSize, equals(14.0));
|
||||
expect(unselectedLabel.text.style!.color, equals(Colors.white.withAlpha(0xB2)));
|
||||
|
||||
// Test default labelPadding.
|
||||
await tester.pumpWidget(buildTabBar(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 tabOne coordinates.
|
||||
expect(tabOneRect.left, equals(kTabLabelPadding.left));
|
||||
expect(tabOneRect.top, equals(kTabLabelPadding.top));
|
||||
expect(tabOneRect.bottom, equals(tabBar.bottom - kTabLabelPadding.bottom - indicatorWeight));
|
||||
|
||||
// Verify tabTwo coordinates.
|
||||
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));
|
||||
|
||||
// Test default indicator color.
|
||||
final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
|
||||
expect(tabBarBox, paints..line(color: theme.indicatorColor));
|
||||
});
|
||||
|
||||
testWidgets('Tab bar default tab indicator size', (WidgetTester tester) async {
|
||||
|
@ -114,6 +114,7 @@ Widget buildFrame({
|
||||
Duration? animationDuration,
|
||||
EdgeInsetsGeometry? padding,
|
||||
TextDirection textDirection = TextDirection.ltr,
|
||||
TabAlignment? tabAlignment,
|
||||
}) {
|
||||
if (secondaryTabBar) {
|
||||
return boilerplate(
|
||||
@ -128,6 +129,7 @@ Widget buildFrame({
|
||||
isScrollable: isScrollable,
|
||||
indicatorColor: indicatorColor,
|
||||
padding: padding,
|
||||
tabAlignment: tabAlignment,
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -145,6 +147,7 @@ Widget buildFrame({
|
||||
isScrollable: isScrollable,
|
||||
indicatorColor: indicatorColor,
|
||||
padding: padding,
|
||||
tabAlignment: tabAlignment,
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -5717,12 +5720,106 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Default TabAlignment', (WidgetTester tester) async {
|
||||
final ThemeData theme = ThemeData(useMaterial3: true);
|
||||
final List<String> tabs = <String>['A', 'B'];
|
||||
|
||||
// Test default TabAlignment when isScrollable is false.
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: theme,
|
||||
home: buildFrame(tabs: tabs, value: 'B'),
|
||||
));
|
||||
|
||||
final Rect tabBar = tester.getRect(find.byType(TabBar));
|
||||
Rect tabOneRect = tester.getRect(find.byType(Tab).first);
|
||||
Rect tabTwoRect = tester.getRect(find.byType(Tab).last);
|
||||
|
||||
// Tabs should fill the width of the TabBar.
|
||||
double tabOneLeft = ((tabBar.width / 2) - tabOneRect.width) / 2;
|
||||
expect(tabOneRect.left, equals(tabOneLeft));
|
||||
double tabTwoRight = tabBar.width - ((tabBar.width / 2) - tabTwoRect.width) / 2;
|
||||
expect(tabTwoRect.right, equals(tabTwoRight));
|
||||
|
||||
// Test default TabAlignment when isScrollable is true.
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: theme,
|
||||
home: buildFrame(tabs: tabs, value: 'B', isScrollable: true),
|
||||
));
|
||||
|
||||
tabOneRect = tester.getRect(find.byType(Tab).first);
|
||||
tabTwoRect = tester.getRect(find.byType(Tab).last);
|
||||
|
||||
// Tabs should be aligned to the start of the TabBar.
|
||||
tabOneLeft = kTabLabelPadding.left;
|
||||
expect(tabOneRect.left, equals(tabOneLeft));
|
||||
tabTwoRight = kTabLabelPadding.horizontal + tabOneRect.width + kTabLabelPadding.left + tabTwoRect.width;
|
||||
expect(tabTwoRect.right, equals(tabTwoRight));
|
||||
});
|
||||
|
||||
testWidgets('TabAlignment.fill only supports non-scrollable tab bar', (WidgetTester tester) async {
|
||||
final ThemeData theme = ThemeData(useMaterial3: true);
|
||||
final List<String> tabs = <String>['A', 'B'];
|
||||
|
||||
// Test TabAlignment.fill with non-scrollable tab bar.
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: theme,
|
||||
home: buildFrame(tabs: tabs, value: 'B', tabAlignment: TabAlignment.fill),
|
||||
));
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
|
||||
// Test TabAlignment.fill with scrollable tab bar.
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: theme,
|
||||
home: buildFrame(tabs: tabs, value: 'B', tabAlignment: TabAlignment.fill, isScrollable: true),
|
||||
));
|
||||
|
||||
expect(tester.takeException(), isAssertionError);
|
||||
});
|
||||
|
||||
testWidgets('TabAlignment.start & TabAlignment.startOffset only supports scrollable tab bar', (WidgetTester tester) async {
|
||||
final ThemeData theme = ThemeData(useMaterial3: true);
|
||||
final List<String> tabs = <String>['A', 'B'];
|
||||
|
||||
// Test TabAlignment.start with scrollable tab bar.
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: theme,
|
||||
home: buildFrame(tabs: tabs, value: 'B', tabAlignment: TabAlignment.start, isScrollable: true),
|
||||
));
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
|
||||
// Test TabAlignment.start with non-scrollable tab bar.
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: theme,
|
||||
home: buildFrame(tabs: tabs, value: 'B', tabAlignment: TabAlignment.start),
|
||||
));
|
||||
|
||||
expect(tester.takeException(), isAssertionError);
|
||||
|
||||
// Test TabAlignment.startOffset with scrollable tab bar.
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: theme,
|
||||
home: buildFrame(tabs: tabs, value: 'B', tabAlignment: TabAlignment.startOffset, isScrollable: true),
|
||||
));
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
|
||||
// Test TabAlignment.startOffset with non-scrollable tab bar.
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: theme,
|
||||
home: buildFrame(tabs: tabs, value: 'B', tabAlignment: TabAlignment.startOffset),
|
||||
));
|
||||
|
||||
expect(tester.takeException(), isAssertionError);
|
||||
});
|
||||
|
||||
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 ThemeData theme = ThemeData(useMaterial3: false);
|
||||
final List<String> tabs = <String>['A', 'B', 'C'];
|
||||
|
||||
const String selectedValue = 'A';
|
||||
@ -5758,7 +5855,10 @@ void main() {
|
||||
const Color labelColor = Color(0xff0000ff);
|
||||
await tester.pumpWidget(
|
||||
Theme(
|
||||
data: ThemeData(tabBarTheme: const TabBarTheme(labelColor: labelColor)),
|
||||
data: ThemeData(
|
||||
tabBarTheme: const TabBarTheme(labelColor: labelColor),
|
||||
useMaterial3: false,
|
||||
),
|
||||
child: buildFrame(tabs: tabs, value: selectedValue),
|
||||
),
|
||||
);
|
||||
@ -5808,6 +5908,42 @@ void main() {
|
||||
// ignore: avoid_dynamic_calls
|
||||
expect((paint.painter as dynamic).dividerColor, dividerColor);
|
||||
});
|
||||
|
||||
testWidgets('Default TabAlignment', (WidgetTester tester) async {
|
||||
final ThemeData theme = ThemeData(useMaterial3: false);
|
||||
final List<String> tabs = <String>['A', 'B'];
|
||||
|
||||
// Test default TabAlignment when isScrollable is false.
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: theme,
|
||||
home: buildFrame(tabs: tabs, value: 'B'),
|
||||
));
|
||||
|
||||
final Rect tabBar = tester.getRect(find.byType(TabBar));
|
||||
Rect tabOneRect = tester.getRect(find.byType(Tab).first);
|
||||
Rect tabTwoRect = tester.getRect(find.byType(Tab).last);
|
||||
|
||||
// Tabs should fill the width of the TabBar.
|
||||
double tabOneLeft = ((tabBar.width / 2) - tabOneRect.width) / 2;
|
||||
expect(tabOneRect.left, equals(tabOneLeft));
|
||||
double tabTwoRight = tabBar.width - ((tabBar.width / 2) - tabTwoRect.width) / 2;
|
||||
expect(tabTwoRect.right, equals(tabTwoRight));
|
||||
|
||||
// Test default TabAlignment when isScrollable is true.
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: theme,
|
||||
home: buildFrame(tabs: tabs, value: 'B', isScrollable: true),
|
||||
));
|
||||
|
||||
tabOneRect = tester.getRect(find.byType(Tab).first);
|
||||
tabTwoRect = tester.getRect(find.byType(Tab).last);
|
||||
|
||||
// Tabs should be aligned to the start of the TabBar.
|
||||
tabOneLeft = kTabLabelPadding.left;
|
||||
expect(tabOneRect.left, equals(tabOneLeft));
|
||||
tabTwoRight = kTabLabelPadding.horizontal + tabOneRect.width + kTabLabelPadding.left + tabTwoRect.width;
|
||||
expect(tabTwoRect.right, equals(tabTwoRight));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user