はじめに
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),
),
],
),
),
);
}
}
このままではデザイナーさんが作ったデザインを正しく再現できません。
次の章で詳しく触れますが、デザインと同じフォントサイズでもデザイナーさんがデザインしたテキストが確保している領域と、FlutterのText Widgetが確保している領域に違いがあるため、そのまま使うとサイズの制約が厳しい場合にデザイン通りTextが収まりきらなかったり、縦にTextを並べたときなどにデザインが崩れていきます。
この記事ではなぜそのような問題が起こるのかと、解決策をまとめます。
Textの構造
まずは先程のTextがどのような構造で作られているのか見てみましょう。
InspectorでこのText
を見てみます。
下の図のFont metrics default height
がText Widgetが確保する領域となり、Inspectorで見たときの文字列の周りの水色の四角い枠がそれに当たります。
Alphabetic Baseline
が文字列の下の緑っぽい色の線です。
今回はstyle
のfontFamily
に何も設定していないので、iOSとAndroidそれぞれのプラットフォームのデフォルトのフォントが使われます。
iOSだと和文フォントにヒラギノ角ゴシック、欧文フォントにSan Francisco Pro、Androidだとその端末にデフォルトでインストールされている和文フォントと欧文フォントにRobotoが適用されます。
Text Widgetには、style
、strutStyle
などのパラメータが用意されていて、それらをそれぞれいじることで以下のようにText Widget内のフォントの位置をいじることができます。
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
たとえば、上の例のようにTextStyle
のheight
に4
を設定したとき、 fontSize * height
でText Widgetの高さが計算されてConfiguration 1
のような結果になります。
今回のポイントとなるのは、Text Widgetに用意されているパラメータの1つのtextHeightBehavior
です。
Text(
'あいうえお',
style: TextStyle(fontSize: 40),
textHeightBehavior: TextHeightBehavior(
applyHeightToFirstAscent: false,
applyHeightToLastDescent: false,
),
),
TextHeightBehavior
には applyHeightToFirstAscent
と applyHeightToLastDescent
というbooleanを設定するパラメータがあります。これによって、Configuration 1
のようにheight
を設定していても、applyHeightToFirstAscent: false
とすることで、Configuration 2
のようにText Top
とFont Ascent
が同じラインに、applyHeightToLastDescent: false
を設定することでConfiguration 4
にようにText Bottom
とFont 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,
),
),
],
),
),
);
}
結果はこうなります。
デフォルトではText Top
とFont Ascent
、Text Bottom
とFont Descent
は同じラインにはおらず、無駄な隙間が入っているようです。
ちなみに、英文を文字列として与えて欧文フォントを出したときは下のような結果になります。
どうやら和文フォントの場合に先程の挙動を起こすようです。
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,
),
),
),
),
さらにこれがもっと高さ制限が強く、高さ48の背景に対してテキストを配置したい場合デザイン上は中に収まっているのにFlutterでは再現できない事が起こります。
実装上の課題
iOSとAndroidでのTextHeightBehavior
の挙動が違う
では、TextHeightBehavior
を設定することで上下の無駄な隙間がなくなり、デザイン通りのテキストの高さをText Widgetでも実現できそうです。それにともなって文字列が下に寄って見える課題も解決できそうです。
ですが、先程の実行結果はiOSデバイスで実行したときです。
Androidデバイスで全く同じコードを実行してみます。
どうやら、textHeightBehavior
が効いていないようです。
これではiOSでは課題が解決できてAndroidでは解決できなさそうです。
Issueに上がっていたのもAndroidで起きているようでした。
これはFlutterのAndroidでのみ起こるバグなのでしょうか?
それとも他になにか原因があるのでしょうか?
次の章で解決策を模索します。
解決策
AndroidでもTextHeightBehavior
を適用させるためには
なぜiOSとAndroidでTextHeightBehavior
の挙動が違うのでしょうか。
Text WidgetのTextStyle
にbackgroundColor
を設定して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 |
---|---|
どうやらiOSとAndroidでそもそもText Widgetの中でフォントが専有している領域の高さに違いがあることがわかります。
iOSだと和文フォントにヒラギノ角ゴシック、欧文フォントにSan Francisco Pro、Androidだとその端末にデフォルトでインストールされている和文フォントと欧文フォントだとRobotoが適用されます。PixelとかだとNoto Sans JPあたりが和文フォントに適用されてるようでした。
このiOSとAndroidで使われているフォントの違いによってTextHeightBehavior
の挙動の違いが起こっていたようです。
TextHeightBehavior
を設定してもフォント自体が確保している領域よりもascentとdescentが狭まらないということのようです。
fontFamily
でヒラギノ角ゴシックを使うように修正してAndroidで実行してみます。
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', // 追記
),
),
],
),
),
);
}
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,
),
),
],
),
),
);
}
AndroidでもTextHeightBehavior
を効かせることができるようになりました
ここからはチームでFlutterでアプリが開発している場合はチーム内でデザイナーさんと相談なのですが、解決する方針としては以下のような方針が考えられそうです。
- iOSとAndroid両プラットフォームで
TextHeightBehavior
が効く同じフォント(例えばヒラギノ角ゴシック)をfontFamily
に設定して使う - 上の方針だとAndroidユーザからは普段目にするフォントと違って違和感が出そうなので、iOSはヒラギノ角ゴシックを使ってAndroidでは別のあまり違和感がなく
TextHeightBehavior
が効く同じフォントを使う - ...
日本語と英語が混ざる場合
フォントを変更する場合もう1つ考慮しなければならないことがあります。
1つのTextに日本語と英語が混ざる場合です。
今回設定したヒラギノ角ゴシックですが、一応英語にも対応しているフォントのため英語を混ぜた場合以下のような結果になります。
j
のようなbaseline
をはみ出す文字の場合、全体のフォント自体が専有する領域は一緒なのですが、descentよりも文字が突き出てしまい、Text WidgetをColumn
で連続で並べたときに下のText Widgetに食い込んでしまいます。
また、デザイン上英語は半角っぽく表示されているのにこれだと全角の英語みたいに見えてしまっていてデザインと異なるということでも出てきそうです。
ちなみに、AndroidでfontFamily
を設定しないで実行した場合このような結果になります。
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,
),
),
欧文フォントのほうが高さが出ているので、Text Widgetを縦に積んだときなどは違和感出てきそうです。baseline
は和文フォントも欧文フォントも揃っていて良さそうです。
そこで、heightを使ってこの高さの違いを吸収したいと思います。
Text(
'あいうえおaaajjj',
style: TextStyle(
backgroundColor: Colors.deepOrange,
fontFamily: 'SF',
fontFamilyFallback: ['Hiragino'],
fontSize: 30,
height: 2, // 追加
),
),
Text Widgetの構造で紹介したように、デフォルトでstyle
のleadingDistribution
にTextLeadingDistribution. proportional
が設定されているためこのうような見た目になってしまいます。TextLeadingDistribution
にはTextLeadingDistribution.even
があるためそれを使ってみます。
上下に均等に余白が入ってくれるので、文字列が下に寄って見てる課題も解決できそうです。
これで、課題は解決できそうです。
最初の背景色に対して文字列が下に寄って見える課題と、高さ制限が厳しいときに文字列が見切れる課題も解決できました。
日本語と英語が混じらないことが保証されていれば、 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,
),
),
),
まとめ
結局の所、fontFamily
に何も設定をしていないとプラットフォームごとで使われるデフォルトのフォントの違いに起因する問題でした。
もし、デザインが1つのフォントで作られていてfontFamily
を特別指定しないでiOSとAndroidの両プラットフォームを実装しようとしていた場合、2つのプラットフォームで別々のフォントが使われるのにも関わらず1つのフォントで作られたデザインを再現しようとしているので無理が出てきそうです。
対処の仕方は数多くありそうなので、チームで話し合って最適な方法を探ると良さそうです。
今回の内容が少しでも解決の手助けになれれば幸いです。