原文:Handling Configuration Changes
—Screen Rotation
概述
存在这样一些情况:当屏幕方向旋转时,Activity实际上可以被销毁并从内存中移除,然后再从头重建一次。在这种情况下,最佳的处理方式是准备为将被重建的Activity,通过正确的方式保存并恢复状态。
保存并恢复Activity状态
当Activity将要停止时,系统调用onSaveInstanceState()
,将Activity的状态信息保存在“键-值”对的集合中。该方法的默认实现会自动保存Activity的视图层次结构状态的信息,例如EditText
组件中的文本或ListView
的滚动位置。
要保存Activity的其他状态信息,必须要实现onSaveInstanceState()
,并向Bundle对象中添加“键-值”对。例如:
public class MainActivity extends Activity {
static final String SOME_VALUE = "int_value";
static final String SOME_OTHER_VALUE = "string_value";
@Override
protected void onSaveInstanceState(Bundle savedInstanceState) {
// 在bundle中保存自定义的value值
savedInstanceState.putInt(SOME_VALUE, someIntValue);
savedInstanceState.putString(SOME_OTHER_VALUE, someStringValue);
// 总是调用父类的方法,来保存视图层次结构状态
super.onSaveInstanceState(savedInstanceState);
}
}
系统将在Activity被销毁前调用上述的方法,之后系统将会调用onRestoreInstanceState
从bundle中恢复状态:
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
// 总是调用父类的方法,来恢复视图层次结构状态
super.onRestoreInstanceState(savedInstanceState);
// 从保存的实例中恢复状态成员
someIntValue = savedInstanceState.getInt(SOME_VALUE);
someStringValue = savedInstanceState.getString(SOME_OTHER_VALUE);
}
实例状态也可以在标准的Activity#onCreate
方法中恢复,但在onRestoreInstanceState
中操作起来更加方便,它可以确保所有的初始化都已完成,并允许子类决定是否使用默认实现。 更多细节请参阅 this stackoverflow post。
注意onSaveInstanceState
和onRestoreInstanceState
方法不能保证一起被调用。Android系统在Activity有可能被销毁时调用onSaveInstanceState()
。有些情况下,onSaveInstanceState()
被调用,但是Activity并没有被销毁,因此onRestoreInstanceState
未被调用。
更多内容请参考指南Recreating an Activity 。
保存并恢复Fragment状态
Fragment也有onSaveInstanceState
方法,当它们的状态需要保存时该方法会被调用:
public class MySimpleFragment extends Fragment {
private int someStateValue;
private final String SOME_VALUE_KEY = "someValueToSave";
// 当配置更改或Fragment需要保存状态时触发
@Override
protected void onSaveInstanceState(Bundle outState) {
outState.putInt(SOME_VALUE_KEY, someStateValue);
super.onSaveInstanceState(outState);
}
}
我们可以从onCreateView
中将保存的数据抽取出来:
public class MySimpleFragment extends Fragment {
// ...
// 基于xml布局文件为Fragment填充视图
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.my_simple_fragment, container, false);
if (savedInstanceState != null) {
someStateValue = savedInstanceState.getInt(SOME_VALUE_KEY);
// 在必要时使用恢复的值
}
return view;
}
}
为了正确地保存Fragment状态,我们必须确保当配置变化时,我们并不是在不必要地重建Fragment。这意味着当现有的Fragment已经存在时,不要再重新初始化。在Activity中初始化的任何Fragment,当配置变化之后,都要按标签查找。
public class ParentActivity extends AppCompatActivity {
private MySimpleFragment fragmentSimple;
private final String SIMPLE_FRAGMENT_TAG = "myfragmenttag";
@Override
protected void onCreate(Bundle savedInstanceState) {
if (savedInstanceState != null) { // 保存实例状态,Fragment可能存在
// 通过标签查找已存在的实例
fragmentSimple = (MySimpleFragment)
getSupportFragmentManager().findFragmentByTag(SIMPLE_FRAGMENT_TAG);
} else if (fragmentSimple == null) {
// 仅在Fragment还未被初始化的情况下创建它们
fragmentSimple = new MySimpleFragment();
}
}
}
这就要求我们在使用事务将Fragment放入Activity中时,注意包含查找标签。
public class ParentActivity extends AppCompatActivity {
private MySimpleFragment fragmentSimple;
private final String SIMPLE_FRAGMENT_TAG = "myfragmenttag";
@Override
protected void onCreate(Bundle savedInstanceState) {
// ... 在这之上查找或初始化Fragment...
// 对将要插入容器内的Fragment总是添加一个标签
if (!fragmentSimple.isInLayout()) {
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.container, fragmentSimple, SIMPLE_FRAGMENT_TAG)
.commit();
}
}
}
通过这种简单的模式,我们可以正确地重新使用Fragment,并在配置变化时恢复它们的状态。
保留Fragments
在很多情况下,我们可以通过简单地使用Fragment重新创建Activity,来避免问题的出现。如果你的视图和状态都在Fragment中,当Activity重建时我们就可以轻松地保留Fragment:
public class RetainedFragment extends Fragment {
// 要保留的数据对象
private MyDataObject data;
// 该方法针对这个Fragment仅会被调用一次
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 当Activity重建时保留这个Fragment
setRetainInstance(true);
}
public void setData(MyDataObject data) {
this.data = data;
}
public MyDataObject getData() {
return data;
}
}
这种方法可以防止Fragment在Activity的生命周期内被破坏。它们保留在Fragment Manager中。更多信息请参阅Android 官方文档。
现在你可以在Fragment创建之前通过标签检测Fragment是否已存在,并且Fragment可以在配置变化期间保留它的状态。更多细节请参阅Handling Runtime Changes。
正确处理列表状态
ListView
通常在旋转屏幕时,应用程序都将失去滚动位置和屏幕上列表的其他状态。要正确地保留ListView
的状态,你可以在onPause
中存储实例的状态并从onViewCreated
中恢复,如下所示:
// YourActivity.java
private static final String LIST_STATE = "listState";
private Parcelable mListState = null;
// 将List的状态值写入bundle
@Override
protected void onSaveInstanceState(Bundle state) {
super.onSaveInstanceState(state);
mListState = getListView().onSaveInstanceState();
state.putParcelable(LIST_STATE, mListState);
}
// 从bundle中恢复list的状态值
@Override
protected void onRestoreInstanceState(Bundle state) {
super.onRestoreInstanceState(state);
mListState = state.getParcelable(LIST_STATE);
}
@Override
protected void onResume() {
super.onResume();
loadData(); // 首先要确保数据已经重新加载到适配器中
// 一旦将数据项加载到适配器中,立即调用这一部分
// 例如,网络请求的成功回调
if (mListState != null) {
myListView.onRestoreInstanceState(mListState);
mListState = null;
}
}
查看这篇博客文章和[stackoverflow文章](stackoverflow post](http://stackoverflow.com/a/5688490)了解更多细节。
请注意,在调用onRestoreInstanceState
之前,必须先将数据项加载到是配置中。换句话说,在数据未从网络或者数据库加载回来之前,请不要在ListView上调用onRestoreInstance
。
RecyclerView
通常在旋转屏幕时,应用程序都将失去滚动位置和屏幕上列表的其他状态。要正确地保留RecyclerView
的状态,你可以在onPause
中存储实例的状态并从onViewCreated
中恢复,如下所示:
// YourActivity.java
public final static int LIST_STATE_KEY = "recycler_list_state";
Parcelable listState;
protected void onSaveInstanceState(Bundle state) {
super.onSaveInstanceState(state);
// 保存list状态
listState = mLayoutManager.onSaveInstanceState();
state.putParcelable(LIST_STATE_KEY, mListState);
}
protected void onRestoreInstanceState(Bundle state) {
super.onRestoreInstanceState(state);
// 检索list状态和列表项的位置
if(state != null)
listState = state.getParcelable(LIST_STATE_KEY);
}
@Override
protected void onResume() {
super.onResume();
if (listState != null) {
mLayoutManager.onRestoreInstanceState(listState);
}
}
查看这篇博客文章和stackoverflow文章 了解更多细节。
锁定屏幕方向
如果你想锁定应用中屏幕方向的变化,只需要在AndroidManifest.xml
文件中给
标签设置android:screenOrientation
属性即可:
现在,Activity总是被强制以“竖屏”方式展示。
手动管理配置变化
如果你的应用在特定的配置变化时不需要更新资源,并且对性能方面有诸多限制,避免Activity重新启动,你可以声明由Activity自己处理配置变化,这将防止系统重新启动Activity。
然而,这种技术应该被当做避免Activity在配置变化时重启的一种不得已而为之的方式,在大多数应用中并不推荐。采用这种方法,你必须添加android:configChanges
节点到AndroidManifest.xml
中的Activity中:
现在,当配置变化时Activity并不会重启,而是会收到一个onConfigurationChanged()
的调用:
// 在收到这些变化的Activity中
// 检测当前设备的方向,相应地弹出提示
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// 检测屏幕方向
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
Toast.makeText(this, "landscape", Toast.LENGTH_SHORT).show();
} else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT){
Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show();
}
}
参考Handling the Change文档. 更多处理Activity中配置变化的内容,请参阅 android:configChanges文档和Configuration类.
参考引用
- http://developer.android.com/guide/topics/resources/runtime-changes.html
- http://developer.android.com/training/basics/activity-lifecycle/recreating.html
- http://www.vogella.com/tutorials/AndroidLifeCycle/article.html#configurationchange
- http://www.androiddesignpatterns.com/2013/04/retaining-objects-across-config-changes.html
- http://www.intertech.com/Blog/saving-and-retrieving-android-instance-state-part-1/
- http://sunil-android.blogspot.com/2013/03/save-and-restore-instance-state.html
- https://medium.com/google-developers/activity-revival-and-the-case-of-the-rotating-device-167e34f9a30d#.nq3b23lxg