[Android]annotationタグを使ってstrings.xmlで文字列を装飾する

Android で文字列を装飾したい場合、XML ファイルで文字列を定義する際にいくつかの HTML タグがサポートされています。
例えば以下のように設定してみます。

```xml
<resources>
    <string name="annotated_text"><b>この部分は太字です。</b>\n<font color="#ff0000">この部分は赤字です。</font>\n<strike>この部分は取り消し線です。</strike></string>
</resources>
```

```xml
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/annotated_text" />
```

すると、以下のように文字列が装飾されて表示されるようになります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/190187/a3b3e17c-1f6e-616a-6c57-f0b2a711f246.png" width=320 />


サポートされている HTML タグは [StringBlock](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/content/res/StringBlock.java) を見ると、以下の種類があります。

- `<b>`
- `<i>`
- `<u>`
- `<tt>`
- `<big>`
- `<small>`
- `<sup>`
- `<sub>`
- `<strike>`
- `<li>`
- `<marquee>`
- `<font>`
  - `height`、`size`、`fgcolor`、`color`、`bgcolor`、`face` の属性が使用可能（HTML でサポートされている属性にカスタムの属性が追加されている模様）
- `<a>`
  - `href` 属性が使用可能
- `<annotation>`

これらのタグだけでもある程度使えそうですが、例えば画像を表示する `ImageSpan` のような装飾を行いたい場合には、上記でサポートされているタグだけでは行えません。

しかし、上記のサポートされているタグの中の最後にある `<annotation>` タグを使うと、開発者がカスタムでスパンの設定を行うことができるので、任意の装飾を行うことができるようになります。

以下ではこの `<annotation>` タグを使った文字列の装飾の手順を紹介します。

今回のサンプルプロジェクトは [こちら](https://github.com/watabee/AnnotatedTextSample) で公開しています。


## サンプル例

以下のキャプチャでは、画像を表示している部分を `<annotation>` タグで設定しています。

`<annotation>` タグを使って、このような表示を行うための設定を見ていきます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/190187/75ee981a-091f-bf93-05db-e5f9aeb59f2e.png" width=320>

## annotation タグを設定する

まずは以下の例のように、`strings.xml` などで定義している文字列に対し、装飾したい文字列の位置に `<annotation>` タグを設定します。

```xml
<resources>
    <string name="annotated_text">この公園では以下のスポーツが楽しめます。\n\n<li><annotation image="sports_basketball"> </annotation>バスケットボール</li>\n<li><annotation image="sports_baseball"> </annotation>野球</li>\n<li><annotation image="sports_handball"> </annotation>ハンドボール</li>\n<li><annotation image="sports_kabaddi"> </annotation>カバディ</li>\n<li><annotation image="sports_tennis"> </annotation>テニス</li></string>
</resources>
```

annotation タグの属性とその値については、開発者が好きに決める形で設定します。

上記の例の場合だと `image="sports_basketball"` のように設定しています。

後述しますが、ここで設定した属性とその値はプログラムから参照する形になります。

## プログラムで装飾する

以下のようなメソッドを定義し、xml から取得した文字列に対して装飾を行います。

```kotlin
fun setAnnotatedText(textView: TextView, @StringRes textResId: Int) {
    val context = textView.context
    val annotatedText = context.getText(textResId) as SpannedString
    val annotations = annotatedText.getSpans<Annotation>(0, annotatedText.length)
    val spannableString = SpannableString(annotatedText)
    annotations.forEach {
        when (it.key) {
            "image" -> {
                val resId = context.resources.getIdentifier(it.value, "drawable", context.packageName)
                if (resId != 0) {
                    spannableString.setSpan(
                        ImageSpan(context, resId),
                        annotatedText.getSpanStart(it),
                        annotatedText.getSpanEnd(it),
                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
                    )
                }
            }
        }
    }

    textView.text = spannableString
}
```

メソッドの先頭で [Context#getText](https://developer.android.com/reference/android/content/Context#getText(int)) メソッドを使って `SpannedString` として文字列を取得しています。

`strings.xml` の文字列定義で `<annotation>` タグを設定した箇所は [Annotation](https://developer.android.com/reference/android/text/Annotation) というスパンが設定されているので、[Spanned#getSpans()](https://developer.android.com/reference/android/text/Spanned#getSpans(int,%20int,%20java.lang.Class%3CT%3E)) メソッドで設定されている全ての `Annotation` スパンを配列として取得します。

`Annotation` スパンには `getKey()` と `getValue()` メソッドがあり、これによって `<annotation>` タグで設定した属性が取得できます。

属性は `image="sports_basketball"` などのように設定しているので、`getKey()` メソッドでは `image` が `getValue()` メソッドでは `sports_basketball` が文字列として取得できます。

`image="xxx"` の `xxx` の部分は drawable のリソースファイル名を指定しており、あらかじめ配置しておいた drawable リソースの ID を `Resources#getIdentifier()` メソッドを使って取得し `ImageSpan` を適用するように処理しています。

この例では `ImageSpan` を設定するのみですが、`<annotation>` タグには任意の属性を設定できるので、他にも開発者が自由に設定を追加することが可能になります。

## おまけ ~ DataBinding で設定できるようにする

複数の TextView に対して文字列装飾を行うことがある場合は、DataBinding で設定できるようにしておくと便利かもしれません。

先ほどのメソッドに `@BindingAdapter` を設定します。

```kotlin
@BindingAdapter("annotatedText")
fun setAnnotatedText(textView: TextView, @StringRes textResId: Int) {
    ...
```

XML ファイルで以下のように `annotatedText` を指定します。

```xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <variable
            name="annotatedTextResId"
            type="int" />
    </data>

    <...>

        <TextView
            ...
            app:annotatedText="@{annotatedTextResId}"
            ... />

    </...>
</layout>
```

最後に XML ファイルに記述した変数 `annotatedTextResId` をコードから設定します。

```kotlin
val binding = ActivityMainBinding.inflate(layoutInflater)
binding.annotatedTextResId = R.string.annotated_text
```

詳細は [サンプルプロジェクト](https://github.com/watabee/AnnotatedTextSample) を参考にしてみてください。

## 参考サイト

- https://developer.android.com/guide/topics/resources/string-resource
- https://medium.com/androiddevelopers/styling-internationalized-text-in-android-f99759fb7b8f