Androidアプリのテキスト入力に画像を挿入して更にタップしたい

# 概要
Androidアプリで、テキスト入力欄（`EditText`）のテキストの途中に画像を挿入したい。

このような感じ
![add_image_succeeded.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124752/7eef0271-b081-9572-a287-39a6f204302d.png)

また、挿入した画像をクリックしたときに何か（例えばビューアを開くなど）できるようにしたい。
実装方法を調査した。

# 調査・検証したこと
## テキスト間に画像を挿入する方法
`Spannable`を使って、テキスト間に画像を挿入することができる。

- [`Spanned`](https://developer.android.com/reference/kotlin/android/text/Spanned.html)  
マークアップオブジェクトを持つテキストのインターフェース  
すべての`text`のクラスに変更可能なマークアップやテキストがあるわけではない
- [`Spannable`](https://developer.android.com/reference/kotlin/android/text/Spannable.html)  
マークアップオブジェクトを付加したり分離したりできるテキストのインターフェース  
すべての`Spannable`のクラスに変更可能なテキストがあるわけではない
- [`Editable`](https://developer.android.com/reference/kotlin/android/text/Editable.html)  
内容やマークアップを変更できるテキストのインターフェース
- [`SpannableString`](https://developer.android.com/reference/kotlin/android/text/SpannableString.html)  
内容は変更不可であるがマークアップオブジェクトを付加したり分離したりできるテキストのクラス
- [`SpannableStringBuilder`](https://developer.android.com/reference/kotlin/android/text/SpannableStringBuilder.html)  
内容やマークアップを変更できるテキストのクラス

画像マークアップは[`ImageSpan`](https://developer.android.com/reference/kotlin/android/text/style/ImageSpan.html)である。
リファレンスに記載されている使用例を以下に示す。

```kotlin
val string = SpannableString("Bottom: span.\nBaseline: span.")
// using the default alignment: ALIGN_BOTTOM
string.setSpan(
  what = ImageSpan(context = this, resourceId = R.mipmap.ic_launcher),
  start = 7,
  end = 8,
  flags = Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
string.setSpan(
  ImageSpan(
    context = this,
    resourceId = R.mipmap.ic_launcher,
    verticalAlignment = DynamicDrawableSpan.ALIGN_BASELINE
  ),
  22,
  23,
  Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
```
スペースのあたりに画像が挿入される（スペースが置換される）。

```kotlin
// イメージ
Bottom:😊span.
Baseline:😊span.
```

以下の画像リソースを`ImageSpan`にできる。

- [`Bitmap`](https://developer.android.com/reference/kotlin/android/graphics/Bitmap.html)
- [`Drawable`](https://developer.android.com/reference/kotlin/android/graphics/drawable/Drawable.html)
- リソースID（上記の`R.mipmap.ic_launcher`など）
- [`Uri`](https://developer.android.com/reference/kotlin/android/net/Uri.html)

## `EditText`のテキストに画像を挿入する
固定の文字列に画像を挿入する方法は判明したため、同様の方法で`EditText`のテキスト間に画像を挿入してみる。

`EditText`のtextは`Editable`であるため`setSpan`等が可能である。
以下のようなコードで、カーソルの位置に画像を挿入できる……

```kotlin
val cursorEnd = editText.getSelectionEnd() // カーソルの終端が何文字目の位置にあるか
editText.text.insert(cursorEnd, " ") // ImageSpanで置換するための文字列を挿入する
editText.text.setSpan(ImageSpan(drawable), cursorEnd, cursorEnd + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
```
……と考えたが、これだけでは「何かが挿入されているっぽいけど何も見えない」状態になる。
![unable_add_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124752/8457c9fc-87b4-870b-74e6-113297f026e8.png)


表示するときの画像の四隅の位置を明示的に設定する必要がある。


```kotlin
drawable.setBounds(
 left = 0,
 top = 0,
 right = drawable.getIntrinsicWidth(),
 bottom = drawable.getIntrinsicHeight()
)
```

`getIntrinsic*()`で`drawable`の幅・高さを取得している。
この`drawable`をSpanのリソースにすることで、カーソルの位置に`drawable`が表示される（`insert`で挿入したスペースが置換される）。
![add_image_succeeded.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124752/7eef0271-b081-9572-a287-39a6f204302d.png)

### カーソルの位置
上記の例では、カーソルの位置として`getSelectionEnd()`で取得した値を用いている。
`getSelectionStart()`もあり、これはカーソルの開始位置が取得できる。
`getSelectionStart()`と`getSelectionEnd()`の値は基本的に一致するが、範囲選択をした場合には異なる値になる。

## `ImageSpan`にクリックイベントを付ける
画像をクリック（タップ）したときに何かできるようにしたい。
[`ClickableSpan`](https://developer.android.com/reference/kotlin/android/text/style/ClickableSpan.html)というSpanを使う。
`ImageSpan`と同じ位置に`ClickableSpan`をセットすると、画像をクリックしたときに`ClickableSpan`の`onClick`が発火するようになる。

```kotlin
editText.text.setSpan(object : ClickableSpan() {
  override fun onClick(view: View) {
    // クリック後に実行することを書く
  }
  override fun updateDrawState(ds: TextPaint) {
    super.updateDrawState(ds)
  }
}, cursorEnd, cursorEnd + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
```

タップ中
![tapping_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124752/4d1b5f63-a8cd-ab5f-7456-1b733204cb8b.png)

## その他
### 行間を調整する
`ImageSpan`はデフォルトでは下揃えで表示される。このとき、行間が広いと、顕著な`ClickableSpan`とのズレが発生する。
textの行間指定attrは割合（`lineSpacingMultiplier`）と固定値（`lineSpacingExtra`）の2種類あり、割合で指定すると画像の高さに応じて行間が広くなってしまう。そこで、行間を固定値で指定すれば、画像の大きさに依らず行間を一定にできる。

```xml
<EditText
    android:lineSpacingMultiplier="1.03" />
<EditText
    android:lineSpacingExtra="3dp" />
```
例えば画像の高さを1000dpとすると、`lineSpacingMultiplier`では行間が30dpになってしまうが、`lineSpacingExtra`であれば3dp固定にできる。

### 画像の右側をクリックしたときの挙動
たとえば、画像のすぐ右側にカーソルを持って行こうとして画像の右側をクリックすると、`ClickableSpan`が反応してイベントが発生してしまう。
![tap_right_of_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124752/833aa6e0-e767-7ed3-365b-485c52147d7b.png)

画像の右側に何か文字があれば回避できるため、画像とあわせて半角スペース等も挿入すると良さそうである。

### 挿入した画像を取得する
`getSpans()`で挿入されているSpan一覧を取得できる。Spanの種類（クラス）を指定することもできる。

```kotlin
spannableString.getSpans(0, spannableString.length - 1, ImageSpan::class.java) // spannableString全体からImageSpanを取得する
```

`spannableString.getSpanStart(span)`で`span`の開始位置を、`span.getDrawable()`で`span`の画像（`Drawable`）を取得できる。

# まとめ
- `ImageSpan`を`EditText`に挿入するときは画像（`Drawable`）に`setBounds`する
- `ImageSpan`と同じ位置に`ClickableSpan`を挿入すると、画像タップでイベントを発火させられるようになる