Open5

FlutterのTextの高さがiOSとAndroidで異なる

matsuchiyomatsuchiyo

タイトルの通り、FlutterのTextの高さがiOSとAndroidで異なる現象に遭遇したので、原因と対処法について調べてみようと思います。

前提

flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.19.0, on macOS 13.5.1 22G90 darwin-x64, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 15.2)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2023.2)
[✓] VS Code (version 1.92.2)
[✓] Connected device (4 available)
[✓] Network resources

• No issues found!
matsuchiyomatsuchiyo

ソース

以下の通り、fontSize, height, fontFamilyを明示的に指定しています。

pubspec.yamlの一部:

flutter:
  fonts:
    - family: NotoSans
      fonts:
        - asset: fonts/NotoSans-Regular.ttf
        - asset: fonts/NotoSans-Bold.ttf
          weight: 700

main.dart:

import 'package:flutter/cupertino.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const CupertinoApp(
      home: MyHomePage(title: "Home"),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  static const text = 'ABCDefgh';
  static const textStyle = TextStyle(
    fontFamily: 'NotoSans',
    fontWeight: FontWeight.w400,
    fontSize: 48,
    height: 48.0 / 48.0,
  );

  late Size calculatedTextSize;

  @override
  void initState() {
    super.initState();
    calculatedTextSize = calculateTextSize(text, textStyle);
  }

  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: const CupertinoNavigationBar(middle: Text('Home')),
      child: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            const Text(
              text,
              style: textStyle,
            ),
            const SizedBox(height: 24),
            Text(
              'calculatedHeight: ${calculatedTextSize.height}',
            ),
          ],
        ),
      ),
    );
  }
}

Size calculateTextSize(String text, TextStyle style, { double? maxWidth, int? maxLines }) {
  final textPainter = TextPainter(
    text: TextSpan(
      text: text,
      style: style,
    ),
    maxLines: maxLines ?? 1,
    textDirection: TextDirection.ltr,
  )..layout(minWidth: 0, maxWidth: maxWidth ?? double.infinity);
  return textPainter.size;
}
matsuchiyomatsuchiyo

実行結果

I expected height of the text to be 48. On iOS this code works as expected. On Android it does not works as expected, the height was 53.

OS Height Works as expected Screenshot Flutter Inspector
iOS 48 Yes
Android10(Galaxy A7) 53 No
matsuchiyomatsuchiyo

Androidのデバイスの種類によって、挙動が変わるかもと思い、他のAndroidデバイスでも確認してみたところ、今度は予想に反して、上記の「Android10(GalaxyA7)」と異なり、Textの高さが期待通りの48になった。

OS Height Works as expected Screenshot Flutter Inspector
Android10(Pixel3a Emulator) 48 Yes
matsuchiyomatsuchiyo

実行結果まとめ

上記の実行結果をまとめると、Androidではデバイスによって、Textの高さが期待通りになったり、ならなかったりする。

原因

同じAndroid OSでも、デバイスによってフォントレンダリングエンジンに違いがあるためみたい。

  • Flutterのissueのコメントによると、Flutterでは異なるプラットフォーム間もしくは異なるデバイス間で、フォントが同じように表示されることは保証できないとこと。

    The font rendering differences could likely come from CoreText vs Freetype. We cannot promise identical font rendering across platforms (or even across different machines on the same platform).
    https://github.com/flutter/flutter/issues/145069#issuecomment-2004580503

    • このissueではiOSとAndroidで同じ表示にならないと言っているけど、Androidの異なるデバイス間で表示が異なる場合についても、このコメントは当てはまると思う。
    • 補足: 上記コメント内の「CoreText」はiOSの文字列を表示したりフォントを制御したりするための低レベル(低レイヤー)のインターフェイスを提供するライブラリ。「Core Text layout engine」って言っているから、フォントエンジンとも捉えられそう。

      Core Text provides a low-level programming interface for laying out text and handling fonts. The Core Text layout engine is designed for high performance, ease of use, and close integration with Core Foundation.
      https://developer.apple.com/documentation/coretext/

    • 補足: 上記コメント内の「Freetype」は...

      FreeType(フリータイプ)は、フォントエンジンを実装したオープンソースのライブラリである。
      https://ja.wikipedia.org/wiki/FreeType#cite_note-3

      • さらに同じページによると、AndroidやiOSでも利用されているらしい。ただし、この情報のソースが2013年であることに注意が必要。

        FreeBSDやAndroid、iOSなどのオペレーティングシステムにおけるフォント描画にも採用されている[3]。

対処法

ひとまず、Androidの場合、期待するTextの高さ(height: textStyle.fontSize! * textStyle.height!)のSizedBoxで、Textをラップしてみる。

  • 今回、高さを固定したいTextの行数が1行なので、この方法でごまかせる。
    • しかし、複数行になってくると、文字の一部が欠けてしまいそう。
      • その場合、AndroidのときはfontSizeまたはheightを小さくするなどの対応が必要そう。
        • ただ、デバイスごとに挙動が異なるから、このやり方ではだめそう。
      • 表示したいTextを何らかの方法でみえない部分に表示して、globalKeyでTextに対応するRenderBoxを取得。そこから今の高さを取得。望みの高さが得られるまで、fontSizeもしくはheightを小さくする?大変そう。