android中正确保存view的状态
英文原文: http://trickyandroid.com/saving-android-view-state-correctly/
转载此译文须注明出处。
今天我们聊一聊安卓中保存和恢复view状态的问题。我刻意强调View状态是因为我发现这个过程要比保存 Activity 和 Fragment状态稍微复杂,还有一个原因是因为网上有太多“重复造的轮子”(有时还是奇丑无比的轮子)。
为什么我们需要保存View的状态?
这个问题问的好!我坚信移动应用应该帮助你解决问题,而不是制造问题。
想象一下一个非常复杂的设置页面:
这并不是从一个移动应用的截图(这不是典型的win32程序吗。。),但是适合用于说明我们的问题:
这里有非常多的文字输入控件,多选框,开关(switch)等等,你花了15分钟填完所有这些格子,总算轮到点击"完成"按钮了,但是突然,你不小心旋转了下屏幕,omg,所有的改动都没了,一切都回归到了初始状态。
当然,总有一些用户喜欢你的app简直到不行,不在乎重新填一次。但是老实说,这样做真的正确吗?(原文有老外常喜欢的喋喋不休的幽默句子,略了)。
别犯傻,我们需要保存用户的修改,除非用户特意让我们不要这样做。
如何保存View的状态?
假设我们这里有一个带有图像,文字和 Switch toggle控件的简单布局:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
<LinearLayout 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):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
<LinearLayout 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的的类中添加一个自定义的状态:
1
2
3
4
5
6
7
8
9
10
|
public class CustomSwitch extends Switch { private int customState; //所谓状态其实就是数据 ....... public void setCustomState(int customState) { this .customState = customState; } } |
下面是我们将如何保存这个状态的过程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
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,这样就不会重叠了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
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中正确保存view的状态的更多相关文章
- Android中activity保存数据和状态在哪个方法实现
以前只知道在Activity销毁之前,要把数据保存在 onSaveInstanceState(Bundle)方法中,后来学习了别人的微博,学到了很多细节问题,所以整理了一下,希望能帮到大家. 如果看官 ...
- android中正确导入第三方jar包
android中正确导入第三方jar包 andriod中如果引入jar包的方式不对就会出现一些奇怪的错误. 工作的时候恰好有一个jar包需要调用,结果用了很长时间才解决出现的bug. 刚开始是这样引用 ...
- Android中如何为自定义控件增加状态?
在android开发中我们常常需要对控件进行相关操作,虽然网上已有很多对控件酷炫的操作,但小编今天给大家分享的纯属实用出发.在查看了一些列安卓教程和文档后,发现了一位大牛分享的非常不错的有关andro ...
- android 中的 window,view,activity具体关系
通过讨论这个问题,我们能够见识到google是对面向对象模式的理解,能够理解android底层的一些调用.这也是一道很常见的面试题. 我们这篇文章就来解决这四个问题: Android 中view的显 ...
- android 中怎么保存当前按钮的状态?就是退出后重新进入还是上一次离开的状态
比如当前Activity中有一个按钮目前是开启,点击后按钮的text变成关闭!然后退出该Activtity,然后重新打开该Activity后当前按钮的状态还是关闭呢? 就是设置一个状态flag.fla ...
- Android中使用自定义View实现下载进度的显示
一般有下载功能的应用都会有这样一个场景,需要一个图标来标识不同的状态.之前在公司的项目中写过一个,今天抽空来整理一下. 一般下载都会有这么几种状态:未开始.等待.正在下载.下载结束,当然有时候会有下载 ...
- Android中实现自定义View组件并使其能跟随鼠标移动
场景 实现效果如下 注: 博客: https://blog.csdn.net/badao_liumang_qizhi 关注公众号 霸道的程序猿 获取编程相关电子书.教程推送与免费下载. 实现 新建An ...
- Android中StateListDrawable的种类(状态的种类)
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="ht ...
- android中activity,window,view之间的关系
activity:控制单元 window:承载模型 view:显示视图 几个小tip: 1.一个 Activity 构造的时候一定会构造一个 Window(PhoneWindow),并且只有一个 2. ...
随机推荐
- Eclipse 导入项目乱码问题(中文乱码)
1.编码不对 a.对某文件或某工程更改编码: 鼠标移到工程名或文件名,右键->Properties->Resource->Text file enCoding ->更改编码 ...
- Java中如何判断当前环境是大端字节顺序还是小端字节顺序
Java非字节类型的基本类型,除了布尔型都是由组合在一起的几个字节组成的.这些数据类 型及其大小总结在表 2-1 中. 表:基本数据类型及其大小 数据类型 大小(以字节表示) Byte 1 Char ...
- 查看sqlserver默认的编码格式
SQL语句:SELECT COLLATIONPROPERTY('Chinese_PRC_Stroke_CI_AI_KS_WS', 'CodePage') 936 简体中文GBK 950 繁体中文BIG ...
- IO流+数据库课后习题
1,读取 试题文件 然后做题算分 File file1=new File("D:\\file","test.txt"); try{ FileReader in1 ...
- 学习笔记之AJAX无刷新分页
利用AJAX实现无刷新分页技术原理: 其主要是利用AJAX的异步处理机制,实现数据的异步传递,它隐藏了客户端向服务端请求数据的状态,在客户端表现为无刷新的显示状态. 实现分页的步骤: 1.客服端点击页 ...
- 转载:svn clean up 失败解决方法
转载网址:http://www.tuicool.com/articles/biy6na 今天svn遇到一个头疼的问题,最开始更新的时候失败了,因为有文件被锁住了.按照以往的操作,我对父目录进行clea ...
- 关于DDOS攻击中TCP半连接数与FD的关系
TCP最大连接数 在tcp应用中,server事先在某个固定端口监听,client主动发起连接,经过三路握手后建立tcp连接.那么对单机,其最大并发tcp连接数是多少? 理论最大值 在确定最大连接数之 ...
- Top 100 English Verbs
accept allow ask believe borrow break bring buy can/be able cancel change clean comb complain cough ...
- notes: the architecture of GDB
1. gdb structure at the largest scale,GDB can be said to have two sides to it:1. The "symbol si ...
- Gmail 一些需注意的事
display none 的问题 如果我们写html 然后用 mail server 发给gmail client . 如果内容涉及 display:none , 请加一个 important 比如 ...