使用自定义属性保存和恢复视图状态
Save and Restore state for view with custom attributes
我有一个自定义视图,旨在在两种货币之间切换。这个想法很简单,一个带有货币缩写的主文本字段,一个切换按钮和一个带有转换金额的筹码。
在自定义视图中,我有另一个自定义视图,其中包含一个 EditText
和一个 TextView
,这是我用于显示主要金额的内容。这是视图 'layout' 的小图:
如您所见,这里的组件数量并不多。但是,在内部,我正在做一定量的计算和逻辑。
在 AmountEditText
视图中,我有一个自定义属性,用于设置 TextView
组件的值,而 EditText
仅处理用户输入(限于数字)并具有支持 属性 将输入的文本转换为 Float
.
在 SwitchableTextField
(扩展 ConstraintLayout
)中,我有更多自定义属性如下:
- defaultCurrency: String <= 用来设置
AmountEditText
中TextView
的内容
- conversionCurrency: String <= 与
AmountEditText
中 EditText
的转换值一起使用,以显示在 Chip
中
- conversionDirection: Boolean <= 用于定义转换的方向
我遇到问题的代码在 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)
}
}
假设我为视图设置了以下属性:
- 默认货币 = "欧元"
- conversionCurrency = "USD"
- conversionDirection = true
在 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)
并为每个需要保存的属性正确实现 SavedState
和 internal var ...
就可以了。
注意: Parcel
的 readBoolean
和 writeBoolean
函数仅存在于 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
}
}
我有一个自定义视图,旨在在两种货币之间切换。这个想法很简单,一个带有货币缩写的主文本字段,一个切换按钮和一个带有转换金额的筹码。
在自定义视图中,我有另一个自定义视图,其中包含一个 EditText
和一个 TextView
,这是我用于显示主要金额的内容。这是视图 'layout' 的小图:
如您所见,这里的组件数量并不多。但是,在内部,我正在做一定量的计算和逻辑。
在 AmountEditText
视图中,我有一个自定义属性,用于设置 TextView
组件的值,而 EditText
仅处理用户输入(限于数字)并具有支持 属性 将输入的文本转换为 Float
.
在 SwitchableTextField
(扩展 ConstraintLayout
)中,我有更多自定义属性如下:
- defaultCurrency: String <= 用来设置
AmountEditText
中 - conversionCurrency: String <= 与
AmountEditText
中EditText
的转换值一起使用,以显示在Chip
中
- conversionDirection: Boolean <= 用于定义转换的方向
TextView
的内容
我遇到问题的代码在 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)
}
}
假设我为视图设置了以下属性:
- 默认货币 = "欧元"
- conversionCurrency = "USD"
- conversionDirection = true
在 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)
并为每个需要保存的属性正确实现 SavedState
和 internal var ...
就可以了。
注意: Parcel
的 readBoolean
和 writeBoolean
函数仅存在于 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
}
}