Introduce `TabBar.textScaler` for tab label upper text scale limit (#147232)
fixes [Tab is hardcoding the height of the icons and the text ](https://github.com/flutter/flutter/issues/13322)
### Description
This PR introduces `TabBar.textScaler` to provide upper text scale limit for tab label.
### Code sample
<details>
<summary>expand to view the code sample</summary>
```dart
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: MediaQuery(
data: const MediaQueryData(textScaler: TextScaler.linear(3.0)),
child: DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: const Text('Sample'),
bottom: const TabBar(
textScaler: TextScaler.linear(2.0),
tabs: <Widget>[
Tab(text: 'Tab 1'),
Tab(text: 'Tab 2'),
Tab(text: 'Tab 3'),
],
),
),
floatingActionButton: Builder(builder: (BuildContext context) {
return FloatingActionButton(
onPressed: () {
print(MediaQuery.textScalerOf(context));
},
child: const Icon(Icons.add),
);
}),
),
),
),
);
}
}
```
</details>
### Without `TabBar.textScaler`

### With `TabBar.textScaler`
```dart
bottom: const TabBar(
textScaler: TextScaler.linear(2.0),
tabs: <Widget>[
Tab(text: 'Tab 1'),
Tab(text: 'Tab 2'),
Tab(text: 'Tab 3'),
],
),
```

diff --git a/packages/flutter/lib/src/material/tab_bar_theme.dart b/packages/flutter/lib/src/material/tab_bar_theme.dart
index 1f45037..8d4f6a8 100644
--- a/packages/flutter/lib/src/material/tab_bar_theme.dart
+++ b/packages/flutter/lib/src/material/tab_bar_theme.dart
@@ -42,6 +42,7 @@
this.splashFactory,
this.mouseCursor,
this.tabAlignment,
+ this.textScaler,
});
/// Overrides the default value for [TabBar.indicator].
@@ -98,6 +99,9 @@
/// Overrides the default value for [TabBar.tabAlignment].
final TabAlignment? tabAlignment;
+ /// Overrides the default value for [TabBar.textScaler].
+ final TextScaler? textScaler;
+
/// Creates a copy of this object but with the given fields replaced with the
/// new values.
TabBarTheme copyWith({
@@ -115,6 +119,7 @@
InteractiveInkFeatureFactory? splashFactory,
MaterialStateProperty<MouseCursor?>? mouseCursor,
TabAlignment? tabAlignment,
+ TextScaler? textScaler,
}) {
return TabBarTheme(
indicator: indicator ?? this.indicator,
@@ -131,6 +136,7 @@
splashFactory: splashFactory ?? this.splashFactory,
mouseCursor: mouseCursor ?? this.mouseCursor,
tabAlignment: tabAlignment ?? this.tabAlignment,
+ textScaler: textScaler ?? this.textScaler,
);
}
@@ -161,6 +167,7 @@
splashFactory: t < 0.5 ? a.splashFactory : b.splashFactory,
mouseCursor: t < 0.5 ? a.mouseCursor : b.mouseCursor,
tabAlignment: t < 0.5 ? a.tabAlignment : b.tabAlignment,
+ textScaler: t < 0.5 ? a.textScaler : b.textScaler,
);
}
@@ -180,6 +187,7 @@
splashFactory,
mouseCursor,
tabAlignment,
+ textScaler,
);
@override
@@ -204,6 +212,7 @@
&& other.overlayColor == overlayColor
&& other.splashFactory == splashFactory
&& other.mouseCursor == mouseCursor
- && other.tabAlignment == tabAlignment;
+ && other.tabAlignment == tabAlignment
+ && other.textScaler == textScaler;
}
}
diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart
index 96e3ad7..8651827 100644
--- a/packages/flutter/lib/src/material/tabs.dart
+++ b/packages/flutter/lib/src/material/tabs.dart
@@ -864,6 +864,7 @@
this.splashFactory,
this.splashBorderRadius,
this.tabAlignment,
+ this.textScaler,
}) : _isPrimary = true,
assert(indicator != null || (indicatorWeight > 0.0));
@@ -915,6 +916,7 @@
this.splashFactory,
this.splashBorderRadius,
this.tabAlignment,
+ this.textScaler,
}) : _isPrimary = false,
assert(indicator != null || (indicatorWeight > 0.0));
@@ -1246,6 +1248,16 @@
/// otherwise [TabAlignment.fill] is used.
final TabAlignment? tabAlignment;
+ /// Specifies the text scaling behavior for the [Tab] label.
+ ///
+ /// If this is null, then the value of [TabBarTheme.textScaler] is used. If that is
+ /// also null, then the text scaling behavior is determined by the [MediaQueryData.textScaler]
+ /// from the ambient [MediaQuery], or 1.0 if there is no [MediaQuery] in scope.
+ ///
+ /// See also:
+ /// * [TextScaler], which is used to scale text based on the device's text scale factor.
+ final TextScaler? textScaler;
+
/// A size whose height depends on if the tabs have both icons and text.
///
/// [AppBar] uses this size to compute its own preferred size.
@@ -1828,7 +1840,10 @@
);
}
- return tabBar;
+ return MediaQuery(
+ data: MediaQuery.of(context).copyWith(textScaler: widget.textScaler ?? tabBarTheme.textScaler),
+ child: tabBar,
+ );
}
}
diff --git a/packages/flutter/test/material/tab_bar_theme_test.dart b/packages/flutter/test/material/tab_bar_theme_test.dart
index 09193d1..2fda543 100644
--- a/packages/flutter/test/material/tab_bar_theme_test.dart
+++ b/packages/flutter/test/material/tab_bar_theme_test.dart
@@ -7,6 +7,7 @@
@Tags(<String>['reduced-test-set'])
library;
+import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
@@ -1539,4 +1540,48 @@
expect(selectedTextStyle.color, selectedColor);
expect(unselectedTextStyle.color, unselectedColor);
});
+
+ testWidgets('TabBarTheme.textScaler overrides tab label text scale, textScaleFactor = noScaling, 1.75, 2.0', (WidgetTester tester) async {
+ final List<String> tabs = <String>['Tab 1', 'Tab 2'];
+
+ Widget buildTabs({ TextScaler? textScaler }) {
+ return MaterialApp(
+ theme: ThemeData(
+ tabBarTheme: TabBarTheme(
+ textScaler: textScaler,
+ ),
+ ),
+ home: MediaQuery(
+ data: const MediaQueryData(textScaler: TextScaler.linear(3.0)),
+ child: DefaultTabController(
+ length: tabs.length,
+ child: Scaffold(
+ appBar: AppBar(
+ bottom: TabBar(
+ tabs: tabs.map((String tab) => Tab(text: tab)).toList(),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ await tester.pumpWidget(buildTabs(textScaler: TextScaler.noScaling));
+
+ Size labelSize = tester.getSize(find.text('Tab 1'));
+ expect(labelSize, equals(const Size(70.5, 20.0)));
+
+ await tester.pumpWidget(buildTabs(textScaler: const TextScaler.linear(1.75)));
+ await tester.pumpAndSettle();
+
+ labelSize = tester.getSize(find.text('Tab 1'));
+ expect(labelSize, equals(const Size(123.0, 35.0)));
+
+ await tester.pumpWidget(buildTabs(textScaler: const TextScaler.linear(2.0)));
+ await tester.pumpAndSettle();
+
+ labelSize = tester.getSize(find.text('Tab 1'));
+ expect(labelSize, equals(const Size(140.5, 40.0)));
+ }, skip: isBrowser && !isSkiaWeb); // https://github.com/flutter/flutter/issues/87543
}
diff --git a/packages/flutter/test/material/tabs_test.dart b/packages/flutter/test/material/tabs_test.dart
index 89f5a4f..b1e32a1 100644
--- a/packages/flutter/test/material/tabs_test.dart
+++ b/packages/flutter/test/material/tabs_test.dart
@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
@@ -7119,4 +7120,42 @@
expect(config.textDirection, TextDirection.rtl);
expect(config.devicePixelRatio, 2.33);
});
+
+ testWidgets('TabBar.textScaler overrides tab label text scale, textScaleFactor = noScaling, 1.75, 2.0', (WidgetTester tester) async {
+ final List<String> tabs = <String>['Tab 1', 'Tab 2'];
+
+ Widget buildTabs({ TextScaler? textScaler }) {
+ return MaterialApp(
+ home: MediaQuery(
+ data: const MediaQueryData(textScaler: TextScaler.linear(3.0)),
+ child: DefaultTabController(
+ length: tabs.length,
+ child: Scaffold(
+ appBar: AppBar(
+ bottom: TabBar(
+ textScaler: textScaler,
+ tabs: tabs.map((String tab) => Tab(text: tab)).toList(),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ await tester.pumpWidget(buildTabs(textScaler: TextScaler.noScaling));
+
+ Size labelSize = tester.getSize(find.text('Tab 1'));
+ expect(labelSize, equals(const Size(70.5, 20.0)));
+
+ await tester.pumpWidget(buildTabs(textScaler: const TextScaler.linear(1.75)));
+
+ labelSize = tester.getSize(find.text('Tab 1'));
+ expect(labelSize, equals(const Size(123.0, 35.0)));
+
+ await tester.pumpWidget(buildTabs(textScaler: const TextScaler.linear(2.0)));
+
+ labelSize = tester.getSize(find.text('Tab 1'));
+ expect(labelSize, equals(const Size(140.5, 40.0)));
+ }, skip: isBrowser && !isSkiaWeb); // https://github.com/flutter/flutter/issues/87543
}