Unverified Commit 32fde139 authored by Taha Tesser's avatar Taha Tesser Committed by GitHub

Fix Material 3 Scrollable `TabBar` (#125974)

fix https://github.com/flutter/flutter/issues/117722

### Description
1. Fix the divider doesn't stretch to take all the available width in the scrollable tab bar in M3
2. Add `dividerHeight` property.
3. Update the default tab alignment for the scrollable tab bar to match the specs (this is backward compatible for M2 with the new `tabAlignment` property).

### Bug (default tab alignment)

![Screenshot 2023-05-05 at 19 04 40](https://user-images.githubusercontent.com/48603081/236509483-1d03af21-a764-4776-acef-2126560f0d51.png)

### Fix (default tab alignment)

![Screenshot 2023-05-05 at 19 04 15](https://user-images.githubusercontent.com/48603081/236509513-2426d456-c54f-42bd-9545-a14dc6ee7e69.png)

### Code sample

<details> 
<summary>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 MaterialApp(
      theme: ThemeData(
        //  tabBarTheme: const TabBarTheme(tabAlignment: TabAlignment.start),
          useMaterial3: true,
      ),
      home: const TabBarExample(),
    );
  }
}

class TabBarExample extends StatefulWidget {
  const TabBarExample({super.key});

  @override
  State<TabBarExample> createState() => _TabBarExampleState();
}

class _TabBarExampleState extends State<TabBarExample> {
  bool rtl = false;

  @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'),
          ),
          body: const Column(
            children: <Widget>[
              Text('Scrollable-TabAlignment.start'),
              TabBar(
                isScrollable: true,
                tabAlignment: TabAlignment.start,
                tabs: <Widget>[
                  Tab(
                    icon: Icon(Icons.cloud_outlined),
                  ),
                  Tab(
                    icon: Icon(Icons.beach_access_sharp),
                  ),
                  Tab(
                    icon: Icon(Icons.brightness_5_sharp),
                  ),
                ],
              ),
              Text('Scrollable-TabAlignment.startOffset'),
              TabBar(
                isScrollable: true,
                tabAlignment: TabAlignment.startOffset,
                tabs: <Widget>[
                  Tab(
                    icon: Icon(Icons.cloud_outlined),
                  ),
                  Tab(
                    icon: Icon(Icons.beach_access_sharp),
                  ),
                  Tab(
                    icon: Icon(Icons.brightness_5_sharp),
                  ),
                ],
              ),
              Text('Scrollable-TabAlignment.center'),
              TabBar(
                isScrollable: true,
                tabAlignment: TabAlignment.center,
                tabs: <Widget>[
                  Tab(
                    icon: Icon(Icons.cloud_outlined),
                  ),
                  Tab(
                    icon: Icon(Icons.beach_access_sharp),
                  ),
                  Tab(
                    icon: Icon(Icons.brightness_5_sharp),
                  ),
                ],
              ),
              Spacer(),
              Text('Non-scrollable-TabAlignment.fill'),
              TabBar(
                tabAlignment: TabAlignment.fill,
                tabs: <Widget>[
                  Tab(
                    icon: Icon(Icons.cloud_outlined),
                  ),
                  Tab(
                    icon: Icon(Icons.beach_access_sharp),
                  ),
                  Tab(
                    icon: Icon(Icons.brightness_5_sharp),
                  ),
                ],
              ),
              Text('Non-scrollable-TabAlignment.center'),
              TabBar(
                tabAlignment: TabAlignment.center,
                tabs: <Widget>[
                  Tab(
                    icon: Icon(Icons.cloud_outlined),
                  ),
                  Tab(
                    icon: Icon(Icons.beach_access_sharp),
                  ),
                  Tab(
                    icon: Icon(Icons.brightness_5_sharp),
                  ),
                ],
              ),
              Spacer(),
            ],
          ),
          floatingActionButton: FloatingActionButton.extended(
            onPressed: () {
              setState(() {
                rtl = !rtl;
              });
            },
            label: const Text('Switch Direction'),
            icon: const Icon(Icons.swap_horiz),
          ),
        ),
      ),
    );
  }
}
``` 
	
</details>

![Screenshot 2023-06-06 at 18 06 12](https://github.com/flutter/flutter/assets/48603081/5ee5386d-cc64-4025-a020-ed2222cb6031)
parent 0da8012c
...@@ -529,6 +529,7 @@ md.comp.primary-navigation-tab.active.hover.state-layer.opacity, ...@@ -529,6 +529,7 @@ md.comp.primary-navigation-tab.active.hover.state-layer.opacity,
md.comp.primary-navigation-tab.active.pressed.state-layer.color, 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.active.pressed.state-layer.opacity,
md.comp.primary-navigation-tab.divider.color, 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.color,
md.comp.primary-navigation-tab.inactive.focus.state-layer.opacity, md.comp.primary-navigation-tab.inactive.focus.state-layer.opacity,
md.comp.primary-navigation-tab.inactive.hover.state-layer.color, md.comp.primary-navigation-tab.inactive.hover.state-layer.color,
...@@ -588,6 +589,7 @@ md.comp.search-view.header.supporting-text.color, ...@@ -588,6 +589,7 @@ md.comp.search-view.header.supporting-text.color,
md.comp.search-view.header.supporting-text.text-style, md.comp.search-view.header.supporting-text.text-style,
md.comp.secondary-navigation-tab.active.label-text.color, md.comp.secondary-navigation-tab.active.label-text.color,
md.comp.secondary-navigation-tab.divider.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.color,
md.comp.secondary-navigation-tab.focus.state-layer.opacity, md.comp.secondary-navigation-tab.focus.state-layer.opacity,
md.comp.secondary-navigation-tab.hover.state-layer.color, md.comp.secondary-navigation-tab.hover.state-layer.color,
......
...@@ -24,6 +24,9 @@ class _${blockName}PrimaryDefaultsM3 extends TabBarTheme { ...@@ -24,6 +24,9 @@ class _${blockName}PrimaryDefaultsM3 extends TabBarTheme {
@override @override
Color? get dividerColor => ${componentColor("md.comp.primary-navigation-tab.divider")}; Color? get dividerColor => ${componentColor("md.comp.primary-navigation-tab.divider")};
@override
double? get dividerHeight => ${getToken('md.comp.primary-navigation-tab.divider.height')};
@override @override
Color? get indicatorColor => ${componentColor("md.comp.primary-navigation-tab.active-indicator")}; Color? get indicatorColor => ${componentColor("md.comp.primary-navigation-tab.active-indicator")};
...@@ -71,7 +74,7 @@ class _${blockName}PrimaryDefaultsM3 extends TabBarTheme { ...@@ -71,7 +74,7 @@ class _${blockName}PrimaryDefaultsM3 extends TabBarTheme {
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory; InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
@override @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')}; static double indicatorWeight = ${getToken('md.comp.primary-navigation-tab.active-indicator.height')};
} }
...@@ -88,6 +91,9 @@ class _${blockName}SecondaryDefaultsM3 extends TabBarTheme { ...@@ -88,6 +91,9 @@ class _${blockName}SecondaryDefaultsM3 extends TabBarTheme {
@override @override
Color? get dividerColor => ${componentColor("md.comp.secondary-navigation-tab.divider")}; Color? get dividerColor => ${componentColor("md.comp.secondary-navigation-tab.divider")};
@override
double? get dividerHeight => ${getToken('md.comp.secondary-navigation-tab.divider.height')};
@override @override
Color? get indicatorColor => ${componentColor("md.comp.primary-navigation-tab.active-indicator")}; Color? get indicatorColor => ${componentColor("md.comp.primary-navigation-tab.active-indicator")};
...@@ -135,7 +141,7 @@ class _${blockName}SecondaryDefaultsM3 extends TabBarTheme { ...@@ -135,7 +141,7 @@ class _${blockName}SecondaryDefaultsM3 extends TabBarTheme {
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory; InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
@override @override
TabAlignment? get tabAlignment => isScrollable ? TabAlignment.start : TabAlignment.fill; TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill;
} }
'''; ''';
......
...@@ -32,6 +32,7 @@ class TabBarTheme with Diagnosticable { ...@@ -32,6 +32,7 @@ class TabBarTheme with Diagnosticable {
this.indicatorColor, this.indicatorColor,
this.indicatorSize, this.indicatorSize,
this.dividerColor, this.dividerColor,
this.dividerHeight,
this.labelColor, this.labelColor,
this.labelPadding, this.labelPadding,
this.labelStyle, this.labelStyle,
...@@ -55,6 +56,9 @@ class TabBarTheme with Diagnosticable { ...@@ -55,6 +56,9 @@ class TabBarTheme with Diagnosticable {
/// Overrides the default value for [TabBar.dividerColor]. /// Overrides the default value for [TabBar.dividerColor].
final Color? dividerColor; final Color? dividerColor;
/// Overrides the default value for [TabBar.dividerHeight].
final double? dividerHeight;
/// Overrides the default value for [TabBar.labelColor]. /// Overrides the default value for [TabBar.labelColor].
/// ///
/// If [labelColor] is a [MaterialStateColor], then the effective color will /// If [labelColor] is a [MaterialStateColor], then the effective color will
...@@ -101,6 +105,7 @@ class TabBarTheme with Diagnosticable { ...@@ -101,6 +105,7 @@ class TabBarTheme with Diagnosticable {
Color? indicatorColor, Color? indicatorColor,
TabBarIndicatorSize? indicatorSize, TabBarIndicatorSize? indicatorSize,
Color? dividerColor, Color? dividerColor,
double? dividerHeight,
Color? labelColor, Color? labelColor,
EdgeInsetsGeometry? labelPadding, EdgeInsetsGeometry? labelPadding,
TextStyle? labelStyle, TextStyle? labelStyle,
...@@ -116,6 +121,7 @@ class TabBarTheme with Diagnosticable { ...@@ -116,6 +121,7 @@ class TabBarTheme with Diagnosticable {
indicatorColor: indicatorColor ?? this.indicatorColor, indicatorColor: indicatorColor ?? this.indicatorColor,
indicatorSize: indicatorSize ?? this.indicatorSize, indicatorSize: indicatorSize ?? this.indicatorSize,
dividerColor: dividerColor ?? this.dividerColor, dividerColor: dividerColor ?? this.dividerColor,
dividerHeight: dividerHeight ?? this.dividerHeight,
labelColor: labelColor ?? this.labelColor, labelColor: labelColor ?? this.labelColor,
labelPadding: labelPadding ?? this.labelPadding, labelPadding: labelPadding ?? this.labelPadding,
labelStyle: labelStyle ?? this.labelStyle, labelStyle: labelStyle ?? this.labelStyle,
...@@ -147,6 +153,7 @@ class TabBarTheme with Diagnosticable { ...@@ -147,6 +153,7 @@ class TabBarTheme with Diagnosticable {
indicatorColor: Color.lerp(a.indicatorColor, b.indicatorColor, t), indicatorColor: Color.lerp(a.indicatorColor, b.indicatorColor, t),
indicatorSize: t < 0.5 ? a.indicatorSize : b.indicatorSize, indicatorSize: t < 0.5 ? a.indicatorSize : b.indicatorSize,
dividerColor: Color.lerp(a.dividerColor, b.dividerColor, t), dividerColor: Color.lerp(a.dividerColor, b.dividerColor, t),
dividerHeight: t < 0.5 ? a.dividerHeight : b.dividerHeight,
labelColor: Color.lerp(a.labelColor, b.labelColor, t), labelColor: Color.lerp(a.labelColor, b.labelColor, t),
labelPadding: EdgeInsetsGeometry.lerp(a.labelPadding, b.labelPadding, t), labelPadding: EdgeInsetsGeometry.lerp(a.labelPadding, b.labelPadding, t),
labelStyle: TextStyle.lerp(a.labelStyle, b.labelStyle, t), labelStyle: TextStyle.lerp(a.labelStyle, b.labelStyle, t),
...@@ -165,6 +172,7 @@ class TabBarTheme with Diagnosticable { ...@@ -165,6 +172,7 @@ class TabBarTheme with Diagnosticable {
indicatorColor, indicatorColor,
indicatorSize, indicatorSize,
dividerColor, dividerColor,
dividerHeight,
labelColor, labelColor,
labelPadding, labelPadding,
labelStyle, labelStyle,
...@@ -189,6 +197,7 @@ class TabBarTheme with Diagnosticable { ...@@ -189,6 +197,7 @@ class TabBarTheme with Diagnosticable {
&& other.indicatorColor == indicatorColor && other.indicatorColor == indicatorColor
&& other.indicatorSize == indicatorSize && other.indicatorSize == indicatorSize
&& other.dividerColor == dividerColor && other.dividerColor == dividerColor
&& other.dividerHeight == dividerHeight
&& other.labelColor == labelColor && other.labelColor == labelColor
&& other.labelPadding == labelPadding && other.labelPadding == labelPadding
&& other.labelStyle == labelStyle && other.labelStyle == labelStyle
......
...@@ -397,6 +397,8 @@ class _IndicatorPainter extends CustomPainter { ...@@ -397,6 +397,8 @@ class _IndicatorPainter extends CustomPainter {
required this.indicatorPadding, required this.indicatorPadding,
required this.labelPaddings, required this.labelPaddings,
this.dividerColor, this.dividerColor,
this.dividerHeight,
required this.width,
}) : super(repaint: controller.animation) { }) : super(repaint: controller.animation) {
if (old != null) { if (old != null) {
saveTabOffsets(old._currentTabOffsets, old._currentTextDirection); saveTabOffsets(old._currentTabOffsets, old._currentTextDirection);
...@@ -408,8 +410,10 @@ class _IndicatorPainter extends CustomPainter { ...@@ -408,8 +410,10 @@ class _IndicatorPainter extends CustomPainter {
final TabBarIndicatorSize? indicatorSize; final TabBarIndicatorSize? indicatorSize;
final EdgeInsetsGeometry indicatorPadding; final EdgeInsetsGeometry indicatorPadding;
final List<GlobalKey> tabKeys; final List<GlobalKey> tabKeys;
final Color? dividerColor;
final List<EdgeInsetsGeometry> labelPaddings; final List<EdgeInsetsGeometry> labelPaddings;
final Color? dividerColor;
final double? dividerHeight;
final double width;
// _currentTabOffsets and _currentTextDirection are set each time TabBar // _currentTabOffsets and _currentTextDirection are set each time TabBar
// layout is completed. These values can be null when TabBar contains no // layout is completed. These values can be null when TabBar contains no
...@@ -502,8 +506,10 @@ class _IndicatorPainter extends CustomPainter { ...@@ -502,8 +506,10 @@ class _IndicatorPainter extends CustomPainter {
textDirection: _currentTextDirection, textDirection: _currentTextDirection,
); );
if (dividerColor != null) { if (dividerColor != null) {
final Paint dividerPaint = Paint()..color = dividerColor!..strokeWidth = 1; final Paint dividerPaint = Paint()..color = dividerColor!..strokeWidth = dividerHeight!;
canvas.drawLine(Offset(0, size.height), Offset(size.width, size.height), dividerPaint); final Offset dividerP1 = Offset(-width, size.height - (dividerPaint.strokeWidth / 2));
final Offset dividerP2 = Offset(width, size.height - (dividerPaint.strokeWidth / 2));
canvas.drawLine(dividerP1, dividerP2, dividerPaint);
} }
_painter!.paint(canvas, _currentRect!.topLeft, configuration); _painter!.paint(canvas, _currentRect!.topLeft, configuration);
} }
...@@ -718,6 +724,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { ...@@ -718,6 +724,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
this.indicator, this.indicator,
this.indicatorSize, this.indicatorSize,
this.dividerColor, this.dividerColor,
this.dividerHeight,
this.labelColor, this.labelColor,
this.labelStyle, this.labelStyle,
this.labelPadding, this.labelPadding,
...@@ -768,6 +775,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { ...@@ -768,6 +775,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
this.indicator, this.indicator,
this.indicatorSize, this.indicatorSize,
this.dividerColor, this.dividerColor,
this.dividerHeight,
this.labelColor, this.labelColor,
this.labelStyle, this.labelStyle,
this.labelPadding, this.labelPadding,
...@@ -895,6 +903,13 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { ...@@ -895,6 +903,13 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
/// [ColorScheme.surfaceVariant] will be used, otherwise divider will not be drawn. /// [ColorScheme.surfaceVariant] will be used, otherwise divider will not be drawn.
final Color? dividerColor; 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. /// The color of selected tab labels.
/// ///
/// If null, then [TabBarTheme.labelColor] is used. If that is also null and /// If null, then [TabBarTheme.labelColor] is used. If that is also null and
...@@ -1269,8 +1284,10 @@ class _TabBarState extends State<TabBar> { ...@@ -1269,8 +1284,10 @@ class _TabBarState extends State<TabBar> {
indicatorPadding: widget.indicatorPadding, indicatorPadding: widget.indicatorPadding,
tabKeys: _tabKeys, tabKeys: _tabKeys,
old: _indicatorPainter, old: _indicatorPainter,
dividerColor: theme.useMaterial3 ? widget.dividerColor ?? tabBarTheme.dividerColor ?? _defaults.dividerColor : null,
labelPaddings: _labelPaddings, labelPaddings: _labelPaddings,
dividerColor: theme.useMaterial3 ? widget.dividerColor ?? tabBarTheme.dividerColor ?? _defaults.dividerColor : null,
dividerHeight: theme.useMaterial3 ? widget.dividerHeight ?? tabBarTheme.dividerHeight ?? _defaults.dividerHeight : null,
width: MediaQuery.sizeOf(context).width,
); );
} }
...@@ -1475,6 +1492,7 @@ class _TabBarState extends State<TabBar> { ...@@ -1475,6 +1492,7 @@ class _TabBarState extends State<TabBar> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context)); assert(debugCheckHasMaterialLocalizations(context));
assert(_debugScheduleCheckHasValidTabsCount()); assert(_debugScheduleCheckHasValidTabsCount());
final ThemeData theme = Theme.of(context);
final TabBarTheme tabBarTheme = TabBarTheme.of(context); final TabBarTheme tabBarTheme = TabBarTheme.of(context);
final TabAlignment effectiveTabAlignment = widget.tabAlignment ?? tabBarTheme.tabAlignment ?? _defaults.tabAlignment!; final TabAlignment effectiveTabAlignment = widget.tabAlignment ?? tabBarTheme.tabAlignment ?? _defaults.tabAlignment!;
assert(_debugTabAlignmentIsValid(effectiveTabAlignment)); assert(_debugTabAlignmentIsValid(effectiveTabAlignment));
...@@ -1627,6 +1645,17 @@ class _TabBarState extends State<TabBar> { ...@@ -1627,6 +1645,17 @@ class _TabBarState extends State<TabBar> {
child: tabBar, child: tabBar,
), ),
); );
if (theme.useMaterial3) {
final AlignmentGeometry effectiveAlignment = switch (effectiveTabAlignment) {
TabAlignment.center => Alignment.center,
TabAlignment.start || TabAlignment.startOffset || TabAlignment.fill => AlignmentDirectional.centerStart,
};
tabBar = Align(
heightFactor: 1.0,
alignment: effectiveAlignment,
child: tabBar,
);
}
} else if (widget.padding != null) { } else if (widget.padding != null) {
tabBar = Padding( tabBar = Padding(
padding: widget.padding!, padding: widget.padding!,
...@@ -2177,6 +2206,9 @@ class _TabsPrimaryDefaultsM3 extends TabBarTheme { ...@@ -2177,6 +2206,9 @@ class _TabsPrimaryDefaultsM3 extends TabBarTheme {
@override @override
Color? get dividerColor => _colors.surfaceVariant; Color? get dividerColor => _colors.surfaceVariant;
@override
double? get dividerHeight => 1.0;
@override @override
Color? get indicatorColor => _colors.primary; Color? get indicatorColor => _colors.primary;
...@@ -2224,7 +2256,7 @@ class _TabsPrimaryDefaultsM3 extends TabBarTheme { ...@@ -2224,7 +2256,7 @@ class _TabsPrimaryDefaultsM3 extends TabBarTheme {
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory; InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
@override @override
TabAlignment? get tabAlignment => isScrollable ? TabAlignment.start : TabAlignment.fill; TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill;
static double indicatorWeight = 3.0; static double indicatorWeight = 3.0;
} }
...@@ -2241,6 +2273,9 @@ class _TabsSecondaryDefaultsM3 extends TabBarTheme { ...@@ -2241,6 +2273,9 @@ class _TabsSecondaryDefaultsM3 extends TabBarTheme {
@override @override
Color? get dividerColor => _colors.surfaceVariant; Color? get dividerColor => _colors.surfaceVariant;
@override
double? get dividerHeight => 1.0;
@override @override
Color? get indicatorColor => _colors.primary; Color? get indicatorColor => _colors.primary;
...@@ -2288,7 +2323,7 @@ class _TabsSecondaryDefaultsM3 extends TabBarTheme { ...@@ -2288,7 +2323,7 @@ class _TabsSecondaryDefaultsM3 extends TabBarTheme {
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory; InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
@override @override
TabAlignment? get tabAlignment => isScrollable ? TabAlignment.start : TabAlignment.fill; TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill;
} }
// END GENERATED TOKEN PROPERTIES - Tabs // END GENERATED TOKEN PROPERTIES - Tabs
...@@ -88,6 +88,7 @@ void main() { ...@@ -88,6 +88,7 @@ void main() {
expect(const TabBarTheme().indicatorColor, null); expect(const TabBarTheme().indicatorColor, null);
expect(const TabBarTheme().indicatorSize, null); expect(const TabBarTheme().indicatorSize, null);
expect(const TabBarTheme().dividerColor, null); expect(const TabBarTheme().dividerColor, null);
expect(const TabBarTheme().dividerHeight, null);
expect(const TabBarTheme().labelColor, null); expect(const TabBarTheme().labelColor, null);
expect(const TabBarTheme().labelPadding, null); expect(const TabBarTheme().labelPadding, null);
expect(const TabBarTheme().labelStyle, null); expect(const TabBarTheme().labelStyle, null);
...@@ -125,27 +126,32 @@ void main() { ...@@ -125,27 +126,32 @@ void main() {
final Rect tabBar = tester.getRect(find.byType(TabBar)); final Rect tabBar = tester.getRect(find.byType(TabBar));
final Rect tabOneRect = tester.getRect(find.byKey(_sizedTabs[0].key!)); final Rect tabOneRect = tester.getRect(find.byKey(_sizedTabs[0].key!));
final Rect tabTwoRect = tester.getRect(find.byKey(_sizedTabs[1].key!)); final Rect tabTwoRect = tester.getRect(find.byKey(_sizedTabs[1].key!));
const double tabStartOffset = 52.0;
// Verify tabOne coordinates. // Verify tabOne coordinates.
expect(tabOneRect.left, equals(kTabLabelPadding.left)); expect(tabOneRect.left, equals(kTabLabelPadding.left + tabStartOffset));
expect(tabOneRect.top, equals(kTabLabelPadding.top)); expect(tabOneRect.top, equals(kTabLabelPadding.top));
expect(tabOneRect.bottom, equals(tabBar.bottom - kTabLabelPadding.bottom - indicatorWeight)); expect(tabOneRect.bottom, equals(tabBar.bottom - kTabLabelPadding.bottom - indicatorWeight));
// Verify tabTwo coordinates. // 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.top, equals(kTabLabelPadding.top));
expect(tabTwoRect.bottom, equals(tabBar.bottom - kTabLabelPadding.bottom - indicatorWeight)); 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)); 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)); final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
expect( expect(
tabBarBox, tabBarBox,
paints paints
..line(color: theme.colorScheme.surfaceVariant) ..line(
// Indicator is a rrect in the primary tab bar. color: theme.colorScheme.surfaceVariant,
strokeWidth: 1.0,
)
..rrect(color: theme.colorScheme.primary), ..rrect(color: theme.colorScheme.primary),
); );
}); });
...@@ -176,27 +182,32 @@ void main() { ...@@ -176,27 +182,32 @@ void main() {
final Rect tabBar = tester.getRect(find.byType(TabBar)); final Rect tabBar = tester.getRect(find.byType(TabBar));
final Rect tabOneRect = tester.getRect(find.byKey(_sizedTabs[0].key!)); final Rect tabOneRect = tester.getRect(find.byKey(_sizedTabs[0].key!));
final Rect tabTwoRect = tester.getRect(find.byKey(_sizedTabs[1].key!)); final Rect tabTwoRect = tester.getRect(find.byKey(_sizedTabs[1].key!));
const double tabStartOffset = 52.0;
// Verify tabOne coordinates. // Verify tabOne coordinates.
expect(tabOneRect.left, equals(kTabLabelPadding.left)); expect(tabOneRect.left, equals(kTabLabelPadding.left + tabStartOffset));
expect(tabOneRect.top, equals(kTabLabelPadding.top)); expect(tabOneRect.top, equals(kTabLabelPadding.top));
expect(tabOneRect.bottom, equals(tabBar.bottom - kTabLabelPadding.bottom - indicatorWeight)); expect(tabOneRect.bottom, equals(tabBar.bottom - kTabLabelPadding.bottom - indicatorWeight));
// Verify tabTwo coordinates. // 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.top, equals(kTabLabelPadding.top));
expect(tabTwoRect.bottom, equals(tabBar.bottom - kTabLabelPadding.bottom - indicatorWeight)); 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)); 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)); final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
expect( expect(
tabBarBox, tabBarBox,
paints paints
..line(color: theme.colorScheme.surfaceVariant) ..line(
// Indicator is a line in the secondary tab bar. color: theme.colorScheme.surfaceVariant,
strokeWidth: 1.0,
)
..line(color: theme.colorScheme.primary), ..line(color: theme.colorScheme.primary),
); );
}); });
...@@ -379,7 +390,7 @@ void main() { ...@@ -379,7 +390,7 @@ void main() {
expect(iconRenderObject.text.style!.color, equals(unselectedLabelColor)); 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 tester.pumpWidget(buildTabBar(useMaterial3: true, isScrollable: true));
await expectLater( await expectLater(
...@@ -388,12 +399,12 @@ void main() { ...@@ -388,12 +399,12 @@ void main() {
); );
}); });
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 tester.pumpWidget(buildTabBar(useMaterial3: true, isScrollable: true));
await expectLater( await expectLater(
find.byKey(_painterKey), find.byKey(_painterKey),
matchesGoldenFile('tab_bar.default.tab_indicator_size.png'), matchesGoldenFile('tab_bar_secondary.default.tab_indicator_size.png'),
); );
}); });
...@@ -547,11 +558,12 @@ void main() { ...@@ -547,11 +558,12 @@ void main() {
expect( expect(
tabBarBox, tabBarBox,
paints paints
// Divider // Divider.
..line( ..line(
color: theme.colorScheme.surfaceVariant, color: theme.colorScheme.surfaceVariant,
strokeWidth: 1.0,
) )
// Tab indicator // Tab indicator.
..line( ..line(
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
strokeWidth: indicatorWeight, strokeWidth: indicatorWeight,
...@@ -599,9 +611,10 @@ void main() { ...@@ -599,9 +611,10 @@ void main() {
expect( expect(
tabBarBox, tabBarBox,
paints paints
// Divider // Divider.
..line( ..line(
color: theme.colorScheme.surfaceVariant, color: theme.colorScheme.surfaceVariant,
strokeWidth: 1.0,
) )
// Tab indicator // Tab indicator
..line( ..line(
...@@ -613,6 +626,202 @@ void main() { ...@@ -613,6 +626,202 @@ void main() {
); );
}); });
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', () { group('Material 2', () {
// These tests are only relevant for Material 2. Once Material 2 // These tests are only relevant for Material 2. Once Material 2
// support is deprecated and the APIs are removed, these tests // support is deprecated and the APIs are removed, these tests
...@@ -690,7 +899,7 @@ void main() { ...@@ -690,7 +899,7 @@ void main() {
expect(tabTwoRect.top, equals(kTabLabelPadding.top)); expect(tabTwoRect.top, equals(kTabLabelPadding.top));
expect(tabTwoRect.bottom, equals(tabBar.bottom - kTabLabelPadding.bottom - indicatorWeight)); 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)); expect(tabOneRect.right, equals(tabTwoRect.left - kTabLabelPadding.left - kTabLabelPadding.right));
// Test default indicator color. // Test default indicator color.
...@@ -804,5 +1013,68 @@ void main() { ...@@ -804,5 +1013,68 @@ void main() {
), ),
); );
}); });
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));
});
}); });
} }
...@@ -5951,11 +5951,12 @@ void main() { ...@@ -5951,11 +5951,12 @@ void main() {
testWidgets('Default TabAlignment', (WidgetTester tester) async { testWidgets('Default TabAlignment', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true); final ThemeData theme = ThemeData(useMaterial3: true);
final List<String> tabs = <String>['A', 'B']; final List<String> tabs = <String>['A', 'B'];
const double tabStartOffset = 52.0;
// Test default TabAlignment when isScrollable is false. // Test default TabAlignment when isScrollable is false.
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
theme: theme, theme: theme,
home: buildFrame(tabs: tabs, value: 'B'), home: buildFrame(tabs: tabs, value: 'B', useMaterial3: theme.useMaterial3),
)); ));
final Rect tabBar = tester.getRect(find.byType(TabBar)); final Rect tabBar = tester.getRect(find.byType(TabBar));
...@@ -5971,7 +5972,12 @@ void main() { ...@@ -5971,7 +5972,12 @@ void main() {
// Test default TabAlignment when isScrollable is true. // Test default TabAlignment when isScrollable is true.
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
theme: theme, theme: theme,
home: buildFrame(tabs: tabs, value: 'B', isScrollable: true), home: buildFrame(
tabs: tabs,
value: 'B',
isScrollable: true,
useMaterial3: theme.useMaterial3,
),
)); ));
tabOneRect = tester.getRect(find.byType(Tab).first); tabOneRect = tester.getRect(find.byType(Tab).first);
...@@ -5979,8 +5985,8 @@ void main() { ...@@ -5979,8 +5985,8 @@ void main() {
// Tabs should be aligned to the start of the TabBar. // Tabs should be aligned to the start of the TabBar.
tabOneLeft = kTabLabelPadding.left; tabOneLeft = kTabLabelPadding.left;
expect(tabOneRect.left, equals(tabOneLeft)); expect(tabOneRect.left, equals(tabOneLeft + tabStartOffset));
tabTwoRight = kTabLabelPadding.horizontal + tabOneRect.width + kTabLabelPadding.left + tabTwoRect.width; tabTwoRight = kTabLabelPadding.horizontal + tabStartOffset + tabOneRect.width + kTabLabelPadding.left + tabTwoRect.width;
expect(tabTwoRect.right, equals(tabTwoRight)); expect(tabTwoRect.right, equals(tabTwoRight));
}); });
...@@ -6042,6 +6048,220 @@ void main() { ...@@ -6042,6 +6048,220 @@ void main() {
expect(tester.takeException(), isAssertionError); expect(tester.takeException(), isAssertionError);
}); });
testWidgets('TabAlignment updates tabs alignment (non-scrollable TabBar)', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true);
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'),
));
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(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));
});
testWidgets('TabAlignment updates tabs alignment (scrollable TabBar)', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true);
final List<String> tabs = <String>['A', 'B'];
const double tabStartOffset = 52.0;
// Test TabAlignment.startOffset (default) when isScrollable is true.
await tester.pumpWidget(MaterialApp(
theme: theme,
home: buildFrame(
tabs: tabs,
value: 'B',
isScrollable: true,
useMaterial3: theme.useMaterial3,
),
));
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(MaterialApp(
theme: theme,
home: buildFrame(
tabs: tabs,
value: 'B',
isScrollable: true,
tabAlignment: TabAlignment.start,
useMaterial3: theme.useMaterial3,
),
));
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(MaterialApp(
theme: theme,
home: buildFrame(
tabs: tabs,
value: 'B',
isScrollable: true,
tabAlignment: TabAlignment.center,
useMaterial3: theme.useMaterial3,
),
));
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(MaterialApp(
theme: theme,
home: buildFrame(
tabs: tabs,
value: 'B',
isScrollable: true,
tabAlignment: TabAlignment.startOffset,
useMaterial3: theme.useMaterial3,
),
));
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('TabAlignment.start & TabAlignment.startOffset respects TextDirection.rtl', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true);
final List<String> tabs = <String>['A', 'B'];
const double tabStartOffset = 52.0;
// Test TabAlignment.startOffset (default) when isScrollable is true.
await tester.pumpWidget(MaterialApp(
theme: theme,
home: buildFrame(
tabs: tabs,
value: 'B',
isScrollable: true,
textDirection: TextDirection.rtl,
useMaterial3: theme.useMaterial3,
),
));
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(MaterialApp(
theme: theme,
home: buildFrame(
tabs: tabs,
value: 'B',
isScrollable: true,
tabAlignment: TabAlignment.start,
textDirection: TextDirection.rtl,
useMaterial3: theme.useMaterial3,
),
));
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(MaterialApp(
theme: theme,
home: buildFrame(
tabs: tabs,
value: 'B',
isScrollable: true,
tabAlignment: TabAlignment.startOffset,
textDirection: TextDirection.rtl,
useMaterial3: theme.useMaterial3,
),
));
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));
});
group('Material 2', () { group('Material 2', () {
// These tests are only relevant for Material 2. Once Material 2 // These tests are only relevant for Material 2. Once Material 2
// support is deprecated and the APIs are removed, these tests // support is deprecated and the APIs are removed, these tests
...@@ -6103,14 +6323,15 @@ void main() { ...@@ -6103,14 +6323,15 @@ void main() {
}); });
testWidgets('Material3 - TabBar inherits the dividerColor of TabBarTheme', (WidgetTester tester) async { testWidgets('Material3 - TabBar inherits the dividerColor of TabBarTheme', (WidgetTester tester) async {
const Color dividerColor = Colors.yellow; const Color dividerColor = Color(0xff00ff00);
final ThemeData theme = ThemeData(
useMaterial3: true,
tabBarTheme: const TabBarTheme(dividerColor: dividerColor),
);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
theme: ThemeData( theme: theme,
useMaterial3: true,
tabBarTheme: const TabBarTheme(dividerColor: dividerColor),
),
home: Scaffold( home: Scaffold(
appBar: AppBar( appBar: AppBar(
bottom: TabBar( bottom: TabBar(
...@@ -6126,10 +6347,9 @@ void main() { ...@@ -6126,10 +6347,9 @@ void main() {
), ),
); );
// Test painter's divider color. final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
final CustomPaint paint = tester.widget<CustomPaint>(find.byType(CustomPaint).last); // Test divider color.
// ignore: avoid_dynamic_calls expect(tabBarBox, paints..line(color: dividerColor));
expect((paint.painter as dynamic).dividerColor, dividerColor);
}); });
testWidgets('Default TabAlignment', (WidgetTester tester) async { testWidgets('Default TabAlignment', (WidgetTester tester) async {
...@@ -6259,6 +6479,43 @@ void main() { ...@@ -6259,6 +6479,43 @@ void main() {
), ),
); );
}); });
testWidgets('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));
});
}); });
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment