如何防止自定义视图在屏幕方向上丢失状态
-
30-09-2019 - |
题
我已经成功实施了 onRetainNonConfigurationInstance()
对于我的主人 Activity
在屏幕方向更改中保存和还原某些关键组件。
但是看来,当方向改变时,我的自定义视图正在从头开始重新创建。这是有道理的,尽管在我的情况下,这是不方便的,因为所讨论的自定义视图是X/y绘图,并且绘制点存储在自定义视图中。
是否有一种狡猾的方法来实施类似的东西 onRetainNonConfigurationInstance()
对于自定义视图,还是我需要在自定义视图中实现方法,以使我获得并设置其“状态”?
解决方案
您通过实施来做到这一点 View#onSaveInstanceState
和 View#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类之间。您应该做往返于往返的阅读和写作的所有工作 Parcel
在里面 SavedState
班级。然后,您的视图类可以完成提取国家成员并完成必要的工作以使课程回到有效状态所必需的工作。
笔记: View#onSavedInstanceState
和 View#onRestoreInstanceState
如果您为您自动称为 View#getId
返回值> = 0。 setId
手动。否则您必须打电话 View#onSaveInstanceState
并将包裹可返回给您进入的包裹 Activity#onSaveInstanceState
保存状态并随后阅读并将其传递给 View#onRestoreInstanceState
从 Activity#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;
}
}
这里的答案已经很棒,但不一定适合自定义ViewGroup。要获取所有自定义视图以保留其状态,您必须覆盖 onSaveInstanceState()
和 onRestoreInstanceState(Parcelable state)
在每个班级中。您还需要确保它们都具有独特的ID,无论是从XML膨胀还是通过编程性添加。
我想到的非常像Kobor42的答案,但是错误之所以存在,是因为我将视图添加到了自定义ViewGroup上,而不是分配唯一的ID。
MATO共享的链接将起作用,但这意味着单个视图都不管理自己的状态 - 整个状态都保存在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);
}
}
我遇到了一个问题,即OnrestoreInstancestate用最后一个视图的状态恢复了我所有的自定义视图。我通过将这两种方法添加到我的自定义视图中来解决:
@Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
dispatchFreezeSelfOnly(container);
}
@Override
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
dispatchThawSelfOnly(container);
}
而不是使用 onSaveInstanceState
和 onRestoreInstanceState
, ,您也可以使用 ViewModel
. 。使您的数据模型扩展 ViewModel
, ,然后您可以使用 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
, ,将以下内容添加到 dependencies
在 app/build.gradle
:
implementation "android.arch.lifecycle:extensions:1.1.1"
implementation "android.arch.lifecycle:viewmodel:1.1.1"
请注意 MyActivity
扩展 FragmentActivity
而不仅仅是扩展 Activity
.
您可以在此处阅读有关ViewModels的更多信息:
要增强其他答案 - 如果您具有具有相同ID的多个自定义复合视图,并且它们都通过对配置更改的最后一个视图的状态恢复,那么您需要做的就是告诉视图以仅调度保存/还原/还原事件通过覆盖几种方法来给自己。
class MyCompoundView : ViewGroup {
...
override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>) {
dispatchFreezeSelfOnly(container)
}
override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) {
dispatchThawSelfOnly(container)
}
}
为了解释正在发生的事情以及为什么起作用的原因, 请参阅此博客文章. 。基本上,您的复合视图的孩子的视图ID是通过每个复合视图共享的,状态修复会感到困惑。仅通过向化合物视图本身派遣状态,我们就可以防止他们的孩子从其他复合视图中获取混合消息。