• Android view状态保存


    为什么我们需要保存View的状态?

    这个问题问的好!我坚信移动应用应该帮助你解决问题,而不是制造问题。

    想象一下一个非常复杂的设置页面:

    Page

    这并不是从一个移动应用的截图(这不是典型的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.  
    3.     private int customState;//所谓状态其实就是数据
    4.  
    5.     .......
    6.  
    7.     public void setCustomState(int customState) {
    8.         this.customState = customState;
    9.     }  
    10. }

    下面是我们将如何保存这个状态的过程:

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

    让我来解释一下上面所做的事情。

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

    让我们过一遍这段乱麻了的代码:

    • 在自定义的布局中没我创建了一个特殊的SaveState类,它持有父类状态以及保存子view状态的独立SparseArray。

    • 在onSaveInstanceState()中我主动存储父类与子view的状态到独立的SparseArray中。

    • 在onRestoreInstanceState()中我主动从保存期间创建的SparseArray中恢复父类和子view的状态。

    • 记住如果这是一个ViewGroup - dispatchSaveInstanceState()还需要遍历子View然后保存它们的状态吗?既然我们现在是手动的了,我需要废弃这种行为。幸运的是使用dispatchFreezeSelfOnly()方法可以告诉安卓只保存viewGroup的状态,不要碰它的子View(在dispatchSaveInstanceState()中调用)。

    • dispatchRestoreInstanceState()需要做同样的事情 - 调用dispatchThawSelfOnly()。告诉安卓只恢复自身的状态 ,子view我们自己来处理。

    下面是SparseArray的示意图:

    正如你看到的每个view group都有了独自的SparseArray,因此他们就不会重叠和覆盖彼此了。

    状态保存了 赚大了!

    这篇文章的代码可以在 GitHub上 找到。

  • 相关阅读:
    [CAMCOCO][C#]我的系统架构.服务器端.(一)
    开博啦,加油!!
    Django RestFul framework Serializer序列化器的使用(定义)
    Django项目快速搭建
    Django简介
    python搭建虚拟环境
    大专生自学web前端前前后后
    实现资源国际化
    利用ajax实现页面动态修改
    七牛使用
  • 原文地址:https://www.cnblogs.com/linghu-java/p/8032819.html
Copyright © 2020-2023  润新知