This article focuses on how to save and restore the state of a custom View. Since each custom View is a different case, we generally need to override the onSaveInstanceState() or onRestoreInstanceState() methods of the View to implement the logic we need.

So the core of this article is to discuss how to override the above two methods to save or restore the data we need.

Custom View state saving

As we know, when an Activity calls the onSaveInstanceState method, it saves its View Tree and calls its onSaveInstanceState() method for each child View to save state.

If you don’t know how to do this, check out the Android source code, because many of the widgets inherit from Views, which are Android’s own “custom views”, and see how their onSaveInstanceState() method is written. Maybe it’ll give us a clue.

Here I choose a relatively simple control: CheckBox, its function need not say more about the author. It inherits from CompoundButton and looks directly at CompoundButton#onSaveInstanceState:

    @Override
    public Parcelable onSaveInstanceState(a) {
        Parcelable superState = super.onSaveInstanceState();

        SavedState ss = new SavedState(superState);

        ss.checked = isChecked();
        return ss;
    }

Copy the code

The onSaveInstanceState() method returns a Parcelable object, which is a serialization object. This is a serialization object provided by Android.

We went back to the source code, the first call the super onSaveInstanceState () method to get a Parcelable object, then pass superState into SavedState constructor, built a SavedState, And set the checked property of SavedState to the value returned by the current isChecked() method, which saves the state inside SavedState and returns SavedState. So SavedState is a class that implements the Parcelable interface. Let’s see:

static class SavedState extends BaseSavedState {
    boolean checked;

    /**
     * Constructor called from {@link CompoundButton#onSaveInstanceState()}
     */
    SavedState(Parcelable superState) {
        super(superState);
    }

    /**
     * Constructor called from {@link #CREATOR}
     */
    private SavedState(Parcel in) {
        super(in);
        checked = (Boolean)in.readValue(null);
    }

    @Override
    public void writeToParcel(Parcel out, int flags) {
        super.writeToParcel(out, flags);
        out.writeValue(checked);
    }

    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 newSavedState[size]; }}; }Copy the code

SavedState inherits from BaseSaveState and has a member variable checked. That is the state of the custom View that we need to save. The structure of SavedState is basically the same as that of the class that implements the Parcelable interface. That is, the parent of SavedState must implement the Parcelable interface, so if we need to save our custom state, we can implement a static inner class SavedState in the custom View, just like CompoundButton. And inherits from BaseSavedState, so you get a Parcelable object.

Of course, you can also write a class that implements the Parcelable interface to store state, which is generally ok. Here’s an example of a custom View. This example is a custom View made by the author before :BannerViewPager, which did not consider the matter of state saving before, resulting in the return to the initial state after each rotation of the screen. This time, we use the knowledge of state saving to add the function of state saving for the custom View.

Here is the author’s rewrite of the onSaveInstanceState() method and the SavedState inner class:

@Override
protected Parcelable onSaveInstanceState(a) {
    Parcelable parcelable = super.onSaveInstanceState();
    SavedState ss = new SavedState(parcelable);
    // Save the current location to SavedState
    ss.currentPosition = mCurrentPosition;
    return ss;
}

static class SavedState extends BaseSavedState{

    // The current ViewPager position
    int currentPosition;

    public SavedState(Parcel source) {
        super(source);
        currentPosition = source.readInt();
    }

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

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

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

        @Override
        public SavedState createFromParcel(Parcel source) {
            return new SavedState(source);
        }

        @Override
        public SavedState[] newArray(int size) {
            return newSavedState[size]; }}; }Copy the code

The logic is relatively simple, the key is that onSaveInstanceState() method in the need to put the data into SavedState, SavedState is inherited from BaseSavedState, we can add new attributes as needed, Here, for example, I added the currentPosition property and operated on it in the corresponding SavedState(Parcel) and writeToParcel(Parcel,int) methods, which is easy to implement with Parcelable knowledge.

Custom View state restoration

The state of the custom View is saved above, and we need to restore the state. Now that we have the saved state, it’s easy to restore the state, because in the onRestoreInstanceState(Parcelable) method, we can retrieve the data that we saved, based on the Parcelable parameter that we passed in, Assign or call some method as needed to restore the state.

For example, if the CheckBox was selected or not, the recovery should re-check or uncheck the CheckBox. Using BannerViewPager as an example, rewrite onRestoreInstanceState(Parcelable) as follows:

@Override
protected void onRestoreInstanceState(Parcelable state) {
    SavedState ss = (SavedState) state;
    super.onRestoreInstanceState(ss.getSuperState());
    // Call another method to reassign the saved data to the current custom View
    mViewPager.setCurrentItem(ss.currentPosition);
}

Copy the code

conclusion

OnSaveInstanceState (), onRestoreInstanceState(Parcelable), SavedState (Parcelable), SavedState (Parcelable), SavedState (Parcelable), SavedState (Parcelable) SavedState does not have to inherit from BaseSavedState; we can implement the Parcelable interface ourselves.

Problems encountered

Finally, let’s talk about my problems. If you follow the above steps, you can generally achieve state preservation and recovery. But sometimes special situations can be very confusing.

The author inherited RecyclerView and expanded its function to add HeaderView. This situation is very common. For example, ListView can add HeadView. Then the author added BannerViewPager as HeaderView into RecyclerView, in other words, HeaderView is a sub-item view of RecyclerView. BannerViewPager’s onSaveInstanceState() and onRestoreInstanceState(Parcelable) methods will not be called after each rotation of the screen, whereas RecyclerView is normally called. That is, the saved and restored events are not passed to the BannerViewPager.

In order to explore the cause of this problem, the author began to view RecyclerView source code. We know from the previous article, save the state of the event from View# dispatchSaveInstanceState started, view RecyclerView source code, find it rewrote the dispatchSaveInstanceState method, Namely RecyclerView# dispatchSaveInstanceState:

    /** * Override to prevent freezing of any views created by the adapter. */
    @Override
    protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
        dispatchFreezeSelfOnly(container);
    }

Copy the code

Instead of iterating through its child views to save their state, RecyclerView calls ViewGroup#dispatchFreezeSelfOnly:

    /**
     * Perform dispatching of a {@link #saveHierarchyState(android.util.SparseArray)}  freeze()}
     * to only this view, not to its children.  For use when overriding
     * {@link #dispatchSaveInstanceState(android.util.SparseArray)}  dispatchFreeze()} to allow
     * subclasses to freeze their own state but not the state of their children.
     *
     * @param container the container
     */
    protected void dispatchFreezeSelfOnly(SparseArray<Parcelable> container) {
        super.dispatchSaveInstanceState(container);
    }

Copy the code

Can be seen from the source code comments, if ViewGroup don’t need to preserve its state of the View, you can call dispatchFreezeSelfOnly method, this method is called directly the View# dispatchSaveInstanceState method, just to save their own state. So here are understood, RecyclerView rewrite the ViewGroup dispatchSaveInstanceState method, there is no subsidiary traverse the View state, so we don’t add HeaderView natural preservation condition, let alone to recover.

Therefore, if after our custom View to be nested inside another custom View as a child View, must pay attention to the parent container have rewritten the dispatchSaveInstanceState method, or have any saving – restoring the event is passed to the View. By paying attention to this problem, you can avoid a lot of trouble.

Well, until now this article also describes the custom View save restore state general writing method and may encounter problems, finally thank you for reading ~ have problems welcome to point out.

Because the level is limited, have wrong place unavoidable, rather misdirect others, welcome big guy to point out! Code word is not easy, thank you for your attention!

🙏 If you are studying with me, you can follow my official account — ❤️ Program ape Development Center ❤️. Every week, we will share the technology of Android regularly.