Android 如何防止自定义视图在屏幕方向更改时丢失状态
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/3542333/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How to prevent custom views from losing state across screen orientation changes
提问by Brad Hein
I've successfully implemented onRetainNonConfigurationInstance()
for my main Activity
to save and restore certain critical components across screen orientation changes.
我已经成功地onRetainNonConfigurationInstance()
为我的主要实现了Activity
跨屏幕方向更改保存和恢复某些关键组件。
But it seems, my custom views are being re-created from scratch when the orientation changes. This makes sense, although in my case it's inconvenient because the custom view in question is an X/Y plot and the plotted points are stored in the custom view.
但看起来,当方向改变时,我的自定义视图正在从头开始重新创建。这是有道理的,尽管在我的情况下这很不方便,因为有问题的自定义视图是 X/Y 图,并且绘制的点存储在自定义视图中。
Is there a crafty way to implement something similar to onRetainNonConfigurationInstance()
for a custom view, or do I need to just implement methods in the custom view which allow me to get and set its "state"?
是否有一种巧妙的方法来实现类似于onRetainNonConfigurationInstance()
自定义视图的东西,或者我是否只需要在自定义视图中实现允许我获取和设置其“状态”的方法?
回答by Kobor42
I think this is a much simpler version. Bundle
is a built-in type which implements Parcelable
我认为这是一个更简单的版本。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);
}
}
回答by Rich Schuler
You do this by implementing View#onSaveInstanceState
and View#onRestoreInstanceState
and extending the View.BaseSavedState
class.
您可以通过执行做到这一点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];
}
};
}
}
The work is split between the View and the View's SavedState class. You should do all the work of reading and writing to and from the Parcel
in the SavedState
class. Then your View class can do the work of extracting the state members and doing the work necessary to get the class back to a valid state.
工作分为 View 和 View 的 SavedState 类。你应该Parcel
在SavedState
课堂上做所有的读写工作。然后,您的 View 类可以执行提取状态成员的工作,并执行使类恢复到有效状态所需的工作。
Notes: View#onSavedInstanceState
and View#onRestoreInstanceState
are called automatically for you if View#getId
returns a value >= 0. This happens when you give it an id in xml or call setId
manually. Otherwise you have to call View#onSaveInstanceState
and write the Parcelable returned to the parcel you get in Activity#onSaveInstanceState
to save the state and subsequently read it and pass it to View#onRestoreInstanceState
from Activity#onRestoreInstanceState
.
注意:如果返回值 >= 0 View#onSavedInstanceState
,View#onRestoreInstanceState
则会自动为您View#getId
调用setId
。当您在 xml 中给它一个 id 或手动调用时会发生这种情况。否则,您必须调用View#onSaveInstanceState
并写入 Parcelable 返回到您进入的包裹Activity#onSaveInstanceState
以保存状态,然后读取它并将其传递给View#onRestoreInstanceState
from Activity#onRestoreInstanceState
。
Another simple example of this is the CompoundButton
另一个简单的例子是 CompoundButton
回答by Blundell
Here is another variant that uses a mix of the two above methods.
Combining the speed and correctness of Parcelable
with the simplicity of a Bundle
:
这是另一种混合使用上述两种方法的变体。将 的速度和正确性Parcelable
与 a 的简单性相结合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;
}
}
回答by Fletcher Johns
The answers here already are great, but don't necessarily work for custom ViewGroups. To get all custom Views to retain their state, you must override onSaveInstanceState()
and onRestoreInstanceState(Parcelable state)
in each class.
You also need to ensure they all have unique ids, whether they're inflated from xml or added programmatically.
这里的答案已经很好,但不一定适用于自定义 ViewGroup。要让所有自定义视图保持其状态,您必须在每个类中覆盖onSaveInstanceState()
和onRestoreInstanceState(Parcelable state)
。您还需要确保它们都具有唯一的 id,无论它们是从 xml 膨胀还是以编程方式添加的。
What I came up with was remarkably like Kobor42's answer, but the error remained because I was adding the Views to a custom ViewGroup programmatically and not assigning unique ids.
我想出的非常像 Kobor42 的答案,但错误仍然存在,因为我以编程方式将视图添加到自定义 ViewGroup 中,而不是分配唯一的 id。
The link shared by mato will work, but it means none of the individual Views manage their own state - the entire state is saved in the ViewGroup methods.
mato 共享的链接将起作用,但这意味着没有单个视图管理自己的状态 - 整个状态保存在 ViewGroup 方法中。
The problem is that when multiple of these ViewGroups are added to a layout, the ids of their elements from the xml are no longer unique (if its defined in xml). At runtime, you can call the static method View.generateViewId()
to get a unique id for a View. This is only available from API 17.
问题是,当将多个这些 ViewGroup 添加到布局时,来自 xml 的它们元素的 id 不再唯一(如果它在 xml 中定义)。在运行时,您可以调用静态方法View.generateViewId()
来获取视图的唯一 id。这仅在 API 17 中可用。
Here is my code from the ViewGroup (it is abstract, and mOriginalValue is a type variable):
这是我来自 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);
}
}
回答by chrigist
I had the problem that onRestoreInstanceState restored all my custom views with the state of the last view. I solved it by adding these two methods to my custom view:
我遇到的问题是 onRestoreInstanceState 用最后一个视图的状态恢复了我所有的自定义视图。我通过将这两种方法添加到我的自定义视图中来解决它:
@Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
dispatchFreezeSelfOnly(container);
}
@Override
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
dispatchThawSelfOnly(container);
}
回答by Benedikt K?ppel
Instead of using onSaveInstanceState
and onRestoreInstanceState
, you can also use a ViewModel
. Make your data model extend ViewModel
, and then you can use ViewModelProviders
to get the same instance of your model every time the Activity is recreated:
除了使用onSaveInstanceState
and 之外onRestoreInstanceState
,您还可以使用 a ViewModel
。使您的数据模型扩展ViewModel
,然后您可以ViewModelProviders
在每次重新创建 Activity 时使用它来获取模型的相同实例:
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);
...
}
}
To use ViewModelProviders
, add the following to dependencies
in app/build.gradle
:
要使用ViewModelProviders
,请将以下内容添加到dependencies
中app/build.gradle
:
implementation "android.arch.lifecycle:extensions:1.1.1"
implementation "android.arch.lifecycle:viewmodel:1.1.1"
Note that your MyActivity
extends FragmentActivity
instead of just extending Activity
.
请注意,您的MyActivity
extendsFragmentActivity
而不仅仅是extends Activity
。
You can read more about ViewModels here:
您可以在此处阅读有关 ViewModel 的更多信息:
回答by Wirling
I found that this answerwas causing some crashes on Android versions 9 and 10. I think it's a good approach but when I was looking at some Android codeI found out it was missing a constructor. The answer is quite old so at the time there probably was no need for it. When I added the missing constructor and called it from the creator the crash was fixed.
我发现这个答案在 Android 版本 9 和 10 上导致了一些崩溃。我认为这是一个很好的方法,但是当我查看一些Android 代码时,我发现它缺少一个构造函数。答案很旧,所以当时可能没有必要。当我添加缺少的构造函数并从创建者那里调用它时,崩溃得到了修复。
So here is the edited code:
所以这是编辑后的代码:
public class CustomView extends View {
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];
}
};
}
}
回答by Tom
To augment other answers - if you have multiple custom compound views with the same ID and they are all being restored with the state of the last view on a configuration change, all you need to do is tell the view to only dispatch save/restore events to itself by overriding a couple of methods.
为了增加其他答案 - 如果您有多个具有相同 ID 的自定义复合视图,并且它们都在配置更改时使用最后一个视图的状态进行恢复,则您需要做的就是告诉视图仅调度保存/恢复事件通过覆盖几个方法来实现自己。
class MyCompoundView : ViewGroup {
...
override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>) {
dispatchFreezeSelfOnly(container)
}
override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) {
dispatchThawSelfOnly(container)
}
}
For an explanation of what is happening and why this works, see this blog post. Basically your compound view's children's view IDs are shared by each compound view and state restoration gets confused. By only dispatching state for the compound view itself, we prevent their children from getting mixed messages from other compound views.
有关正在发生的事情以及为什么会起作用的说明,请参阅此博客文章。基本上,您的复合视图的子视图 ID 由每个复合视图共享,状态恢复会变得混乱。通过只为复合视图本身调度状态,我们可以防止它们的子视图从其他复合视图中获取混合消息。