こんにちは。マネックス・ラボの佐藤です。今回は、Androidネイティブアプリのフォント指定で使える、XMLフォントについて検証したので、これについて書いてみます。公式ドキュメントだと情報量が少なく、ウェブで検索してもWebにおけるfont-familyの話ばかりで、ネイティブアプリでのfont-familyの構成のナレッジがほとんどないので、記事にしてみました。
XMLフォントについて
XMLフォントとは、下記の公式ドキュメントにあるような、XMLファイルでfont-familyを作成できる機能です。このXMLファイルを定義すれば、レイアウトファイル側でUIコンポーネントのfontFamilyプロパティでこの XMLファイルを指定し、さらにtextFontWeight
とtextStyle
を指定すれば、自動的に適切なフォントファイルを選んで表示してくれます。直接フォントファイルを指定してしまうと、後からアプリ全体のフォントを変えたいときに困ってしまうので、便利ですね。
https://developer.android.com/guide/topics/ui/look-and-feel/fonts-in-xml
ドキュメントに書かれていない、XMLフォントの疑問
しかしこのドキュメントはかなり大雑把で、細かいところがよくわかりません。
- APIレベル28未満の場合に、UIコンポーネントで
textStyle=”bold”
に指定したとき、XMLフォントで定義したどの<font/>
が使われるかがわからない。- これは、UIコンポーネントとXMLフォントでのウェイトの指定の仕方が異なることによります。UIコンポーネントの
textFontWeight
プロパティを使ったウェイト指定はAPIレベル28以上であれば利用できますが、28未満の場合には、textStyle
プロパティを使って、textStyle=”bold”
といった大雑把な指定しかできません。このとき、XMLフォントで定義した<font-family/>
から、どの<font/>
が使われるのかドキュメントからはわかりません。
- これは、UIコンポーネントとXMLフォントでのウェイトの指定の仕方が異なることによります。UIコンポーネントの
- XMLフォントで定義した
<font/>
に一致するものが見つからなかった場合にどの<font/>
が使われるのかがわからない。- APIレベル26以上の場合でも、一致するものが見つからなかった場合の挙動については何も書かれていません。
- 結局APIレベル26未満と26以上をサポートしたい場合に、スタイルやUIへのフォントの指定をどうしたら良いのかがわからない。
(本筋からそれますが、XMLフォントはAPIレベル26以上から利用でき、レイアウトにおけるtextFontWeight
プロパティは、APIレベル28以上から指定できるというややこしさもあります。)
これらの疑問について、いくつかのGoogleフォントを使って検証してみましたが、こういうのは最初にまとめを書いた方が良いと思うので、先にまとめてしまいます。この結論に至った経緯に興味がある方はその先も読んでいただければと思います。
まとめ
- UIコンポーネントで
textStyle=”bold”
にすると、textFontWeight=”700”
と同じ扱いになる(ように見える。ここは少し推測が入ってます)。- 推測の理由としては、XMLフォントにおける
<font-family/>
の定義から、fontWeight=”700”
に一致するフォントが選択されるためです。 - これは割と納得のいく答えで、CSSの
font-weight
の定義と同じになっています。 https://developer.mozilla.org/ja/docs/Web/CSS/font-weight
- 推測の理由としては、XMLフォントにおける
- XMLフォントで定義した
<font-family/>
の中でfontWeight
、fontStyle
に一致する<font/>
がない場合は、その<font-family/>
の中から一番近い<font/>
が選択される。 - APIレベル26未満でも26以上でも一つの実装で同じ見た目にするためには、下記が必要。
- アプリ全体のフォントを、RegularとBoldに限定する。デザイナと相談が必要。
- UIコンポーネントのフォント指定では、
textStyle
を使ってRegularとBoldを切り替える。textFontWeight
は使わない。 - どうしてもSemi-Boldのように、RegularとBoldとは異なるウェイトのフォントを表示したい場合には、
fontFamily
プロパティに直接Semi-Boldのフォントファイルを指定する。あるいは、Styleを定義してそれを当てる。
結局のところ、アプリ内でウェイトを細かく指定したいのであれば、アプリが対応しているOSをAPIレベル28(AndroidOS9)以上にした方が、XMLフォントを使いつつ保守性を維持しながら作りやすい、ということです。しかしAndroidOS9は2018年にリリースされたものであり、AndroidのOSシェアは割と古いOSでもそれなりの比率を占めるので、対応するOSバージョンを絞りすぎることになってしまいます。
一方、APIレベルを気にせず細かくウェイトを指定したいのであれば、RegularとBold以外を指定したい箇所についてはStyle定義すればある程度保守性を保ちながらUIを作り込むこともできますが、XMLフォントだけを使う場合に比べてメンテナンス性は劣るかなと思います。
検証した内容
フォントの選定
試したいのは、UIコンポーネントのtextStyle
やtextFontWeight
プロパティの有無や値に応じて、XMLフォントからどのフォントファイルが選ばれてレンダリングされるか、です。違いが分かりやすくなるように、複数の見た目が異なるフォントファイルを用意し、どれが選ばれたかがわかりやすいようにする方が良いです。今回は、下記を選んでみました。
Bai Jamjuree
https://fonts.google.com/specimen/Bai+Jamjuree
全体に丸みを帯びたフォントで、Bの文字が特に印象的です。Regularとして使います。
DancingScript
https://fonts.google.com/specimen/Dancing+Script
筆記体のようなフォントです。Semi-Boldとして使います。
Noto Serif JP
https://fonts.google.com/specimen/Noto+Serif+JP
落ち着きのあるフォントです。Boldとして使います。
これらのフォントをダウンロードしてきて、使うウェイトの.ttf
ファイルだけをAndroidStudioのプロジェクトに取り込みます。ファイル名はAndroidのリソース命名規則に従わないといけないので、適当にスネークケースに置き換えます。今回は、bai_jamjuree_regular.ttf
、dancing_script_semi_bold.ttf
、noto_serif_jp_bold.ttf
としました。
XMLフォントの作成
作成したXMLフォントは下記の通りです。ファイル名は、font_file_test.xml
です。
<font-family/>
タグの中に複数の<font/>
タグを書いて、font-familyを構成します。各<font/>
タグには、上記の3つのフォントを定義します。
<?xml version="1.0" encoding="utf-8"?> <font-family xmlns:app="http://schemas.android.com/apk/res-auto"> <font app:font="@font/bai_jamjuree_regular" app:fontWeight="400" app:fontStyle="normal" /> <font app:font="@font/dancing_script_semi_bold" app:fontWeight="600" app:fontStyle="normal" /> <font app:font="@font/noto_serif_jp_bold" app:fontWeight="700" app:fontStyle="normal" /> </font-family>
サンプルUIの作成
また、このXMLフォントを使ったサンプルのUIを作ります。今回は、3つのTextViewを並べました。上から順に、Bai Jamjuree、DancingScript、Noto Serif JPを表示することを意図して、異なるtextFontWeight
を設定しています。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <TextView android:layout_height="wrap_content" android:layout_width="match_parent" android:text="012abcABC" android:fontFamily="@font/font_file_test" android:textSize="16sp" android:textFontWeight="400" /> <TextView android:layout_height="wrap_content" android:layout_width="match_parent" android:text="012abcABC" android:fontFamily="@font/font_file_test" android:textSize="16sp" android:textFontWeight="600" /> <TextView android:layout_height="wrap_content" android:layout_width="match_parent" android:text="012abcABC" android:fontFamily="@font/font_file_test" android:textSize="16sp" android:textFontWeight="700" /> </LinearLayout>
APIレベル28以上で、表示を確認する
まずは、APIレベル28以上(AndroidOS9以上)で、それぞれのTextViewで指定したtextFontWeight
に一致したフォントが選択されるかどうかを確認してみます。実行した結果は下記の通り。それぞれで設定したウェイトに合わせて、フォントが選ばれています。
XMLフォントとレイアウトファイルを変えずにAPIレベル24で実行する
次はレイアウトファイルを変えずに、APIレベル24(AndroidOS7.0)で実行してみます。見事にtextFontWeight
の指定が無視され、全てRegularで指定したフォントで表示されました。サポートしていないと言っているので当たり前ですが。
textStyle="bold"
を指定した時に選択される<font/>
を調べる
APIレベル28未満ではtextFontWeightプロパティには意味がないことがわかったので削除し、3つ目のTextViewだけ、textStyle=”bold”
を指定してみます。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <TextView android:layout_height="wrap_content" android:layout_width="match_parent" android:text="012abcABC" android:fontFamily="@font/font_file_test" android:textSize="16sp" /> <TextView android:layout_height="wrap_content" android:layout_width="match_parent" android:text="012abcABC" android:fontFamily="@font/font_file_test" android:textSize="16sp" /> <TextView android:layout_height="wrap_content" android:layout_width="match_parent" android:text="012abcABC" android:fontFamily="@font/font_file_test" android:textSize="16sp" android:textStyle="bold" /> <!-- textStyle="bold" にする --> </LinearLayout>
APIレベル24で実行した結果がこちら。3つ目のTextViewに、XMLフォント側でfontWeight="700"
を指定してあるNoto Serif JPのBoldが表示されていることがわかります。
なお、APIレベル28で実行した場合はこのようになります。APIレベル24と同じであり、一つのレイアウトの実装で、異なるAPIレベルに対応できていることがわかります。
XMLフォントの<font/>
と一致しないウェイトをレイアウトファイルで指定したとき
ここまでの検証でどう作れば良いのかはわかりましたが、最後に指定したウェイトの<font/>
が無かった場合にどうなるのか試してみます。一番最初のレイアウトファイルで、2つ目のTextViewのtextFontWeight
を"800"
に変更してみます。XMLフォントではfontWeight=”800”
の<font/>
がないので、どのフォントファイルが選ばれるのでしょうか。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <TextView android:layout_height="wrap_content" android:layout_width="match_parent" android:text="012abcABC" android:fontFamily="@font/font_file_test" android:textSize="16sp" android:textFontWeight="400" /> <TextView android:layout_height="wrap_content" android:layout_width="match_parent" android:text="012abcABC" android:fontFamily="@font/font_file_test" android:textSize="16sp" android:textFontWeight="800" /><!-- textFontWeightを、XMLフォントの中で定義がない"800"にする --> <TextView android:layout_height="wrap_content" android:layout_width="match_parent" android:text="012abcABC" android:fontFamily="@font/font_file_test" android:textSize="16sp" android:textFontWeight="700" /> </LinearLayout>
APIレベル28での実行結果がこちら。2つ目のTextViewが、fontWeight=”700”
で指定したNoto Serif JPで描画されています。一致しないからといっていきなりOSのデフォルトフォントを使うのではなく、できるだけ近い<font/>
を探してきてくれることは確かです。
最後に
今回は、ドキュメントに書かれていないことを調べるために、実験的なアプローチを取りました。プログラマであるからにはソースコードを読み解いたりGitHub上のIssueを探すような方法で裏取りをしたかったのですが、あまり時間を割きたくないこと、フロントエンドは結局のところ機種依存のような問題も出てきますし、フォントのレンダリングについてはロジックに影響を与えるようなものでもないので、今回のような調査方法を取りました。
証券会社というのはお客様の資産をお預かりして正しい根拠を積み上げてシステムを開発/運用していく必要がありますが、領域によっては異なるアプローチが必要になることもあり、特に変化の激しいWebやネイティブアプリといったフロントエンドは、Web上の情報や、手元での検証に基づいた判断も必要になってきます。こういった開発の文化に対する社内の理解も得ながら、金融機関でフロントエンドを作っていくのは、大変でもあり、また面白いことでもあるのかなと思います。
佐藤 俊介マネックス・ラボ