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)  ### Fix (default tab alignment)  ### 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> 
This commit is contained in:
parent
0da8012cc7
commit
32fde139bc
@ -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,
|
|||||||
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 {
|
|||||||
@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 {
|
|||||||
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 {
|
|||||||
@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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
/// 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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
indicatorColor,
|
indicatorColor,
|
||||||
indicatorSize,
|
indicatorSize,
|
||||||
dividerColor,
|
dividerColor,
|
||||||
|
dividerHeight,
|
||||||
labelColor,
|
labelColor,
|
||||||
labelPadding,
|
labelPadding,
|
||||||
labelStyle,
|
labelStyle,
|
||||||
@ -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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
/// [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
|
||||||
@ -1154,8 +1169,8 @@ class _TabBarState extends State<TabBar> {
|
|||||||
TabBarTheme get _defaults {
|
TabBarTheme get _defaults {
|
||||||
if (Theme.of(context).useMaterial3) {
|
if (Theme.of(context).useMaterial3) {
|
||||||
return widget._isPrimary
|
return widget._isPrimary
|
||||||
? _TabsPrimaryDefaultsM3(context, widget.isScrollable)
|
? _TabsPrimaryDefaultsM3(context, widget.isScrollable)
|
||||||
: _TabsSecondaryDefaultsM3(context, widget.isScrollable);
|
: _TabsSecondaryDefaultsM3(context, widget.isScrollable);
|
||||||
} else {
|
} else {
|
||||||
return _TabsDefaultsM2(context, widget.isScrollable);
|
return _TabsDefaultsM2(context, widget.isScrollable);
|
||||||
}
|
}
|
||||||
@ -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> {
|
|||||||
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> {
|
|||||||
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 {
|
|||||||
@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 {
|
|||||||
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 {
|
|||||||
@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 {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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,29 +182,34 @@ 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),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Tab bar theme overrides label color (selected)', (WidgetTester tester) async {
|
testWidgets('Tab bar theme overrides label color (selected)', (WidgetTester tester) async {
|
||||||
@ -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() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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() {
|
|||||||
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() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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() {
|
|||||||
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() {
|
|||||||
// 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() {
|
|||||||
|
|
||||||
// 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() {
|
|||||||
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() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 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() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user