LINEヤフー Tech Blog

LINEヤフー株式会社のサービスを支える、技術・開発文化を発信しています。

This post is also available in the following languages. English, Korean

LINEのAndroidアプリにフォントカスタマイズ機能を導入する方法

LINE株式会社とヤフー株式会社(現 LINEヤフー株式会社)の合併に伴い、既存のYahoo!プレミアムで提供していた特典に加え、新たにLINEの特典まで利用できる月額会員制サービス「LYPプレミアム」の提供を開始しました。このLYPプレミアムの機能の一つとして、LINEの表示フォントを自分好みのフォントに変更できる「会員限定フォント」機能が追加されました。この機能を実装するにあたり、以下のように大きく3つの要件があります。

  • フォントリストをユーザーに表示する。
  • ユーザーが選択したフォントを動的にダウンロードする。
  • ダウンロードしたフォントをアプリ全体に適用する。

この記事では、上記の中で2と3の要件を実装する過程で、行ったことを紹介します。

Androidでフォントを適用する方法

Androidでフォントを適用する方法を知るには、まず、以下の3つの用語の定義と違いを知る必要があります。

  • フォントファイル:文字を画面に表現するための情報(style,weight,size)をまとめたファイル
  • font-family:styleとweightによってどのようなフォントに見えるかを定義したフォント集合
  • Typeface:フォントをメモリに読み込み、システムリソースとして適用できるクラス

上記の用語を使って前述の要件を再定義すると、「サーバーからフォントファイルを動的にダウンロードし」、それを「Typefaceクラスに読み込んでビューに適用」することといえます。

次にAndroidでフォントを適用する静的な方法と、動的な方法について説明します。

静的フォントの適用方法

Androidでフォントを適用する最も基本的な方法は、フォントファイルをアプリリソースに追加し、それをスタイルリソースとして定義して必要なところで活用する方法です。スタイルとして定義したフォントは、テーマを指定し、アプリ全体で一括適用できます。ただし、この方法には以下のようなデメリットが2つあります。

  • すべてのフォントリストがアプリのバンドルに含まれ、アプリのサイズが必要以上に大きくなる
  • アプリがアップデートされるまで、フォントのアップデートができない

動的フォントの適用方法 - ダウンロード可能なフォント

Androidでは、上記のような静的フォント方式のデメリットを解決するために、動的フォントシステムであるダウンロード可能なフォント(Downloadable Fonts)機能を提供しています。これを利用すると、静的な方法とは異なり、フォントファイルをアプリリソースに追加する必要はありません。この機能の仕組みと使い方について説明します。

ダウンロード可能なフォント機能でユーザーが選択したフォントを動的に取得する

Androidの動的フォントシステムであるダウンロード可能なフォント機能の仕組みを理解するために、この機能を使ってGoogle Fontsからフォントを取得する方法を見てみましょう。

ダウンロード可能なフォント機能を使ってGoogle Fontsからフォントを取得する

下図は、ダウンロード可能なフォント機能が動作するプロセスを示したものです。ダウンロード可能なフォント機能が動作するプロセス

<出典:Android Developer Guide - Downloadable Fonts>

仮にApp1で特定のフォントファイルを使用したいとします。App1が必要なフォント情報をFontsContractを通じてリクエストすると、FontProviderはネットワークからフォントファイルをダウンロードするか、キャッシュされたファイルを返します。一度ダウンロードしたフォントはOSレベルで管理し、別のアプリから同じフォントのリクエストがある場合、以前にダウンロードしたフォントファイルを再利用します。これにより、ネットワーク使用量とディスク容量を節約できます。

ここでは、FontProviderがGoolge Fontsというフォントプロバイダーから、フォントファイルをダウンロードします。Google Fontsはオープンソースライブラリで、APIを通じてフォントを提供しています。AndroidではGoogle Fontsを利用し、さまざまなフォントを一つのデバイス上のさまざまなアプリで共有して使用できるように提供しています。

ダウンロード可能なフォント機能の仕組みをもう少し詳しく見てみましょう。

ダウンロード可能なフォント機能の詳細な仕組み

  1. 新しいフォントを使いたいクライアントは、FontRequestクラスにContentProviderの仕様(authority、pacakage、cert)とどのようなフォントを使いたいか(query)を含め、FontsContractCompatにリクエストします
  2. FontsContractCompatは、クライアントが送信したFontRequestをFontProviderに渡します
  3. FontProviderは、PackageManagerを通じて仕様に一致するContentProviderを見つけます(この例ではGoogle FontsのContentProviderが返されます)
  4. FontProviderは、FontRequest#Queryを使ってContentProviderにフォントをリクエストします
  5. ContentProviderは該当するフォントをまずキャッシュから探し、そこにない場合はサーバーからダウンロードします
  6. ContentProviderから受け取ったフォントをFontInfoに入れて返します
  7. 最終的に、FontsContractCompatはFontInfoをTypefaceに読み込んで返します

以下はサンプルコードです。以下のように、FontRequestに情報を入れ、FontsContractCompatを通じてプログラムでフォントをリクエストし、返されたTypefaceを使用できます。

class FontRepository {
    fun getTypeFace(): TypeFace {
        val request = FontRequest(
            "com.google.android.gms.fonts",
            "com.google.android.gms",
            "ABeeZee",
            R.array.com_google_android_gms_fonts_certs"
        )
        return suspendCancellableCoroutine { continuation ->
            val callback = object : FontsContractCompat.FontRequestCallback() {
                override fun onTypefaceRetrieved(typeface: Typeface?) {
                    continuation.resume(typeface)
                }

                override fun onTypefaceRequestFailed(reason: Int) {
                    continuation.resume(null)
                }
            }
            FontsContractCompat.requestFont(context, request, callback, handler)
        }
    }
}

また、XMLでフォントリソースにアクセスし、TextViewのfont-familyとして使えます。

[res/font/abeeZee.xml]
<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:app="http://schemas.android.com/apk/res-auto"
    app:fontProviderAuthority="com.google.android.gms.fonts"
    app:fontProviderPackage="com.google.android.gms"
    app:fontProviderQuery="ABeeZee" 
    app:fontProviderCerts="@array/com_google_android_gms_fonts_certs" />
---
[main_activity.xml]
<LinearLayout...>
    <TextView
        android:id="@+id/text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:fontFamily="@font/abeezee" />
<LinearLayout/>

仕組みについての説明は難しいかもしれませんが、上記のコードのように実際に使うときは、FontRequestに情報を入れて正しくリクエストすれば、Google Fontsから使いたいフォントを簡単に取得できます。

しかし、LINEで使用できるフォントは、LINEが別途ライセンスを取得したフォントです。Google Fontsから取得できず、他のアプリと共有もできません。では、Google Fontsではなく、LINEのサーバーからフォントを取得するにはどうすればいいのでしょうか?

ダウンロード可能なフォント機能を利用してLINEサーバーからフォントを取得する

前述のように、FontRequestでリクエストすると、このFontRequestがFontProviderに渡され、FontProviderはFontRequestの仕様に一致するContentProviderを見つけます。そこで、ContentProviderをカスタマイズし、それに合った仕様でリクエストすれば、LINEで使えるフォントを取得できると考えました。

私たちは、下図のようにLINEサーバーからフォントを取得するContentProviderを構成し、そのContentProviderの仕様に合わせたFontRequestを渡す方法で実装しました。

LINEサーバーからフォントを取得するContentProviderを構成し、そのContentProviderの仕様に合わせたFontRequestを渡す実装方法を紹介した図

以下は、実際のコードです。

pakcage com.example.line.font

class LineFontProvider: ContentProvider() {
    override fun query(...): Cursor? {
        // このContentProviderが対応できるURIかどうかをチェックする。
        if (uri is LineContentUri) return MatrixCursor(...)
        else return null
    }

    override fun openFile(uri: Uri, mode: Mode): ParcelFileDescriptor {
        // フォントファイルをLINEサーバーまたはローカルキャッシュから取得する。
        val file = LineFontRepository.getFontFile(uri)
        return ParcelFileDescriptor.open(fontFile, MODE_READ_ONLY)
    }
}
<font-family xmlns:app="http://schemas.android.com/apk/res-auto"
        app:fontProviderAuthority="com.example.line.font"
        app:fontProviderPackage="com.example.line"
        app:fontProviderQuery="LineFont"
        app:fontProviderCerts="@array/line_fonts_certs">
</font-family>

このように構成してFontsContractCompatにリクエストすると、FontProviderはLINEで実装したLineFontProviderにフォントをリクエストし、Typefaceとして返します。

ダウンロードしたフォントを動的にLINEに適用する

ここまでで、ダウンロード可能なフォント機能を利用してLINEから、使いたいフォントをTypeFaceとして取得する方法までを紹介しました。このように取得したTypeFaceをLINE全体に適用する方法はいくつかありますが、どの方法が効率的かを見てみましょう。

ビューごとに適用する方法

ビューごとに適用する方法には、静的な方法やプログラムによる方法、フォントをあらかじめ適用したカスタムビュー方法があります。各方法の使い方やメリット、デメリットは以下のとおりです。

静的な方法
<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginHorizontal="30dp"
    android:fontFamily="@font/custom_font"
    android:text="Apply line font in xml" />
  • 使い方:上記で定義した@font/custom_fontリソースをXMLレイアウトでTextViewのfont-familyとして割り当てる
  • デメリット
    • XMLレイアウトでは、ユーザーがカスタムフォントを使っているかどうかを動的に判断して適用できない
    • 各サービスの担当者が、テキストが表示されるビューごとに適用する必要があり、手間がかかる
プログラムによる方法
class MainActivity {
    override fun onCreate() {
        ...
        if (needToApplyCustomFont) {
            binding.textView.typeface = LineFontRepository.getTypeface()
        }
    }
}
  • 使い方:フォントの使用有無に応じて適切なフォントが適用されるようにonCreate()で設定する
  • メリット:この方法では、動的にTypefaceを設定できるので、カスタムフォントの使用有無に応じて適用できる
  • デメリット:各サービスの担当者が、テキストが表示されるビューごとに適用する必要があり、手間がかかる
フォントをあらかじめ適用したカスタムビュー方法
class FontedTextView(
    context: Context,
    attributeSet: AttributeSet
) : androidx.appcompat.widget.AppCompatTextView(context, attributeSet) {
    init {
        if (needToApplyCustomFont) {
            typeface = LineFontRepository.getTypeface()
        }
    }
}
  • 使い方:フォントの使用有無に応じて適切なフォントが適用されるカスタムビューを作成する
  • メリット:各サービスではフォントの使用有無を考慮する必要がなく、不要なコードを減らせる
  • デメリット:
    • TextViewだけでなく、テキストが表示されるButton、Toast、Headerなど、さまざまなビューをカスタマイズする必要がある
    • すべてのビューをFontedCustomViewに置き換えるには、膨大な工数が必要
    • 新しいレイアウトを実装する際、カスタムビューを利用しないとフォントが適用されないことを見落としがち

      LINEは非常に膨大です。そのため、上記のようなビュー単位の対応方法を使うと、膨大な手間がかかります。そこで、より効率的に問題を解決できる方法を模索するために、さらに調査しました。その調査の中で、冒頭で紹介したフォントスタイルをテーマとして設定して、適用する方法を見つけました。

      ダウンロード可能なフォント機能をテーマとして設定する方法

      フォントスタイルをテーマとして設定する方法は、フォントリソースをXMLで事前に定義できないとスタイルとして参照できないため、除外していた方法です。しかし、ダウンロード可能なフォント機能を使うと、フォントをLINEサーバーから動的に取得できることがわかりました。また、XMLレイアウトでも参照して適用できることも確認でき、十分に使える方法だとわかりました。適用方法は以下のとおりです。

      1. @font/custom_fontリソースをスタイルリソースとして指定します。
      <?xml version="1.0" encoding="utf-8"?>
      <resources>
          <style name="default_font">
              <item name="fontFamily" />
          </style>
          <style name="custom_font">
              <item name="fontFamily">@font/custom_font</item>
          </style>
      </resources>
      1. スタイルリソースを、Application#ActivityLifecycleCallbacksを使って、各アクティビティの作成(onCreate)時にテーマとして設定します。
      class CustomFontThemeApplier : Application.ActivityLifecycleCallbacks {
      
          override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
              if (needToApplyCustomFont) {
                  activity.setTheme(customFontStyle)
              }
          }
          ...
      }

      ダウンロード可能なフォント機能をテーマとして設定する方法を利用し、LINE内でテキストが表示されるすべての場所に、使いたいフォントを適用できました。各サービスは、フォントを適用するために個別に対応する必要がなく、意図に応じて除外すべき場合のみ処理すれば問題ないので、機能の適用に必要な工数を大幅に減らせました。

      おわりに

      会員限定フォント機能を実装するにあたり、動的にフォントを取得してテーマとして適用する方法を採用し、多くのメリットを得られました。しかし、まだ改善が必要な部分もあります。

      • 再起動の問題:適用するフォントが変わると、スタイルリソースを再読み込みする必要があるため、アプリの再起動が必要です
      • 読み込み遅延の問題:一部のデバイスで動的にフォントを読み込むのに時間がかかり、読み込んでいる間、基本フォントが表示されることがあります

      上記の問題を解決するために、FontRequestのquery方式を変更したり、スプラッシュ画面(splash screen)でプリロード(preload)を利用したりするなど、さまざまな方法を試しながら研究を進めています。

      今回の適用事例の投稿を皮切りに、フォントを動的に適用して多くの情報が共有されることを願っています。長文でしたが、最後まで読んでいただきありがとうございました。

      参考資料

      Portions of this page are reproduced from work created and shared by the Android Open Source Project and used according to terms described in the Creative Commons 2.5 Attribution License.(外部サイト)