Unverified Commit 2c71881f authored by Taha Tesser's avatar Taha Tesser Committed by GitHub
Browse files

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)
parent 9c471a94
No related merge requests found
Showing with 967 additions and 95 deletions
+967 -95
......@@ -530,6 +530,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.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.color,
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,
......
......@@ -24,6 +24,9 @@ class _${blockName}PrimaryDefaultsM3 extends TabBarTheme {
@override
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")};
......@@ -71,7 +74,7 @@ class _${blockName}PrimaryDefaultsM3 extends TabBarTheme {
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')};
}
......@@ -88,6 +91,9 @@ class _${blockName}SecondaryDefaultsM3 extends TabBarTheme {
@override
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")};
......@@ -135,7 +141,7 @@ class _${blockName}SecondaryDefaultsM3 extends TabBarTheme {
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
@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 {
this.indicatorColor,
this.indicatorSize,
this.dividerColor,
this.dividerHeight,
this.labelColor,
this.labelPadding,
this.labelStyle,
......@@ -55,6 +56,9 @@ class TabBarTheme with Diagnosticable {
/// 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 @@ class TabBarTheme with Diagnosticable {
Color? indicatorColor,
TabBarIndicatorSize? indicatorSize,
Color? dividerColor,
double? dividerHeight,
Color? labelColor,
EdgeInsetsGeometry? labelPadding,
TextStyle? labelStyle,
......@@ -116,6 +121,7 @@ class TabBarTheme with Diagnosticable {
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 @@ class TabBarTheme with Diagnosticable {
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 @@ class TabBarTheme with Diagnosticable {
indicatorColor,
indicatorSize,
dividerColor,
dividerHeight,
labelColor,
labelPadding,
labelStyle,
......@@ -189,6 +197,7 @@ class TabBarTheme with Diagnosticable {
&& other.indicatorColor == indicatorColor
&& other.indicatorSize == indicatorSize
&& other.dividerColor == dividerColor
&& other.dividerHeight == dividerHeight
&& other.labelColor == labelColor
&& other.labelPadding == labelPadding
&& other.labelStyle == labelStyle
......
......@@ -387,6 +387,39 @@ double _indexChangeProgress(TabController controller) {
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 @@ class _IndicatorPainter extends CustomPainter {
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 @@ class _IndicatorPainter extends CustomPainter {
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 @@ class _IndicatorPainter extends CustomPainter {
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 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
this.indicator,
this.indicatorSize,
this.dividerColor,
this.dividerHeight,
this.labelColor,
this.labelStyle,
this.labelPadding,
......@@ -768,6 +808,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
this.indicator,
this.indicatorSize,
this.dividerColor,
this.dividerHeight,
this.labelColor,
this.labelStyle,
this.labelPadding,
......@@ -895,6 +936,13 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
/// [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 @@ class _TabBarState extends State<TabBar> {
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 @@ class _TabBarState extends State<TabBar> {
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 @@ class _TabBarState extends State<TabBar> {
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 @@ class _TabBarState extends State<TabBar> {
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 @@ class _TabBarState extends State<TabBar> {
);
}
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 @@ class _TabBarState extends State<TabBar> {
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!,
......@@ -2177,6 +2247,9 @@ class _TabsPrimaryDefaultsM3 extends TabBarTheme {
@override
Color? get dividerColor => _colors.surfaceVariant;
@override
double? get dividerHeight => 1.0;
@override
Color? get indicatorColor => _colors.primary;
......@@ -2224,7 +2297,7 @@ class _TabsPrimaryDefaultsM3 extends TabBarTheme {
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;
}
......@@ -2241,6 +2314,9 @@ class _TabsSecondaryDefaultsM3 extends TabBarTheme {
@override
Color? get dividerColor => _colors.surfaceVariant;
@override
double? get dividerHeight => 1.0;
@override
Color? get indicatorColor => _colors.primary;
......@@ -2288,7 +2364,7 @@ class _TabsSecondaryDefaultsM3 extends TabBarTheme {
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
......@@ -90,6 +90,7 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(tabBarTheme: tabBarTheme),
theme: ThemeData(tabBarTheme: tabBarTheme, useMaterial3: false),
home: Scaffold(body:
RepaintBoundary(
key: _painterKey,
......@@ -369,6 +380,61 @@ void main() {
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 @@ void main() {
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 @@ 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 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 @@ void main() {
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 @@ void main() {
expect(
tabBarBox,
paints
// Divider
// Divider.
..line(
color: theme.colorScheme.surfaceVariant,
strokeWidth: 1.0,
)
// Tab indicator
..line(
......@@ -615,6 +683,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', () {
// 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 @@ void main() {
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 @@ 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));
});
});
}
This diff is collapsed.
Supports Markdown
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