如何防止自定义视图在屏幕方向变化中丢失状态

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

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

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

111451 次浏览

你可以通过实现View#onSaveInstanceStateView#onRestoreInstanceState并扩展View.BaseSavedState类来做到这一点。

public class CustomView extends View {


private int stateToSave;


...


@Override
public Parcelable onSaveInstanceState() {
//begin boilerplate code that allows parent classes to save state
Parcelable superState = super.onSaveInstanceState();


SavedState ss = new SavedState(superState);
//end


ss.stateToSave = this.stateToSave;


return ss;
}


@Override
public void onRestoreInstanceState(Parcelable state) {
//begin boilerplate code so parent classes can restore state
if(!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}


SavedState ss = (SavedState)state;
super.onRestoreInstanceState(ss.getSuperState());
//end


this.stateToSave = ss.stateToSave;
}


static class SavedState extends BaseSavedState {
int stateToSave;


SavedState(Parcelable superState) {
super(superState);
}


private SavedState(Parcel in) {
super(in);
this.stateToSave = in.readInt();
}


@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(this.stateToSave);
}


//required field that makes Parcelables from a Parcel
public static final Parcelable.Creator<SavedState> CREATOR =
new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}

该工作在视图和视图的SavedState类之间进行分割。你应该在SavedState类中完成从Parcel中读取和写入的所有工作。然后,您的View类可以执行提取状态成员的工作,并执行将类恢复到有效状态所需的工作。

注意:如果View#getId返回值>= 0,则会自动调用View#onSavedInstanceStateView#onRestoreInstanceState。当你在xml中给它一个id或手动调用setId时,就会发生这种情况。否则,你必须调用View#onSaveInstanceState并将Parcelable返回到你在Activity#onSaveInstanceState中获得的包中以保存状态,然后读取它并将它从Activity#onRestoreInstanceState传递给View#onRestoreInstanceState

另一个简单的例子是CompoundButton

我认为这是一个更简单的版本。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);
}
}
下面是另一种混合使用上述两种方法的变体。 将Parcelable的速度和正确性与Bundle的简单性相结合:

@Override
public Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
// The vars you want to save - in this instance a string and a boolean
String someString = "something";
boolean someBoolean = true;
State state = new State(super.onSaveInstanceState(), someString, someBoolean);
bundle.putParcelable(State.STATE, state);
return bundle;
}


@Override
public void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
State customViewState = (State) bundle.getParcelable(State.STATE);
// The vars you saved - do whatever you want with them
String someString = customViewState.getText();
boolean someBoolean = customViewState.isSomethingShowing());
super.onRestoreInstanceState(customViewState.getSuperState());
return;
}
// Stops a bug with the wrong state being passed to the super
super.onRestoreInstanceState(BaseSavedState.EMPTY_STATE);
}


protected static class State extends BaseSavedState {
protected static final String STATE = "YourCustomView.STATE";


private final String someText;
private final boolean somethingShowing;


public State(Parcelable superState, String someText, boolean somethingShowing) {
super(superState);
this.someText = someText;
this.somethingShowing = somethingShowing;
}


public String getText(){
return this.someText;
}


public boolean isSomethingShowing(){
return this.somethingShowing;
}
}
这里的答案已经很好了,但并不一定适用于自定义ViewGroups。为了让所有自定义视图保留它们的状态,你必须在每个类中重写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);
}
}

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

class MyCompoundView : ViewGroup {


...


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


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

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

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

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


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

除了使用onSaveInstanceStateonRestoreInstanceState,你也可以使用ViewModel。让你的数据模型扩展ViewModel,然后你可以在每次Activity被重新创建时使用ViewModelProviders来获得你的模型的相同实例:

class MyData extends ViewModel {
// have all your properties with getters and setters here
}


public class MyActivity extends FragmentActivity {
@Override
public void onCreate(Bundle savedInstanceState) {


super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);


// the first time, ViewModelProvider will create a new MyData
// object. When the Activity is recreated (e.g. because the screen
// is rotated), ViewModelProvider will give you the initial MyData
// object back, without creating a new one, so all your property
// values are retained from the previous view.
myData = ViewModelProviders.of(this).get(MyData.class);


...
}
}

要使用ViewModelProviders,将以下内容添加到app/build.gradle中的dependencies:

implementation "android.arch.lifecycle:extensions:1.1.1"
implementation "android.arch.lifecycle:viewmodel:1.1.1"

注意,你的MyActivity扩展了FragmentActivity,而不仅仅是扩展了Activity

你可以在这里阅读更多关于ViewModels的信息:

我发现这个答案在Android版本9和10上导致了一些崩溃。我认为这是一个很好的方法,但是当我在看一些Android代码时,我发现它缺少一个构造函数。答案是相当古老的,所以在当时可能没有必要。当我添加了缺失的构造函数并从创建者调用它时,崩溃就修复了。

下面是编辑后的代码:

public class CustomView extends LinearLayout {


private int stateToSave;


...


@Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);


// your custom state
ss.stateToSave = this.stateToSave;


return ss;
}


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


@Override
public void onRestoreInstanceState(Parcelable state) {
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());


// your custom state
this.stateToSave = ss.stateToSave;
}


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


static class SavedState extends BaseSavedState {
int stateToSave;


SavedState(Parcelable superState) {
super(superState);
}


private SavedState(Parcel in) {
super(in);
this.stateToSave = in.readInt();
}


// This was the missing constructor
@RequiresApi(Build.VERSION_CODES.N)
SavedState(Parcel in, ClassLoader loader)
{
super(in, loader);
this.stateToSave = in.readInt();
}


@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(this.stateToSave);
}
        

public static final Creator<SavedState> CREATOR =
new ClassLoaderCreator<SavedState>() {
          

// This was also missing
@Override
public SavedState createFromParcel(Parcel in, ClassLoader loader)
{
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ? new SavedState(in, loader) : new SavedState(in);
}


@Override
public SavedState createFromParcel(Parcel in) {
return new SavedState(in, null);
}


@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}

用kotlin很容易

@Parcelize
class MyState(val superSavedState: Parcelable?, val loading: Boolean) : View.BaseSavedState(superSavedState), Parcelable




class MyView : View {


var loading: Boolean = false


override fun onSaveInstanceState(): Parcelable? {
val superState = super.onSaveInstanceState()
return MyState(superState, loading)
}


override fun onRestoreInstanceState(state: Parcelable?) {
val myState = state as? MyState
super.onRestoreInstanceState(myState?.superSaveState ?: state)


loading = myState?.loading ?: false
//redraw
}
}

根据@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"
}
}