🐈

カスタマイズ可能なカスタムViewの作成

2021/06/26に公開

この記事の内容

複数のViewから構成されるコンポーネント群を再利用しやすくするためのカスタムView(複合コントロール)を作成します。(公式のドキュメントはこちら
利用先で要素をカスタマイズできるようにします。
今回は以下のような円形の背景色と、中のアイコンを変更できるようにしたカスタムViewを作成します。

Step1. 単純なカスタムViewを作成

まずは複数のViewをまとめただけのカスタムViewを作成します。
要素のカスタマイズはStep2で説明します。

レイアウトを定義

layout_icon_clircle_view.xml
<merge
    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"
    tools:parentTag="android.widget.FrameLayout">

    <ImageView
        android:id="@+id/imageViewCircle"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_gravity="center"
        android:src="@drawable/shape_circle"
        app:tint="?attr/colorPrimary"/>

    <ImageView
        android:id="@+id/imageViewIcon"
        android:layout_width="24dp"
        android:layout_height="24dp"
        android:layout_gravity="center"
        android:src="@drawable/ic_folder"
        app:tint="?attr/colorOnPrimary"/>
</merge>
  • あとでレイアウト(今回はFrameLayout)を継承したクラス内にインフレートするため、ルート要素は<merge>にする
  • tools:parentTag属性でマージ先にするレイアウトを指定すればレイアウトプレビューに反映される

カスタムViewクラスを作成

IconCircleView.kt
class IconCircleView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {

    init {
        // カスタムレイアウトをインフレート
        View.inflate(context, R.layout.layout_icon_circle_view, this)
    }
}
  • レイアウトを継承したクラスを作成
  • initでレイアウトをインフレート

利用方法

activity_main.xml
<LinearLayout
    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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <jp.co.sample.IconCircleView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <jp.co.sample.IconCircleView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <jp.co.sample.IconCircleView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>


activity_main.xmlに配置したIconCircleView

  • 使用したい箇所(レイアウト)に配置してあげれば表示される
  • Viewのタグはパッケージ名付クラス名になる(今回はjp.co.sample.IconCircleView

Step2. カスタマイズ機能を追加

Step1で作成したものをベースにカスタマイズ機能を追加します。

カスタム属性を定義

res/values/attrs.xml
<resources>
    <declare-styleable name="IconCircleView">
        <attr name="circleTint" format="reference" />
        <attr name="icon" format="reference" />
    </declare-styleable>
</resources>
  • declare-styleable属性に作成したカスタムViewのクラス名を指定
  • attr属性にそのカスタムViewに定義する属性名とフォーマットを指定
    • フォーマットにはintegerstring等を指定できる(今回は2つともリソースへの参照であるreference

カスタムViewクラスへ属性値取得とそれを子Viewへ反映する処理を追加

IconCircleView.kt
class IconCircleView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {

    companion object {
        // カスタム属性に値未設定時の初期値
        private const val NO_ID = -1
    }

    // 子Viewへの参照
    private val circleView: ImageView
    private val iconView: ImageView

    // カスタム属性'circleTint'の値を保持するプロパティー
    var circleTint: Int = NO_ID
        set(value) {
            field = value
            if (value != NO_ID) {
                circleView.setColorFilter(
                    ContextCompat.getColor(context, value),
                    android.graphics.PorterDuff.Mode.SRC_IN
                )
                invalidate()
                requestLayout()
            }
        }

    // カスタム属性'icon'の値を保持するプロパティー
    var icon: Int = NO_ID
        set(value) {
            field = value
            if (value != NO_ID) {
                iconView.setImageResource(icon)
                invalidate()
                requestLayout()
            }
        }

    init {
        View.inflate(context, R.layout.layout_icon_circle_view, this)

        // 子Viewの参照を取得
        circleView = findViewById(R.id.imageViewCircle)
        iconView = findViewById(R.id.imageViewIcon)

        // カスタム属性へ設定された値を取得
        context.theme.obtainStyledAttributes(
            attrs,
            R.styleable.IconCircleView,
            0,0
        ).apply {
            try {
                circleTint = getResourceId(R.styleable.IconCircleView_circleTint, NO_ID)
                icon = getResourceId(R.styleable.IconCircleView_icon, NO_ID)
            } finally {
                recycle()
            }
        }
    }
}
  • カスタム属性を保持するプロパティーのSetterset(value)で、値を設定した時にViewの状態を変更する処理をさせる
    • 処理の最後にinvalidate()requestLayout()を実行しないとViewに反映されない
  • カスタム属性へ設定された値は属性のフォーマットに応じてobtainStyledAttributes内のgetIntegergetStringメソッドで取得できる(今回の例ではgetResourceId
    • このメソッドでは、カスタム属性を定義した際に自動生成されるインデックスR.stylable.[カスタムViewクラス名]_[カスタム属性名]と初期値を指定(初期値は不要な場合もある)

利用方法

activity_main.xml
<LinearLayout
    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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <jp.co.sample.IconCircleView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <jp.co.sample.IconCircleView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:circleTint="?attr/colorAccent"
        app:icon="@drawable/ic_edit" />

    <jp.co.sample.IconCircleView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:circleTint="?attr/colorError"
        app:icon="@drawable/ic_error" />
</LinearLayout>


activity_main.xmlに配置したIconCircleView

  • app:circleTintapp:iconで定義したカスタム属性へリソースを指定すれば反映される
  • ActivityやFragmentから使用する場合は以下の通り
MainActivity.kt
val iconCircleView = IconCircleView(context = this).apply {
    circleTint = R.color.black
    icon = R.drawable.ic_favorite
}
parentView.addView(iconCircleView)

Discussion