前回のあらずじ
前回は、custom view の情報をレイアウトファイルから設定できるようにし、その振る舞いをレイアウトエディタ上から確認しました。
前回の不満点
- 画面回転などにより値がリセットされてしまう。
今回の課題
- custom view に状態保存機能を実装する。
関連する関数
-
protected open fun onSaveInstanceState(): Parcelable?
- 内部状態表現を生成する
-
protected open fun onRestoreInstanceState(state: Parcelable!): Unit
- onSaveInstanceState() で生成された情報から状態を復元する
-
open fun setSaveEnabled(enabled: Boolean): Unit
- view の状態保存を有効にするかどうかを設定する(idは必須)
- 【私見】外部からの設定を前提としているため、custom view 内部から呼び出すものではないと思う。
現状確認
▼ SHOW CURRENT TIME ボタン押下直後
ボタン押下時の現在時刻が表示されている。
▼ 画面回転直後
セットされた時刻がリセットされてしまっている。
変更実装
◆ build.gradle
API 23 で DateTimeFormatter を利用したかったので、 desugaring を利用するために以下を追加しました。
android {
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.9'
}
◆ Custom View クラス
- SimpleDateFormat 等のクラスを DateTimeFormatter 等の Java 8 new Time APIs に置き換えました。
- onSaveInstanceState(), onRestoreInstanceState(Parcelable) をオーバーライドすることにより状態保存の仕組みを導入しました。
package com.objectfanatics.chrono10.ex_cv
import android.content.Context
import android.os.Parcelable
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView
import com.objectfanatics.chrono10.R
import com.objectfanatics.infra.android.view.ThisInstanceStateBase
import com.objectfanatics.infra.android.view.getLongAttr
import com.objectfanatics.infra.android.view.restoreThisInstanceStateAndGetSuperInstanceState
import kotlinx.android.parcel.Parcelize
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.*
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::unixEpoch.setter)
}
var unixEpoch: Long
get() = Instant.from(unixEpochFormatter.parse(text)).toEpochMilli()
set(unixEpoch) {
text = unixEpoch.unixEpochString
}
companion object {
private val unixEpochFormatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd\nHH:mm:ss.SSS", Locale.US).withZone(ZoneId.of("Asia/Tokyo"))
private val Long.unixEpochString: String
get() = unixEpochFormatter.format(Instant.ofEpochMilli(this))
}
override fun onSaveInstanceState(): Parcelable =
ThisInstanceInstanceState(super.onSaveInstanceState(), unixEpoch)
override fun onRestoreInstanceState(thisStateParcel: Parcelable) =
super.onRestoreInstanceState(restoreThisInstanceStateAndGetSuperInstanceState(thisStateParcel, this::restoreThisState))
private fun restoreThisState(thisInstanceState: ThisInstanceInstanceState) {
unixEpoch = thisInstanceState.unixEpoch
}
@Parcelize
private data class ThisInstanceInstanceState(
override val superInstanceState: Parcelable?,
val unixEpoch: Long
) : ThisInstanceStateBase, Parcelable
}
◆ Viewの状態保存に関する共通コード
interface ThisInstanceStateBase {
val superInstanceState: Parcelable?
}
fun <T : ThisInstanceStateBase> restoreThisInstanceStateAndGetSuperInstanceState(thisInstanceState: Parcelable, restoreThisInstanceState: (T) -> Unit): Parcelable? =
@Suppress("UNCHECKED_CAST")
(thisInstanceState as T).run {
restoreThisInstanceState(this)
superInstanceState
}
◆ SampleActivity
unixEpochTextView.setUnixEpoch(System.currentTimeMillis())
が unixEpochTextView.unixEpoch = System.currentTimeMillis()
に変わっただけです。
package com.objectfanatics.chrono10.ex_cv
import android.os.Bundle
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity
import com.objectfanatics.chrono10.R
class SampleActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.sample_activity)
val unixEpochTextView = findViewById<UnixEpochTextView>(R.id.unix_epoch_text_view)
val showCurrentTimeButton = findViewById<Button>(R.id.show_current_time_button)
showCurrentTimeButton.setOnClickListener { unixEpochTextView.unixEpoch = System.currentTimeMillis() }
}
}
考察
◆ 今回やったこと
今回は、UnixEpochTextView に状態保存の仕組みを追加してみました。
◆ 問題点
- SDK 等から提供される多くの View は、レイアウトファイル内部での属性指定を、databinding でもサポートしていることが多いが、現在の UnixEpochTextView ではサポートしていない。
◆ 次回
次回は、UnixEpochTextView を Data Binding に対応させてみようと思います。
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)