Font Metric Mismatch Breaks Vertical Centering in Flutter on iOS and Android
Cross-platform apps inherit whichever font metric each OS trusts, so a font that looks centered on iOS shifts on Android, and a line-height multiplier that fixes Latin text breaks Chinese and Japanese. Teams shipping multilingual Flutter UIs need font-level normalization and auto line heights, not per-platform tweaks.
Font vertical metrics decide how much space a glyph occupies above and below its baseline, and Flutter exposes the differences. iOS reads the hhea table (the font in question reported ascent 1050, descent -274, line gap 0), while Android reads OS/2 typo (750, -250, 324). The same font lands on different baselines with different whitespace on each platform, so a layout vertically centered on iOS sits off-center on Android. The fix is font-level: unify hhea and OS/2 typo to the same ascent, descent, and line gap (normalized here to 1029 / -295 / 0), calibrate the baseline, and leave glyphs, widths, and total line height untouched.
A second failure mode comes from mixing scripts. English, Chinese, and Japanese fonts carry different natural line heights, so one hard-coded line-height multiplier cannot center all of them. Flutter's Material theme does not default to auto; bodyMedium, for instance, applies a line height of 1.43. Setting a fixed height for English centers Latin text but changes the line box and whitespace for Chinese, pushing it off-center again. The proposed global remedy is to set the theme height to kTextHeightNone (value 0), which means native line height, not zero height, letting each language use its own natural metrics.
One font file can render correctly on iOS and incorrectly on Android because the two platforms read different metadata, so widget-level tweaks treat a symptom.
Hard-coding one line-height multiplier only ever fixes one script; native line heights remove the need to tune each language separately.
Normalizing font metrics changes only vertical spacing metadata while leaving glyph shapes and widths intact, which is why the fix holds across platforms.
Adding two metrics that aren't in your table: besides sTypo, OS/2 also has usWinAscent/usWinDescent as a third set, and when bit 7 of fsSelection (USE_TYPO_METRICS) is not set, the spec calls for exactly usWin, not sTypo. Aligning hhea and sTypo without setting that bit will still cause drift when you switch to another layout engine (in fonttools, it's font["OS/2"].fsSelection |= 1<<7). Another number: all three of your groups sum to 1324 (750+250+324, 1050+274+0, 1029+295+0). That means the line height never changed; the issue is purely the baseline's position within the line box. The fix essentially moves Android's baseline from 750 to 1029 — a downward shift of 0.279em relative to the original Android, still 21 units short of iOS's 1050. So this move isn't 'centering', it's 'pushing the text down' — places that were previously aligned via padding will now be off by 0.279em downward. To completely avoid the font's own metrics, setting a global Auto height isn't enough: when falling back to system fonts, their metrics are yet another set. In Flutter, the targeted solution is strut; with forceStrutHeight set to true, the line box only respects (height+leading)×fontSize, regardless of which font is rendering.