概要
- custom view に対して、レイアウトファイルから custom attribute の設定を可能とする。
- custom attribute の設定を Android Studio のレイアウトエディタ上に反映させる。
前回のあらずじ
前回は、AppCompatTextView を継承して custom view を作成しました。
前回の不満点
レイアウトファイルから値の指定ができない
SDK 等から提供される多くの View は、レイアウトファイルから属性という形で初期値が設定できます。たとえば TextView であれば、レイアウトファイルから android:text="10"
のような指定ができます。しかし、 前回作成した UnixEpochTextView ではできませんでした。
アプリに組み込んで実行しないと動作を確認できない
SDK 等から提供される多くの View はレイアウトエディタ上で動作を確認できますが、前回作成した UnixEpochTextView ではできませんでした。
今回の課題
- レイアウトファイルにて unixEpoch を設定できるようにし、レイアウトエディタ上で表示を確認する。
実現方法
Define Custom Attributes の方法により、下記のように custom attribute を指定できるようにします。
<com.objectfanatics.chrono10.ex_cv.UnixEpochTextView
app:unixEpoch="1577804400000"/>
実装
◆ レイアウトファイル
unixEpoch=1577804400000 (2020-01-01 00:00:00.000 JST) を指定した場合に、以下のように表示されるレイアウトファイルを作成します。
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.objectfanatics.chrono10.ex_cv.UnixEpochTextView
android:id="@+id/unix_epoch_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="30sp"
app:layout_constraintBottom_toTopOf="@id/show_current_time_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
app:unixEpoch="1577804400000" />
<Button
android:id="@+id/show_current_time_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="show current time"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/unix_epoch_text_view" />
</androidx.constraintlayout.widget.ConstraintLayout>
◆ attrs.xml
レイアウトファイルから指定する custom attribute を定義します。
※ styleable は Long 型をサポートしていないので String で代用しています。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="UnixEpochTextView">
<attr name="unixEpoch" format="string" />
</declare-styleable>
</resources>
◆ Custom View クラス
- AppCompatTextView を継承
- 全コンストラクタから initAttrs(AttributeSet?) を呼び出す
- initAttrs(AttributeSet?) にて custom attributes を読み出し custom view に値をセットする
class UnixEpochTextView : AppCompatTextView {
constructor(context: Context?) : super(context) {
initAttrs(null)
}
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
initAttrs(attrs)
}
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
initAttrs(attrs)
}
private fun initAttrs(attrs: AttributeSet?) {
getLongAttr(attrs, R.styleable.UnixEpochTextView, R.styleable.UnixEpochTextView_unixEpoch, 0, this::setUnixEpoch)
}
fun setUnixEpoch(unixEpoch: Long) {
text = unixEpoch.unixEpochString
}
companion object {
private val Long.unixEpochString: String
get() = SimpleDateFormat("yyyy-MM-dd\nHH:mm:ss.SSS", Locale.US)
.apply { timeZone = TimeZone.getTimeZone("Asia/Tokyo") }
.format(Date(this))
}
}
◆ Ext.kt
custom attribute を取得するためのユーティリティクラス。
package com.objectfanatics.infra.android.view
import android.content.res.Resources
import android.content.res.TypedArray
import android.util.AttributeSet
import android.view.View
// @formatter:off
// boolean
fun View.getBooleanAttr (attrs: AttributeSet?, styleable: IntArray, index: Int, defValue: Boolean , callback: (Boolean ) -> Unit): Boolean = getBooleanAttr (attrs, styleable, index, defValue).apply(callback)
fun View.getBooleanAttr (attrs: AttributeSet?, styleable: IntArray, index: Int , callback: (Boolean ) -> Unit): Boolean = getBooleanAttr (attrs, styleable, index ).apply(callback)
fun View.getNullableBooleanAttr (attrs: AttributeSet?, styleable: IntArray, index: Int , callback: (Boolean? ) -> Unit): Boolean? = getNullableBooleanAttr (attrs, styleable, index ).apply(callback)
fun View.getBooleanAttr (attrs: AttributeSet?, styleable: IntArray, index: Int, defValue: Boolean ): Boolean = getNullableBooleanAttrInternal (attrs, styleable, index, defValue)!!
fun View.getBooleanAttr (attrs: AttributeSet?, styleable: IntArray, index: Int ): Boolean = getNullableBooleanAttrInternal (attrs, styleable, index, null ) ?: throw AttributeValueNotFoundException()
fun View.getNullableBooleanAttr (attrs: AttributeSet?, styleable: IntArray, index: Int ): Boolean? = getNullableBooleanAttrInternal (attrs, styleable, index, null )
// integer
fun View.getIntegerAttr (attrs: AttributeSet?, styleable: IntArray, index: Int, defValue: Int , callback: (Int ) -> Unit): Int = getIntegerAttr (attrs, styleable, index, defValue).apply(callback)
fun View.getIntegerAttr (attrs: AttributeSet?, styleable: IntArray, index: Int , callback: (Int ) -> Unit): Int = getIntegerAttr (attrs, styleable, index ).apply(callback)
fun View.getNullableIntegerAttr (attrs: AttributeSet?, styleable: IntArray, index: Int , callback: (Int? ) -> Unit): Int? = getNullableIntegerAttr (attrs, styleable, index ).apply(callback)
fun View.getIntegerAttr (attrs: AttributeSet?, styleable: IntArray, index: Int, defValue: Int ): Int = getNullableIntegerAttrInternal (attrs, styleable, index, defValue)!!
fun View.getIntegerAttr (attrs: AttributeSet?, styleable: IntArray, index: Int ): Int = getNullableIntegerAttrInternal (attrs, styleable, index, null ) ?: throw AttributeValueNotFoundException()
fun View.getNullableIntegerAttr (attrs: AttributeSet?, styleable: IntArray, index: Int ): Int? = getNullableIntegerAttrInternal (attrs, styleable, index, null )
// float
fun View.getFloatAttr (attrs: AttributeSet?, styleable: IntArray, index: Int, defValue: Float , callback: (Float ) -> Unit): Float = getFloatAttr (attrs, styleable, index, defValue).apply(callback)
fun View.getFloatAttr (attrs: AttributeSet?, styleable: IntArray, index: Int , callback: (Float ) -> Unit): Float = getFloatAttr (attrs, styleable, index ).apply(callback)
fun View.getNullableFloatAttr (attrs: AttributeSet?, styleable: IntArray, index: Int , callback: (Float? ) -> Unit): Float? = getNullableFloatAttr (attrs, styleable, index ).apply(callback)
fun View.getFloatAttr (attrs: AttributeSet?, styleable: IntArray, index: Int, defValue: Float ): Float = getNullableFloatAttrInternal (attrs, styleable, index, defValue)!!
fun View.getFloatAttr (attrs: AttributeSet?, styleable: IntArray, index: Int ): Float = getNullableFloatAttrInternal (attrs, styleable, index, null ) ?: throw AttributeValueNotFoundException()
fun View.getNullableFloatAttr (attrs: AttributeSet?, styleable: IntArray, index: Int ): Float? = getNullableFloatAttrInternal (attrs, styleable, index, null )
// string
fun View.getStringAttr (attrs: AttributeSet?, styleable: IntArray, index: Int, defValue: String , callback: (String ) -> Unit): String = getStringAttr (attrs, styleable, index, defValue).apply(callback)
fun View.getStringAttr (attrs: AttributeSet?, styleable: IntArray, index: Int , callback: (String ) -> Unit): String = getStringAttr (attrs, styleable, index ).apply(callback)
fun View.getNullableStringAttr (attrs: AttributeSet?, styleable: IntArray, index: Int , callback: (String? ) -> Unit): String? = getNullableStringAttr (attrs, styleable, index ).apply(callback)
fun View.getStringAttr (attrs: AttributeSet?, styleable: IntArray, index: Int, defValue: String ): String = getNullableStringAttrInternal (attrs, styleable, index, defValue)!!
fun View.getStringAttr (attrs: AttributeSet?, styleable: IntArray, index: Int ): String = getNullableStringAttrInternal (attrs, styleable, index, null ) ?: throw AttributeValueNotFoundException()
fun View.getNullableStringAttr (attrs: AttributeSet?, styleable: IntArray, index: Int ): String? = getNullableStringAttrInternal (attrs, styleable, index, null )
// integer array
fun View.getIntegerArrayAttr (attrs: AttributeSet?, styleable: IntArray, index: Int, defValue: IntArray , callback: (IntArray ) -> Unit): IntArray = getIntegerArrayAttr (attrs, styleable, index, defValue).apply(callback)
fun View.getIntegerArrayAttr (attrs: AttributeSet?, styleable: IntArray, index: Int , callback: (IntArray ) -> Unit): IntArray = getIntegerArrayAttr (attrs, styleable, index ).apply(callback)
fun View.getNullableIntegerArrayAttr (attrs: AttributeSet?, styleable: IntArray, index: Int , callback: (IntArray? ) -> Unit): IntArray? = getNullableIntegerArrayAttr (attrs, styleable, index ).apply(callback)
fun View.getIntegerArrayAttr (attrs: AttributeSet?, styleable: IntArray, index: Int, defValue: IntArray ): IntArray = getNullableIntegerArrayAttrInternal(attrs, styleable, index, defValue)!!
fun View.getIntegerArrayAttr (attrs: AttributeSet?, styleable: IntArray, index: Int ): IntArray = getNullableIntegerArrayAttrInternal(attrs, styleable, index, null ) ?: throw AttributeValueNotFoundException()
fun View.getNullableIntegerArrayAttr (attrs: AttributeSet?, styleable: IntArray, index: Int ): IntArray? = getNullableIntegerArrayAttrInternal(attrs, styleable, index, null )
// string array
fun View.getStringArrayAttr (attrs: AttributeSet?, styleable: IntArray, index: Int, defValue: Array<String>, callback: (Array<String> ) -> Unit): Array<String> = getStringArrayAttr (attrs, styleable, index, defValue).apply(callback)
fun View.getStringArrayAttr (attrs: AttributeSet?, styleable: IntArray, index: Int , callback: (Array<String> ) -> Unit): Array<String> = getStringArrayAttr (attrs, styleable, index ).apply(callback)
fun View.getNullableStringArrayAttr (attrs: AttributeSet?, styleable: IntArray, index: Int , callback: (Array<String>?) -> Unit): Array<String>? = getNullableStringArrayAttr (attrs, styleable, index ).apply(callback)
fun View.getStringArrayAttr (attrs: AttributeSet?, styleable: IntArray, index: Int, defValue: Array<String> ): Array<String> = getNullableStringArrayAttrInternal (attrs, styleable, index, defValue)!!
fun View.getStringArrayAttr (attrs: AttributeSet?, styleable: IntArray, index: Int ): Array<String> = getNullableStringArrayAttrInternal (attrs, styleable, index, null ) ?: throw AttributeValueNotFoundException()
fun View.getNullableStringArrayAttr (attrs: AttributeSet?, styleable: IntArray, index: Int ): Array<String>? = getNullableStringArrayAttrInternal (attrs, styleable, index, null )
// long by string
fun View.getLongAttr (attrs: AttributeSet?, styleable: IntArray, index: Int, defValue: Long , callback: (Long ) -> Unit): Long = getLongAttr (attrs, styleable, index, defValue).apply(callback)
fun View.getLongAttr (attrs: AttributeSet?, styleable: IntArray, index: Int , callback: (Long ) -> Unit): Long = getLongAttr (attrs, styleable, index ).apply(callback)
fun View.getNullableLongAttr (attrs: AttributeSet?, styleable: IntArray, index: Int , callback: (Long? ) -> Unit): Long? = getNullableLongAttr (attrs, styleable, index ).apply(callback)
fun View.getLongAttr (attrs: AttributeSet?, styleable: IntArray, index: Int, defValue: Long ): Long = getStringAttr (attrs, styleable, index, defValue.toString()).toLong()
fun View.getLongAttr (attrs: AttributeSet?, styleable: IntArray, index: Int ): Long = getStringAttr (attrs, styleable, index ).toLong()
fun View.getNullableLongAttr (attrs: AttributeSet?, styleable: IntArray, index: Int ): Long? = getNullableStringAttr (attrs, styleable, index )?.toLong()
// internal non-array
private fun View.getNullableBooleanAttrInternal (attrs: AttributeSet?, styleable: IntArray, index: Int, defValue: Boolean? ): Boolean? = getNullableAttrInternal (attrs, styleable, index, defValue) { typedArray: TypedArray -> typedArray.getBoolean (index, DUMMY_BOOLEAN) }
private fun View.getNullableIntegerAttrInternal (attrs: AttributeSet?, styleable: IntArray, index: Int, defValue: Int? ): Int? = getNullableAttrInternal (attrs, styleable, index, defValue) { typedArray: TypedArray -> typedArray.getInteger (index, DUMMY_INT ) }
private fun View.getNullableFloatAttrInternal (attrs: AttributeSet?, styleable: IntArray, index: Int, defValue: Float? ): Float? = getNullableAttrInternal (attrs, styleable, index, defValue) { typedArray: TypedArray -> typedArray.getFloat (index, DUMMY_FLOAT ) }
private fun View.getNullableStringAttrInternal (attrs: AttributeSet?, styleable: IntArray, index: Int, defValue: String? ): String? = getNullableAttrInternal (attrs, styleable, index, defValue) { typedArray: TypedArray -> typedArray.getString (index ) }
private fun View.getNullableReferenceAttrInternal (attrs: AttributeSet?, styleable: IntArray, index: Int, defValue: Int? ): Int? = getNullableAttrInternal (attrs, styleable, index, defValue) { typedArray: TypedArray -> typedArray.getResourceId(index, DUMMY_INT ) }
// internal array
private fun View.getNullableIntegerArrayAttrInternal(attrs: AttributeSet?, styleable: IntArray, index: Int, defValue: IntArray? ): IntArray? = getNullableArrayAttrInternal (attrs, styleable, index, defValue) { resources, resId -> resources.getIntArray(resId) }
private fun View.getNullableStringArrayAttrInternal (attrs: AttributeSet?, styleable: IntArray, index: Int, defValue: Array<String>? ): Array<String>? = getNullableArrayAttrInternal (attrs, styleable, index, defValue) { resources, resId -> resources.getStringArray(resId) }
// @formatter:on
private fun <T> View.getNullableAttrInternal(attrs: AttributeSet?, styleable: IntArray, index: Int, defValue: T?, getValue: (TypedArray) -> T?): T? =
when (attrs) {
null -> defValue
else -> context.theme.obtainStyledAttributes(attrs, styleable, 0, 0).run {
try {
when {
!hasValue(index) -> defValue
else -> getValue(this)
}
} finally {
recycle()
}
}
}
private fun <T> View.getNullableArrayAttrInternal(attrs: AttributeSet?, styleable: IntArray, index: Int, defValue: T?, getValue: (Resources, Int) -> T?): T? =
when (val resId: Int? = getNullableReferenceAttrInternal(attrs, styleable, index, null)) {
null -> defValue
else -> getValue(resources, resId)
}
class AttributeValueNotFoundException : IllegalStateException("This attr must be set. Please set attr in layout file or set default value")
private const val DUMMY_BOOLEAN: Boolean = false
private const val DUMMY_INT: Int = 0
private const val DUMMY_FLOAT: Float = 0f
結果
レイアウトファイル上から custom view に custom attribute を指定できた。
<com.objectfanatics.chrono10.ex_cv.UnixEpochTextView
app:unixEpoch="1577804400000"/>
レイアウトエディタ上で custom view の動作が確認できた。
考察
◆ 今回やったこと
今回は、UnixEpochTextView に custom attribute を追加し、レイアウトエディタ上から表示の確認することができました。
◆ styleable と Long 型
styleable は Long 型をサポートしていないため、styleable 側は String 型で定義し、プログラム側で Long 型に変換して対応しました。
これにより、実行時エラーの可能性が出てくるわけですが、現状ではこれよりベターな対策が見当たらないのでやむなしと考えています。
なお、間違った値を入れるとアプリが落ちてしまうという点については、あえてそうしています。1
◆ その他
Ext.kt は color とか dimension とかもっと拡張すると汎用的になりそう。
◆ 問題点
- 画面回転などにより値がリセットされてしまう。
◆ 次回
次回は、意図せず画面の内容がリセットされてしまうことがないように対策してみようと思います。
Links
第1回: Custom View 探求記(TextView 継承編 その1)
第2回: Custom View 探求記(TextView 継承編 その2)← いまココ!
第3回: Custom View 探求記(TextView 継承編 その3)
第4回: Custom View 探求記(TextView 継承編 その4)
第5回: Custom View 探求記(DataBindingを使うべきか使わぬべきかそれが問題だ編 その1)
-
バグのための対策コードは指数関数的にコードの負債を増していくのは自明ですから。 ↩