🐈
カスタマイズ可能なカスタムViewの作成
この記事の内容
複数の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に定義する属性名とフォーマットを指定- フォーマットには
integer
やstring
等を指定できる(今回は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()
}
}
}
}
- カスタム属性を保持するプロパティーのSetter
set(value)
で、値を設定した時にViewの状態を変更する処理をさせる- 処理の最後に
invalidate()
とrequestLayout()
を実行しないとViewに反映されない
- 処理の最後に
- カスタム属性へ設定された値は属性のフォーマットに応じて
obtainStyledAttributes
内のgetInteger
やgetString
メソッドで取得できる(今回の例では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:circleTint
やapp:icon
で定義したカスタム属性へリソースを指定すれば反映される - ActivityやFragmentから使用する場合は以下の通り
MainActivity.kt
val iconCircleView = IconCircleView(context = this).apply {
circleTint = R.color.black
icon = R.drawable.ic_favorite
}
parentView.addView(iconCircleView)
Discussion