概要
Androidアプリで、テキスト入力欄(EditText)のテキストの途中に画像を挿入したい。
また、挿入した画像をクリックしたときに何か(例えばビューアを開くなど)できるようにしたい。
実装方法を調査した。
調査・検証したこと
テキスト間に画像を挿入する方法
Spannableを使って、テキスト間に画像を挿入することができる。
-
Spanned
マークアップオブジェクトを持つテキストのインターフェース
すべてのtextのクラスに変更可能なマークアップやテキストがあるわけではない -
Spannable
マークアップオブジェクトを付加したり分離したりできるテキストのインターフェース
すべてのSpannableのクラスに変更可能なテキストがあるわけではない -
Editable
内容やマークアップを変更できるテキストのインターフェース -
SpannableString
内容は変更不可であるがマークアップオブジェクトを付加したり分離したりできるテキストのクラス -
SpannableStringBuilder
内容やマークアップを変更できるテキストのクラス
画像マークアップはImageSpanである。
リファレンスに記載されている使用例を以下に示す。
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
)
スペースのあたりに画像が挿入される(スペースが置換される)。
// イメージ
Bottom:😊span.
Baseline:😊span.
以下の画像リソースをImageSpanにできる。
EditTextのテキストに画像を挿入する
固定の文字列に画像を挿入する方法は判明したため、同様の方法でEditTextのテキスト間に画像を挿入してみる。
EditTextのtextはEditableであるためsetSpan等が可能である。
以下のようなコードで、カーソルの位置に画像を挿入できる……
val cursorEnd = editText.getSelectionEnd() // カーソルの終端が何文字目の位置にあるか
editText.text.insert(cursorEnd, " ") // ImageSpanで置換するための文字列を挿入する
editText.text.setSpan(ImageSpan(drawable), cursorEnd, cursorEnd + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
……と考えたが、これだけでは「何かが挿入されているっぽいけど何も見えない」状態になる。

表示するときの画像の四隅の位置を明示的に設定する必要がある。
drawable.setBounds(
left = 0,
top = 0,
right = drawable.getIntrinsicWidth(),
bottom = drawable.getIntrinsicHeight()
)
getIntrinsic*()でdrawableの幅・高さを取得している。
このdrawableをSpanのリソースにすることで、カーソルの位置にdrawableが表示される(insertで挿入したスペースが置換される)。

カーソルの位置
上記の例では、カーソルの位置としてgetSelectionEnd()で取得した値を用いている。
getSelectionStart()もあり、これはカーソルの開始位置が取得できる。
getSelectionStart()とgetSelectionEnd()の値は基本的に一致するが、範囲選択をした場合には異なる値になる。
ImageSpanにクリックイベントを付ける
画像をクリック(タップ)したときに何かできるようにしたい。
ClickableSpanというSpanを使う。
ImageSpanと同じ位置にClickableSpanをセットすると、画像をクリックしたときにClickableSpanのonClickが発火するようになる。
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)
その他
行間を調整する
ImageSpanはデフォルトでは下揃えで表示される。このとき、行間が広いと、顕著なClickableSpanとのズレが発生する。
textの行間指定attrは割合(lineSpacingMultiplier)と固定値(lineSpacingExtra)の2種類あり、割合で指定すると画像の高さに応じて行間が広くなってしまう。そこで、行間を固定値で指定すれば、画像の大きさに依らず行間を一定にできる。
<EditText
android:lineSpacingMultiplier="1.03" />
<EditText
android:lineSpacingExtra="3dp" />
例えば画像の高さを1000dpとすると、lineSpacingMultiplierでは行間が30dpになってしまうが、lineSpacingExtraであれば3dp固定にできる。
画像の右側をクリックしたときの挙動
たとえば、画像のすぐ右側にカーソルを持って行こうとして画像の右側をクリックすると、ClickableSpanが反応してイベントが発生してしまう。

画像の右側に何か文字があれば回避できるため、画像とあわせて半角スペース等も挿入すると良さそうである。
挿入した画像を取得する
getSpans()で挿入されているSpan一覧を取得できる。Spanの種類(クラス)を指定することもできる。
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を挿入すると、画像タップでイベントを発火させられるようになる
