使用自定义属性保存和恢复视图状态

Save and Restore state for view with custom attributes

我有一个自定义视图,旨在在两种货币之间切换。这个想法很简单,一个带有货币缩写的主文本字段,一个切换按钮和一个带有转换金额的筹码。

在自定义视图中,我有另一个自定义视图,其中包含一个 EditText 和一个 TextView,这是我用于显示主要金额的内容。这是视图 'layout' 的小图:

如您所见,这里的组件数量并不多。但是,在内部,我正在做一定量的计算和逻辑。

AmountEditText 视图中,我有一个自定义属性,用于设置 TextView 组件的值,而 EditText 仅处理用户输入(限于数字)并具有支持 属性 将输入的文本转换为 Float.

SwitchableTextField(扩展 ConstraintLayout)中,我有更多自定义属性如下:

我遇到问题的代码在 SwitchableTextField 中,如下(为简单起见,一些部分已被删除):

class SwitchableTextField(context: Context, attrs: AttributeSet) :
    ConstraintLayout(context, attrs) {

    var defaultCurrency: String = CURRENCY_EUR
        set(value) {
            if (field != value) {
                field = value
                binding.mainAmountField.currency = value
            }
        }

    var mainAmount: Float = 0f
        get() = binding.mainAmountField.tokenAmount.toString().parseDecimalNumber()
        set(value) {
            if (field != value) {
                field = value
                convertAmount(value)
            }
        }

    var conversionCurrency: String = CURRENCY_SYMBOL
        set(value) {
            if (field != value) {
                field = value
            }
        }

    var convertedAmount: Float = 0f

    var assetAmount: Float = 0f

    var conversionDirection: Boolean = true

    var listener: AmountInputListener? = null

    init {
        isSaveEnabled = true
        applyUserAttributes(context, attrs)
        binding.mainAmountField.currency = defaultCurrency
        binding.mainAmountField.addOnTextChangedListener { text ->
            mainAmount = text.parseDecimalNumber()
        }
        binding.currencySwitchButton.setOnClickListener {
            switchConversionDirection()
        }
    }

    private fun applyUserAttributes(context: Context, attrs: AttributeSet?) {
        val typedArray = context.theme.obtainStyledAttributes(
            attrs,
            R.styleable.SwitchableCurrencyField,
            0,
            0
        )

        defaultCurrency = typedArray.getString(R.styleable.SwitchableCurrencyField_defaultCurrency)
            ?: defaultCurrency

        conversionCurrency =
            typedArray.getString(R.styleable.SwitchableCurrencyField_conversionCurrency)
                ?: conversionCurrency

        conversionDirection =
            typedArray.getBoolean(R.styleable.SwitchableCurrencyField_convertFromAsset, true)

        typedArray.recycle()
    }

    // Some internal code

    override fun onSaveInstanceState(): Parcelable? {
        return SavedState(super.onSaveInstanceState()).apply {
            childrenStates = saveChildViewStates()
        }
    }

    override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>?) {
        dispatchFreezeSelfOnly(container)
    }

    internal class SavedState : AbsSavedState {
        internal var childrenStates: SparseArray<Parcelable>? = null

        constructor(superState: Parcelable?) : super(superState)

        @Suppress("UNCHECKED_CAST")
        constructor(source: Parcel) : super(source) {
            childrenStates = source.readSparseArray(javaClass.classLoader)
        }

        @Suppress("UNCHECKED_CAST")
        override fun writeToParcel(out: Parcel, flags: Int) {
            super.writeToParcel(out, flags)
            out.writeSparseArray(childrenStates as SparseArray<Any>)
        }

        companion object {
            @Suppress("UNUSED")
            @JvmField
            val CREATOR = object : Parcelable.Creator<SavedState> {
                override fun createFromParcel(source: Parcel) = SavedState(source)
                override fun newArray(size: Int): Array<SavedState?> = arrayOfNulls(size)
            }
        }
    }

    public override fun onRestoreInstanceState(state: Parcelable?) {
        when (state) {
            is SavedState -> {
                super.onRestoreInstanceState(state.superState)
                state.childrenStates?.let { restoreChildViewStates(it) }
            }
            else -> super.onRestoreInstanceState(state)
        }
    }

    override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>?) {
        dispatchThawSelfOnly(container)
    }
}

假设我为视图设置了以下属性:

EditText 中输入“10”后,视图显示如下:

 10 EUR
// Button
11.87 USD

按 ImageButton 会按预期工作,并且值和货币缩写都会切换。但是,我遇到的问题是,当我在切换后导航到下一个片段时(意味着美元在 AmountEditText 而不是 EUR 中),返回到具有此视图的片段将金额保持在 EditText 但 returns 定义的属性为它们最初的值:defaultCurrency = "EUR", conversionCurrency = "USD", conversionDirection = true

我希望发生的工作流程如下:

 10 EUR                             11.87 USD                                11.87 USD
// Button -- Switching currency --> // Button -- Next fragment then back --> // Button
11.87 USD                            10 EUR                                   10 EUR

实际发生的是:

 10 EUR                             11.87 USD                                11.87 EUR
// Button -- Switching currency --> // Button -- Next fragment then back --> // Button
11.87 USD                            10 EUR                                  14.09 USD

意味着在 EditText 中输入的文本保留(这很好)但其余部分已重置,因此转换不正确。

我怀疑问题出在从 TypedArray 读取自定义属性并重置恢复的数据,但我不知道该如何更正此问题。

解决方案相当简单,但我花了很长时间才找到。

从属性中删除 if (field != value) 并为每个需要保存的属性正确实现 SavedStateinternal var ... 就可以了。

注意: ParcelreadBooleanwriteBoolean 函数仅存在于 API 29 中,因此使用另一种类型是必需的(在我的例子中是一个自定义枚举,用于生成一个 int 的属性)。

这是生成的 onSaveInstanceState 函数,您可以从中推导出其他函数中的剩余代码:

override fun onSaveInstanceState(): Parcelable? {
    return SavedState(super.onSaveInstanceState()).apply {
        childrenStates = saveChildViewStates()
        defaultCurrency = this@SwitchableCurrencyField.defaultCurrency
        conversionCurrency = this@SwitchableCurrencyField.conversionCurrency
        conversionDirection = this@SwitchableCurrencyField.conversionDirection
        mainAmount = this@SwitchableCurrencyField.mainAmount
    }
}