Android中正確儲存view的狀態
今天我們聊一聊安卓中儲存和恢復view狀態的問題。我刻意強調View狀態是因為我發現這個過程要比儲存 Activity 和 Fragment狀態稍微複雜,還有一個原因是因為網上有太多“重複造的輪子”(有時還是奇醜無比的輪子)。
為什麼我們需要儲存View的狀態?
這個問題問的好!我堅信移動應用應該幫助你解決問題,而不是製造問題。
想象一下一個非常複雜的設定頁面:
這並不是從一個移動應用的截圖(這不是典型的win32程式嗎。。),但是適合用於說明我們的問題:
這裡有非常多的文字輸入控制元件,多選框,開關(switch)等等,你花了15分鐘填完所有這些格子,總算輪到點選”完成”按鈕了,但是突然,你不小心旋轉了下螢幕,omg,所有的改動都沒了,一切都回歸到了初始狀態。
當然,總有一些使用者喜歡你的app簡直到不行,不在乎重新填一次。但是老實說,這樣做真的正確嗎?(原文有老外常喜歡的喋喋不休的幽默句子,略了)。
別犯傻,我們需要儲存使用者的修改,除非使用者特意讓我們不要這樣做。
如何儲存View的狀態?
假設我們這裡有一個帶有影像,文字和 Switch toggle控制元件的簡單佈局:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" android:padding="@dimen/activity_horizontal_margin"> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_launcher"/> <TextView android:layout_width="0dip" android:layout_weight="1" android:layout_height="wrap_content" android:text="My Text"/> <Switch android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="8dip"/> </LinearLayout>
看吧,非常簡單的佈局。但是當我們滑動一下switch開關然後旋轉螢幕方向,switch又回到了原來的狀態。
通常,安卓會自動儲存這些View(一般是系統控制元件)的狀態,但是為什麼在我們的案例中不起作用了呢?
讓我們先停下來,弄明白安卓是如何管理View狀態的。這裡是正常情況下儲存與恢復的示意圖:
- saveHierarchyState(SparseArray<Parcelable> container)- 當狀態需要儲存的時候被安卓framework呼叫,通常會呼叫dispatchSaveInstanceState() 。
- dispatchSaveInstanceState(SparseArray<Parcelable> container)- 被saveHierarchyState()呼叫。 在其內部呼叫onSaveInstanceState(),並且返回一個代表當前狀態的Parcelable。這個Parcelable被儲存在container引數中,container引數是一個鍵值對的map集合。View的ID是加鍵Parcelable是值。如果這是一個ViewGroup,還需要遍歷其子view,儲存子View的狀態。
- Parcelable onSaveInstanceState()- 被 dispatchSaveInstanceState()呼叫。這個方法應該在View的實現中被重寫以返回實際的View狀態。
- restoreHierarchyState(SparseArray<Parcelable> container)- 在需要恢復View狀態的時候被android呼叫,作為傳入的SparseArray引數,包含了在儲存過程中的所有view狀態。
- dispatchRestoreInstanceState(SparseArray<Parcelable> container)- 被restoreHierarchyState()呼叫。根據View的ID找出相應的Parcelable,同時傳遞給onRestoreInstanceState()。如果這是一個ViewGroup,還要恢復其子View的資料。
- onRestoreInstanceState(Parcelable state)- 被dispatchRestoreInstanceState()呼叫。如果container中有某個view,ViewID所對應的狀態被傳遞在這個方法中。
理解這個過程的重點是,container在整個view層級中是被共享的。我們將看到為什麼它這麼重要。
既然View的狀態是基於它的ID儲存的 , 因此如果一個VIew沒有ID,那麼將不會被儲存到container中。沒有儲存的支點(id),我們也無法恢復沒有ID的view的狀態,因為不知道這個狀態是屬於哪個View的。
其實這是安卓的策略,假如我們來做也許會這樣設計,大致這樣:所有view按照一定的順序依次儲存,在恢復的時候只需知道這個View在儲存的時候的順序就可以了,不過顯然這樣要耗費更多的開銷。- 譯者注。
看樣子這就是switch開關狀態沒有被儲存的原因。那我們試試在switch開關上新增id(其他的View也加上id):
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" android:padding="@dimen/activity_horizontal_margin"> <ImageView android:id="@+id/image" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_launcher"/> <TextView android:id="@+id/text" android:layout_width="0dip" android:layout_weight="1" android:layout_height="wrap_content" android:text="My Text"/> <Switch android:id="@+id/toggle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="8dip"/> </LinearLayout>
ok,看結果,確實可行。在configuration changes期間狀態是可以保持的。下面是SparseArray的示意圖:
就如你看到的那樣,每個view都有一個id來把狀態儲存在container的SparseArray中。
你可能會問這是如何發生的 – 我們並沒有提供任何Parcelable來代表狀態啊。答案是 – 安卓處理好了這個事情,安卓知道如何儲存系統自帶控制元件的狀態。 在經過上面的一番解釋之後,這句話來的太遲了吧 -譯者注。
除了ID之外,你還需要明確的告訴安卓你的view需要儲存狀態,呼叫setSaveEnabled(true)就可以了。通常你不需要對自帶的控制元件這樣做,但是如果你從零開始開發一個自定義的view,則需要手動設定(setSaveEnabled)。
要儲存view的狀態,至少有兩點需要滿足:
- view要有id
- 要呼叫setSaveEnabled(true)
現在我們知道如何儲存自帶控制元件的狀態,但是如果我們有一些自定義的狀態,想在configuration變化的時候保持這些狀態又該如何呢?
儲存自定義的狀態
下面,讓我們舉一個更為複雜的例子。我想在繼承自Switch的的類中新增一個自定義的狀態:
public class CustomSwitch extends Switch { private int customState;//所謂狀態其實就是資料 ....... public void setCustomState(int customState) { this.customState = customState; } }
下面是我們將如何儲存這個狀態的過程:
public class CustomSwitch extends Switch { private int customState; ............. public void setCustomState(int customState) { this.customState = customState; } @Override public Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); SavedState ss = new SavedState(superState); ss.state = customState; return ss; } @Override public void onRestoreInstanceState(Parcelable state) { SavedState ss = (SavedState) state; super.onRestoreInstanceState(ss.getSuperState()); setCustomState(ss.state); } static class SavedState extends BaseSavedState { int state; SavedState(Parcelable superState) { super(superState); } private SavedState(Parcel in) { super(in); state = in.readInt(); } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); out.writeInt(state); } 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]; } }; } }
讓我來解釋一下上面所做的事情。
首先,既然重寫了onSaveInstanceState,我就必須呼叫其父類的相應方法讓父類儲存它想儲存的所有東西。在我的情況中,Switch將建立一個Parcelable,將狀態放進去然後返回給自己。不幸的是,我們無法在這個parcelable中新增更多的狀態,因此需要建立一個封裝類來封裝這個父類的狀態,然後放入額外的狀態。在安卓中有一個類(View.BaseSavedState)專門做這件事情 – 通過繼承它來實現儲存上一級的狀態同時允許你儲存自定義的屬性。
在onRestoreInstanceState()期間我們則需要做相反的事情 – 從指定的Parcelable中獲取上一級的狀態 ,同時讓你的父類通過呼叫super.onRestoreInstanceState(ss.getSuperState())來恢復它的狀態。之後我們才能恢復我們自己的狀態。
Since you override onSaveInstanceState() – always save super state – state of your super class.
View的ID必須唯一
現在我們決定將佈局放在一個自定義的view中達到重用的效果,然後在其他的佈局中include幾次:
注:這裡是include了兩次。
當我們改變configuration之後,所有的狀態都一團糟了,讓我們看看在SparseArray中是什麼情況:
哈哈!因為狀態的儲存是基於view id的,而SparseArray container是整個View層次結構中共享的 ,所以view的id必須唯一。否則你的狀態就會被另外一個具有相同id的view覆蓋。在這裡有兩個view的id都是@id/toggle,而container只持有一個它的例項- 儲存過程中最後到來的一個。
到了恢復資料的時候 – 這兩個view都從container那裡得到一個相同的狀態。
那麼該如何解決這個問題?
最直接的答案是 – 每個子view都具有獨立的SparseArray container,這樣就不會重疊了:
public class MyCustomLayout extends LinearLayout { ......... @Override public Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); SavedState ss = new SavedState(superState); ss.childrenStates = new SparseArray(); for (int i = 0; i < getChildCount(); i++) { getChildAt(i).saveHierarchyState(ss.childrenStates); } return ss; } @Override public void onRestoreInstanceState(Parcelable state) { SavedState ss = (SavedState) state; super.onRestoreInstanceState(ss.getSuperState()); for (int i = 0; i < getChildCount(); i++) { getChildAt(i).restoreHierarchyState(ss.childrenStates); } } @Override protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) { dispatchFreezeSelfOnly(container); } @Override protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { dispatchThawSelfOnly(container); } static class SavedState extends BaseSavedState { SparseArray childrenStates; SavedState(Parcelable superState) { super(superState); } private SavedState(Parcel in, ClassLoader classLoader) { super(in); childrenStates = in.readSparseArray(classLoader); } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); out.writeSparseArray(childrenStates); } public static final ClassLoaderCreator<SavedState> CREATOR = new ClassLoaderCreator<SavedState>() { @Override public SavedState createFromParcel(Parcel source, ClassLoader loader) { return new SavedState(source, loader); } @Override public SavedState createFromParcel(Parcel source) { return createFromParcel(null); } public SavedState[] newArray(int size) { return new SavedState[size]; } }; } }
讓我們過一遍這段亂麻了的程式碼:
- 在自定義的佈局中沒我建立了一個特殊的SaveState類,它持有父類狀態以及儲存子view狀態的獨立SparseArray。
- 在onSaveInstanceState()中我主動儲存父類與子view的狀態到獨立的SparseArray中。
- 在onRestoreInstanceState()中我主動從儲存期間建立的SparseArray中恢復父類和子view的狀態。
- 記住如果這是一個ViewGroup – dispatchSaveInstanceState()還需要遍歷子View然後儲存它們的狀態嗎?既然我們現在是手動的了,我需要廢棄這種行為。幸運的是使用dispatchFreezeSelfOnly()方法可以告訴安卓只儲存viewGroup的狀態,不要碰它的子View(在dispatchSaveInstanceState()中呼叫)。
- dispatchRestoreInstanceState()需要做同樣的事情 – 呼叫dispatchThawSelfOnly()。告訴安卓只恢復自身的狀態 ,子view我們自己來處理。
下面是SparseArray的示意圖:
正如你看到的每個view group都有了獨自的SparseArray,因此他們就不會重疊和覆蓋彼此了。
狀態儲存了 賺大了!
這篇文章的程式碼可以在 GitHub上 找到。
相關文章
- Android 元件系列-----Activity儲存狀態Android元件
- Android Activity 重建之狀態儲存與恢復Android
- canvas 儲存與還原狀態Canvas
- Azure Terraform(四)狀態檔案儲存ORM
- [譯]Android Activity 和 Fragment 狀態儲存與恢復的最佳實踐AndroidFragment
- Android App 中正確地使用 Splash Screen(譯)AndroidAPP
- 利用Dectorator分模組儲存Vuex狀態(下)Vue
- iOS UI狀態儲存和恢復(三)iOSUI
- 利用Dectorator分模組儲存Vuex狀態(上)Vue
- OpenHarmony頁面級UI狀態儲存:LocalStorageUI
- 使用history儲存列表頁ajax請求的狀態
- 自動儲存、靜態儲存和動態儲存
- 使用NAS動態儲存卷建立有狀態應用
- 分散式儲存Ceph之PG狀態詳解分散式
- 19c pdb如何儲存啟動狀態
- MYSQL innodb buffer 狀態資料的儲存和載入MySql
- 日期的正確儲存方式
- Android將view儲存為圖片並放在相簿中AndroidView
- 物件和函式的區別就是物件可以儲存狀態物件函式
- Amazon EKS 上有狀態服務啟用儲存加密加密
- jQuery 操作checkbox翻頁儲存選中狀態jQuery
- cookie儲存使用者狀態 無session系統CookieSession
- 零程式碼儲存視窗執行狀態 (轉)
- Synology群暉NAS儲存正確建立儲存池和儲存空間的方法
- 儲存NETAPP處於takeover狀態解決方法。APP
- Activity 知識梳理(3) Activity狀態儲存和恢復
- Fragment 知識梳理(2) Fragment 狀態儲存和恢復Fragment
- 在Java中正確使用註釋Java
- Android儲存Android
- dci中角色有狀態嗎,誰來怎麼儲存呢
- iNeuOS工業網際網路作業系統,釋出實時儲存方式:實時儲存、變化儲存、定時儲存,增加裝置振動狀態和電能狀態監測驅動,v3.6.2作業系統
- Android儲存(2)– 介面卡儲存Android
- Android中的資料儲存之檔案儲存Android
- 在Python中正確使用UnicodePythonUnicode
- 關於有狀態BEAN如何透過關鍵字儲存使用者狀態的問題,請幫忙Bean
- 國產儲存晶片現狀如何?晶片
- Android 自定義 View:包含多種狀態的下載用圓形進度條AndroidView
- jquery實現的換膚並且能夠重新整理儲存狀態jQuery