Fix Scrollable `TabBar` for Material 3 (#131409)
fixes [Material 3 `TabBar` does not take full width when `isScrollable: true`](https://github.com/flutter/flutter/issues/117722)
### Description
1. Fixed the divider doesn't stretch to take all the available width in the scrollable tab bar in M3
2. Added `dividerHeight` property.
### Code sample
<details>
<summary>expand to view the code sample</summary>
```dart
import 'package:flutter/material.dart';
/// Flutter code sample for [TabBar].
void main() => runApp(const TabBarApp());
class TabBarApp extends StatelessWidget {
const TabBarApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: TabBarExample(),
);
}
}
class TabBarExample extends StatefulWidget {
const TabBarExample({super.key});
@override
State<TabBarExample> createState() => _TabBarExampleState();
}
class _TabBarExampleState extends State<TabBarExample> {
bool rtl = false;
bool customColors = false;
bool removeDivider = false;
Color dividerColor = Colors.amber;
Color indicatorColor = Colors.red;
@override
Widget build(BuildContext context) {
return DefaultTabController(
initialIndex: 1,
length: 3,
child: Directionality(
textDirection: rtl ? TextDirection.rtl : TextDirection.ltr,
child: Scaffold(
appBar: AppBar(
title: const Text('TabBar Sample'),
actions: <Widget>[
IconButton.filledTonal(
tooltip: 'Switch direction',
icon: const Icon(Icons.swap_horiz),
onPressed: () {
setState(() {
rtl = !rtl;
});
},
),
IconButton.filledTonal(
tooltip: 'Use custom colors',
icon: const Icon(Icons.color_lens),
onPressed: () {
setState(() {
customColors = !customColors;
});
},
),
IconButton.filledTonal(
tooltip: 'Show/hide divider',
icon: const Icon(Icons.remove_rounded),
onPressed: () {
setState(() {
removeDivider = !removeDivider;
});
},
),
],
),
body: Column(
children: <Widget>[
const Spacer(),
const Text('Scrollable - TabAlignment.start'),
TabBar(
isScrollable: true,
tabAlignment: TabAlignment.start,
dividerColor: customColors ? dividerColor : null,
indicatorColor: customColors ? indicatorColor : null,
dividerHeight: removeDivider ? 0 : null,
tabs: const <Widget>[
Tab(
icon: Icon(Icons.cloud_outlined),
),
Tab(
icon: Icon(Icons.beach_access_sharp),
),
Tab(
icon: Icon(Icons.brightness_5_sharp),
),
],
),
const Text('Scrollable - TabAlignment.startOffset'),
TabBar(
isScrollable: true,
tabAlignment: TabAlignment.startOffset,
dividerColor: customColors ? dividerColor : null,
indicatorColor: customColors ? indicatorColor : null,
dividerHeight: removeDivider ? 0 : null,
tabs: const <Widget>[
Tab(
icon: Icon(Icons.cloud_outlined),
),
Tab(
icon: Icon(Icons.beach_access_sharp),
),
Tab(
icon: Icon(Icons.brightness_5_sharp),
),
],
),
const Text('Scrollable - TabAlignment.center'),
TabBar(
isScrollable: true,
tabAlignment: TabAlignment.center,
dividerColor: customColors ? dividerColor : null,
indicatorColor: customColors ? indicatorColor : null,
dividerHeight: removeDivider ? 0 : null,
tabs: const <Widget>[
Tab(
icon: Icon(Icons.cloud_outlined),
),
Tab(
icon: Icon(Icons.beach_access_sharp),
),
Tab(
icon: Icon(Icons.brightness_5_sharp),
),
],
),
const Spacer(),
const Text('Non-scrollable - TabAlignment.fill'),
TabBar(
tabAlignment: TabAlignment.fill,
dividerColor: customColors ? dividerColor : null,
indicatorColor: customColors ? indicatorColor : null,
dividerHeight: removeDivider ? 0 : null,
tabs: const <Widget>[
Tab(
icon: Icon(Icons.cloud_outlined),
),
Tab(
icon: Icon(Icons.beach_access_sharp),
),
Tab(
icon: Icon(Icons.brightness_5_sharp),
),
],
),
const Text('Non-scrollable - TabAlignment.center'),
TabBar(
tabAlignment: TabAlignment.center,
dividerColor: customColors ? dividerColor : null,
indicatorColor: customColors ? indicatorColor : null,
dividerHeight: removeDivider ? 0 : null,
tabs: const <Widget>[
Tab(
icon: Icon(Icons.cloud_outlined),
),
Tab(
icon: Icon(Icons.beach_access_sharp),
),
Tab(
icon: Icon(Icons.brightness_5_sharp),
),
],
),
const Spacer(),
const Text('Secondary - TabAlignment.fill'),
TabBar.secondary(
tabAlignment: TabAlignment.fill,
dividerColor: customColors ? dividerColor : null,
indicatorColor: customColors ? indicatorColor : null,
dividerHeight: removeDivider ? 0 : null,
tabs: const <Widget>[
Tab(
icon: Icon(Icons.cloud_outlined),
),
Tab(
icon: Icon(Icons.beach_access_sharp),
),
Tab(
icon: Icon(Icons.brightness_5_sharp),
),
],
),
const Text('Secondary - TabAlignment.center'),
TabBar.secondary(
tabAlignment: TabAlignment.center,
dividerColor: customColors ? dividerColor : null,
indicatorColor: customColors ? indicatorColor : null,
dividerHeight: removeDivider ? 0 : null,
tabs: const <Widget>[
Tab(
icon: Icon(Icons.cloud_outlined),
),
Tab(
icon: Icon(Icons.beach_access_sharp),
),
Tab(
icon: Icon(Icons.brightness_5_sharp),
),
],
),
const Spacer(),
],
),
),
),
);
}
}
```
</details>
### Before
![Screenshot 2023-07-27 at 14 12 36](https://github.com/flutter/flutter/assets/48603081/1c08a9d2-ac15-4d33-8fa1-c765b4b10f92)
### After
![Screenshot 2023-07-27 at 14 13 12](https://github.com/flutter/flutter/assets/48603081/7e662dfe-9f32-46c9-a128-3024a4782882)
This also contains regression test for https://github.com/flutter/flutter/pull/125974#discussion_r1239089151
```dart
// This is a regression test for https://github.com/flutter/flutter/pull/125974#discussion_r1239089151.
testWidgets('Divider can be constrained', (WidgetTester tester) async {
```
![Screenshot 2023-07-27 at 14 16 37](https://github.com/flutter/flutter/assets/48603081/ac2ef49b-2410-46d0-8ae2-d9b77236abba)
diff --git a/dev/tools/gen_defaults/generated/used_tokens.csv b/dev/tools/gen_defaults/generated/used_tokens.csv
index c7eb2a3..1275486 100644
--- a/dev/tools/gen_defaults/generated/used_tokens.csv
+++ b/dev/tools/gen_defaults/generated/used_tokens.csv
@@ -530,6 +530,7 @@
md.comp.primary-navigation-tab.active.pressed.state-layer.color,
md.comp.primary-navigation-tab.active.pressed.state-layer.opacity,
md.comp.primary-navigation-tab.divider.color,
+md.comp.primary-navigation-tab.divider.height,
md.comp.primary-navigation-tab.inactive.focus.state-layer.color,
md.comp.primary-navigation-tab.inactive.focus.state-layer.opacity,
md.comp.primary-navigation-tab.inactive.hover.state-layer.color,
@@ -589,6 +590,7 @@
md.comp.search-view.header.supporting-text.text-style,
md.comp.secondary-navigation-tab.active.label-text.color,
md.comp.secondary-navigation-tab.divider.color,
+md.comp.secondary-navigation-tab.divider.height,
md.comp.secondary-navigation-tab.focus.state-layer.color,
md.comp.secondary-navigation-tab.focus.state-layer.opacity,
md.comp.secondary-navigation-tab.hover.state-layer.color,
diff --git a/dev/tools/gen_defaults/lib/tabs_template.dart b/dev/tools/gen_defaults/lib/tabs_template.dart
index f6388a8..f520f61 100644
--- a/dev/tools/gen_defaults/lib/tabs_template.dart
+++ b/dev/tools/gen_defaults/lib/tabs_template.dart
@@ -25,6 +25,9 @@
Color? get dividerColor => ${componentColor("md.comp.primary-navigation-tab.divider")};
@override
+ double? get dividerHeight => ${getToken('md.comp.primary-navigation-tab.divider.height')};
+
+ @override
Color? get indicatorColor => ${componentColor("md.comp.primary-navigation-tab.active-indicator")};
@override
@@ -71,7 +74,7 @@
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
@override
- TabAlignment? get tabAlignment => isScrollable ? TabAlignment.start : TabAlignment.fill;
+ TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill;
static double indicatorWeight = ${getToken('md.comp.primary-navigation-tab.active-indicator.height')};
}
@@ -89,6 +92,9 @@
Color? get dividerColor => ${componentColor("md.comp.secondary-navigation-tab.divider")};
@override
+ double? get dividerHeight => ${getToken('md.comp.secondary-navigation-tab.divider.height')};
+
+ @override
Color? get indicatorColor => ${componentColor("md.comp.primary-navigation-tab.active-indicator")};
@override
@@ -135,7 +141,7 @@
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
@override
- TabAlignment? get tabAlignment => isScrollable ? TabAlignment.start : TabAlignment.fill;
+ TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill;
}
''';
diff --git a/packages/flutter/lib/src/material/tab_bar_theme.dart b/packages/flutter/lib/src/material/tab_bar_theme.dart
index 87c43d7..e65e8b3 100644
--- a/packages/flutter/lib/src/material/tab_bar_theme.dart
+++ b/packages/flutter/lib/src/material/tab_bar_theme.dart
@@ -32,6 +32,7 @@
this.indicatorColor,
this.indicatorSize,
this.dividerColor,
+ this.dividerHeight,
this.labelColor,
this.labelPadding,
this.labelStyle,
@@ -55,6 +56,9 @@
/// Overrides the default value for [TabBar.dividerColor].
final Color? dividerColor;
+ /// Overrides the default value for [TabBar.dividerHeight].
+ final double? dividerHeight;
+
/// Overrides the default value for [TabBar.labelColor].
///
/// If [labelColor] is a [MaterialStateColor], then the effective color will
@@ -101,6 +105,7 @@
Color? indicatorColor,
TabBarIndicatorSize? indicatorSize,
Color? dividerColor,
+ double? dividerHeight,
Color? labelColor,
EdgeInsetsGeometry? labelPadding,
TextStyle? labelStyle,
@@ -116,6 +121,7 @@
indicatorColor: indicatorColor ?? this.indicatorColor,
indicatorSize: indicatorSize ?? this.indicatorSize,
dividerColor: dividerColor ?? this.dividerColor,
+ dividerHeight: dividerHeight ?? this.dividerHeight,
labelColor: labelColor ?? this.labelColor,
labelPadding: labelPadding ?? this.labelPadding,
labelStyle: labelStyle ?? this.labelStyle,
@@ -147,6 +153,7 @@
indicatorColor: Color.lerp(a.indicatorColor, b.indicatorColor, t),
indicatorSize: t < 0.5 ? a.indicatorSize : b.indicatorSize,
dividerColor: Color.lerp(a.dividerColor, b.dividerColor, t),
+ dividerHeight: t < 0.5 ? a.dividerHeight : b.dividerHeight,
labelColor: Color.lerp(a.labelColor, b.labelColor, t),
labelPadding: EdgeInsetsGeometry.lerp(a.labelPadding, b.labelPadding, t),
labelStyle: TextStyle.lerp(a.labelStyle, b.labelStyle, t),
@@ -165,6 +172,7 @@
indicatorColor,
indicatorSize,
dividerColor,
+ dividerHeight,
labelColor,
labelPadding,
labelStyle,
@@ -189,6 +197,7 @@
&& other.indicatorColor == indicatorColor
&& other.indicatorSize == indicatorSize
&& other.dividerColor == dividerColor
+ && other.dividerHeight == dividerHeight
&& other.labelColor == labelColor
&& other.labelPadding == labelPadding
&& other.labelStyle == labelStyle
diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart
index 756e61c..2bb5f44 100644
--- a/packages/flutter/lib/src/material/tabs.dart
+++ b/packages/flutter/lib/src/material/tabs.dart
@@ -387,6 +387,39 @@
return (controllerValue - currentIndex).abs() / (currentIndex - previousIndex).abs();
}
+class _DividerPainter extends CustomPainter {
+ _DividerPainter({
+ required this.dividerColor,
+ required this.dividerHeight,
+ });
+
+ final Color dividerColor;
+ final double dividerHeight;
+
+ @override
+ void paint(Canvas canvas, Size size) {
+ if (dividerHeight <= 0.0) {
+ return;
+ }
+
+ final Paint paint = Paint()
+ ..color = dividerColor
+ ..strokeWidth = dividerHeight;
+
+ canvas.drawLine(
+ Offset(0, size.height - (paint.strokeWidth / 2)),
+ Offset(size.width, size.height - (paint.strokeWidth / 2)),
+ paint,
+ );
+ }
+
+ @override
+ bool shouldRepaint(_DividerPainter oldDelegate) {
+ return oldDelegate.dividerColor != dividerColor
+ || oldDelegate.dividerHeight != dividerHeight;
+ }
+}
+
class _IndicatorPainter extends CustomPainter {
_IndicatorPainter({
required this.controller,
@@ -397,6 +430,8 @@
required this.indicatorPadding,
required this.labelPaddings,
this.dividerColor,
+ this.dividerHeight,
+ required this.showDivider,
}) : super(repaint: controller.animation) {
if (old != null) {
saveTabOffsets(old._currentTabOffsets, old._currentTextDirection);
@@ -408,8 +443,10 @@
final TabBarIndicatorSize? indicatorSize;
final EdgeInsetsGeometry indicatorPadding;
final List<GlobalKey> tabKeys;
- final Color? dividerColor;
final List<EdgeInsetsGeometry> labelPaddings;
+ final Color? dividerColor;
+ final double? dividerHeight;
+ final bool showDivider;
// _currentTabOffsets and _currentTextDirection are set each time TabBar
// layout is completed. These values can be null when TabBar contains no
@@ -501,9 +538,11 @@
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);
+ if (showDivider && dividerHeight !> 0) {
+ final Paint dividerPaint = Paint()..color = dividerColor!..strokeWidth = dividerHeight!;
+ final Offset dividerP1 = Offset(0, size.height - (dividerPaint.strokeWidth / 2));
+ final Offset dividerP2 = Offset(size.width, size.height - (dividerPaint.strokeWidth / 2));
+ canvas.drawLine(dividerP1, dividerP2, dividerPaint);
}
_painter!.paint(canvas, _currentRect!.topLeft, configuration);
}
@@ -718,6 +757,7 @@
this.indicator,
this.indicatorSize,
this.dividerColor,
+ this.dividerHeight,
this.labelColor,
this.labelStyle,
this.labelPadding,
@@ -768,6 +808,7 @@
this.indicator,
this.indicatorSize,
this.dividerColor,
+ this.dividerHeight,
this.labelColor,
this.labelStyle,
this.labelPadding,
@@ -895,6 +936,13 @@
/// [ColorScheme.surfaceVariant] will be used, otherwise divider will not be drawn.
final Color? dividerColor;
+ /// The height of the divider.
+ ///
+ /// If null and [ThemeData.useMaterial3] is true, [TabBarTheme.dividerHeight] is used.
+ /// If that is also null and [ThemeData.useMaterial3] is true, 1.0 will be used.
+ /// Otherwise divider will not be drawn.
+ final double? dividerHeight;
+
/// The color of selected tab labels.
///
/// If null, then [TabBarTheme.labelColor] is used. If that is also null and
@@ -1154,8 +1202,8 @@
TabBarTheme get _defaults {
if (Theme.of(context).useMaterial3) {
return widget._isPrimary
- ? _TabsPrimaryDefaultsM3(context, widget.isScrollable)
- : _TabsSecondaryDefaultsM3(context, widget.isScrollable);
+ ? _TabsPrimaryDefaultsM3(context, widget.isScrollable)
+ : _TabsSecondaryDefaultsM3(context, widget.isScrollable);
} else {
return _TabsDefaultsM2(context, widget.isScrollable);
}
@@ -1269,8 +1317,10 @@
indicatorPadding: widget.indicatorPadding,
tabKeys: _tabKeys,
old: _indicatorPainter,
- dividerColor: theme.useMaterial3 ? widget.dividerColor ?? tabBarTheme.dividerColor ?? _defaults.dividerColor : null,
labelPaddings: _labelPaddings,
+ dividerColor: widget.dividerColor ?? tabBarTheme.dividerColor ?? _defaults.dividerColor,
+ dividerHeight: widget.dividerHeight ?? tabBarTheme.dividerHeight ?? _defaults.dividerHeight,
+ showDivider: theme.useMaterial3 && !widget.isScrollable,
);
}
@@ -1299,7 +1349,9 @@
widget.indicatorWeight != oldWidget.indicatorWeight ||
widget.indicatorSize != oldWidget.indicatorSize ||
widget.indicatorPadding != oldWidget.indicatorPadding ||
- widget.indicator != oldWidget.indicator) {
+ widget.indicator != oldWidget.indicator ||
+ widget.dividerColor != oldWidget.dividerColor ||
+ widget.dividerHeight != oldWidget.dividerHeight) {
_initIndicatorPainter();
}
@@ -1475,6 +1527,7 @@
Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context));
assert(_debugScheduleCheckHasValidTabsCount());
+ final ThemeData theme = Theme.of(context);
final TabBarTheme tabBarTheme = TabBarTheme.of(context);
final TabAlignment effectiveTabAlignment = widget.tabAlignment ?? tabBarTheme.tabAlignment ?? _defaults.tabAlignment!;
assert(_debugTabAlignmentIsValid(effectiveTabAlignment));
@@ -1486,7 +1539,6 @@
);
}
-
final List<Widget> wrappedTabs = List<Widget>.generate(widget.tabs.length, (int index) {
const double verticalAdjustment = (_kTextAndIconTabHeight - _kTabHeight)/2.0;
EdgeInsetsGeometry? adjustedPadding;
@@ -1627,6 +1679,24 @@
child: tabBar,
),
);
+ if (theme.useMaterial3) {
+ final AlignmentGeometry effectiveAlignment = switch (effectiveTabAlignment) {
+ TabAlignment.center => Alignment.center,
+ TabAlignment.start || TabAlignment.startOffset || TabAlignment.fill => AlignmentDirectional.centerStart,
+ };
+
+ tabBar = CustomPaint(
+ painter: _DividerPainter(
+ dividerColor: widget.dividerColor ?? tabBarTheme.dividerColor ?? _defaults.dividerColor!,
+ dividerHeight: widget.dividerHeight ?? tabBarTheme.dividerHeight ?? _defaults.dividerHeight!,
+ ),
+ child: Align(
+ heightFactor: 1.0,
+ alignment: effectiveAlignment,
+ child: tabBar,
+ ),
+ );
+ }
} else if (widget.padding != null) {
tabBar = Padding(
padding: widget.padding!,
@@ -2178,6 +2248,9 @@
Color? get dividerColor => _colors.surfaceVariant;
@override
+ double? get dividerHeight => 1.0;
+
+ @override
Color? get indicatorColor => _colors.primary;
@override
@@ -2224,7 +2297,7 @@
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
@override
- TabAlignment? get tabAlignment => isScrollable ? TabAlignment.start : TabAlignment.fill;
+ TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill;
static double indicatorWeight = 3.0;
}
@@ -2242,6 +2315,9 @@
Color? get dividerColor => _colors.surfaceVariant;
@override
+ double? get dividerHeight => 1.0;
+
+ @override
Color? get indicatorColor => _colors.primary;
@override
@@ -2288,7 +2364,7 @@
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
@override
- TabAlignment? get tabAlignment => isScrollable ? TabAlignment.start : TabAlignment.fill;
+ TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill;
}
// END GENERATED TOKEN PROPERTIES - Tabs
diff --git a/packages/flutter/test/material/tab_bar_theme_test.dart b/packages/flutter/test/material/tab_bar_theme_test.dart
index 3b686c1..0f69b10 100644
--- a/packages/flutter/test/material/tab_bar_theme_test.dart
+++ b/packages/flutter/test/material/tab_bar_theme_test.dart
@@ -90,6 +90,7 @@
expect(const TabBarTheme().indicatorColor, null);
expect(const TabBarTheme().indicatorSize, null);
expect(const TabBarTheme().dividerColor, null);
+ expect(const TabBarTheme().dividerHeight, null);
expect(const TabBarTheme().labelColor, null);
expect(const TabBarTheme().labelPadding, null);
expect(const TabBarTheme().labelStyle, null);
@@ -127,27 +128,32 @@
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!));
+ const double tabStartOffset = 52.0;
// Verify tabOne coordinates.
- expect(tabOneRect.left, equals(kTabLabelPadding.left));
+ expect(tabOneRect.left, equals(kTabLabelPadding.left + tabStartOffset));
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));
+ final double tabTwoRight = tabStartOffset + kTabLabelPadding.horizontal + tabOneRect.width
+ + kTabLabelPadding.left + tabTwoRect.width;
+ expect(tabTwoRect.right, tabTwoRight);
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 are 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 and divider color.
+ // Test default indicator & divider color.
final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
expect(
tabBarBox,
paints
- ..line(color: theme.colorScheme.surfaceVariant)
- // Indicator is a rrect in the primary tab bar.
+ ..line(
+ color: theme.colorScheme.surfaceVariant,
+ strokeWidth: 1.0,
+ )
..rrect(color: theme.colorScheme.primary),
);
});
@@ -178,29 +184,34 @@
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!));
+ const double tabStartOffset = 52.0;
// Verify tabOne coordinates.
- expect(tabOneRect.left, equals(kTabLabelPadding.left));
+ expect(tabOneRect.left, equals(kTabLabelPadding.left + tabStartOffset));
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));
+ final double tabTwoRight = tabStartOffset + kTabLabelPadding.horizontal + tabOneRect.width
+ + kTabLabelPadding.left + tabTwoRect.width;
+ expect(tabTwoRect.right, tabTwoRight);
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 are 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 and divider color.
+ // Test default indicator & divider color.
final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
expect(
tabBarBox,
paints
- ..line(color: theme.colorScheme.surfaceVariant)
- // Indicator is a line in the secondary tab bar.
+ ..line(
+ color: theme.colorScheme.surfaceVariant,
+ strokeWidth: 1.0,
+ )
..line(color: theme.colorScheme.primary),
- );
+ );
});
testWidgets('Tab bar theme overrides label color (selected)', (WidgetTester tester) async {
@@ -315,7 +326,7 @@
expect(unselectedLabel.text.style!.fontFamily, equals(unselectedLabelStyle.fontFamily));
});
- testWidgets('Tab bar label padding overrides theme label padding', (WidgetTester tester) async {
+ testWidgets('Material2 - Tab bar label padding overrides theme label padding', (WidgetTester tester) async {
const double verticalPadding = 10.0;
const double horizontalPadding = 10.0;
const EdgeInsetsGeometry labelPadding = EdgeInsets.symmetric(
@@ -336,7 +347,7 @@
await tester.pumpWidget(
MaterialApp(
- theme: ThemeData(tabBarTheme: tabBarTheme),
+ theme: ThemeData(tabBarTheme: tabBarTheme, useMaterial3: false),
home: Scaffold(body:
RepaintBoundary(
key: _painterKey,
@@ -369,6 +380,61 @@
expect(tabOneRect.right, equals(tabTwoRect.left - (2 * horizontalPadding)));
});
+ testWidgets('Material3 - Tab bar label padding overrides theme label padding', (WidgetTester tester) async {
+ const double tabStartOffset = 52.0;
+ const double verticalPadding = 10.0;
+ const double horizontalPadding = 10.0;
+ const EdgeInsetsGeometry labelPadding = EdgeInsets.symmetric(
+ vertical: verticalPadding,
+ horizontal: horizontalPadding,
+ );
+
+ const double verticalThemePadding = 20.0;
+ const double horizontalThemePadding = 20.0;
+ const EdgeInsetsGeometry themeLabelPadding = EdgeInsets.symmetric(
+ vertical: verticalThemePadding,
+ horizontal: horizontalThemePadding,
+ );
+
+ const double indicatorWeight = 2.0; // default value
+
+ const TabBarTheme tabBarTheme = TabBarTheme(labelPadding: themeLabelPadding);
+
+ await tester.pumpWidget(
+ MaterialApp(
+ theme: ThemeData(tabBarTheme: tabBarTheme, useMaterial3: true),
+ home: Scaffold(body:
+ RepaintBoundary(
+ key: _painterKey,
+ child: TabBar(
+ tabs: _sizedTabs,
+ isScrollable: true,
+ controller: TabController(length: _sizedTabs.length, vsync: const TestVSync()),
+ labelPadding: labelPadding,
+ ),
+ ),
+ ),
+ ),
+ );
+
+ 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(horizontalPadding + tabStartOffset));
+ expect(tabOneRect.top, equals(verticalPadding));
+ expect(tabOneRect.bottom, equals(tabBar.bottom - verticalPadding - indicatorWeight));
+
+ // verify coordinates of tabTwo
+ expect(tabTwoRect.right, equals(tabStartOffset + horizontalThemePadding + tabOneRect.width + tabTwoRect.width + (horizontalThemePadding / 2)));
+ expect(tabTwoRect.top, equals(verticalPadding));
+ expect(tabTwoRect.bottom, equals(tabBar.bottom - verticalPadding - indicatorWeight));
+
+ // verify tabOne and tabTwo are separated by 2x horizontalPadding
+ expect(tabOneRect.right, equals(tabTwoRect.left - (2 * horizontalPadding)));
+ });
+
testWidgets('Tab bar theme overrides label color (unselected)', (WidgetTester tester) async {
const Color unselectedLabelColor = Colors.black;
const TabBarTheme tabBarTheme = TabBarTheme(unselectedLabelColor: unselectedLabelColor);
@@ -381,7 +447,7 @@
expect(iconRenderObject.text.style!.color, equals(unselectedLabelColor));
});
- testWidgets('Tab bar default tab indicator size', (WidgetTester tester) async {
+ testWidgets('Tab bar default tab indicator size (primary)', (WidgetTester tester) async {
await tester.pumpWidget(buildTabBar(useMaterial3: true, isScrollable: true));
await expectLater(
@@ -390,12 +456,12 @@
);
});
- testWidgets('Tab bar default tab indicator size', (WidgetTester tester) async {
+ testWidgets('Tab bar default tab indicator size (secondary)', (WidgetTester tester) async {
await tester.pumpWidget(buildTabBar(useMaterial3: true, isScrollable: true));
await expectLater(
find.byKey(_painterKey),
- matchesGoldenFile('tab_bar.default.tab_indicator_size.png'),
+ matchesGoldenFile('tab_bar_secondary.default.tab_indicator_size.png'),
);
});
@@ -549,11 +615,12 @@
expect(
tabBarBox,
paints
- // Divider
+ // Divider.
..line(
color: theme.colorScheme.surfaceVariant,
+ strokeWidth: 1.0,
)
- // Tab indicator
+ // Tab indicator.
..line(
color: theme.colorScheme.primary,
strokeWidth: indicatorWeight,
@@ -601,9 +668,10 @@
expect(
tabBarBox,
paints
- // Divider
+ // Divider.
..line(
color: theme.colorScheme.surfaceVariant,
+ strokeWidth: 1.0,
)
// Tab indicator
..line(
@@ -615,6 +683,202 @@
);
});
+ testWidgets('TabBar divider can use TabBarTheme.dividerColor & TabBarTheme.dividerHeight', (WidgetTester tester) async {
+ const Color dividerColor = Color(0xff00ff00);
+ const double dividerHeight = 10.0;
+
+ await tester.pumpWidget(
+ MaterialApp(
+ theme: ThemeData(
+ tabBarTheme: const TabBarTheme(
+ dividerColor: dividerColor,
+ dividerHeight: dividerHeight,
+ ),
+ useMaterial3: true,
+ ),
+ home: Scaffold(
+ appBar: AppBar(
+ bottom: TabBar(
+ controller: TabController(length: 3, vsync: const TestVSync()),
+ tabs: const <Widget>[
+ Tab(text: 'Tab 1'),
+ Tab(text: 'Tab 2'),
+ Tab(text: 'Tab 3'),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+
+ final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
+ // Test divider color.
+ expect(tabBarBox, paints..line(color: dividerColor, strokeWidth: dividerHeight));
+ });
+
+ testWidgets('dividerColor & dividerHeight overrides TabBarTheme.dividerColor', (WidgetTester tester) async {
+ const Color dividerColor = Color(0xff0000ff);
+ const double dividerHeight = 8.0;
+
+ await tester.pumpWidget(
+ MaterialApp(
+ theme: ThemeData(
+ useMaterial3: true,
+ tabBarTheme: const TabBarTheme(
+ dividerColor: Colors.pink,
+ dividerHeight: 5.0,
+ ),
+ ),
+ home: Scaffold(
+ appBar: AppBar(
+ bottom: TabBar(
+ dividerColor: dividerColor,
+ dividerHeight: dividerHeight,
+ controller: TabController(length: 3, vsync: const TestVSync()),
+ tabs: const <Widget>[
+ Tab(text: 'Tab 1'),
+ Tab(text: 'Tab 2'),
+ Tab(text: 'Tab 3'),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+
+ final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
+ // Test divider color.
+ expect(tabBarBox, paints..line(color: dividerColor, strokeWidth: dividerHeight));
+ });
+
+ testWidgets('TabBar respects TabBarTheme.tabAlignment', (WidgetTester tester) async {
+ // Test non-scrollable tab bar.
+ await tester.pumpWidget(
+ MaterialApp(
+ theme: ThemeData(
+ tabBarTheme: const TabBarTheme(tabAlignment: TabAlignment.center),
+ useMaterial3: true,
+ ),
+ home: Scaffold(
+ appBar: AppBar(
+ bottom: TabBar(
+ controller: TabController(length: 2, vsync: const TestVSync()),
+ tabs: const <Widget>[
+ Tab(text: 'Tab 1'),
+ Tab(text: 'Tab 3'),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+
+ const double availableWidth = 800.0;
+ Rect tabOneRect = tester.getRect(find.byType(Tab).first);
+ Rect tabTwoRect = tester.getRect(find.byType(Tab).last);
+
+ double tabOneLeft = (availableWidth / 2) - tabOneRect.width - kTabLabelPadding.left;
+ expect(tabOneRect.left, equals(tabOneLeft));
+ double tabTwoRight = (availableWidth / 2) + tabTwoRect.width + kTabLabelPadding.right;
+ expect(tabTwoRect.right, equals(tabTwoRight));
+
+ // Test scrollable tab bar.
+ await tester.pumpWidget(
+ MaterialApp(
+ theme: ThemeData(
+ tabBarTheme: const TabBarTheme(tabAlignment: TabAlignment.start),
+ useMaterial3: true,
+ ),
+ home: Scaffold(
+ appBar: AppBar(
+ bottom: TabBar(
+ isScrollable: true,
+ controller: TabController(length: 2, vsync: const TestVSync()),
+ tabs: const <Widget>[
+ Tab(text: 'Tab 1'),
+ Tab(text: 'Tab 3'),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ tabOneRect = tester.getRect(find.byType(Tab).first);
+ tabTwoRect = tester.getRect(find.byType(Tab).last);
+
+ tabOneLeft = kTabLabelPadding.left;
+ expect(tabOneRect.left, equals(tabOneLeft));
+ tabTwoRight = kTabLabelPadding.horizontal + tabOneRect.width + kTabLabelPadding.left + tabTwoRect.width;
+ expect(tabTwoRect.right, equals(tabTwoRight));
+ });
+
+ testWidgets('TabBar.tabAlignment overrides TabBarTheme.tabAlignment', (WidgetTester tester) async {
+ /// Test non-scrollable tab bar.
+ await tester.pumpWidget(
+ MaterialApp(
+ theme: ThemeData(
+ tabBarTheme: const TabBarTheme(tabAlignment: TabAlignment.fill),
+ useMaterial3: true,
+ ),
+ home: Scaffold(
+ appBar: AppBar(
+ bottom: TabBar(
+ tabAlignment: TabAlignment.center,
+ controller: TabController(length: 2, vsync: const TestVSync()),
+ tabs: const <Widget>[
+ Tab(text: 'Tab 1'),
+ Tab(text: 'Tab 3'),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+
+ const double availableWidth = 800.0;
+ Rect tabOneRect = tester.getRect(find.byType(Tab).first);
+ Rect tabTwoRect = tester.getRect(find.byType(Tab).last);
+
+ double tabOneLeft = (availableWidth / 2) - tabOneRect.width - kTabLabelPadding.left;
+ expect(tabOneRect.left, equals(tabOneLeft));
+ double tabTwoRight = (availableWidth / 2) + tabTwoRect.width + kTabLabelPadding.right;
+ expect(tabTwoRect.right, equals(tabTwoRight));
+
+ /// Test scrollable tab bar.
+ await tester.pumpWidget(
+ MaterialApp(
+ theme: ThemeData(
+ tabBarTheme: const TabBarTheme(tabAlignment: TabAlignment.center),
+ useMaterial3: true,
+ ),
+ home: Scaffold(
+ appBar: AppBar(
+ bottom: TabBar(
+ isScrollable: true,
+ tabAlignment: TabAlignment.start,
+ controller: TabController(length: 2, vsync: const TestVSync()),
+ tabs: const <Widget>[
+ Tab(text: 'Tab 1'),
+ Tab(text: 'Tab 3'),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ tabOneRect = tester.getRect(find.byType(Tab).first);
+ tabTwoRect = tester.getRect(find.byType(Tab).last);
+
+ tabOneLeft = kTabLabelPadding.left;
+ expect(tabOneRect.left, equals(tabOneLeft));
+ tabTwoRight = kTabLabelPadding.horizontal + tabOneRect.width + kTabLabelPadding.left + tabTwoRect.width;
+ expect(tabTwoRect.right, equals(tabTwoRight));
+ });
+
group('Material 2', () {
// These tests are only relevant for Material 2. Once Material 2
// support is deprecated and the APIs are removed, these tests
@@ -692,7 +956,7 @@
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 are 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.
@@ -806,5 +1070,68 @@
),
);
});
+
+ testWidgets('TabBar respects TabBarTheme.tabAlignment', (WidgetTester tester) async {
+ // Test non-scrollable tab bar.
+ await tester.pumpWidget(
+ MaterialApp(
+ theme: ThemeData(
+ tabBarTheme: const TabBarTheme(tabAlignment: TabAlignment.center),
+ useMaterial3: false,
+ ),
+ home: Scaffold(
+ appBar: AppBar(
+ bottom: TabBar(
+ controller: TabController(length: 2, vsync: const TestVSync()),
+ tabs: const <Widget>[
+ Tab(text: 'Tab 1'),
+ Tab(text: 'Tab 3'),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+
+ final Rect tabOneRect = tester.getRect(find.byType(Tab).first);
+ final Rect tabTwoRect = tester.getRect(find.byType(Tab).last);
+
+ final double tabOneLeft = (800 / 2) - tabOneRect.width - kTabLabelPadding.left;
+ expect(tabOneRect.left, equals(tabOneLeft));
+ final double tabTwoRight = (800 / 2) + tabTwoRect.width + kTabLabelPadding.right;
+ expect(tabTwoRect.right, equals(tabTwoRight));
+ });
+
+ testWidgets('TabBar.tabAlignment overrides TabBarTheme.tabAlignment', (WidgetTester tester) async {
+ // Test non-scrollable tab bar.
+ await tester.pumpWidget(
+ MaterialApp(
+ theme: ThemeData(
+ tabBarTheme: const TabBarTheme(tabAlignment: TabAlignment.fill),
+ useMaterial3: false,
+ ),
+ home: Scaffold(
+ appBar: AppBar(
+ bottom: TabBar(
+ tabAlignment: TabAlignment.center,
+ controller: TabController(length: 2, vsync: const TestVSync()),
+ tabs: const <Widget>[
+ Tab(text: 'Tab 1'),
+ Tab(text: 'Tab 3'),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+
+ final Rect tabOneRect = tester.getRect(find.byType(Tab).first);
+ final Rect tabTwoRect = tester.getRect(find.byType(Tab).last);
+
+ final double tabOneLeft = (800 / 2) - tabOneRect.width - kTabLabelPadding.left;
+ expect(tabOneRect.left, equals(tabOneLeft));
+ final double tabTwoRight = (800 / 2) + tabTwoRect.width + kTabLabelPadding.right;
+ expect(tabTwoRect.right, equals(tabTwoRight));
+ });
});
}
diff --git a/packages/flutter/test/material/tabs_test.dart b/packages/flutter/test/material/tabs_test.dart
index 62e4f17..df4d34c 100644
--- a/packages/flutter/test/material/tabs_test.dart
+++ b/packages/flutter/test/material/tabs_test.dart
@@ -701,10 +701,16 @@
expect(controller.index, 0);
});
- testWidgets('Scrollable TabBar tap centers selected tab', (WidgetTester tester) async {
+ testWidgets('Material2 - Scrollable TabBar tap centers selected tab', (WidgetTester tester) async {
final List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE', 'FFFFFF', 'GGGGGG', 'HHHHHH', 'IIIIII', 'JJJJJJ', 'KKKKKK', 'LLLLLL'];
const Key tabBarKey = Key('TabBar');
- await tester.pumpWidget(buildFrame(tabs: tabs, value: 'AAAAAA', isScrollable: true, tabBarKey: tabBarKey));
+ await tester.pumpWidget(buildFrame(
+ tabs: tabs,
+ value: 'AAAAAA',
+ isScrollable: true,
+ tabBarKey: tabBarKey,
+ useMaterial3: false,
+ ));
final TabController controller = DefaultTabController.of(tester.element(find.text('AAAAAA')));
expect(controller, isNotNull);
expect(controller.index, 0);
@@ -720,12 +726,44 @@
expect(tester.getCenter(find.text('FFFFFF')).dx, moreOrLessEquals(400.0, epsilon: 1.0));
});
- testWidgets('Scrollable TabBar, with padding, tap centers selected tab', (WidgetTester tester) async {
+ testWidgets('Material3 - Scrollable TabBar tap centers selected tab', (WidgetTester tester) async {
+ final List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE', 'FFFFFF', 'GGGGGG', 'HHHHHH', 'IIIIII', 'JJJJJJ', 'KKKKKK', 'LLLLLL'];
+ const Key tabBarKey = Key('TabBar');
+ await tester.pumpWidget(buildFrame(
+ tabs: tabs,
+ value: 'AAAAAA',
+ isScrollable: true,
+ tabBarKey: tabBarKey,
+ useMaterial3: true,
+ ));
+ final TabController controller = DefaultTabController.of(tester.element(find.text('AAAAAA')));
+ expect(controller, isNotNull);
+ expect(controller.index, 0);
+
+ expect(tester.getSize(find.byKey(tabBarKey)).width, equals(800.0));
+ // The center of the FFFFFF item is to the right of the TabBar's center
+ expect(tester.getCenter(find.text('FFFFFF')).dx, greaterThan(401.0));
+
+ await tester.tap(find.text('FFFFFF'));
+ await tester.pumpAndSettle();
+ expect(controller.index, 5);
+ // The center of the FFFFFF item is now at the TabBar's center
+ expect(tester.getCenter(find.text('FFFFFF')).dx, moreOrLessEquals(452.0, epsilon: 1.0));
+ });
+
+ testWidgets('Material2 - Scrollable TabBar, with padding, tap centers selected tab', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/112776
final List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE', 'FFFFFF', 'GGGGGG', 'HHHHHH', 'IIIIII', 'JJJJJJ', 'KKKKKK', 'LLLLLL'];
const Key tabBarKey = Key('TabBar');
const EdgeInsetsGeometry padding = EdgeInsets.only(right: 30, left: 60);
- await tester.pumpWidget(buildFrame(tabs: tabs, value: 'AAAAAA', isScrollable: true, tabBarKey: tabBarKey, padding: padding));
+ await tester.pumpWidget(buildFrame(
+ tabs: tabs,
+ value: 'AAAAAA',
+ isScrollable: true,
+ tabBarKey: tabBarKey,
+ padding: padding,
+ useMaterial3: false,
+ ));
final TabController controller = DefaultTabController.of(tester.element(find.text('AAAAAA')));
expect(controller, isNotNull);
expect(controller.index, 0);
@@ -741,7 +779,35 @@
expect(tester.getCenter(find.text('FFFFFF')).dx, moreOrLessEquals(400.0, epsilon: 1.0));
});
- testWidgets('Scrollable TabBar, with padding and TextDirection.rtl, tap centers selected tab', (WidgetTester tester) async {
+ testWidgets('Material3 - Scrollable TabBar, with padding, tap centers selected tab', (WidgetTester tester) async {
+ // Regression test for https://github.com/flutter/flutter/issues/112776
+ final List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE', 'FFFFFF', 'GGGGGG', 'HHHHHH', 'IIIIII', 'JJJJJJ', 'KKKKKK', 'LLLLLL'];
+ const Key tabBarKey = Key('TabBar');
+ const EdgeInsetsGeometry padding = EdgeInsets.only(right: 30, left: 60);
+ await tester.pumpWidget(buildFrame(
+ tabs: tabs,
+ value: 'AAAAAA',
+ isScrollable: true,
+ tabBarKey: tabBarKey,
+ padding: padding,
+ useMaterial3: true,
+ ));
+ final TabController controller = DefaultTabController.of(tester.element(find.text('AAAAAA')));
+ expect(controller, isNotNull);
+ expect(controller.index, 0);
+
+ expect(tester.getSize(find.byKey(tabBarKey)).width, equals(800.0));
+ // The center of the FFFFFF item is to the right of the TabBar's center
+ expect(tester.getCenter(find.text('FFFFFF')).dx, greaterThan(401.0));
+
+ await tester.tap(find.text('FFFFFF'));
+ await tester.pumpAndSettle();
+ expect(controller.index, 5);
+ // The center of the FFFFFF item is now at the TabBar's center
+ expect(tester.getCenter(find.text('FFFFFF')).dx, moreOrLessEquals(452.0, epsilon: 1.0));
+ });
+
+ testWidgets('Material2 - Scrollable TabBar, with padding and TextDirection.rtl, tap centers selected tab', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/112776
final List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE', 'FFFFFF', 'GGGGGG', 'HHHHHH', 'IIIIII', 'JJJJJJ', 'KKKKKK', 'LLLLLL'];
const Key tabBarKey = Key('TabBar');
@@ -753,6 +819,7 @@
tabBarKey: tabBarKey,
padding: padding,
textDirection: TextDirection.rtl,
+ useMaterial3: false,
));
final TabController controller = DefaultTabController.of(tester.element(find.text('AAAAAA')));
expect(controller, isNotNull);
@@ -769,10 +836,45 @@
expect(tester.getCenter(find.text('FFFFFF')).dx, moreOrLessEquals(400.0, epsilon: 1.0));
});
- testWidgets('TabBar can be scrolled independent of the selection', (WidgetTester tester) async {
+ testWidgets('Material3 - Scrollable TabBar, with padding and TextDirection.rtl, tap centers selected tab', (WidgetTester tester) async {
+ // Regression test for https://github.com/flutter/flutter/issues/112776
+ final List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE', 'FFFFFF', 'GGGGGG', 'HHHHHH', 'IIIIII', 'JJJJJJ', 'KKKKKK', 'LLLLLL'];
+ const Key tabBarKey = Key('TabBar');
+ const EdgeInsetsGeometry padding = EdgeInsets.only(right: 30, left: 60);
+ await tester.pumpWidget(buildFrame(
+ tabs: tabs,
+ value: 'AAAAAA',
+ isScrollable: true,
+ tabBarKey: tabBarKey,
+ padding: padding,
+ textDirection: TextDirection.rtl,
+ useMaterial3: true,
+ ));
+ final TabController controller = DefaultTabController.of(tester.element(find.text('AAAAAA')));
+ expect(controller, isNotNull);
+ expect(controller.index, 0);
+
+ expect(tester.getSize(find.byKey(tabBarKey)).width, equals(800.0));
+ // The center of the FFFFFF item is to the left of the TabBar's center
+ expect(tester.getCenter(find.text('FFFFFF')).dx, lessThan(401.0));
+
+ await tester.tap(find.text('FFFFFF'));
+ await tester.pumpAndSettle();
+ expect(controller.index, 5);
+ // The center of the FFFFFF item is now at the TabBar's center
+ expect(tester.getCenter(find.text('FFFFFF')).dx, moreOrLessEquals(348.0, epsilon: 1.0));
+ });
+
+ testWidgets('Material2 - TabBar can be scrolled independent of the selection', (WidgetTester tester) async {
final List<String> tabs = <String>['AAAA', 'BBBB', 'CCCC', 'DDDD', 'EEEE', 'FFFF', 'GGGG', 'HHHH', 'IIII', 'JJJJ', 'KKKK', 'LLLL'];
const Key tabBarKey = Key('TabBar');
- await tester.pumpWidget(buildFrame(tabs: tabs, value: 'AAAA', isScrollable: true, tabBarKey: tabBarKey));
+ await tester.pumpWidget(buildFrame(
+ tabs: tabs,
+ value: 'AAAA',
+ isScrollable: true,
+ tabBarKey: tabBarKey,
+ useMaterial3: false,
+ ));
final TabController controller = DefaultTabController.of(tester.element(find.text('AAAA')));
expect(controller, isNotNull);
expect(controller.index, 0);
@@ -788,6 +890,31 @@
expect(controller.index, 0);
});
+ testWidgets('Material3 - TabBar can be scrolled independent of the selection', (WidgetTester tester) async {
+ final List<String> tabs = <String>['AAAA', 'BBBB', 'CCCC', 'DDDD', 'EEEE', 'FFFF', 'GGGG', 'HHHH', 'IIII', 'JJJJ', 'KKKK', 'LLLL'];
+ const Key tabBarKey = Key('TabBar');
+ await tester.pumpWidget(buildFrame(
+ tabs: tabs,
+ value: 'AAAA',
+ isScrollable: true,
+ tabBarKey: tabBarKey,
+ useMaterial3: true,
+ ));
+ final TabController controller = DefaultTabController.of(tester.element(find.text('AAAA')));
+ expect(controller, isNotNull);
+ expect(controller.index, 0);
+
+ // Fling-scroll the TabBar to the left
+ expect(tester.getCenter(find.text('HHHH')).dx, lessThan(720.0));
+ await tester.fling(find.byKey(tabBarKey), const Offset(-200.0, 0.0), 10000.0);
+ await tester.pump();
+ await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
+ expect(tester.getCenter(find.text('HHHH')).dx, lessThan(500.0));
+
+ // Scrolling the TabBar doesn't change the selection
+ expect(controller.index, 0);
+ });
+
testWidgets('TabBarView maintains state', (WidgetTester tester) async {
final List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE'];
String value = tabs[0];
@@ -2981,9 +3108,10 @@
expect(tabBarBox.size.width, tabRight);
});
- testWidgets('TabBar with padding isScrollable: true', (WidgetTester tester) async {
+ testWidgets('Material3 - TabBar with padding isScrollable: true', (WidgetTester tester) async {
const double indicatorWeight = 2.0; // default indicator weight
const EdgeInsets padding = EdgeInsets.only(left: 3.0, top: 7.0, right: 5.0, bottom: 3.0);
+ const double tabStartOffset = 52.0;
final List<Widget> tabs = <Widget>[
SizedBox(key: UniqueKey(), width: 130.0, height: 30.0),
@@ -3008,6 +3136,7 @@
tabs: tabs,
),
),
+ useMaterial3: true,
),
);
@@ -3016,7 +3145,7 @@
expect(tabBarBox.size.height, tabBarHeight);
// Tab0 width = 130, height = 30
- double tabLeft = padding.left;
+ double tabLeft = padding.left + tabStartOffset;
double tabRight = tabLeft + 130.0;
double tabTop = (tabBarHeight - indicatorWeight + (padding.top - padding.bottom) - 30.0) / 2.0;
double tabBottom = tabTop + 30.0;
@@ -3040,7 +3169,7 @@
expect(tester.getRect(find.byKey(tabs[2].key!)), tabRect);
tabRight += padding.right;
- expect(tabBarBox.size.width, tabRight);
+ expect(tabBarBox.size.width, tabRight + 320.0); // Right tab + remaining space of the stretched tab bar.
});
testWidgets('TabBar with labelPadding', (WidgetTester tester) async {
@@ -5950,15 +6079,12 @@
);
});
- testWidgets('Default TabAlignment', (WidgetTester tester) async {
- final ThemeData theme = ThemeData(useMaterial3: true);
+ testWidgets('Material3 - Default TabAlignment', (WidgetTester tester) async {
final List<String> tabs = <String>['A', 'B'];
+ const double tabStartOffset = 52.0;
// Test default TabAlignment when isScrollable is false.
- await tester.pumpWidget(MaterialApp(
- theme: theme,
- home: buildFrame(tabs: tabs, value: 'B'),
- ));
+ await tester.pumpWidget(buildFrame(tabs: tabs, value: 'B', useMaterial3: true));
final Rect tabBar = tester.getRect(find.byType(TabBar));
Rect tabOneRect = tester.getRect(find.byType(Tab).first);
@@ -5971,18 +6097,20 @@
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),
+ await tester.pumpWidget(buildFrame(
+ tabs: tabs,
+ value: 'B',
+ isScrollable: true,
+ useMaterial3: 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;
+ tabOneLeft = kTabLabelPadding.left + tabStartOffset;
expect(tabOneRect.left, equals(tabOneLeft));
- tabTwoRight = kTabLabelPadding.horizontal + tabOneRect.width + kTabLabelPadding.left + tabTwoRect.width;
+ tabTwoRight = kTabLabelPadding.horizontal + tabStartOffset + tabOneRect.width + kTabLabelPadding.left + tabTwoRect.width;
expect(tabTwoRect.right, equals(tabTwoRight));
});
@@ -6044,6 +6172,262 @@
expect(tester.takeException(), isAssertionError);
});
+ testWidgets('Material3 - TabAlignment updates tabs alignment (non-scrollable TabBar)', (WidgetTester tester) async {
+ final List<String> tabs = <String>['A', 'B'];
+
+ // Test TabAlignment.fill (default) when isScrollable is false.
+ await tester.pumpWidget(buildFrame(tabs: tabs, value: 'B', useMaterial3: true));
+
+ const double availableWidth = 800.0;
+ Rect tabOneRect = tester.getRect(find.byType(Tab).first);
+ Rect tabTwoRect = tester.getRect(find.byType(Tab).last);
+
+ // By defaults tabs should fill the width of the TabBar.
+ double tabOneLeft = ((availableWidth / 2) - tabOneRect.width) / 2;
+ expect(tabOneRect.left, equals(tabOneLeft));
+ double tabTwoRight = availableWidth - ((availableWidth / 2) - tabTwoRect.width) / 2;
+ expect(tabTwoRect.right, equals(tabTwoRight));
+
+ // Test TabAlignment.center when isScrollable is false.
+ await tester.pumpWidget(buildFrame(
+ tabs: tabs,
+ value: 'B',
+ tabAlignment: TabAlignment.center,
+ useMaterial3: true,
+ ));
+ await tester.pumpAndSettle();
+
+ tabOneRect = tester.getRect(find.byType(Tab).first);
+ tabTwoRect = tester.getRect(find.byType(Tab).last);
+
+ // Tabs should not fill the width 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('Material3 - TabAlignment updates tabs alignment (scrollable TabBar)', (WidgetTester tester) async {
+ final List<String> tabs = <String>['A', 'B'];
+ const double tabStartOffset = 52.0;
+
+ // Test TabAlignment.startOffset (default) when isScrollable is true.
+ await tester.pumpWidget(buildFrame(
+ tabs: tabs,
+ value: 'B',
+ isScrollable: true,
+ useMaterial3: true,
+ ));
+
+ final Rect tabBar = tester.getRect(find.byType(TabBar));
+ Rect tabOneRect = tester.getRect(find.byType(Tab).first);
+ Rect tabTwoRect = tester.getRect(find.byType(Tab).last);
+
+ // By default tabs should be aligned to the start of the TabBar with
+ // an horizontal offset of 52.0 pixels.
+ double tabOneLeft = kTabLabelPadding.left + tabStartOffset;
+ expect(tabOneRect.left, equals(tabOneLeft));
+ double tabTwoRight = tabStartOffset + kTabLabelPadding.horizontal + tabOneRect.width
+ + kTabLabelPadding.left + tabTwoRect.width;
+ expect(tabTwoRect.right, equals(tabTwoRight));
+
+ // Test TabAlignment.start when isScrollable is true.
+ await tester.pumpWidget(buildFrame(
+ tabs: tabs,
+ value: 'B',
+ isScrollable: true,
+ tabAlignment: TabAlignment.start,
+ useMaterial3: true,
+ ));
+ await tester.pumpAndSettle();
+
+ 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));
+
+ // Test TabAlignment.center when isScrollable is true.
+ await tester.pumpWidget(buildFrame(
+ tabs: tabs,
+ value: 'B',
+ isScrollable: true,
+ tabAlignment: TabAlignment.center,
+ useMaterial3: true,
+ ));
+ await tester.pumpAndSettle();
+
+ tabOneRect = tester.getRect(find.byType(Tab).first);
+ tabTwoRect = tester.getRect(find.byType(Tab).last);
+
+ // Tabs should be centered in the TabBar.
+ tabOneLeft = (tabBar.width / 2) - tabOneRect.width - kTabLabelPadding.right;
+ expect(tabOneRect.left, equals(tabOneLeft));
+ tabTwoRight = (tabBar.width / 2) + tabTwoRect.width + kTabLabelPadding.left;
+ expect(tabTwoRect.right, equals(tabTwoRight));
+
+ // Test TabAlignment.startOffset when isScrollable is true.
+ await tester.pumpWidget(buildFrame(
+ tabs: tabs,
+ value: 'B',
+ isScrollable: true,
+ tabAlignment: TabAlignment.startOffset,
+ useMaterial3: true,
+ ));
+ await tester.pumpAndSettle();
+
+ tabOneRect = tester.getRect(find.byType(Tab).first);
+ tabTwoRect = tester.getRect(find.byType(Tab).last);
+
+ // Tabs should be aligned to the start of the TabBar with an
+ // horizontal offset of 52.0 pixels.
+ tabOneLeft = kTabLabelPadding.left + tabStartOffset;
+ expect(tabOneRect.left, equals(tabOneLeft));
+ tabTwoRight = tabStartOffset + kTabLabelPadding.horizontal + tabOneRect.width
+ + kTabLabelPadding.left + tabTwoRect.width;
+ expect(tabTwoRect.right, equals(tabTwoRight));
+ });
+
+ testWidgets('Material3 - TabAlignment.start & TabAlignment.startOffset respects TextDirection.rtl', (WidgetTester tester) async {
+ final List<String> tabs = <String>['A', 'B'];
+ const double tabStartOffset = 52.0;
+
+ // Test TabAlignment.startOffset (default) when isScrollable is true.
+ await tester.pumpWidget(buildFrame(
+ tabs: tabs,
+ value: 'B',
+ isScrollable: true,
+ textDirection: TextDirection.rtl,
+ useMaterial3: true,
+ ));
+
+ 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 be aligned to the start of the TabBar with an
+ // horizontal offset of 52.0 pixels.
+ double tabOneRight = tabBar.width - kTabLabelPadding.right - tabStartOffset;
+ expect(tabOneRect.right, equals(tabOneRight));
+ double tabTwoLeft = tabBar.width - tabStartOffset - kTabLabelPadding.horizontal - tabOneRect.width
+ - kTabLabelPadding.right - tabTwoRect.width;
+ expect(tabTwoRect.left, equals(tabTwoLeft));
+
+ // Test TabAlignment.start when isScrollable is true.
+ await tester.pumpWidget(buildFrame(
+ tabs: tabs,
+ value: 'B',
+ isScrollable: true,
+ tabAlignment: TabAlignment.start,
+ textDirection: TextDirection.rtl,
+ useMaterial3: true,
+ ));
+ await tester.pumpAndSettle();
+
+ tabOneRect = tester.getRect(find.byType(Tab).first);
+ tabTwoRect = tester.getRect(find.byType(Tab).last);
+
+ // Tabs should be aligned to the start of the TabBar.
+ tabOneRight = tabBar.width - kTabLabelPadding.right;
+ expect(tabOneRect.right, equals(tabOneRight));
+ tabTwoLeft = tabBar.width - kTabLabelPadding.horizontal - tabOneRect.width
+ - kTabLabelPadding.left - tabTwoRect.width;
+ expect(tabTwoRect.left, equals(tabTwoLeft));
+
+ // Test TabAlignment.startOffset when isScrollable is true.
+ await tester.pumpWidget(buildFrame(
+ tabs: tabs,
+ value: 'B',
+ isScrollable: true,
+ tabAlignment: TabAlignment.startOffset,
+ textDirection: TextDirection.rtl,
+ useMaterial3: true,
+ ));
+ await tester.pumpAndSettle();
+
+ tabOneRect = tester.getRect(find.byType(Tab).first);
+ tabTwoRect = tester.getRect(find.byType(Tab).last);
+
+ // Tabs should be aligned to the start of the TabBar with an
+ // horizontal offset of 52.0 pixels.
+ tabOneRight = tabBar.width - kTabLabelPadding.right - tabStartOffset;
+ expect(tabOneRect.right, equals(tabOneRight));
+ tabTwoLeft = tabBar.width - tabStartOffset - kTabLabelPadding.horizontal - tabOneRect.width
+ - kTabLabelPadding.right - tabTwoRect.width;
+ expect(tabTwoRect.left, equals(tabTwoLeft));
+ });
+
+ testWidgets('Material3 - TabBar inherits the dividerColor of TabBarTheme', (WidgetTester tester) async {
+ const Color dividerColor = Colors.yellow;
+ await tester.pumpWidget(
+ MaterialApp(
+ theme: ThemeData(
+ useMaterial3: true,
+ tabBarTheme: const TabBarTheme(dividerColor: dividerColor),
+ ),
+ home: Scaffold(
+ appBar: AppBar(
+ bottom: TabBar(
+ controller: TabController(length: 3, vsync: const TestVSync()),
+ tabs: const <Widget>[
+ Tab(text: 'Tab 1'),
+ Tab(text: 'Tab 2'),
+ Tab(text: 'Tab 3'),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+
+ // Test painter's divider color.
+ final CustomPaint paint = tester.widget<CustomPaint>(find.byType(CustomPaint).last);
+ // ignore: avoid_dynamic_calls
+ expect((paint.painter as dynamic).dividerColor, dividerColor);
+ });
+
+ // This is a regression test for https://github.com/flutter/flutter/pull/125974#discussion_r1239089151.
+ testWidgets('Divider can be constrained', (WidgetTester tester) async {
+ const Color dividerColor = Colors.yellow;
+ await tester.pumpWidget(
+ MaterialApp(
+ theme: ThemeData(
+ useMaterial3: true,
+ tabBarTheme: const TabBarTheme(dividerColor: dividerColor),
+ ),
+ home: Scaffold(
+ body: DefaultTabController(
+ length: 2,
+ child: Center(
+ child: ConstrainedBox(
+ constraints: const BoxConstraints(maxWidth: 360),
+ child: ColoredBox(
+ color: Colors.grey[200]!,
+ child: const TabBar.secondary(
+ tabAlignment: TabAlignment.start,
+ isScrollable: true,
+ tabs: <Widget>[
+ Tab(text: 'Test 1'),
+ Tab(text: 'Test 2'),
+ ],
+ ),
+ )
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ // Test tab bar width.
+ expect(tester.getSize(find.byType(TabBar)).width, 360);
+ // Test divider width.
+ expect(tester.getSize(find.byType(CustomPaint).at(1)).width, 360);
+ });
+
group('Material 2', () {
// These tests are only relevant for Material 2. Once Material 2
// support is deprecated and the APIs are removed, these tests
@@ -6104,45 +6488,11 @@
);
});
- testWidgets('Material3 - TabBar inherits the dividerColor of TabBarTheme', (WidgetTester tester) async {
- const Color dividerColor = Colors.yellow;
-
- await tester.pumpWidget(
- MaterialApp(
- theme: ThemeData(
- useMaterial3: true,
- tabBarTheme: const TabBarTheme(dividerColor: dividerColor),
- ),
- home: Scaffold(
- appBar: AppBar(
- bottom: TabBar(
- controller: TabController(length: 3, vsync: const TestVSync()),
- tabs: const <Widget>[
- Tab(text: 'Tab 1'),
- Tab(text: 'Tab 2'),
- Tab(text: 'Tab 3'),
- ],
- ),
- ),
- ),
- ),
- );
-
- // Test painter's divider color.
- final CustomPaint paint = tester.widget<CustomPaint>(find.byType(CustomPaint).last);
- // ignore: avoid_dynamic_calls
- expect((paint.painter as dynamic).dividerColor, dividerColor);
- });
-
- testWidgets('Default TabAlignment', (WidgetTester tester) async {
- final ThemeData theme = ThemeData(useMaterial3: false);
+ testWidgets('Material2 - Default TabAlignment', (WidgetTester tester) async {
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'),
- ));
+ await tester.pumpWidget(buildFrame(tabs: tabs, value: 'B', useMaterial3: false));
final Rect tabBar = tester.getRect(find.byType(TabBar));
Rect tabOneRect = tester.getRect(find.byType(Tab).first);
@@ -6155,9 +6505,11 @@
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),
+ await tester.pumpWidget(buildFrame(
+ tabs: tabs,
+ value: 'B',
+ isScrollable: true,
+ useMaterial3: false,
));
tabOneRect = tester.getRect(find.byType(Tab).first);
@@ -6261,6 +6613,106 @@
),
);
});
+
+ testWidgets('Material2 - TabBar with padding isScrollable: true', (WidgetTester tester) async {
+ const double indicatorWeight = 2.0; // default indicator weight
+ const EdgeInsets padding = EdgeInsets.only(left: 3.0, top: 7.0, right: 5.0, bottom: 3.0);
+
+ final List<Widget> tabs = <Widget>[
+ SizedBox(key: UniqueKey(), width: 130.0, height: 30.0),
+ SizedBox(key: UniqueKey(), width: 140.0, height: 40.0),
+ SizedBox(key: UniqueKey(), width: 150.0, height: 50.0),
+ ];
+
+ final TabController controller = TabController(
+ vsync: const TestVSync(),
+ length: tabs.length,
+ );
+
+ await tester.pumpWidget(
+ boilerplate(
+ child: Container(
+ alignment: Alignment.topLeft,
+ child: TabBar(
+ padding: padding,
+ labelPadding: EdgeInsets.zero,
+ isScrollable: true,
+ controller: controller,
+ tabs: tabs,
+ ),
+ ),
+ useMaterial3: false,
+ ),
+ );
+
+ final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
+ final double tabBarHeight = 50.0 + indicatorWeight + padding.top + padding.bottom; // 50 = max tab height
+ expect(tabBarBox.size.height, tabBarHeight);
+
+ // Tab0 width = 130, height = 30
+ double tabLeft = padding.left;
+ double tabRight = tabLeft + 130.0;
+ double tabTop = (tabBarHeight - indicatorWeight + (padding.top - padding.bottom) - 30.0) / 2.0;
+ double tabBottom = tabTop + 30.0;
+ Rect tabRect = Rect.fromLTRB(tabLeft, tabTop, tabRight, tabBottom);
+ expect(tester.getRect(find.byKey(tabs[0].key!)), tabRect);
+
+ // Tab1 width = 140, height = 40
+ tabLeft = tabRight;
+ tabRight = tabLeft + 140.0;
+ tabTop = (tabBarHeight - indicatorWeight + (padding.top - padding.bottom) - 40.0) / 2.0;
+ tabBottom = tabTop + 40.0;
+ tabRect = Rect.fromLTRB(tabLeft, tabTop, tabRight, tabBottom);
+ expect(tester.getRect(find.byKey(tabs[1].key!)), tabRect);
+
+ // Tab2 width = 150, height = 50
+ tabLeft = tabRight;
+ tabRight = tabLeft + 150.0;
+ tabTop = (tabBarHeight - indicatorWeight + (padding.top - padding.bottom) - 50.0) / 2.0;
+ tabBottom = tabTop + 50.0;
+ tabRect = Rect.fromLTRB(tabLeft, tabTop, tabRight, tabBottom);
+ expect(tester.getRect(find.byKey(tabs[2].key!)), tabRect);
+
+ tabRight += padding.right;
+ expect(tabBarBox.size.width, tabRight);
+ });
+
+ testWidgets('Material2 - TabAlignment updates tabs alignment (non-scrollable TabBar)', (WidgetTester tester) async {
+ final ThemeData theme = ThemeData(useMaterial3: false);
+ final List<String> tabs = <String>['A', 'B'];
+
+ // Test TabAlignment.fill (default) 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);
+
+ // By default 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 TabAlignment.center when isScrollable is false.
+ await tester.pumpWidget(MaterialApp(
+ theme: theme,
+ home: buildFrame(tabs: tabs, value: 'B', tabAlignment: TabAlignment.center),
+ ));
+ await tester.pumpAndSettle();
+
+ tabOneRect = tester.getRect(find.byType(Tab).first);
+ tabTwoRect = tester.getRect(find.byType(Tab).last);
+
+ // Tabs should not fill the width 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));
+ });
});
}