Android 如何保存與恢復(fù)自定義View的狀態(tài)胰坟?

前言

在上一篇文章Android狀態(tài)保存與恢復(fù)流程 完全解析留拾,筆者詳細(xì)地介紹了Activity戳晌、Fragment、View等的狀態(tài)保存與恢復(fù)流程痴柔,相信大家對(duì)狀態(tài)的保存與恢復(fù)都有了一定熟悉沦偎。而這篇文章就著重介紹自定義View該怎樣保存與恢復(fù)狀態(tài),因?yàn)槊總€(gè)自定義View都是不同的情況竞帽,所以我們一般需要重寫View的onSaveInstanceState()或onRestoreInstanceState()方法來實(shí)現(xiàn)我們需要的邏輯扛施。那么本文的核心就是討論怎樣重寫上面兩個(gè)方法來保存或恢復(fù)我們需要的數(shù)據(jù)。

自定義View的狀態(tài)保存

我們知道屹篓,當(dāng)Activity調(diào)用了onSaveInstanceState方法后疙渣,便會(huì)對(duì)它的View Tree進(jìn)行保存,而進(jìn)一步對(duì)每一個(gè)子View調(diào)用其onSaveInstanceState()方法來保存狀態(tài)堆巧。我們?cè)诓恢涝撛鯓酉率值臅r(shí)候妄荔,可以參考一下Android源碼,因?yàn)樗泻芏郬idget是繼承自View的谍肤,也就是Android系統(tǒng)自身的“自定義View”啦租,我們可以看看它們的onSaveInstanceState()方法是怎樣寫的,說不定可以啟發(fā)我們的思路荒揣。這里筆者選取一個(gè)比較簡(jiǎn)單的控件:CheckBox篷角,它的功能不用筆者多說了。它繼承自CompoundButton系任,直接看CompoundButton#onSaveInstanceState:

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

        SavedState ss = new SavedState(superState);

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

onSaveInstanceState()方法返回Parcelable對(duì)象恳蹲,也即是序列化對(duì)象虐块,是Android提供的一種序列化方式,如果不了解序列化的讀者可以看這篇文章Android IPC機(jī)制(一):序列化與反序列化嘉蕾,這里就不展開細(xì)說了贺奠。我們回到上面的源碼,首先調(diào)用了super.onSaveInstanceState()方法來獲取一個(gè)Parcelable對(duì)象错忱,接著把superState傳遞進(jìn)SavedState的構(gòu)造方法儡率,構(gòu)建了一個(gè)SavedState,并且設(shè)置SavedState的checked屬性為當(dāng)前isChecked()方法的返回值以清,也即把狀態(tài)保存在SavedState里面儿普,并且返回SavedState。所以說SavedState是一個(gè)實(shí)現(xiàn)了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箕肃,有一個(gè)成員變量checked,這就是我們需要保存的自定義View的狀態(tài)了今魔,而且SavedState的結(jié)構(gòu)與實(shí)現(xiàn)Parcelable接口的類的結(jié)構(gòu)基本是一樣的勺像,也就是說SavedState的父類必然實(shí)現(xiàn)了Parcelable接口,所以我們?nèi)绻枰4嫖覀兊淖远x狀態(tài)错森,我們可以仿照CompoundButton吟宦,在自定義View內(nèi)實(shí)現(xiàn)一個(gè)靜態(tài)內(nèi)部類SavedState,并繼承自BaseSavedState涩维,這樣就能得到一個(gè)Parcelable對(duì)象了殃姓。當(dāng)然,你也可以直接寫一個(gè)類實(shí)現(xiàn)Parcelable接口來保存狀態(tài)瓦阐,這樣一般也是可以的蜗侈。
接下來舉一個(gè)自定義View的例子。這個(gè)例子是筆者之前制作的一個(gè)自定義View:BannerViewPager睡蟋,之前并沒有考慮狀態(tài)保存的事情踏幻,導(dǎo)致每次旋轉(zhuǎn)屏幕后都回到了初始狀態(tài),這次利用學(xué)到的狀態(tài)保存的知識(shí)戳杀,為該自定義View加上保存狀態(tài)的功能该面。下面是筆者的重寫的onSaveInstanceState()方法以及SavedState內(nèi)部類:

@Override
protected Parcelable onSaveInstanceState() {
    Parcelable parcelable = super.onSaveInstanceState();
    SavedState ss = new SavedState(parcelable);
    //把當(dāng)前的位置保存進(jìn)SavedState
    ss.currentPosition = mCurrentPosition;
    return ss;
}

    

static class SavedState extends BaseSavedState{

    //當(dāng)前的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];
        }
    };
}

邏輯還算是比較簡(jiǎn)單的,關(guān)鍵在于onSaveInstanceState()方法內(nèi)把需要的數(shù)據(jù)放進(jìn)SavedState即可信卡,而SavedState是繼承自BaseSavedState隔缀,我們根據(jù)需要添加新的屬性即可,比如這里筆者添加了currentPosition屬性傍菇,并在相應(yīng)的SavedState(Parcel)和writeToParcel(Parcel,int)方法內(nèi)對(duì)該屬性進(jìn)行了操作猾瘸,只要了解Parcelable即可輕松實(shí)現(xiàn)。

自定義View的狀態(tài)恢復(fù)

上面對(duì)自定義View的狀態(tài)進(jìn)行了保存,接下來我們就要恢復(fù)這些狀態(tài)牵触。既然有了已經(jīng)保存的狀態(tài)仔蝌,那么恢復(fù)狀態(tài)也很簡(jiǎn)單了,因?yàn)樵趏nRestoreInstanceState(Parcelable)方法內(nèi)荒吏,根據(jù)傳遞進(jìn)來的Parcelable參數(shù),我們可以拿到我們之前保存的數(shù)據(jù)渊鞋,再根據(jù)需要進(jìn)行賦值或者調(diào)用某些方法來恢復(fù)狀態(tài)就行了绰更。比如說,CheckBox锡宋,之前保存的是是否選擇了這個(gè)CheckBox的狀態(tài)儡湾,那么恢復(fù)就應(yīng)該對(duì)CheckBox重新選中或不選中。同樣以BannerViewPager為例执俩,重寫onRestoreInstanceState(Parcelable)方法如下:

@Override
protected void onRestoreInstanceState(Parcelable state) {
    SavedState ss = (SavedState) state;
    super.onRestoreInstanceState(ss.getSuperState());
    //調(diào)用別的方法徐钠,把保存的數(shù)據(jù)重新賦值給當(dāng)前的自定義View
    mViewPager.setCurrentItem(ss.currentPosition);
}

總結(jié)

從上面看出,保存與恢復(fù)自定義View的狀態(tài)并不能役首,核心在于onSaveInstanceState()方法尝丐、onRestoreInstanceState(Parcelable)方法以及SavedState的實(shí)現(xiàn),當(dāng)然了衡奥,SavedState不是必須繼承自BaseSavedState的爹袁,我們完全可以自行實(shí)現(xiàn)Parcelable接口。

遇到的問題

最后矮固,再來談?wù)劰P者遇到的問題失息。如果按照上面所說的去做,一般都能實(shí)現(xiàn)狀態(tài)的保存與恢復(fù)档址。但是有時(shí)候遇到的某些特殊情況也會(huì)讓人感到很困惑盹兢。筆者曾繼承了RecyclerView,對(duì)它的功能拓展守伸,目的是添加HeaderView绎秒,這種情況很常見,比如ListView就可以添加HeadView尼摹。然后筆者把BannerViewPager作為HeaderView添加進(jìn)RecyclerView里面替裆,換句話說HeaderView是RecyclerView的一個(gè)子item view。然而問題出現(xiàn)了窘问,每次旋轉(zhuǎn)屏幕后BannerViewPager的onSaveInstanceState()和onRestoreInstanceState(Parcelable)方法都不會(huì)被調(diào)用辆童,而RecyclerView的是正常調(diào)用,也即是說惠赫,保存與恢復(fù)的事件并沒有傳遞給BannerViewPager把鉴。為了探究出現(xiàn)這個(gè)問題的原因,筆者開始查看RecyclerView的源碼。從上一篇文章我們知道庭砍,保存狀態(tài)的事件是從View#dispatchSaveInstanceState開始的场晶,查看RecyclerView的源碼,發(fā)現(xiàn)它重寫了dispatchSaveInstanceState方法怠缸,即RecyclerView#dispatchSaveInstanceState:

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

可以發(fā)現(xiàn)诗轻,RecyclerView并沒有遍歷它的子View來保存它們的狀態(tài),而是直接調(diào)用了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的狀態(tài)扳炬,可以調(diào)用dispatchFreezeSelfOnly方法,該方法直接調(diào)用了View#dispatchSaveInstanceState方法搔体,僅僅保存了自身的狀態(tài)恨樟。所以到這里也就明白了,RecyclerView重寫了ViewGroup的dispatchSaveInstanceState方法疚俱,沒有對(duì)子View進(jìn)行遍歷保存狀態(tài)劝术,因此我們添加的HeaderView自然不會(huì)保存狀態(tài),更別說恢復(fù)狀態(tài)了呆奕。因此养晋,以后如果我們的自定義View要嵌套在另一個(gè)自定義View內(nèi)作為子View,必須注意該父容器有沒有重寫了dispatchSaveInstanceState方法梁钾,或者說有沒有把保存-恢復(fù)事件傳遞給子View匙握。注意這個(gè)問題后,就能避免很多麻煩了陈轿。
好了圈纺,到現(xiàn)在本文也講述了自定義View的保存恢復(fù)狀態(tài)一般寫法以及可能會(huì)遇到的問題,最后感謝你的閱讀~有問題歡迎指出麦射。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蛾娶,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子潜秋,更是在濱河造成了極大的恐慌蛔琅,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件峻呛,死亡現(xiàn)場(chǎng)離奇詭異罗售,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)钩述,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門寨躁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人牙勘,你說我怎么就攤上這事职恳∷鳎” “怎么了?”我有些...
    開封第一講書人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵放钦,是天一觀的道長(zhǎng)色徘。 經(jīng)常有香客問我,道長(zhǎng)操禀,這世上最難降的妖魔是什么褂策? 我笑而不...
    開封第一講書人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮颓屑,結(jié)果婚禮上斤寂,老公的妹妹穿的比我還像新娘。我一直安慰自己邢锯,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開白布搀别。 她就那樣靜靜地躺著丹擎,像睡著了一般。 火紅的嫁衣襯著肌膚如雪歇父。 梳的紋絲不亂的頭發(fā)上蒂培,一...
    開封第一講書人閱讀 51,541評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音榜苫,去河邊找鬼护戳。 笑死,一個(gè)胖子當(dāng)著我的面吹牛垂睬,可吹牛的內(nèi)容都是我干的媳荒。 我是一名探鬼主播,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼驹饺,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼钳枕!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起赏壹,我...
    開封第一講書人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤鱼炒,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后蝌借,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體昔瞧,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年菩佑,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了自晰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡稍坯,死狀恐怖缀磕,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤袜蚕,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布糟把,位于F島的核電站,受9級(jí)特大地震影響牲剃,放射性物質(zhì)發(fā)生泄漏遣疯。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一凿傅、第九天 我趴在偏房一處隱蔽的房頂上張望缠犀。 院中可真熱鬧,春花似錦聪舒、人聲如沸辨液。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)滔迈。三九已至,卻和暖如春被辑,著一層夾襖步出監(jiān)牢的瞬間燎悍,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工盼理, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留谈山,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓宏怔,卻偏偏與公主長(zhǎng)得像奏路,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子臊诊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容