【Android】TextViewの横に画像を表示するレイアウト方法 5選

#はじめに
TextViewの横に画像のリソースを表示をさせたい状況がありました。
試行錯誤したレイアウト方法をメモしておきます。
困った時に参考にしてください。

まずベースとなるテキストだけのLayout定義はこちらです。

```xml:layout.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:card_view="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/purple_200">

    <androidx.cardview.widget.CardView
        android:id="@+id/card_view"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:layout_marginTop="8dp"
        android:minHeight="200dp"
        app:cardCornerRadius="5dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        card_view:contentPadding="5dp">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/card_view_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:minHeight="200dp"
            app:layout_constraintBottom_toBottomOf="@id/card_view"
            app:layout_constraintEnd_toEndOf="@id/card_view"
            app:layout_constraintStart_toStartOf="@id/card_view"
            app:layout_constraintTop_toTopOf="@id/card_view">

            <TextView
                android:id="@+id/card_view_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="5dp"
                android:layout_marginLeft="5dp"
                android:gravity="center_vertical"
                android:maxWidth="180dp"
                android:text="hoge hoge hoge hoge テキスト"
                app:layout_constrainedWidth="true"
                app:layout_constraintBottom_toBottomOf="@id/card_view_layout"
                app:layout_constraintStart_toStartOf="@id/card_view_layout"
                app:layout_constraintTop_toTopOf="@id/card_view_layout" />

        </androidx.constraintlayout.widget.ConstraintLayout>
    </androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
```

これを表示すると以下のようになります。

![ノーマル.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264726/0d520b95-bdf5-f673-834b-03e72af7ebb5.png)

構成を簡単に説明するとCardView上にテキストが乗っていて文字が一定数以上だと改行するみたいなレイアウトになっています。

この時に、テキストの右横に画像を付けたい場合の方法、利点、弱点を紹介していきます。
表示する画像ですが、用意しなくても良いように AndroidSDK内にあるファイル
R.drawable.ic_btn_speak_now 
![マイク.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264726/408e9f7b-bd75-c6c1-4064-9949f0649adb.png)
のマイクのような画像を使います。

#1.TextViewのdrawable[Left/Right/Top/Bottom]を利用する方法

最初に紹介する方法は
drawableRight
を利用した方法です。
レイアウトは以下の定義となります。

```xml:layout.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:card_view="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/purple_200">

    <androidx.cardview.widget.CardView
        android:id="@+id/card_view"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:layout_marginTop="8dp"
        android:minHeight="200dp"
        app:cardCornerRadius="5dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        card_view:contentPadding="5dp">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/card_view_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:minHeight="200dp"
            app:layout_constraintBottom_toBottomOf="@id/card_view"
            app:layout_constraintEnd_toEndOf="@id/card_view"
            app:layout_constraintStart_toStartOf="@id/card_view"
            app:layout_constraintTop_toTopOf="@id/card_view">

            <TextView
                android:id="@+id/card_view_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="5dp"
                android:layout_marginLeft="5dp"
                android:drawableRight="@android:drawable/ic_btn_speak_now"
                android:drawablePadding="2dp"
                android:gravity="center_vertical"
                android:maxWidth="180dp"
                android:text="hoge hoge hoge hoge テキスト"
                app:layout_constrainedWidth="true"
                app:layout_constraintBottom_toBottomOf="@id/card_view_layout"
                app:layout_constraintStart_toStartOf="@id/card_view_layout"
                app:layout_constraintTop_toTopOf="@id/card_view_layout" />

        </androidx.constraintlayout.widget.ConstraintLayout>
    </androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
```

これで表示を確認すると↓ようになりました。

![drawableRight.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264726/d7784d75-4093-f8e4-cc74-e8478c98f69b.png)

利点は、レイアウトファイルに定義するだけなので簡単です。drawablePaddingの定義で文字との距離が確保できます。
画像もTextViewの一部と判定されるので maxWidth 制約による改行の位置が変わりました。
弱点は画像のサイズが元々の画像に依存します。表示位置の微調整も難しいです。文字列の最後尾に置くといった文字列との共存は難しいようです。

その他にdrawableRight、drawableTop、drawableBottomの定義も可能です。
試しにdrawableBottomに定義し表示を確認すると↓のようになります。

![drawableBottom.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264726/6ab9c317-b069-ce20-06c8-fc1c9b9d24ca.png)

#2.Spannableを利用する方法

SpannableStringのAPIを使用して実装した例が↓です。

```kotlin:fragment.kt
class FirstFragment : Fragment() {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val string = SpannableString("hoge hoge hoge hoge テキスト ")
        string.setSpan(
            ImageSpan(context as Context, android.R.drawable.ic_btn_speak_now),
            24,
            25,
            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
        )

        view.findViewById<TextView>(R.id.card_view_text).setText(string)
    }
}
```

これで表示を確認すると↓のようになります。
setSpan()の第2引数と第3引数の値で指定した箇所あたりに画像が表示されます。

![Spannable.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264726/cdcdb17e-b94a-b768-3fee-2791081e4aeb.png)

この方法の利点は、文字列内に画像が表示されます。改行にも対応してくれます。
弱点はコードに手を入れることになるので、可読性への影響やUIテストへの影響があります。画像サイズの調整や位置の微調整ができません。

setSpan()の第2引数と第3引数の指定を14, 15に変えて表示を確認すると↓のようになります。

```kotlin
string.setSpan(
    ImageSpan(context as Context, android.R.drawable.ic_btn_speak_now),
    14,
    15,
    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
```

![Spannable2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264726/a0c2c1ea-2b36-ac77-e1d1-6e7e0ce28a44.png)

#3.HTMLを利用する方法

HTMLのAPIを使用して実装した例が↓です。

```kotlin:fragment.kt
class FirstFragment : Fragment() {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val imageGetter = Html.ImageGetter { source ->
            val resId: Int = when (source) {
                "ic_btn_speak_now" -> android.R.drawable.ic_btn_speak_now
                else -> throw IllegalArgumentException()
            }

            when (resId) {
                android.R.drawable.ic_btn_speak_now -> {
                    context?.let {
                        ContextCompat.getDrawable(it, resId)?.apply {
                            setBounds(0, 0, intrinsicWidth, intrinsicHeight)
                        }
                    }
                }
                else -> throw IllegalArgumentException()
            }
        }

        val string = "hoge hoge hoge hoge テキスト <img src='ic_btn_speak_now'>"

        view.findViewById<TextView>(R.id.card_view_text).setText(
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                Html.fromHtml(string, Html.FROM_HTML_MODE_LEGACY, imageGetter, null)
            } else {
                Html.fromHtml(string, imageGetter, null)
            }
        )
    }
}
```

これで表示を確認すると↓のようになります。
![HTML_EX1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264726/df2164a4-bdf9-b02d-52b4-9df38635555f.png)

文字列を変えると表示も変わります。

```kotlin
val string = "hoge hoge <img src='ic_btn_speak_now'> hoge hoge テキスト"
```

![HTML_EX2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264726/ea2c525f-5ca0-7eb9-aa45-7087108ffdb5.png)

この方法だと画像の表示サイズが微調整可能です

```kotlin
setBounds(0, 0, (intrinsicWidth * 0.5).toInt(), (intrinsicHeight * 0.5).toInt())
```

![HTML_EX3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264726/942c7996-802e-e175-62cf-77d273143926.png)

#4.ImageViewをTextViewの横に置く方法

TextViewとImageViewを並べるというシンプルな方法です。
レイアウト定義の例はこちらです。

```xml:layout.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:card_view="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/purple_200">

    <androidx.cardview.widget.CardView
        android:id="@+id/card_view"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:layout_marginTop="8dp"
        android:minHeight="200dp"
        app:cardCornerRadius="5dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        card_view:contentPadding="5dp">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/card_view_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:minHeight="200dp"
            app:layout_constraintBottom_toBottomOf="@id/card_view"
            app:layout_constraintEnd_toEndOf="@id/card_view"
            app:layout_constraintStart_toStartOf="@id/card_view"
            app:layout_constraintTop_toTopOf="@id/card_view">

            <TextView
                android:id="@+id/card_view_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="5dp"
                android:layout_marginLeft="5dp"
                android:gravity="center_vertical"
                android:maxWidth="180dp"
                android:text="hoge hoge hoge hoge テキスト"
                app:layout_constrainedWidth="true"
                app:layout_constraintBottom_toBottomOf="@id/card_view_layout"
                app:layout_constraintStart_toStartOf="@id/card_view_layout"
                app:layout_constraintTop_toTopOf="@id/card_view_layout" />

            <ImageView
                android:id="@+id/card_view_image"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:src="@android:drawable/ic_btn_speak_now"
                app:layout_constraintBottom_toBottomOf="@id/card_view_text"
                app:layout_constraintStart_toEndOf="@id/card_view_text"
                app:layout_constraintTop_toTopOf="@id/card_view_text" />

        </androidx.constraintlayout.widget.ConstraintLayout>
    </androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
```

これを表示すると

![ImageView.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264726/525f9711-f174-2f7b-8bf5-68eed30e3afa.png)

この方法は
一方、この方法の良さはImageViewなのでのサイズを自由に指定できます。
例えば、layout_widthとlayout_heightを50dpにしてみます。

```xml:layout.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:card_view="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/purple_200">

    <androidx.cardview.widget.CardView
        android:id="@+id/card_view"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:layout_marginTop="8dp"
        android:minHeight="200dp"
        app:cardCornerRadius="5dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        card_view:contentPadding="5dp">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/card_view_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:minHeight="200dp"
            app:layout_constraintBottom_toBottomOf="@id/card_view"
            app:layout_constraintEnd_toEndOf="@id/card_view"
            app:layout_constraintStart_toStartOf="@id/card_view"
            app:layout_constraintTop_toTopOf="@id/card_view">

            <TextView
                android:id="@+id/card_view_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="5dp"
                android:layout_marginLeft="5dp"
                android:gravity="center_vertical"
                android:maxWidth="180dp"
                android:text="hoge hoge hoge hoge テキスト"
                app:layout_constrainedWidth="true"
                app:layout_constraintBottom_toBottomOf="@id/card_view_layout"
                app:layout_constraintStart_toStartOf="@id/card_view_layout"
                app:layout_constraintTop_toTopOf="@id/card_view_layout" />

            <ImageView
                android:id="@+id/card_view_image"
                android:layout_width="50dp"
                android:layout_height="50dp"
                android:src="@android:drawable/ic_btn_speak_now"
                app:layout_constraintBottom_toBottomOf="@id/card_view_text"
                app:layout_constraintStart_toEndOf="@id/card_view_text"
                app:layout_constraintTop_toTopOf="@id/card_view_text" />

        </androidx.constraintlayout.widget.ConstraintLayout>
    </androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
```

これで表示すると↓になりました。

![ImageView2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264726/2a209974-82d8-7729-9009-f978726b5b7f.png)

#5.絵文字を利用する方法
「画像を表示する」という意図からはずれますが絵文字は文字なので、TextViewと共存することは可能です。絵文字で代用できるなら案としては1つあります。
絵文字は10進数で定義すると表示できました。

にっこり笑う絵文字（16進数：1F600 10進数：128512）の挿入例です。

```xml:layout.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:card_view="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/purple_200">

    <androidx.cardview.widget.CardView
        android:id="@+id/card_view"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:layout_marginTop="8dp"
        android:minHeight="200dp"
        app:cardCornerRadius="5dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        card_view:contentPadding="5dp">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/card_view_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:minHeight="200dp"
            app:layout_constraintBottom_toBottomOf="@id/card_view"
            app:layout_constraintEnd_toEndOf="@id/card_view"
            app:layout_constraintStart_toStartOf="@id/card_view"
            app:layout_constraintTop_toTopOf="@id/card_view">

            <TextView
                android:id="@+id/card_view_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="5dp"
                android:layout_marginLeft="5dp"
                android:gravity="center_vertical"
                android:maxWidth="180dp"
                android:text="hoge hoge hoge hoge テキスト &#128512;"
                app:layout_constrainedWidth="true"
                app:layout_constraintBottom_toBottomOf="@id/card_view_layout"
                app:layout_constraintStart_toStartOf="@id/card_view_layout"
                app:layout_constraintTop_toTopOf="@id/card_view_layout" />

        </androidx.constraintlayout.widget.ConstraintLayout>
    </androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
```

![絵文字1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264726/7a55e6b9-a091-7f33-0669-14c1916ec8de.png)

この方法の利点は、挿入したい場所に自由に挿入できます。
サイズ感もちょうど良いです。

例えば以下のような文字列の真ん中に挿入も簡単です。

```xml
android:text="hoge hoge &#128512; hoge hoge テキスト"
```

![絵文字2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264726/49038c65-3cab-3471-41f1-23d33f950fb8.png)

#まとめ
紹介した5つの方法の良いところ悪いところをまとめてみました。
要件によって何で実装するか上手く選択すると良いと思います。

|  | 良いところ | 悪いところ |
|:-----------|:-----------|:-----------|
| TextViewのdrawableを利用する方法 | レイアウトファイルに定義するだけで使える | 文字列の中に画像は入れられない |
| Spannableを利用する方法 | 文字列の中に画像を入れられる | ソースコードを実装する必要がある |
| HTMLを利用する方法 | 文字列の中に画像を入れられ微調整が可能 | ソースコードを実装する必要がある |
| ImageViewをTextViewの横に置く方法 | レイアウトファイルに定義するだけで使え自由度は高い | 文字列の中に画像は入れられない |
| 絵文字を利用する方法 | Stringの定義だけで表示できる | 画像の表示ではない |