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


248

我已经成功实现onRetainNonConfigurationInstance()了主要功能,Activity可以在屏幕方向变化中保存和还原某些关键组件。

但是似乎,当方向改变时,我的自定义视图正在从头开始创建。这是有道理的,尽管对我而言这很不方便,因为相关的自定义视图是X / Y图,并且绘制的点存储在自定义视图中。

是否有巧妙的方法来实现类似于onRetainNonConfigurationInstance()自定义视图的内容,还是只需要在自定义视图中实现允许我获取并设置其“状态”的方法?

Answers:


415

您可以通过执行做到这一点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];
          }
    };
  }
}

这项工作分为View和View的SavedState类。您应该完成ParcelSavedState班级之间的来回读写工作。然后,您的View类可以完成提取状态成员的工作,并完成使类恢复为有效状态所需的工作。

注意:View#onSavedInstanceStateView#onRestoreInstanceState会在View#getId返回值> = 0时自动为您调用setId。当您在xml中为其提供ID或手动调用时,就会发生这种情况。否则,您必须调用View#onSaveInstanceState并将返回的Parcelable写入返回的包裹中Activity#onSaveInstanceState以保存状态,然后读取状态并将其传递给View#onRestoreInstanceStatefrom Activity#onRestoreInstanceState

另一个简单的例子是 CompoundButton


14
对于到达这里的用户,因为在将Fragments与v4支持库一起使用时此方法不起作用,我注意到该支持库似乎没有为您调用View的onSaveInstanceState / onRestoreInstanceState;您必须从FragmentActivity或Fragment中的方便位置自己显式调用它。
MagneticMonster

69
请注意,您对此应用的CustomView应该具有唯一的ID集,否则它们将彼此共享状态。SavedState是根据CustomView的ID存储的,因此,如果您有多个具有相同ID或没有ID的CustomView,则保存在最终CustomView.onSaveInstanceState()中的宗地将在传递给CustomView.onRestoreInstanceState()的所有调用中传递视图已还原。
尼克街,

5
对于两个自定义视图(一个扩展另一个视图),这种方法不适用于我。恢复我的视图时,我一直收到ClassNotFoundException。我不得不在Kobor42的答案中使用捆绑方法。
克里斯·菲斯特

3
onSaveInstanceState()并且onRestoreInstanceState()应该protected(像他们的超类一样),而不是public。没有理由揭露他们......
XåpplI'-I0llwlg'I -

7
当为扩展了RecyclerView的类保存自定义项时,这种方法不能很好地工作BaseSaveState,您将获得以下内容Parcel﹕ Class not found when unmarshalling: android.support.v7.widget.RecyclerView$SavedState java.lang.ClassNotFoundException: android.support.v7.widget.RecyclerView$SavedState的错误修复:github.com/ksoichiro/Android-ObservableScrollView/commit/…(使用RecyclerView.class加载超级状态)
EpicPandaForce 2015年

459

我认为这是一个简单得多的版本。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);
  }
}

5
如果返回捆绑包,为什么 onRestoreInstanceState通过捆绑包调用onSaveInstanceState呢?
Qwertie 2012年

5
OnRestoreInstance被继承。我们无法更改标题。Parcelable只是一个接口,Bundle是一个实现。
Kobor42年

5
感谢这种方法更好,并且在将SavedState框架用于自定义视图时避免了BadParcelableException,因为保存的状态似乎无法为自定义的SavedState正确设置类加载器!
伊恩·沃威克

3
我在一个活动中有几个相同视图的实例。它们在xml中都有唯一的ID。但是仍然所有人都可以获取最后一个视图的设置。有任何想法吗?
Christoffer 2014年

15
此解决方案可能还可以,但绝对不安全。通过执行此操作,您假设基本View状态不是Bundle。当然,这是目前正确的情况,但是您所依赖的是当前的实现事实,但不能保证它是正确的。
德米特里·扎伊采夫2014年

18

这是混合使用上述两种方法的另一个变体。将的速度和正确性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;
    }
}

3
这行不通。我得到一个ClassCastException ...这是因为它需要一个公共的静态CREATOR,以便State从包裹中实例化您。请看看:charlesharley.com/2012/programming/...
马托格罗索

8

这里的答案已经很不错了,但不一定适用于自定义ViewGroup。若要使所有自定义视图保持其状态,必须在每个类中重写onSaveInstanceState()onRestoreInstanceState(Parcelable state)。您还需要确保它们都有唯一的ID,无论它们是从xml膨胀还是以编程方式添加。

我的想法非常类似于Kobor42的答案,但是错误仍然存​​在,因为我是通过编程将视图添加到自定义ViewGroup中的,而不是分配唯一的ID。

mato共享的链接可以使用,但是这意味着各个View都不管理自己的状态-整个状态都保存在ViewGroup方法中。

问题是,当将多个这些ViewGroup添加到布局中时,它们在xml中的元素的id不再是唯一的(如果它是在xml中定义的)。在运行时,您可以调用静态方法View.generateViewId()以获取View的唯一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确实是一个问题,但我认为应该在视图初始化时处理,而不是在状态保存时处理。
Kobor42 '16

好点子。您是否建议在构造函数中设置mViewIds,然后在状态恢复后覆盖?
Fletcher Johns

2

我遇到了一个问题,即onRestoreInstanceState用最后一个视图的状态还原了所有自定义视图。我通过在自定义视图中添加以下两种方法解决了该问题:

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

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

dispatchFreezeSelfOnly和dispatchThawSelfOnly方法属于ViewGroup,而不属于View。因此,以防万一,您的自定义视图是从内置视图扩展的。您的解决方案不适用。
豪·鲁

1

除了使用onSaveInstanceStateonRestoreInstanceState,还可以使用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);

        ...
    }
}

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

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

请注意,您的MyActivity扩展FragmentActivity而不是仅扩展Activity

您可以在此处阅读有关ViewModels的更多信息:



1
@JJD我同意您发布的文章,仍然需要正确处理保存和还原。ViewModel如果要在状态更改(例如屏幕旋转)期间保留大量数据集,则此功能特别方便。我更喜欢使用ViewModel代替而不是将其写入其中,Application因为它的作用域很明确,并且我可以使同一应用程序的多个Activity行为正确。
BenediktKöppel19年

1

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

因此,这是编辑后的代码:

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];
            }
        };
    }
}

0

要增加其他答案-如果您有多个具有相同ID的自定义复合视图,并且所有这些视图均以配置更改时的最后一个视图的状态进行还原,那么您要做的就是告诉该视图仅调度保存/恢复事件通过覆盖几种方法来实现自身。

class MyCompoundView : ViewGroup {

    ...

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

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

有关发生的事情以及为什么起作用的解释,请参阅此博客文章。基本上,每个复合视图都共享复合视图的子视图ID,并且状态恢复会令人困惑。通过仅为复合视图本身调度状态,我们可以防止其子级从其他复合视图获取混合消息。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.