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

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-20 10:41:35  来源:igfitidea点击:

How to prevent custom views from losing state across screen orientation changes

androidandroid-activitystatescreen-orientation

提问by Brad Hein

I've successfully implemented onRetainNonConfigurationInstance()for my main Activityto 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. Bundleis 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#onSaveInstanceStateand View#onRestoreInstanceStateand extending the View.BaseSavedStateclass.

您可以通过执行做到这一点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 Parcelin the SavedStateclass. 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 类。你应该ParcelSavedState课堂上做所有的读写工作。然后,您的 View 类可以执行提取状态成员的工作,并执行使类恢复到有效状态所需的工作。

Notes: View#onSavedInstanceStateand View#onRestoreInstanceStateare called automatically for you if View#getIdreturns a value >= 0. This happens when you give it an id in xml or call setIdmanually. Otherwise you have to call View#onSaveInstanceStateand write the Parcelable returned to the parcel you get in Activity#onSaveInstanceStateto save the state and subsequently read it and pass it to View#onRestoreInstanceStatefrom Activity#onRestoreInstanceState.

注意:如果返回值 >= 0 View#onSavedInstanceStateView#onRestoreInstanceState则会自动为您View#getId调用setId。当您在 xml 中给它一个 id 或手动调用时会发生这种情况。否则,您必须调用View#onSaveInstanceState并写入 Parcelable 返回到您进入的包裹Activity#onSaveInstanceState以保存状态,然后读取它并将其传递给View#onRestoreInstanceStatefrom 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 Parcelablewith 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 onSaveInstanceStateand onRestoreInstanceState, you can also use a ViewModel. Make your data model extend ViewModel, and then you can use ViewModelProvidersto get the same instance of your model every time the Activity is recreated:

除了使用onSaveInstanceStateand 之外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 dependenciesin app/build.gradle:

要使用ViewModelProviders,请将以下内容添加到dependenciesapp/build.gradle

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

Note that your MyActivityextends FragmentActivityinstead of just extending Activity.

请注意,您的MyActivityextendsFragmentActivity而不仅仅是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 由每个复合视图共享,状态恢复会变得混乱。通过只为复合视图本身调度状态,我们可以防止它们的子视图从其他复合视图中获取混合消息。