Android で日本語でも色々なウェイトのフォントを使いたい

この記事は、[フラー株式会社のカレンダー | Advent Calendar 2023 - Qiita](https://qiita.com/advent-calendar/2023/fuller-inc) の 9 日目の記事です[^backstory]。

[^backstory]: 本当は違うネタを書くつもりでしたが、うまくいかず急遽思いついたことを書いてます。

8 日目は [いのりこ (id:inoriko711)](https://inoriko.hatenablog.com/about) さんで [弊社エンジニアリンググループ初、産休取得してみた](https://inoriko.hatenablog.com/entry/2023/12/08/115449) でした。

## はじめに

デザインの観点から、複数の Light や Medium といった色々なウエイトのフォントを使いたいということがあります。しかし、Android の標準では日本語フォントは Regular と Bold の 2 種類しか表示できないです[^default]。

[^default]: Android には多種多様な種類の端末があるため、例外はあります。多くの端末では、ということです。

画像のように他のウエイトを設定しても、日本語フォントでは Regular か Bold のどちらかで表示されることになります。

![デフォルトでの表示](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/699841/a1586dce-e8be-b008-fd54-3cf787b2db35.png)

<details><summary>コード</summary>

```kotlin
val textStyle = TextStyle(
    platformStyle = PlatformTextStyle(includeFontPadding = false),
    lineHeight = 1.2.em,
)

@Preview
@Composable
fun MulchWeights1() {
    Column(modifier = Modifier.padding(8.dp)) {
        for (weight in 100..900 step 100) {
            Text(
                text = "サンプル「『』」 weight=$weight",
                style = textStyle,
                fontWeight = FontWeight(weight),
            )
        }
    }
}
```
</details>

このような場合は、そのウエイトに対応したフォントを [Google Fonts](https://fonts.google.com/) などからダウンロードして対応します。

[完]

これだけでは面白くないので、こだわりを持って複数ウエイトに対応する場合を考えます。

## Android に搭載される日本語フォントについて

Android 12 以降に搭載されているフォントには chws (Contextual Half-width Spacing) により約物が連続した時のアキを調整する機能があります[^chws]。

[^chws]: もちろん知ってましたよね？ 知らないかった人は廊下に立ってください。

[Google Fonts の Noto Sans JP](https://fonts.google.com/noto/specimen/Noto+Sans+JP) では chws の機能のない ttf しかダウンロードできません。安直にこのフォントを設定した場合複数ウエイトに対応することはできても、約物が残念になってしまいます。

![Google Fonts の Noto Sans JP での表示](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/699841/74da58eb-cf0f-0c27-4bd9-fa2039f1b0b2.png)

<details><summary>コード</summary>

```kotlin
@OptIn(ExperimentalTextApi::class)
val notoSansJpGoogleFonts = FontFamily(
    (100..900 step 100).map { weight ->
        Font(
            resId = R.font.noto_sans_jp_google_fonts,
            weight = FontWeight(weight),
            variationSettings = FontVariation.Settings(FontWeight(weight), FontStyle.Normal)
        )
    }
)

val textStyle = TextStyle(
    platformStyle = PlatformTextStyle(includeFontPadding = false),
    lineHeight = 1.2.em,
)

@Preview
@Composable
fun MulchWeights2() {
    Column(modifier = Modifier.padding(8.dp)) {
        for (weight in 100..900 step 100) {
            Text(
                text = "サンプル「『』」 weight=$weight",
                style = textStyle,
                fontWeight = FontWeight(weight),
                fontFamily = notoSansJpGoogleFonts,
            )
        }
    }
}
```
</details>

## chws 機能付きの Noto Sans JP を求めて

chws 機能がついた Noto Sans JP を探したところ [simonsmh/notocjk](https://github.com/simonsmh/notocjk) というリポジトリを見つけました。このリポジトリでは Android デバイスに含まれるパッチを加えた全てのウエイトの Noto Sans CJK / Noto Serif CJK を公開しています。

日本語の Sans だけが必要なため、`NotoSansCJK-VF.otf.ttc` をダウンロードし、日本語の部分の otf を取得します[^get-jp]。

[^get-jp]: Python の fontTools とか使って適当にやってください！ ググればできる！

これにより複数ウエイトかつ約物の間隔が綺麗な表示に対応できます。

![複数ウエイトかつ chws が設定された表示](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/699841/befbb1d7-6679-fad0-d20e-faddc1386a95.png)

<details><summary>コード</summary>

```kotlin
@OptIn(ExperimentalTextApi::class)
val notoSansJpWithPatch = FontFamily(
    (100..900 step 100).map { weight ->
        Font(
            resId = R.font.noto_sans_jp_with_patch,
            weight = FontWeight(weight),
            variationSettings = FontVariation.Settings(FontWeight(weight), FontStyle.Normal)
        )
    }
)

val textStyle = TextStyle(
    platformStyle = PlatformTextStyle(includeFontPadding = false),
    lineHeight = 1.2.em,
)

@Preview
@Composable
fun MulchWeights3() {
    Column(modifier = Modifier.padding(8.dp)) {
        for (weight in 100..900 step 100) {
            Text(
                text = "サンプル「『』」 weight=$weight",
                style = textStyle,
                fontWeight = FontWeight(weight),
                fontFamily = notoSansJpWithPatch,
            )
        }
    }
}
```
</details>

バリアブルフォントを使用しているので、100 より細かい単位でウエイトを変えることもできます。

![細かい単位でのウエイト指定表示](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/699841/599e2319-648b-5ee1-ceea-96c0d2b47d22.png)

<details><summary>コード</summary>

```kotlin
@OptIn(ExperimentalTextApi::class)
val notoSansJpWithPatch = FontFamily(
    (400..500 step 10).map { weight ->
        Font(
            resId = R.font.noto_sans_jp_with_patch,
            weight = FontWeight(weight),
            variationSettings = FontVariation.Settings(FontWeight(weight), FontStyle.Normal)
        )
    }
)

val textStyle = TextStyle(
    platformStyle = PlatformTextStyle(includeFontPadding = false),
    lineHeight = 1.2.em,
)

@Preview
@Composable
fun MulchWeights4() {
    Column(modifier = Modifier.padding(8.dp)) {
        for (weight in 400..500 step 10) {
            Text(
                text = "サンプル「『』」 weight=$weight",
                style = textStyle,
                fontWeight = FontWeight(weight),
                fontFamily = notoSansJpWithPatch,
            )
        }
    }
}
```
</details>

## 最後に

今回の方法では、標準の Roboto + Noto Sans JP にならず、すべて Noto Sans JP になります。その話はまたいつの日か…