我已经成功地实现了onRetainNonConfigurationInstance()为我的主活动保存和恢复某些关键组件跨屏幕方向的变化。

但看起来,当方向改变时,我的自定义视图正在从头重新创建。这是有意义的,尽管在我的例子中不方便,因为所讨论的自定义视图是一个X/Y图形,并且绘制的点存储在自定义视图中。

是否有一种巧妙的方法来实现类似于onRetainNonConfigurationInstance()的自定义视图,或者我只需要在自定义视图中实现方法,允许我获得和设置其“状态”?


当前回答

为了补充其他答案-如果您有多个具有相同ID的自定义复合视图,并且它们都在配置更改时使用上一个视图的状态进行恢复,您所需要做的就是告诉视图仅通过覆盖两个方法将保存/恢复事件分派给自己。

class MyCompoundView : ViewGroup {

    ...

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

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

对于正在发生的事情和为什么这样做的解释,请参阅这篇博客文章。基本上你的复合视图的子视图id是由每个复合视图共享的,状态恢复会很混乱。通过仅为复合视图本身调度状态,我们可以防止它们的子视图从其他复合视图获得混合消息。

其他回答

这里的答案已经很好了,但并不一定适用于自定义viewgroup。为了让所有自定义视图保留它们的状态,你必须在每个类中重写onSaveInstanceState()和onRestoreInstanceState(Parcelable state)。 您还需要确保它们都具有唯一的id,无论它们是从xml扩展的还是以编程方式添加的。

我想出的答案与Kobor42的答案非常相似,但错误仍然存在,因为我以编程方式将视图添加到自定义ViewGroup中,而没有分配唯一的id。

由mato共享的链接可以工作,但这意味着没有任何单个视图管理它们自己的状态——整个状态保存在ViewGroup方法中。

问题是,当多个viewgroup被添加到一个布局中时,它们在xml中的元素id不再是唯一的(如果它是在xml中定义的)。在运行时,你可以调用静态方法View. generateviewid()来获取一个视图的唯一id。这只在API 17中可用。

下面是我在ViewGroup中的代码(它是抽象的,mOriginalValue是一个类型变量):

public abstract class DetailRow<E> extends LinearLayout {

    private static final String SUPER_INSTANCE_STATE = "saved_instance_state_parcelable";
    private static final String STATE_VIEW_IDS = "state_view_ids";
    private static final String STATE_ORIGINAL_VALUE = "state_original_value";

    private E mOriginalValue;
    private int[] mViewIds;

// ...

    @Override
    protected Parcelable onSaveInstanceState() {

        // Create a bundle to put super parcelable in
        Bundle bundle = new Bundle();
        bundle.putParcelable(SUPER_INSTANCE_STATE, super.onSaveInstanceState());
        // Use abstract method to put mOriginalValue in the bundle;
        putValueInTheBundle(mOriginalValue, bundle, STATE_ORIGINAL_VALUE);
        // Store mViewIds in the bundle - initialize if necessary.
        if (mViewIds == null) {
            // We need as many ids as child views
            mViewIds = new int[getChildCount()];
            for (int i = 0; i < mViewIds.length; i++) {
                // generate a unique id for each view
                mViewIds[i] = View.generateViewId();
                // assign the id to the view at the same index
                getChildAt(i).setId(mViewIds[i]);
            }
        }
        bundle.putIntArray(STATE_VIEW_IDS, mViewIds);
        // return the bundle
        return bundle;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {

        // We know state is a Bundle:
        Bundle bundle = (Bundle) state;
        // Get mViewIds out of the bundle
        mViewIds = bundle.getIntArray(STATE_VIEW_IDS);
        // For each id, assign to the view of same index
        if (mViewIds != null) {
            for (int i = 0; i < mViewIds.length; i++) {
                getChildAt(i).setId(mViewIds[i]);
            }
        }
        // Get mOriginalValue out of the bundle
        mOriginalValue = getValueBackOutOfTheBundle(bundle, STATE_ORIGINAL_VALUE);
        // get super parcelable back out of the bundle and pass it to
        // super.onRestoreInstanceState(Parcelable)
        state = bundle.getParcelable(SUPER_INSTANCE_STATE);
        super.onRestoreInstanceState(state);
    } 
}

我认为这是一个更简单的版本。Bundle是实现Parcelable的内置类型

public class CustomView extends View
{
  private int stuff; // stuff

  @Override
  public Parcelable onSaveInstanceState()
  {
    Bundle bundle = new Bundle();
    bundle.putParcelable("superState", super.onSaveInstanceState());
    bundle.putInt("stuff", this.stuff); // ... save stuff 
    return bundle;
  }

  @Override
  public void onRestoreInstanceState(Parcelable state)
  {
    if (state instanceof Bundle) // implicit null check
    {
      Bundle bundle = (Bundle) state;
      this.stuff = bundle.getInt("stuff"); // ... load stuff
      state = bundle.getParcelable("superState");
    }
    super.onRestoreInstanceState(state);
  }
}

根据@Fletcher Johns的回答,我想出了:

自定义布局 可以从XML膨胀 能够保存/恢复直接和间接的儿童。我改进了@Fletcher Johns的答案,将Id保存在String->Id map中,而不是IntArray。 唯一的小缺点是必须预先声明可保存的子视图。


open class AddressView @JvmOverloads constructor(
        context: Context,
        attrs: AttributeSet? = null,
        defStyleAttr: Int = 0,
        defStyleRes: Int = 0
) : LinearLayout(context, attrs, defStyleAttr, defStyleRes) {

    protected lateinit var countryInputLayout: TextInputLayout
    protected lateinit var countryAutoCompleteTextView: CountryAutoCompleteTextView
    protected lateinit var cityInputLayout: TextInputLayout
    protected lateinit var cityEditText: CityEditText
    protected lateinit var postCodeInputLayout: TextInputLayout
    protected lateinit var postCodeEditText: PostCodeEditText
    protected lateinit var streetInputLayout: TextInputLayout
    protected lateinit var streetEditText: StreetEditText
    
    init {
        initView()
    }

    private fun initView() {
        val view = inflate(context, R.layout.view_address, this)

        orientation = VERTICAL

        countryInputLayout = view.findViewById(R.id.countryInputLayout)
        countryAutoCompleteTextView = view.findViewById(R.id.countryAutoCompleteTextView)

        streetInputLayout = view.findViewById(R.id.streetInputLayout)
        streetEditText = view.findViewById(R.id.streetEditText)

        cityInputLayout = view.findViewById(R.id.cityInputLayout)
        cityEditText = view.findViewById(R.id.cityEditText)

        postCodeInputLayout = view.findViewById(R.id.postCodeInputLayout)
        postCodeEditText = view.findViewById(R.id.postCodeEditText)
    }

    // Declare your direct and indirect child views that need to be saved
    private val childrenToSave get() = mapOf<String, View>(
            "coutryIL" to countryInputLayout,
            "countryACTV" to countryAutoCompleteTextView,
            "streetIL" to streetInputLayout,
            "streetET" to streetEditText,
            "cityIL" to cityInputLayout,
            "cityET" to cityEditText,
            "postCodeIL" to postCodeInputLayout,
            "postCodeET" to postCodeEditText,
    )
    private var viewIds: HashMap<String, Int>? = null

    override fun onSaveInstanceState(): Parcelable? {
        // Create a bundle to put super parcelable in
        val bundle = Bundle()
        bundle.putParcelable(SUPER_INSTANCE_STATE, super.onSaveInstanceState())
        // Store viewIds in the bundle - initialize if necessary.
        if (viewIds == null) {
            childrenToSave.values.forEach { view -> view.id = generateViewId() }
            viewIds = HashMap<String, Int>(childrenToSave.mapValues { (key, view) -> view.id })
        }

        bundle.putSerializable(STATE_VIEW_IDS, viewIds)

        return bundle
    }

    override fun onRestoreInstanceState(state: Parcelable?) {
        // We know state is a Bundle:
        val bundle = state as Bundle
        // Get mViewIds out of the bundle
        viewIds = bundle.getSerializable(STATE_VIEW_IDS) as HashMap<String, Int>
        // For each id, assign to the view of same index
        if (viewIds != null) {
            viewIds!!.forEach { (key, id) -> childrenToSave[key]!!.id = id }
        }
        super.onRestoreInstanceState(bundle.getParcelable(SUPER_INSTANCE_STATE))
    }

    companion object {
        private const val SUPER_INSTANCE_STATE = "saved_instance_state_parcelable"
        private const val STATE_VIEW_IDS = "state_view_ids"
    }
}

我有一个问题,onRestoreInstanceState恢复了我所有的自定义视图与最后一个视图的状态。我通过向我的自定义视图添加这两个方法来解决这个问题:

@Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
    dispatchFreezeSelfOnly(container);
}

@Override
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
    dispatchThawSelfOnly(container);
}

为了补充其他答案-如果您有多个具有相同ID的自定义复合视图,并且它们都在配置更改时使用上一个视图的状态进行恢复,您所需要做的就是告诉视图仅通过覆盖两个方法将保存/恢复事件分派给自己。

class MyCompoundView : ViewGroup {

    ...

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

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

对于正在发生的事情和为什么这样做的解释,请参阅这篇博客文章。基本上你的复合视图的子视图id是由每个复合视图共享的,状态恢复会很混乱。通过仅为复合视图本身调度状态,我们可以防止它们的子视图从其他复合视图获得混合消息。