Skip to content

Commit 23fe988

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 23fe988

File tree

2 files changed

+79
-1
lines changed

2 files changed

+79
-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: 60 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,64 @@ 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+
}
221+
await tester.pumpWidget(
222+
MaterialApp(
223+
home: Builder(builder: (context) => Text('',
224+
style: TextStyle(letterSpacing: getValue(context))))));
225+
226+
final TextStyle? style = tester.widget<Text>(find.byType(Text)).style;
227+
final actualLetterSpacing = style!.letterSpacing!;
228+
check((actualLetterSpacing - expected).abs()).isLessThan(0.0001);
229+
});
230+
}
231+
232+
// From trying the options on an iPhone 13 Pro running iOS 16.6.1:
233+
const textScaleFactors = <double>[
234+
0.8235, // smallest
235+
1,
236+
1.3529, // largest without using the "Larger Accessibility Sizes" setting
237+
3.1176, // largest
238+
];
239+
240+
testLetterSpacing('smoke 1',
241+
getValue: (context) => proportionalLetterSpacing(context, 0.01, baseFontSize: 14),
242+
expected: 0.14);
243+
244+
testLetterSpacing('smoke 2',
245+
getValue: (context) => proportionalLetterSpacing(context, 0.02, baseFontSize: 16),
246+
expected: 0.32);
247+
248+
for (final textScaleFactor in textScaleFactors) {
249+
testLetterSpacing('ambient text scale factor $textScaleFactor, no override',
250+
ambientTextScaleFactor: textScaleFactor,
251+
getValue: (context) => proportionalLetterSpacing(context, 0.01, baseFontSize: 14),
252+
expected: 0.14 * textScaleFactor);
253+
254+
testLetterSpacing('ambient text scale factor $textScaleFactor, override with no scaling',
255+
ambientTextScaleFactor: textScaleFactor,
256+
getValue: (context) => proportionalLetterSpacing(context,
257+
0.01, baseFontSize: 14, textScaler: TextScaler.noScaling),
258+
expected: 0.14);
259+
260+
final clampingTextScaler = TextScaler.linear(textScaleFactor)
261+
.clamp(minScaleFactor: 0.9, maxScaleFactor: 1.1);
262+
testLetterSpacing('ambient text scale factor $textScaleFactor, override with clamping',
263+
ambientTextScaleFactor: textScaleFactor,
264+
getValue: (context) => proportionalLetterSpacing(context,
265+
0.01, baseFontSize: 14, textScaler: clampingTextScaler),
266+
expected: clampingTextScaler.scale(14) * 0.01);
267+
}
268+
});
210269
}

0 commit comments

Comments
 (0)