flutter / flutter

Flutter makes it easy and fast to build beautiful apps for mobile and beyond
https://flutter.dev
BSD 3-Clause "New" or "Revised" License
166.72k stars 27.62k forks source link

Flutter's text rendering has different letter spacing from iOS native #150824

Open dkwingsmt opened 5 months ago

dkwingsmt commented 5 months ago

Flutter's Text() has noticeably different letter spacing from iOS's Text() by default.

Background

iOS renders text with "SF Pro Text" font for font size <20, and "SF Pro Display" font for font size >= 20. In Flutter they're called "CupertinoSystemText" and "CupertinoSystemDisplay" respectively.

Comparison with current Flutter

(light grey Flutter, black native iOS)

image

Adjustment

I tried to apply letterSpacing to Flutter's text to get a result close to the native one. The needed adjustment turns out to be a non-linear curve, which is interpolated as a cubic polynomial.

image image

Comparison after adjustment

(light grey Flutter, black native iOS)

image

(Yes, I know line height at small font size still deviates noticeably.)

Note

Reproduction code

Dart code ```dart import 'dart:math'; import 'package:flutter/cupertino.dart'; void main() => runApp(const Main()); class Main extends StatelessWidget { const Main({super.key}); @override Widget build(BuildContext context) { return CupertinoPageScaffold( child: SafeArea( child: Center( child: Transform.translate( offset: const Offset(0, -0), child: Container( width: 400, height: 500, decoration: BoxDecoration( border: Border.all(), ), child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ ...[3, 4, 5, 7, 8, 9, 10, 11, 13, 15, 17, 19].map( (double i) => Text( 'CupertinoSystemText size ${i.round()}', style: TextStyle( fontFamily: 'CupertinoSystemText', fontSize: i, letterSpacing: smallFontSpacingMap(i), ), ) ), ...[20, 22, 25, 28, 32, 36].map( (double i) => Text( 'SystemDisplay size ${i.round()}', style: TextStyle( fontFamily: 'CupertinoSystemDisplay', fontSize: i, letterSpacing: bigFontSpacingMap(i), ), ) ), ], ), ), ), ), ), ), ); } } double smallFontSpacingMap(double i) { return switch (i) { // Experimental value 3 => 0.11, 4 => 0.16, 5 => 0.178, 7 => 0.2, 8 => 0.173, 9 => 0.142, 10 => 0.1, 11 => 0.03, 13 => -0.05, 15 => -0.21, 17 => -0.44, 19 => -0.54, // Interpolation curve _ => -0.158 + 0.123 * i * (1 - 0.0983 * i * (1 - 0.0195 * i)), }; } double bigFontSpacingMap(double i) { return switch (i) { // Experimental value 20 => 0.395, 22 => 0.325, 25 => 0.26, 28 => 0.34, 32 => 0.355, 36 => 0.335, // Interpolation curve _ => ((double j) => 7.05 - 0.73 * i * (1 - 0.0353 * i * (1 - 0.0116 * i)) )(min(36, i)), // Extrapolate flatly above 36 }; } ```
SwiftUI code ```swift import SwiftUI struct ContentView: View { var body: some View { Rectangle() .fill(Color.white) .frame(width: 400, height: 500) .overlay( VStack { ForEach([3, 4, 5, 7, 8, 9, 10, 11, 13, 15, 17, 19], id: \.self) { i in Text(String(format: "CupertinoSystemText size %d", Int(i))) .font(.system(size: i)) .foregroundColor(.black) } ForEach([20, 22, 25, 28, 32, 36], id: \.self) { i in Text(String(format: "SystemDisplay size %d", Int(i))) .font(.system(size: i)) .foregroundColor(.black) } } ) } } #Preview { ContentView() } ```
dkwingsmt commented 5 months ago

cc @jmagman

jmagman commented 5 months ago

@jason-simmons do you know how the letter spacing is controlled?

jason-simmons commented 5 months ago

The initial positioning of glyphs is done by HarfBuzz.

Additional letter spacing specified by TextStyle.letterSpacing is applied by SkParagraph's Run::addSpacesEvenly.

@Rusino

Rusino commented 5 months ago

Yes, positioning is done by harfbuzz (and is defined by harfbuzz logic and the font metrics). The letter spacing is done by SkParagraph.

makoConstruct commented 1 month ago

which complained about HarfBuzz's text tracking deviates from that of macOS in 2019. Although I don't quite get the content in that page and whether the issue was considered fixed.

The harfbuzz maintainers say they decided to keep track of the issue via this chrome issue, which was marked fixed January 2020.