74
37

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Flutter】Text Widgetに文字を正しく表示させるための調査結果

Last updated at Posted at 2021-06-11

はじめに

Flutterでアプリ開発をするとき、以下のようにデフォルトの設定でただTextに表示したい文字列とフォントサイズなどが入ったstyleを当てるだけでは実はうまくいかないことがあります。

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'あいうえお',
              style: TextStyle(fontSize: 40),
            ),
          ],
        ),
      ),
    );
  }
}

Screen Shot 2021-06-11 at 9.09.19.png

このままではデザイナーさんが作ったデザインを正しく再現できません。

次の章で詳しく触れますが、デザインと同じフォントサイズでもデザイナーさんがデザインしたテキストが確保している領域と、FlutterのText Widgetが確保している領域に違いがあるため、そのまま使うとサイズの制約が厳しい場合にデザイン通りTextが収まりきらなかったり、縦にTextを並べたときなどにデザインが崩れていきます。

この記事ではなぜそのような問題が起こるのかと、解決策をまとめます。

Textの構造

まずは先程のTextがどのような構造で作られているのか見てみましょう。

InspectorでこのTextを見てみます。

Screen Shot 2021-06-09 at 23.40.32.png

下の図のFont metrics default heightがText Widgetが確保する領域となり、Inspectorで見たときの文字列の周りの水色の四角い枠がそれに当たります。
Alphabetic Baselineが文字列の下の緑っぽい色の線です。
image.png

今回はstylefontFamilyに何も設定していないので、iOSとAndroidそれぞれのプラットフォームのデフォルトのフォントが使われます。

iOSだと和文フォントにヒラギノ角ゴシック、欧文フォントにSan Francisco Pro、Androidだとその端末にデフォルトでインストールされている和文フォントと欧文フォントにRobotoが適用されます。

Text Widgetには、stylestrutStyleなどのパラメータが用意されていて、それらをそれぞれいじることで以下のようにText Widget内のフォントの位置をいじることができます。
image.png

Configuration 1: The default. leadingDistribution is set to TextLeadingDistribution.proportional.
Configuration 2: same as Configuration 1, except TextHeightBehavior.applyHeightToFirstAscent is set to false.
Configuration 3: leadingDistribution is set to TextLeadingDistribution.even.
Configuration 4: same as Configuration 3, except TextHeightBehavior.applyHeightToLastDescent is set to false.

図リンク
https://api.flutter.dev/flutter/painting/TextStyle-class.html
https://api.flutter.dev/flutter/painting/StrutStyle-class.html

たとえば、上の例のようにTextStyleheight4を設定したとき、 fontSize * heightでText Widgetの高さが計算されてConfiguration 1のような結果になります。

今回のポイントとなるのは、Text Widgetに用意されているパラメータの1つのtextHeightBehaviorです。

            Text(
              'あいうえお',
              style: TextStyle(fontSize: 40),
              textHeightBehavior: TextHeightBehavior(
                applyHeightToFirstAscent: false,
                applyHeightToLastDescent: false,
              ),
            ),

TextHeightBehaviorには applyHeightToFirstAscentapplyHeightToLastDescentというbooleanを設定するパラメータがあります。これによって、Configuration 1のようにheightを設定していても、applyHeightToFirstAscent: falseとすることで、Configuration 2のようにText TopFont Ascentが同じラインに、applyHeightToLastDescent: falseを設定することでConfiguration 4にようにText BottomFont Descentが同じラインに設定されて、heightの影響を受けなくなります。

ちなみに、以下のコードを実行した場合

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'あいう',
              style: TextStyle(fontSize: 30),
            ),
            Text(
              'あいう',
              style: TextStyle(fontSize: 30),
              textHeightBehavior: TextHeightBehavior(
                applyHeightToFirstAscent: false,
                applyHeightToLastDescent: false,
              ),
            ),
          ],
        ),
      ),
    );
  }

結果はこうなります。

Screen Shot 2021-06-10 at 23.05.22.png

デフォルトではText TopFont AscentText BottomFont Descentは同じラインにはおらず、無駄な隙間が入っているようです。

ちなみに、英文を文字列として与えて欧文フォントを出したときは下のような結果になります。
どうやら和文フォントの場合に先程の挙動を起こすようです。
Screen Shot 2021-06-10 at 23.08.36.png

FlutterでのText Widgetの課題

ここでようやく本題のText Widgetを使うときの課題(注意点)をまとめたいと思います。

デザイン上の課題

デザインのテキストの高さとText Widgetの高さが異なる

先程述べたように、和文フォントではtextHeightBehaviorを設定しないと上下に無駄な隙間が入ってしまっています。それによって、本来デザイナーさんがデザインしたUIよりも1つあたりのTextの高さが高くなってしまって、デザイン通りのピクセル数の高さのCardの中に同じフォントサイズてText Widgetを縦に配置しても入り切らなかったりして正しくデザインをFlutterで再現できない事が起こります。

文字列が下に寄って見える

この課題はFlutterのIssueにも上がっています。
https://github.com/flutter/flutter/issues/79931
和文フォント以外でも中国語でもtextHeightBehaviorを設定しないと上下に無駄な隙間が入ってしまうかつ、欧文フォントはbaselineの上にぴったり文字列が並びますが和文フォントはbaselineに文字列が少し被るまたは下にはみ出てしまっています。また、descentの下よりもascentの上のほうが無駄な隙間が大きく取られるのも原因です。

以上の課題によって、例えば高さが52の背景がついた領域にpaddingを全方向に8、フォントサイズ30の日本語が入力されたText Widgetを配置することを考えてみます。

デザイン上ではもちろん高さ52の背景に対してテキストは垂直方向に真ん中に配置されてほしいですが、うまくいきません。

            Container(
              height: 52,
              color: Colors.grey,
              padding: const EdgeInsets.all(8),
              child: Center(
                child: Text(
                  'ああああああ',
                  style: TextStyle(
                    fontSize: 30,
                  ),
                  textHeightBehavior: TextHeightBehavior(
                    applyHeightToFirstAscent: false,
                    applyHeightToLastDescent: false,
                  ),
                ),
              ),
            ),

結果
Screen Shot 2021-06-11 at 2.37.18.png

さらにこれがもっと高さ制限が強く、高さ48の背景に対してテキストを配置したい場合デザイン上は中に収まっているのにFlutterでは再現できない事が起こります。
Screen Shot 2021-06-11 at 2.40.58.png

実装上の課題

iOSとAndroidでのTextHeightBehaviorの挙動が違う

では、TextHeightBehaviorを設定することで上下の無駄な隙間がなくなり、デザイン通りのテキストの高さをText Widgetでも実現できそうです。それにともなって文字列が下に寄って見える課題も解決できそうです。

ですが、先程の実行結果はiOSデバイスで実行したときです。
Androidデバイスで全く同じコードを実行してみます。
Screen Shot 2021-06-10 at 23.12.41.png
どうやら、textHeightBehaviorが効いていないようです。
これではiOSでは課題が解決できてAndroidでは解決できなさそうです。
Issueに上がっていたのもAndroidで起きているようでした。

これはFlutterのAndroidでのみ起こるバグなのでしょうか?
それとも他になにか原因があるのでしょうか?

次の章で解決策を模索します。

解決策

AndroidでもTextHeightBehaviorを適用させるためには

なぜiOSとAndroidでTextHeightBehaviorの挙動が違うのでしょうか。
Text WidgetのTextStylebackgroundColorを設定してText Widgetの中でフォントが確保している領域を見てみましょう。以下のコードをiOSとAndroidで実行してみます。

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'あいう',
              style: TextStyle(
                fontSize: 30,
                backgroundColor: Colors.deepOrange,
              ),
            ),
          ],
        ),
      ),
    );
  }

実行結果

iOS Android
Screen Shot 2021-06-11 at 0.21.35.png Screen Shot 2021-06-11 at 0.22.25.png

どうやらiOSとAndroidでそもそもText Widgetの中でフォントが専有している領域の高さに違いがあることがわかります。

iOSだと和文フォントにヒラギノ角ゴシック、欧文フォントにSan Francisco Pro、Androidだとその端末にデフォルトでインストールされている和文フォントと欧文フォントだとRobotoが適用されます。PixelとかだとNoto Sans JPあたりが和文フォントに適用されてるようでした。

このiOSとAndroidで使われているフォントの違いによってTextHeightBehaviorの挙動の違いが起こっていたようです。
TextHeightBehaviorを設定してもフォント自体が確保している領域よりもascentとdescentが狭まらないということのようです。

fontFamilyでヒラギノ角ゴシックを使うように修正してAndroidで実行してみます。

フォントの追加
Screen Shot 2021-06-11 at 1.05.18.png

pubspec.yamlに追記

pubspec.yaml
flutter:
  uses-material-design: true
  fonts:
    - family: Hiragino
      fonts:
        - asset: assets/fonts/hiragino.ttc

fontFamilyを設定

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'あいう',
              style: TextStyle(
                fontSize: 30,
                backgroundColor: Colors.deepOrange,
                fontFamily: 'Hiragino', // 追記
              ),
            ),
          ],
        ),
      ),
    );
  }

実行結果
Screen Shot 2021-06-11 at 1.26.17.png

familyFont適用前
Screen Shot 2021-06-11 at 0.22.25.png

Android OSがデフォルトで設定するフォントから自分でヒラギノ角ゴシックをfontFamilyに設定すると、AndroidでもText Widgetの中のフォントが占有する領域を縮める事ができました。

これにtextHeightBehaviorを設定するともちろん

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'あいう',
              style: TextStyle(
                fontSize: 30,
                backgroundColor: Colors.deepOrange,
                fontFamily: 'Hiragino',
              ),
              textHeightBehavior: TextHeightBehavior(
                applyHeightToFirstAscent: false,
                applyHeightToLastDescent: false,
              ),
            ),
          ],
        ),
      ),
    );
  }

Screen Shot 2021-06-11 at 1.38.00.png

AndroidでもTextHeightBehaviorを効かせることができるようになりました :tada:

ここからはチームでFlutterでアプリが開発している場合はチーム内でデザイナーさんと相談なのですが、解決する方針としては以下のような方針が考えられそうです。

  • iOSとAndroid両プラットフォームでTextHeightBehaviorが効く同じフォント(例えばヒラギノ角ゴシック)をfontFamilyに設定して使う
  • 上の方針だとAndroidユーザからは普段目にするフォントと違って違和感が出そうなので、iOSはヒラギノ角ゴシックを使ってAndroidでは別のあまり違和感がなくTextHeightBehaviorが効く同じフォントを使う
  • ...

日本語と英語が混ざる場合

フォントを変更する場合もう1つ考慮しなければならないことがあります。
1つのTextに日本語と英語が混ざる場合です。

今回設定したヒラギノ角ゴシックですが、一応英語にも対応しているフォントのため英語を混ぜた場合以下のような結果になります。
Screen Shot 2021-06-11 at 1.51.31.png
jのようなbaselineをはみ出す文字の場合、全体のフォント自体が専有する領域は一緒なのですが、descentよりも文字が突き出てしまい、Text WidgetをColumnで連続で並べたときに下のText Widgetに食い込んでしまいます。
Screen Shot 2021-06-11 at 1.55.03.png
また、デザイン上英語は半角っぽく表示されているのにこれだと全角の英語みたいに見えてしまっていてデザインと異なるということでも出てきそうです。

ちなみに、AndroidでfontFamilyを設定しないで実行した場合このような結果になります。
Screen Shot 2021-06-11 at 1.58.59.png
ascentの位置は違うものの、jのような文字でもdescentを突き出るようなことはありません。

以上のことから、ヒラギノ角ゴシックのようにtextHeightBehaviorが日本語に効いてアルファベットにも対応している和文フォントを使いつつ、英語は別でいい感じに表示するようにしたいニーズが出てきます。

日本語と英語が混ざった場合の解決策

そこで、Text WidgetのstyleにあるfontFamilyFallbackというパラメータがあります。
https://api.flutter.dev/flutter/painting/TextStyle/fontFamilyFallback.html

これは、Text Widgetに入力された文字列がの中にstyleで設定したfontFamilyで表せない文字列があった場合に使うフォントを設定することができます。fontFamilyFallbackはフォント名の文字列の配列を渡せて、使って欲しいフォントの優先度順に設定することができます。
これを使うことで、fontFamilyに欧文フォント(RobotoやSan Francisco Pro)を設定し、fontFamilyFallbackにヒラギノ角ゴシックのようなアルファベットにも対応した和文フォントを設定することで、

日本語と英語が含まれた文字列がText Widgetに入力された 👉 一番優先されてfontFamilyに設定された欧文フォントで文字列を出力しようとするので、英語は欧文フォントが適用される👉日本語はfontFamilyで設定された欧文フォントでは出力できないのでfontFamilyFallbackで設定したヒラギノ角ゴシックで出力される

ということが実現でき、英語は別の欧文フォントで、日本語は別の和文フォントで出力するということが実現できるようになります。

しかし、このとき和文フォントと欧文フォントでascentとdescentがずれているのが気になります。

              Text(
                'あいうえおaaajjj',
                style: TextStyle(
                  backgroundColor: Colors.deepOrange,
                  fontFamily: 'SF',
                  fontFamilyFallback: ['Hiragino'],
                  fontSize: 30,
                ),
                textHeightBehavior: TextHeightBehavior(
                  applyHeightToLastDescent: false,
                  applyHeightToFirstAscent: false,
                ),
              ),

スクリーンショット 2021-06-11 16.50.15.png
欧文フォントのほうが高さが出ているので、Text Widgetを縦に積んだときなどは違和感出てきそうです。baselineは和文フォントも欧文フォントも揃っていて良さそうです。

そこで、heightを使ってこの高さの違いを吸収したいと思います。

              Text(
                'あいうえおaaajjj',
                style: TextStyle(
                  backgroundColor: Colors.deepOrange,
                  fontFamily: 'SF',
                  fontFamilyFallback: ['Hiragino'],
                  fontSize: 30,
                  height: 2, // 追加
                ),
              ),

スクリーンショット 2021-06-11 16.55.37.png
Text Widgetの構造で紹介したように、デフォルトでstyleleadingDistributionTextLeadingDistribution. proportionalが設定されているためこのうような見た目になってしまいます。TextLeadingDistributionにはTextLeadingDistribution.evenがあるためそれを使ってみます。

スクリーンショット 2021-06-11 17.00.43.png
上下に均等に余白が入ってくれるので、文字列が下に寄って見てる課題も解決できそうです。

これで、課題は解決できそうです。
最初の背景色に対して文字列が下に寄って見える課題と、高さ制限が厳しいときに文字列が見切れる課題も解決できました。
日本語と英語が混じらないことが保証されていれば、 textHeightBehaviorを設定するだけで良さそうですが、混ざる場合はそれは不要で以下のような設定でうまく動くようになります。

              Container(
                height: 48,
                color: Colors.grey,
                padding: const EdgeInsets.all(8),
                child: Text(
                  'あいうえおaaajjj',
                  style: TextStyle(
                    fontFamily: 'SF',
                    fontFamilyFallback: ['Hiragino'],
                    fontSize: 30,
                    leadingDistribution: TextLeadingDistribution.even,
                    height: 1,
                  ),
                ),
              ),

スクリーンショット 2021-06-11 17.05.33.png

まとめ

結局の所、fontFamilyに何も設定をしていないとプラットフォームごとで使われるデフォルトのフォントの違いに起因する問題でした。
もし、デザインが1つのフォントで作られていてfontFamilyを特別指定しないでiOSとAndroidの両プラットフォームを実装しようとしていた場合、2つのプラットフォームで別々のフォントが使われるのにも関わらず1つのフォントで作られたデザインを再現しようとしているので無理が出てきそうです。

対処の仕方は数多くありそうなので、チームで話し合って最適な方法を探ると良さそうです。

今回の内容が少しでも解決の手助けになれれば幸いです。

参考記事

74
37
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
74
37

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?