Skip to content

Commit 4998f32

Browse files
committed
text: Add proportionalLetterSpacing
To confirm that [TextStyle.letterSpacing] is taken literally as logical pixels, as its dartdoc suggests it is, I rendered the text "||" with an extreme [TextStyle.letterSpacing] value of 40. Then it was easy to see that the letter spacing -- the horizontal distance between the two "|" characters -- wasn't changing as I adjusted the text-size slider in my iPhone settings. In the tests, the `textScaleFactors` list is copied from emoji_reaction_test. Perhaps there's a convenient place to define it centrally.
1 parent 6a615da commit 4998f32

File tree

2 files changed

+80
-1
lines changed

2 files changed

+80
-1
lines changed

lib/widgets/text.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,3 +280,22 @@ FontWeight clampVariableFontWeight(double wght) {
280280
/// font's own custom-defined "wght" axis. But it's a great guess,
281281
/// at least without knowledge of the particular font.
282282
double wghtFromFontWeight(FontWeight fontWeight) => fontWeight.value.toDouble();
283+
284+
/// A [TextStyle.letterSpacing] value from a given proportion of the font size.
285+
///
286+
/// Returns [baseFontSize] scaled by the ambient [MediaQueryData.textScaler],
287+
/// multiplied by [proportion] (e.g., 0.01).
288+
///
289+
/// Using [MediaQueryData.textScaler] ensures that [proportion] is still
290+
/// respected when the device font size setting is adjusted.
291+
/// To opt out of this behavior, pass [TextScaler.noScaling] or some other value
292+
/// for [textScaler].
293+
double proportionalLetterSpacing(
294+
BuildContext context,
295+
double proportion, {
296+
required double baseFontSize,
297+
TextScaler? textScaler,
298+
}) {
299+
final effectiveTextScaler = textScaler ?? MediaQuery.textScalerOf(context);
300+
return effectiveTextScaler.scale(baseFontSize) * proportion;
301+
}

test/widgets/text_test.dart

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
import 'package:checks/checks.dart';
32
import 'package:flutter/material.dart';
43
import 'package:flutter_test/flutter_test.dart';
@@ -207,4 +206,65 @@ void main() {
207206
check(clampVariableFontWeight(999)) .equals(FontWeight.w900);
208207
check(clampVariableFontWeight(1000)) .equals(FontWeight.w900);
209208
});
209+
210+
group('proportionalLetterSpacing', () {
211+
Future<void> testLetterSpacing(
212+
String description, {
213+
required double Function(BuildContext context) getValue,
214+
double? ambientTextScaleFactor,
215+
required double expected,
216+
}) async {
217+
testWidgets(description, (WidgetTester tester) async {
218+
if (ambientTextScaleFactor != null) {
219+
tester.platformDispatcher.textScaleFactorTestValue = ambientTextScaleFactor;
220+
addTearDown(tester.platformDispatcher.clearTextScaleFactorTestValue);
221+
}
222+
await tester.pumpWidget(
223+
MaterialApp(
224+
home: Builder(builder: (context) => Text('',
225+
style: TextStyle(letterSpacing: getValue(context))))));
226+
227+
final TextStyle? style = tester.widget<Text>(find.byType(Text)).style;
228+
final actualLetterSpacing = style!.letterSpacing!;
229+
check((actualLetterSpacing - expected).abs()).isLessThan(0.0001);
230+
});
231+
}
232+
233+
// From trying the options on an iPhone 13 Pro running iOS 16.6.1:
234+
const textScaleFactors = <double>[
235+
0.8235, // smallest
236+
1,
237+
1.3529, // largest without using the "Larger Accessibility Sizes" setting
238+
3.1176, // largest
239+
];
240+
241+
testLetterSpacing('smoke 1',
242+
getValue: (context) => proportionalLetterSpacing(context, 0.01, baseFontSize: 14),
243+
expected: 0.14);
244+
245+
testLetterSpacing('smoke 2',
246+
getValue: (context) => proportionalLetterSpacing(context, 0.02, baseFontSize: 16),
247+
expected: 0.32);
248+
249+
for (final textScaleFactor in textScaleFactors) {
250+
testLetterSpacing('ambient text scale factor $textScaleFactor, no override',
251+
ambientTextScaleFactor: textScaleFactor,
252+
getValue: (context) => proportionalLetterSpacing(context, 0.01, baseFontSize: 14),
253+
expected: 0.14 * textScaleFactor);
254+
255+
testLetterSpacing('ambient text scale factor $textScaleFactor, override with no scaling',
256+
ambientTextScaleFactor: textScaleFactor,
257+
getValue: (context) => proportionalLetterSpacing(context,
258+
0.01, baseFontSize: 14, textScaler: TextScaler.noScaling),
259+
expected: 0.14);
260+
261+
final clampingTextScaler = TextScaler.linear(textScaleFactor)
262+
.clamp(minScaleFactor: 0.9, maxScaleFactor: 1.1);
263+
testLetterSpacing('ambient text scale factor $textScaleFactor, override with clamping',
264+
ambientTextScaleFactor: textScaleFactor,
265+
getValue: (context) => proportionalLetterSpacing(context,
266+
0.01, baseFontSize: 14, textScaler: clampingTextScaler),
267+
expected: clampingTextScaler.scale(14) * 0.01);
268+
}
269+
});
210270
}

0 commit comments

Comments
 (0)