【Android】SpannableStringを使って文字列に画像を挿入する


はじめに

この記事はand factory Advent Calendar 2020 の12日目の記事です。
昨日は@k_shinnさんの 【Android】ちょっと便利なDrawableを書く でした。

やりたいこと

  • 画像を文字列として扱いたい

背景

Androidアプリを作っていく中で「テキストの横に画像を表示したいんだけど〜」
という旨のオーダーをもらうことがありまして
「そんなの簡単です!」と素直〜にImageViewの横にTextViewを置いて…と下記xmlで実装しました。

hoge.xml
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="20dp"
        android:layout_marginEnd="20dp"
        android:layout_marginBottom="20dp"
        android:background="@color/background">

        <ImageView
            android:id="@+id/icon"
            android:layout_width="20dp"
            android:layout_height="20dp"
            android:src="@mipmap/ic_launcher"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/repository_name"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:ellipsize="end"
            android:textSize="20sp"
            android:textStyle="bold"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toEndOf="@+id/icon"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="テキストテキストテキスト" />
    </androidx.constraintlayout.widget.ConstraintLayout>

できた!

ですが長い文字列が来た際に困ったことが起きました。
そうです、文字の折り返し地点が不自然なものとなってしまうのです…!

今回求められていた要件としては
文字列の2行目の位置がアイコンの真下に来るイメージのものが求められていました。

★hogehoge
hogehoge

そのため、SpannableStringでちょっとゴリった実装をしていくことにしました。

ザックリ言ってしまうと画像を挿入する分の文字スペースを作って
そのスペースにSpannableStringで画像をねじ込んでいくスタイルです。

コード

IconSpannable.kt
class IconSpannable(private val targetTextView: TextView) {

    fun insertIconPrefix(@DrawableRes prefixDrawableRes: Int, sourceString: String): SpannableString {
        // 画像を挿入するため、一文字分の空白を作る
        val resultString = SpannableString("  $sourceString")
        return insertImage(prefixDrawableRes, resultString, 0, 1)
    }

    private fun insertImage(
        @DrawableRes drawableRes: Int,
        resultString: SpannableString,
        startSpan: Int,
        endSpan: Int
    ): SpannableString {
        val context = targetTextView.context
        // 対象のTextViewの高さを計測
        val size = (targetTextView.paint.descent() - targetTextView.paint.ascent()).toInt()
        val drawable = context.resources.getDrawable(drawableRes, null)
        drawable?.let {
            it.setBounds(0, 0, size, size)
            val span = ImageSpan(it)
            resultString.setSpan(span, startSpan, endSpan, Spanned.SPAN_EXCLUSIVE_INCLUSIVE)
        }
        return resultString
    }
}

使い方

    binding.repositoryName.text =
        IconSpannable(binding.repositoryName).insertIconPrefix(
            R.mipmap.ic_launcher,
            "テキストテキストテキストテキストテキストテキスト"
        )

結果

無事にTextViewの2行目が画像の下にでてきてくれました

終わりに

以上、SpannableStringを使って文字列に画像を挿入する方法でした!
単純そうな要件であっても意外とめんどくさいのがAndroid、というよりエンジニアの常ですよね。
どなたかの助けになれば幸いです。