Android 如何保存与恢复自定义View的状态?

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/a553181867/article/details/54633151

前言

在上一篇文章Android状态保存与恢复流程 完全解析,笔者详细地介绍了Activity、Fragment、View等的状态保存与恢复流程,相信大家对状态的保存与恢复都有了一定熟悉。而这篇文章就着重介绍自定义View该怎样保存与恢复状态,因为每个自定义View都是不同的情况,所以我们一般需要重写View的onSaveInstanceState()或onRestoreInstanceState()方法来实现我们需要的逻辑。那么本文的核心就是讨论怎样重写上面两个方法来保存或恢复我们需要的数据。

自定义View的状态保存

我们知道,当Activity调用了onSaveInstanceState方法后,便会对它的View Tree进行保存,而进一步对每一个子View调用其onSaveInstanceState()方法来保存状态。我们在不知道该怎样下手的时候,可以参考一下Android源码,因为它有很多Widget是继承自View的,也就是Android系统自身的“自定义View”,我们可以看看它们的onSaveInstanceState()方法是怎样写的,说不定可以启发我们的思路。这里笔者选取一个比较简单的控件:CheckBox,它的功能不用笔者多说了。它继承自CompoundButton,直接看CompoundButton#onSaveInstanceState:

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

        SavedState ss = new SavedState(superState);

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

onSaveInstanceState()方法返回Parcelable对象,也即是序列化对象,是Android提供的一种序列化方式,如果不了解序列化的读者可以看这篇文章Android IPC机制(一):序列化与反序列化,这里就不展开细说了。我们回到上面的源码,首先调用了super.onSaveInstanceState()方法来获取一个Parcelable对象,接着把superState传递进SavedState的构造方法,构建了一个SavedState,并且设置SavedState的checked属性为当前isChecked()方法的返回值,也即把状态保存在SavedState里面,并且返回SavedState。所以说SavedState是一个实现了Parcelable接口的类,我们来看看:

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 new SavedState[size];
        }
    };
}

该SavedState继承自BaseSaveState,有一个成员变量checked,这就是我们需要保存的自定义View的状态了,而且SavedState的结构与实现Parcelable接口的类的结构基本是一样的,也就是说SavedState的父类必然实现了Parcelable接口,所以我们如果需要保存我们的自定义状态,我们可以仿照CompoundButton,在自定义View内实现一个静态内部类SavedState,并继承自BaseSavedState,这样就能得到一个Parcelable对象了。当然,你也可以直接写一个类实现Parcelable接口来保存状态,这样一般也是可以的。
接下来举一个自定义View的例子。这个例子是笔者之前制作的一个自定义View:BannerViewPager,之前并没有考虑状态保存的事情,导致每次旋转屏幕后都回到了初始状态,这次利用学到的状态保存的知识,为该自定义View加上保存状态的功能。下面是笔者的重写的onSaveInstanceState()方法以及SavedState内部类:

@Override
protected Parcelable onSaveInstanceState() {
    Parcelable parcelable = super.onSaveInstanceState();
    SavedState ss = new SavedState(parcelable);
    //把当前的位置保存进SavedState
    ss.currentPosition = mCurrentPosition;
    return ss;
}



static class SavedState extends BaseSavedState{

    //当前的ViewPager的位置
    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 new SavedState[size];
        }
    };
}

逻辑还算是比较简单的,关键在于onSaveInstanceState()方法内把需要的数据放进SavedState即可,而SavedState是继承自BaseSavedState,我们根据需要添加新的属性即可,比如这里笔者添加了currentPosition属性,并在相应的SavedState(Parcel)和writeToParcel(Parcel,int)方法内对该属性进行了操作,只要了解Parcelable即可轻松实现。

自定义View的状态恢复

上面对自定义View的状态进行了保存,接下来我们就要恢复这些状态。既然有了已经保存的状态,那么恢复状态也很简单了,因为在onRestoreInstanceState(Parcelable)方法内,根据传递进来的Parcelable参数,我们可以拿到我们之前保存的数据,再根据需要进行赋值或者调用某些方法来恢复状态就行了。比如说,CheckBox,之前保存的是是否选择了这个CheckBox的状态,那么恢复就应该对CheckBox重新选中或不选中。同样以BannerViewPager为例,重写onRestoreInstanceState(Parcelable)方法如下:

@Override
protected void onRestoreInstanceState(Parcelable state) {
    SavedState ss = (SavedState) state;
    super.onRestoreInstanceState(ss.getSuperState());
    //调用别的方法,把保存的数据重新赋值给当前的自定义View
    mViewPager.setCurrentItem(ss.currentPosition);
}

总结

从上面看出,保存与恢复自定义View的状态并不能,核心在于onSaveInstanceState()方法、onRestoreInstanceState(Parcelable)方法以及SavedState的实现,当然了,SavedState不是必须继承自BaseSavedState的,我们完全可以自行实现Parcelable接口。

遇到的问题

最后,再来谈谈笔者遇到的问题。如果按照上面所说的去做,一般都能实现状态的保存与恢复。但是有时候遇到的某些特殊情况也会让人感到很困惑。笔者曾继承了RecyclerView,对它的功能拓展,目的是添加HeaderView,这种情况很常见,比如ListView就可以添加HeadView。然后笔者把BannerViewPager作为HeaderView添加进RecyclerView里面,换句话说HeaderView是RecyclerView的一个子item view。然而问题出现了,每次旋转屏幕后BannerViewPager的onSaveInstanceState()和onRestoreInstanceState(Parcelable)方法都不会被调用,而RecyclerView的是正常调用,也即是说,保存与恢复的事件并没有传递给BannerViewPager。为了探究出现这个问题的原因,笔者开始查看RecyclerView的源码。从上一篇文章我们知道,保存状态的事件是从View#dispatchSaveInstanceState开始的,查看RecyclerView的源码,发现它重写了dispatchSaveInstanceState方法,即RecyclerView#dispatchSaveInstanceState:

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

可以发现,RecyclerView并没有遍历它的子View来保存它们的状态,而是直接调用了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);
    }

从源码注释可以看出,如果ViewGroup不需要保存它的子View的状态,可以调用dispatchFreezeSelfOnly方法,该方法直接调用了View#dispatchSaveInstanceState方法,仅仅保存了自身的状态。所以到这里也就明白了,RecyclerView重写了ViewGroup的dispatchSaveInstanceState方法,没有对子View进行遍历保存状态,因此我们添加的HeaderView自然不会保存状态,更别说恢复状态了。因此,以后如果我们的自定义View要嵌套在另一个自定义View内作为子View,必须注意该父容器有没有重写了dispatchSaveInstanceState方法,或者说有没有把保存-恢复事件传递给子View。注意这个问题后,就能避免很多麻烦了。
好了,到现在本文也讲述了自定义View的保存恢复状态一般写法以及可能会遇到的问题,最后感谢你的阅读~有问题欢迎指出。

猜你喜欢

转载自blog.csdn.net/a553181867/article/details/54633151