Androidでルビ(ふりがな)表示を調査してみた

# 概要

Androidでルビ(ふりがな)表示するライブラリのいくつかについて、その特性を調べてみたので内容をまとめてみました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78503/ad0b88c7-f163-6757-6212-4cdc64d05116.png" width="240" />



# 背景

iOSにはCTRubyAnnotationでルビを表示する機能があるそうですが、現状Androidにはアプリ上でルビを表示する機能はありません。WebViewであれば、rubyタグによってルビを表示することはできますが、TextViewのようなView上で表示させるためには、外部のライブラリ等に頼る必要があります。
Androidアプリ開発でルビ対応を行う必要があったのでGitHubを探してみたところ、該当するライブラリがいくつかありました。しかし、どれを選べば良いかよく分からなかったので、それぞれの特性について調べることにしました。

# 対象ライブラリ

以下、GitHubで検索して見つけたものをピックアップしてみました。順番は適当です。他に何か良さそうなのあれば教えて下さい。

* [FuriganaTextView ( lofe90 / FuriganaTextView )](https://github.com/lofe90/FuriganaTextView)
* [FuriganaView ( guentoan / FuriganaView )](https://github.com/guentoan/FuriganaView)
* [FuriganaView ( Vexu / Furigana-TextView )](https://github.com/Vexu/Furigana-TextView)
* [FuriganaView ( sh0 / furigana-view )](https://github.com/sh0/furigana-view)
* [RubyTextView ( b84330808 / RubyTextView )](https://github.com/b84330808/RubyTextView)
* [RubySpan ( mljli / rubyspan )](https://github.com/mljli/rubyspan)

名前が被っているのがいくつかあって混乱しやすいため、開発者名を併記するようにします。

それ以外に、[vjap](https://github.com/taizan/vjap)というものも見かけましたが、縦書き用であることと、専用のレイアウトを使わないと表示できない様子だったので見送りました。



# ソース

GitHubに調査したソースを置いておきます。
https://github.com/wa2c/test-ruby-text

各ライブラリをプロジェクトに取り込んだ上で、AndroidX対応などの修正を行い、最新の環境で動くようにしています。
ライブラリの設定を調べるのが面倒なので、とりあえず標準状態で動かしています。



# 調査の観点

主に次のような点で調査をしました。いじわる試験みたいな項目もありますが、興味本位で調べてみただけです。

* 文字の入力方法
* ソースの実装方法
* ルビが表示されるか
* ルビの間隔が適切か
* テキストとルビの文字数に極端な差異があっても表示が行われるか
* 行頭・行末や改行の表示が適切に行われるか
* (オマケ)テキストの絵文字に対応しているか (ルビに絵文字は使わないので未調査)



# 調査の結果

## [FuriganaTextView ( lofe90 / FuriganaTextView )](https://github.com/lofe90/FuriganaTextView)

### 特徴

* ライセンス: Creative Commons BY-SA 3.0
* 最終コミット日: 2018-10-13
* TextViewを継承
* XMLレイアウト対応
* 入力フォーマット：「&lt;ruby&gt;テキスト&lt;rt&gt;ルビ&lt;/rt&gt;&lt;/ruby&gt;」

### 確認ソース

```kotlin
se.fekete.furiganatextview.furiganaview.FuriganaTextView(this).also {
    it.setFuriganaText("<ruby>Android<rt>アンドロイド</rt></ruby>は、<ruby>Google<rt>グーグル</rt></ruby>が<ruby>開発<rt>かいはつ</rt></ruby>した<ruby>携帯汎用<rt>けいたいはんよう</rt></ruby>オペレーティングシステムである。", true)
    base.addView(it)
}

se.fekete.furiganatextview.furiganaview.FuriganaTextView(this).also {
    it.setFuriganaText("<ruby>あいうえお<rt>あいうえお</rt></ruby><ruby>ABCDEFG<rt>ABCDEFG</rt></ruby><ruby>１２<rt>１２３４５６７８</rt></ruby><ruby>12<rt>12345678</rt></ruby>",true)
    base.addView(it)
}

se.fekete.furiganatextview.furiganaview.FuriganaTextView(this).also {
    it.setFuriganaText("<ruby>あ<rt>あいうえお</rt></ruby><ruby>あ<rt>あいうえお</rt></ruby><ruby>あ<rt>あいうえお</rt></ruby><ruby>あ<rt>あいうえお</rt></ruby><ruby>あ<rt>あいうえお</rt></ruby><ruby>あ<rt>あいうえお</rt></ruby><ruby>あ<rt>あいうえお</rt></ruby><ruby>あ<rt>あいうえお</rt></ruby><ruby>あ<rt>あいうえお</rt></ruby><ruby>あ<rt>あいうえお</rt></ruby><ruby>あ<rt>あいうえお</rt></ruby><ruby>あ<rt>あいうえお</rt></ruby><ruby>あ<rt>あいうえお</rt></ruby><ruby>あ<rt>あいうえお</rt></ruby><ruby>あ<rt>あいうえお</rt></ruby>",true)
    base.addView(it)
}

se.fekete.furiganatextview.furiganaview.FuriganaTextView(this).also {
    it.setFuriganaText("　<ruby>\uD83D\uDC4F<rt>ぱちぱちぱち</rt></ruby><ruby>\uD83D\uDC4F<rt>ぱちぱちぱち</rt></ruby>", true)
    base.addView(it)
}
```

### 確認結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78503/500028be-b7e5-203d-fda6-12d1551f89c3.png)


* 文章がなぜか途中で途切れることがありました。原因は不明。
* ルビが長い場合、テキストの位置は調整されないようで隣のルビに被りました。
* 行頭や末尾のルビが長いと途切れました。
* 絵文字のルビは表示されました。



## [FuriganaView ( guentoan / FuriganaView )](https://github.com/guentoan/FuriganaView)

### 特徴

* ライセンス: 不明
* 最終コミット日: 2016-07-03
* TextViewを継承
* XMLレイアウト対応
* 入力フォーマット：「{テキスト;ルビ}」
* &lt;b&gt;&lt;i&gt;&lt;br&gt;といったタグに対応

### 確認ソース

```kotlin
com.akira.nguyen.furigana.widget.FuriganaView(this).also {
    it.setJText("{Android;アンドロイド}は、{Google;グーグル}が{開発;かいはつ}した{携帯汎用;けいたいはんよう}オペレーティングシステムである。")
    base.addView(it)
}

com.akira.nguyen.furigana.widget.FuriganaView(this).also {
    it.setJText("{あいえうお;あいうえお}{ABCDEFG;ABCDEFG}{１２;１２３４５６７８}{12;12345678}")
    base.addView(it)
}

com.akira.nguyen.furigana.widget.FuriganaView(this).also {
    it.setJText("{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}")
    base.addView(it)
}

com.akira.nguyen.furigana.widget.FuriganaView(this).also {
    it.setJText("　{\uD83D\uDC4F;ぱちぱちぱち}{\uD83D\uDC4F;ぱちぱちぱち}")
    base.addView(it)
}
```

### 確認結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78503/4c69d863-6833-08dd-970d-a23279abfff4.png)

* アスキー文字などに大してルビが振れませんでした。文字種別を見て判定している模様です。
* デフォルトだと少し縦の余白が大きい感じです。
* ルビが長い場合に、テキストに適切な余白がとられました。
* 行頭、行末、改行の処理は適切に行われました。



## [FuriganaView ( Vexu / Furigana-TextView )](https://github.com/Vexu/Furigana-TextView)

### 特徴

* ライセンス: MIT License
* 最終コミット日:  2017-08-08
* TextViewを継承
* XMLレイアウト対応
* 入力フォーマット：「{テキスト;ルビ}」
* &lt;b&gt;&lt;i&gt;&lt;u&gt;&lt;br&gt;といったタグに対応

### 確認ソース

```kotlin
com.wa2c.testrubytext.FuriganaView(this).also {
    it.setText("{Android;アンドロイド}は、{Google;グーグル}が{開発;かいはつ}した{携帯汎用;けいたいはんよう}オペレーティングシステムである。")
    base.addView(it)
}

com.wa2c.testrubytext.FuriganaView(this).also {
    it.setText("{あいえうお;あいうえお}{ABCDEFG;ABCDEFG}{１２;１２３４５６７８}{12;12345678}")
    base.addView(it)
}
com.wa2c.testrubytext.FuriganaView(this).also {
    it.setText("{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}")
    base.addView(it)
}

com.wa2c.testrubytext.FuriganaView(this).also {
    it.setText("　{\uD83D\uDC4F;ぱちぱちぱち}{\uD83D\uDC4F;ぱちぱちぱち}")
    base.addView(it)
}
```

### 確認結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78503/3cb07bab-431d-3c82-9a2c-2771e66728f9.png)

* 微妙に上下のレイアウトが被って文字が切れている部分があるので、縦幅は調整した方が良いかもしれません
* ルビが長い場合に、テキストに適切な余白がとられました。
* ルビが短い場合に、ルビに余白をとってバランスよく文字が配置されました。
* 行頭、行末、改行の処理は適切に行われました。
* 絵文字は表示されませんでしたが、ルビは表示されました。



## [FuriganaView ( sh0 / furigana-view )](https://github.com/sh0/furigana-view)

### 特徴

* ライセンス: Creative Commons BY-SA 3.0
* 最終コミット日: 2013-06-29
* Viewを継承
* XMLレイアウト(恐らく)未対応
* 入力フォーマット：「{テキスト;ルビ}」
* mark_s, mark_e の指定で一部範囲を太字にできる

### 確認ソース

```kotlin
ee.yutani.furiganaview.FuriganaView(this).also {
    val tp = TextPaint()
    tp.textSize = 36f
    val text = "{Android;アンドロイド}は、{Google;グーグル}が{開発;かいはつ}した{携帯汎用;けいたいはんよう}オペレーティングシステムである。"
    it.text_set(tp, text, 0, 0)
    base.addView(it)
}

ee.yutani.furiganaview.FuriganaView(this).also {
    val tp = TextPaint()
    tp.textSize = 36f
    val text = "{あいえうお;あいうえお}{ABCDEFG;ABCDEFG}{１２;１２３４５６７８}{12;12345678}"
    it.text_set(tp, text, 0, 7)
    base.addView(it)
}

ee.yutani.furiganaview.FuriganaView(this).also {
    val tp = TextPaint()
    tp.textSize = 36f
    val text = "{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}{あ;あいうえお}"
    it.text_set(tp, text, 0, 7)
    base.addView(it)
}

ee.yutani.furiganaview.FuriganaView(this).also {
    val tp = TextPaint()
    tp.textSize = 36f
    val text = "　{\uD83D\uDC4F;ぱちぱちぱち}{\uD83D\uDC4F;ぱちぱちぱち}"
    it.text_set(tp, text, 0, 0)
    base.addView(it)
}
```

### 確認結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78503/86aadeeb-476b-3bae-c526-013ddfc5e322.png)

* TextViewではないので、TextViewとスタイルを合わせるにはTextPaintに適切な設定する必要があります。
* ルビが長い場合、テキストの位置は調整されないようで隣のルビに被りました。
* 行頭や末尾のルビが長いと途切れました。
* 絵文字のルビは表示されました。



## [RubyTextView ( b84330808 / RubyTextView )](https://github.com/b84330808/RubyTextView)

### 特徴

* ライセンス: Apache License 2.0
* 最終コミット日: 2019-09-17
* AppCompatTextViewを継承
* XMLレイアウト対応
* 入力フォーマット：「{ テキスト|ルビ }」
* ルビの色、スタイル、余白などを調整出来る。

### 確認ソース

```kotlin
me.weilunli.views.RubyTextView(this).also {
    it.combinedText = "Android|アンドロイド は、 Google|グーグル が 開発|かいはつ した 携帯汎用|けいたいはんよう オペレーティングシステムである。"
    base.addView(it)
}

me.weilunli.views.RubyTextView(this).also {
    it.combinedText = "あいえうお|あいうえお ABCDEFG|ABCDEFG １２|１２３４５６７８ 12|12345678"
    base.addView(it)
}

me.weilunli.views.RubyTextView(this).also {
    it.combinedText = "あ|あいうえお あ|あいうえお あ|あいうえお あ|あいうえお あ|あいうえお あ|あいうえお あ|あいうえお あ|あいうえお あ|あいうえお あ|あいうえお あ|あいうえお あ|あいうえお あ|あいうえお あ|あいうえお "
    base.addView(it)
}

me.weilunli.views.RubyTextView(this).also {
    it.combinedText = "　 \uD83D\uDC4F|ぱちぱちぱち \uD83D\uDC4F|ぱちぱちぱち"
    base.addView(it)
}
```

### 確認結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78503/af0f8147-5ef9-830d-96a3-e7e4f79d75a5.png)

* ルビが長い場合、テキストの位置は調整されないようで隣のルビに被りました。
* 行頭や末尾のルビが長いと途切れました。
* 絵文字のルビは表示されました。

## [RubySpan ( mljli / rubyspan )](https://github.com/mljli/rubyspan)

### 特徴

* ライセンス: Apache License 2.0
* 最終コミット日: 2020-01-10
* ReplacementSpanを継承
* XMLレイアウト未対応
* Spannableで文字を装飾するのと同様に、ルビの範囲をコードで指定

### 確認ソース

```kotlin
TextView(this).also {
    val ssb = SpannableStringBuilder("Androidは、Googleが開発した携帯汎用オペレーティングシステムである。")
    ssb.setSpan(RubySpan("アンドロイド"), 0, 7, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
    ssb.setSpan(RubySpan("グーグル"), 9, 15, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
    ssb.setSpan(RubySpan("かいはつ"), 16, 18, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
    ssb.setSpan(RubySpan("けいたいはんよう"), 20, 24, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
    it.text = ssb
    it.height = 120
    it.gravity = Gravity.BOTTOM
    base.addView(it)
}

TextView(this).also {
    val ssb = SpannableStringBuilder("あいえうおABCDEFG１２12")
    ssb.setSpan(RubySpan("あいえうお"), 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
    ssb.setSpan(RubySpan("ABCDEFG"), 5, 12, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
    ssb.setSpan(RubySpan("１２３４５６７８"), 12, 14, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
    //ssb.setSpan(RubySpan("12345678"), 14, 16, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
    it.text = ssb
    it.height = 64
    it.gravity = Gravity.BOTTOM
    base.addView(it)
}

TextView(this).also {
    val ssb = SpannableStringBuilder("ああああああああああああああ")
    ssb.setSpan(RubySpan("あいえうお"), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
    ssb.setSpan(RubySpan("あいえうお"), 1, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
    ssb.setSpan(RubySpan("あいえうお"), 2, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
    ssb.setSpan(RubySpan("あいえうお"), 3, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
    ssb.setSpan(RubySpan("あいえうお"), 4, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
    ssb.setSpan(RubySpan("あいえうお"), 5, 6, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
    ssb.setSpan(RubySpan("あいえうお"), 6, 7, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
    ssb.setSpan(RubySpan("あいえうお"), 7, 8, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
    ssb.setSpan(RubySpan("あいえうお"), 8, 9, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
    ssb.setSpan(RubySpan("あいえうお"), 9, 10, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
    ssb.setSpan(RubySpan("あいえうお"), 10, 11, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
    ssb.setSpan(RubySpan("あいえうお"), 11, 12, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
    ssb.setSpan(RubySpan("あいえうお"), 12, 13, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
    //ssb.setSpan(RubySpan("あいえうお"), 13, 14, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
    it.text = ssb
    it.height = 200
    it.gravity = Gravity.CENTER_VERTICAL
    base.addView(it)
}

TextView(this).also {
    val ssb = SpannableStringBuilder("　\uD83D\uDC4F\uD83D\uDC4F")
    ssb.setSpan(RubySpan("ぱちぱちぱち"), 1, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
    ssb.setSpan(RubySpan("ぱちぱちぱち"), 3, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
    it.text = ssb
    it.height =64
    it.gravity = Gravity.BOTTOM
    base.addView(it)
}
```

### 確認結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78503/275feafd-7d3f-56ee-c8ed-1049f7f86bec.png)

* ルビが表示される余白は自分で調整する必要があります。
* テキストに小さな文字(いわゆる半角文字)が含まれる場合、テキストに空白ができます。
* ルビが長い場合にテキストに余白がとられますが、余白の取り方があまり適切ではないように思います。
* 行頭、行末の場合、ルビの位置が少しずれます。
* 改行がうまく行われません。
* 末尾の文字にルビがあると、テキスト自体出力されません。(サンプルコードでは末尾のルビだけ削ってあります)
* 絵文字は表示されませんでしたが、ルビは表示されました。



## まとめ

それぞれライブラリを動かして調査してみましたが、思ったより扱いが難しそうな印象でした。

どれか一つを選ぶとすれば、[FuriganaView ( Vexu / Furigana-TextView )](https://github.com/Vexu/Furigana-TextView)になるかと思います。ルビやテキストの余白の処理や改行が最も綺麗に行われており、使い方であまり悩まなくて済みそうな印象があったためです。行の高さを少し調整する必要がありそうな点は注意ですが。絵文字が使えないのは、あまぁレアケースなので気にしないです(ちなみにルビ無しで普通に絵文字を挿入する分には問題ないです)。

他に何か良さそうなライブラリ等があれば教えていただけると幸いです。