为什么我们需要保存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上 找到。

最新文章

  1. Change Git Default Editor in Windows
  2. iOS 含有 中文的URL 转码问题
  3. Unity3D使用Assetbundle打包加载(Prefab、场景)
  4. ios block和函数的区别
  5. 从手工测试转型web自动化测试继而转型成专门做自动化测试的学习路线。
  6. esp8266烧写机智云固件方法
  7. hdu 4612 Warm up(缩点+树上最长链)
  8. node系列4
  9. java中setDate(Date date)方法和String与Date之间的转换
  10. 自学Zabbix3.5.5-监控项item-User parameters(自定义key)
  11. [django1.6]跑批任务错误(2006, &#39;MySQL server has gone away&#39;)
  12. 京东B2B业务架构演变
  13. JavaWeb在线电子相册springmvc
  14. 5分钟入门Tornado
  15. Go语言规格说明书 之 类型声明(Type declarations)
  16. thinkphp 参数传递方式(基础)
  17. mac下zephir第一步,安装+hello zephir!
  18. np.random.random()函数 参数用法以及numpy.random系列函数大全
  19. JS实现中英文混合文字溢出友好截取功能
  20. 使用VBS打开程序和关闭程序

热门文章

  1. javascript slice array to num subarray
  2. IIS Manager 配置文件修该,允许跨域CORS访问
  3. myBaits association的使用
  4. Remote error: Provider not exported: DataSetProvider1
  5. 获取properties配置
  6. vs2010下设置release版本调试设置
  7. bootstrap 的页码显示问题-------------德州
  8. 获取当前UnixTime的零点时间戳
  9. 数组和集合(四)、Map集合的使用总结
  10. Platform Dependent Compilation