Custom View 探求記（TextView 継承編 その３）

# 前回のあらずじ
前回は、custom view の情報をレイアウトファイルから設定できるようにし、その振る舞いをレイアウトエディタ上から確認しました。

# 前回の不満点
- 画面回転などにより値がリセットされてしまう。

# 今回の課題
- custom view に状態保存機能を実装する。

# 関連する関数
- [protected open fun onSaveInstanceState(): Parcelable?](https://developer.android.com/reference/kotlin/android/view/View#onsaveinstancestate)
    - 内部状態表現を生成する
- [protected open fun onRestoreInstanceState(state: Parcelable!): Unit](https://developer.android.com/reference/kotlin/android/view/View#onrestoreinstancestate)
    - [onSaveInstanceState()](https://developer.android.com/reference/kotlin/android/view/View#onsaveinstancestate) で生成された情報から状態を復元する
- [open fun setSaveEnabled(enabled: Boolean): Unit](https://developer.android.com/reference/kotlin/android/view/View#setsaveenabled)
    - view の状態保存を有効にするかどうかを設定する(idは必須)
    - 【私見】外部からの設定を前提としているため、custom view 内部から呼び出すものではないと思う。

# 現状確認
▼ SHOW CURRENT TIME ボタン押下直後
ボタン押下時の現在時刻が表示されている。
<img width="240px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219474/60ea26e9-7fb7-22b7-9723-6fa8a4e3c436.png">

▼ 画面回転直後
セットされた時刻がリセットされてしまっている。
<img width="240px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219474/3c331dc3-c738-7f4b-0eba-71c574e314e8.png">

# 変更実装

## ◆ build.gradle
API 23 で DateTimeFormatter を利用したかったので、 [desugaring](https://qiita.com/beyondseeker/items/737baaffc7f0635c9e91) を利用するために以下を追加しました。

```xml
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()](https://developer.android.com/reference/kotlin/android/view/View#onSaveInstanceState()), [onRestoreInstanceState(Parcelable)](https://developer.android.com/reference/kotlin/android/view/View#onRestoreInstanceState(android.os.Parcelable)) をオーバーライドすることにより状態保存の仕組みを導入しました。

```kotlin:UnixEpochTextView.kt
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の状態保存に関する共通コード
```kotlin
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()` に変わっただけです。

```kotlin:SampleActivity.kt
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
第１回： [Custom View 探求記（TextView 継承編 その１）](https://qiita.com/beyondseeker/items/612b6bcee2937c98aedb)
第２回： [Custom View 探求記（TextView 継承編 その２）](https://qiita.com/beyondseeker/items/119873801b4bb07d4d5c)
第３回： [Custom View 探求記（TextView 継承編 その３）](https://qiita.com/beyondseeker/items/e9e04a6fe96be9e65bda)← <font color="red">いまココ！</font>
第４回： [Custom View 探求記（TextView 継承編 その４）](https://qiita.com/beyondseeker/items/64ca710fd408e559840d)
第５回： [Custom View 探求記（DataBindingを使うべきか使わぬべきかそれが問題だ編 その１）](https://qiita.com/beyondseeker/items/92c7598e6c55f23b1018)