1. 程式人生 > >android view狀態保存

android view狀態保存

line 幫助 static sel 兩個 freeze 除了 get 否則

為什麽我們需要保存View的狀態?

這個問題問的好!我堅信移動應用應該幫助你解決問題,而不是制造問題。

想象一下一個非常復雜的設置頁面:

技術分享圖片

這並不是從一個移動應用的截圖(這不是典型的win32程序嗎。。),但是適合用於說明我們的問題:

這裏有非常多的文字輸入控件,多選框,開關(switch)等等,你花了15分鐘填完所有這些格子,總算輪到點擊"完成"按鈕了,但是突然,你不小心旋轉了下屏幕,omg,所有的改動都沒了,一切都回歸到了初始狀態。

當然,總有一些用戶喜歡你的app簡直到不行,不在乎重新填一次。但是老實說,這樣做真的正確嗎?(原文有老外常喜歡的喋喋不休的幽默句子,略了)。

別犯傻,我們需要保存用戶的修改,除非用戶特意讓我們不要這樣做。

如何保存View的狀態?

假設我們這裏有一個帶有圖像,文字和 Switch toggle控件的簡單布局:

  1. <LinearLayout
  2. xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent"
  5. android:orientation="horizontal"
  6. android:padding="@dimen/activity_horizontal_margin">
  7. <ImageView
  8. android:layout_width="wrap_content"
  9. android:layout_height="wrap_content"
  10. android:src="@drawable/ic_launcher"/>
  11. <TextView
  12. android:layout_width="0dip"
  13. android:layout_weight="1"
  14. android:layout_height="wrap_content"
  15. android:text="My Text"/>
  16. <Switch
  17. android:layout_width="wrap_content"
  18. android:layout_height="wrap_content"
  19. android:layout_margin="8dip"/>
  20. </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):

  1. <LinearLayout
  2. xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent"
  5. android:orientation="horizontal"
  6. android:padding="@dimen/activity_horizontal_margin">
  7. <ImageView
  8. android:id="@+id/image"
  9. android:layout_width="wrap_content"
  10. android:layout_height="wrap_content"
  11. android:src="@drawable/ic_launcher"/>
  12. <TextView
  13. android:id="@+id/text"
  14. android:layout_width="0dip"
  15. android:layout_weight="1"
  16. android:layout_height="wrap_content"
  17. android:text="My Text"/>
  18. <Switch
  19. android:id="@+id/toggle"
  20. android:layout_width="wrap_content"
  21. android:layout_height="wrap_content"
  22. android:layout_margin="8dip"/>
  23. </LinearLayout>

ok,看結果,確實可行。在configuration changes期間狀態是可以保持的。下面是SparseArray的示意圖:

技術分享圖片

就如你看到的那樣,每個view都有一個id來把狀態保存在container的SparseArray中。

你可能會問這是如何發生的 - 我們並沒有提供任何Parcelable來代表狀態啊。答案是 - 安卓處理好了這個事情,安卓知道如何保存系統自帶控件的狀態。 在經過上面的一番解釋之後,這句話來的太遲了吧 -譯者註。

除了ID之外,你還需要明確的告訴安卓你的view需要保存狀態,調用setSaveEnabled(true)就可以了。通常你不需要對自帶的控件這樣做,但是如果你從開始開發一個自定義的view,則需要手動設置(setSaveEnabled)。

要保存view的狀態,至少有兩點需要滿足:

  1. view要有id

  2. 要調用setSaveEnabled(true)

現在我們知道如何保存自帶控件的狀態,但是如果我們有一些自定義的狀態,想在configuration變化的時候保持這些狀態又該如何呢?

保存自定義的狀態

下面,讓我們舉一個更為復雜的例子。我想在繼承自Switch的的類中添加一個自定義的狀態:

  1. public class CustomSwitch extends Switch {
  2. private int customState;//所謂狀態其實就是數據
  3. .......
  4. public void setCustomState(int customState) {
  5. this.customState = customState;
  6. }
  7. }

下面是我們將如何保存這個狀態的過程:

  1. public class CustomSwitch extends Switch {
  2. private int customState;
  3. .............
  4. public void setCustomState(int customState) {
  5. this.customState = customState;
  6. }
  7. @Override
  8. public Parcelable onSaveInstanceState() {
  9. Parcelable superState = super.onSaveInstanceState();
  10. SavedState ss = new SavedState(superState);
  11. ss.state = customState;
  12. return ss;
  13. }
  14. @Override
  15. public void onRestoreInstanceState(Parcelable state) {
  16. SavedState ss = (SavedState) state;
  17. super.onRestoreInstanceState(ss.getSuperState());
  18. setCustomState(ss.state);
  19. }
  20. static class SavedState extends BaseSavedState {
  21. int state;
  22. SavedState(Parcelable superState) {
  23. super(superState);
  24. }
  25. private SavedState(Parcel in) {
  26. super(in);
  27. state = in.readInt();
  28. }
  29. @Override
  30. public void writeToParcel(Parcel out, int flags) {
  31. super.writeToParcel(out, flags);
  32. out.writeInt(state);
  33. }
  34. public static final Parcelable.Creator<SavedState> CREATOR
  35. = new Parcelable.Creator<SavedState>() {
  36. public SavedState createFromParcel(Parcel in) {
  37. return new SavedState(in);
  38. }
  39. public SavedState[] newArray(int size) {
  40. return new SavedState[size];
  41. }
  42. };
  43. }
  44. }

讓我來解釋一下上面所做的事情。

首先,既然重寫了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,這樣就不會重疊了:

  1. public class MyCustomLayout extends LinearLayout {
  2. .........
  3. @Override
  4. public Parcelable onSaveInstanceState() {
  5. Parcelable superState = super.onSaveInstanceState();
  6. SavedState ss = new SavedState(superState);
  7. ss.childrenStates = new SparseArray();
  8. for (int i = 0; i < getChildCount(); i++) {
  9. getChildAt(i).saveHierarchyState(ss.childrenStates);
  10. }
  11. return ss;
  12. }
  13. @Override
  14. public void onRestoreInstanceState(Parcelable state) {
  15. SavedState ss = (SavedState) state;
  16. super.onRestoreInstanceState(ss.getSuperState());
  17. for (int i = 0; i < getChildCount(); i++) {
  18. getChildAt(i).restoreHierarchyState(ss.childrenStates);
  19. }
  20. }
  21. @Override
  22. protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
  23. dispatchFreezeSelfOnly(container);
  24. }
  25. @Override
  26. protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
  27. dispatchThawSelfOnly(container);
  28. }
  29. static class SavedState extends BaseSavedState {
  30. SparseArray childrenStates;
  31. SavedState(Parcelable superState) {
  32. super(superState);
  33. }
  34. private SavedState(Parcel in, ClassLoader classLoader) {
  35. super(in);
  36. childrenStates = in.readSparseArray(classLoader);
  37. }
  38. @Override
  39. public void writeToParcel(Parcel out, int flags) {
  40. super.writeToParcel(out, flags);
  41. out.writeSparseArray(childrenStates);
  42. }
  43. public static final ClassLoaderCreator<SavedState> CREATOR
  44. = new ClassLoaderCreator<SavedState>() {
  45. @Override
  46. public SavedState createFromParcel(Parcel source, ClassLoader loader) {
  47. return new SavedState(source, loader);
  48. }
  49. @Override
  50. public SavedState createFromParcel(Parcel source) {
  51. return createFromParcel(null);
  52. }
  53. public SavedState[] newArray(int size) {
  54. return new SavedState[size];
  55. }
  56. };
  57. }
  58. }

讓我們過一遍這段亂麻了的代碼:

  • 在自定義的布局中沒我創建了一個特殊的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 view狀態保存