现在的位置: 首页 > 综合 > 正文

【Android每周专题】横竖屏切换和Activity中View状态的保持

2018年03月22日 ⁄ 综合 ⁄ 共 7864字 ⁄ 字号 评论关闭

本系列文章均为A2BGeek原创,转载务必在明显处注明:
转载自A2BGeek的【Android每周专题】系列,原文链接:http://blog.csdn.net/a2bgeek/article/details/14119801

每周专题名不符实,实在是惭愧,前段时间有点忙,没有及时更新,自我反省一下。

本周专题分为两个部分,第一部分是横竖屏切换的相关问题,第二部分是Activity中数据状态的保持。

横竖屏切换

横竖屏切换时Activity的生命周期

1、启动Activity(竖屏时启动的)

11-01 01:48:17.144: I/a2bgeek(945): onCreate
11-01 01:48:17.594: I/a2bgeek(945): onStart
11-01 01:48:17.594: I/a2bgeek(945): onResume

2、由竖屏切换为横屏(把真机横过来或者Ctrl+F11把模拟器横过来)

11-01 01:50:45.533: I/a2bgeek(945): onPause
11-01 01:50:45.533: I/a2bgeek(945): onStop
11-01 01:50:45.543: I/a2bgeek(945): onDestroy
11-01 01:50:45.593: I/a2bgeek(945): onCreate
11-01 01:50:45.934: I/a2bgeek(945): onStart
11-01 01:50:45.934: I/a2bgeek(945): onResume

3、由横屏再切换为竖屏

11-01 01:51:15.214: I/a2bgeek(945): onPause
11-01 01:51:15.214: I/a2bgeek(945): onStop
11-01 01:51:15.214: I/a2bgeek(945): onDestroy
11-01 01:51:15.277: I/a2bgeek(945): onCreate
11-01 01:51:15.704: I/a2bgeek(945): onStart
11-01 01:51:15.704: I/a2bgeek(945): onResume

从日志中可以清楚地看到,横竖屏切换会让Activity销毁再重建

横竖屏切换时Activity的布局问题

在做开发的时候可能很少考虑横屏布局的问题,尤其是接触Android开发不久的同学,说实话我个人也没有把手机横过来用的习惯,但是有的时候有的人有些事却不得不让开发者考虑横屏布局的问题。
在只有一套布局的情况下,Android系统会为横屏也使用这套布局,有的情况下界面看起来会非常难看,因为这套布局通常是为竖屏写的,这个时候开发者就需要专门为适配横屏写一套布局,还是和竖屏同样的那些View,只是大小、排布方式需要有所改变,然后把这套布局放到layout-land文件夹下面,这下如果手机再横屏,Android系统就会使用layout-land下面的这套布局了。如图下图所示,横屏的时候会使用layout-land下面的布局,竖屏的时候会使用layout-port下面的布局,layout这个文件夹在这种情况下是没有用的,可以删了。注意两套布局文件的命名要一致。

横竖屏切换时Activity的销毁重建对开发者的影响

横竖屏切换时的Activity的销毁重建是Android系统的缺省行为,但是这个行为却给开发者和用户带来了很多困扰。比如,用户躺着使用手机在一个EditText中输入内容,可能一个翻身就会让屏幕旋转,Activity销毁重建,EditText中的内容就被清空或者被初始化了,那么用户一定会非常苦恼,作为开发者,这是必须为用户解决的。
1、未解决时(一套布局或者两套布局)
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello_world" />

    <EditText
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:hint="input something" />

</LinearLayout>

2、解决方案一
只需为EditText指定一个id(两套布局时同一View要指定同一id)
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello_world" />

    <EditText
        android:id="@+id/et"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:hint="input something" />

</LinearLayout>

是不是很神奇?因为Android框架中几乎所有UI控件都实现了onSaveInstanceState和onRestoreInstanceState方法, 因此当Activity销毁和重建时, 这些UI控件会自动保存和恢复状态数据,当然你也不能什么都不做,你需要为这些控件指定一个id,剩下的事情Android框架会自己去做,如果你不指定id,那么控件的状态数据是不会自动保存和恢复的。这里就不截图了,请读者自己实验。

3、解决方案二
能不能尝试不去做销毁和重建呢?答案是肯定的,没错,就是onConfigurationChanged方法。怎么用?下面说步骤:
   1、在AndroidManifest.xml中声明权限

<uses-permission android:name="android.permission.CHANGE_CONFIGURATION"></uses-permission>

其实改变屏幕方向并不需要这个权限,至少我做实验的时候是不需要的。

   2、网上很多都说是orientation|keyboard,看了官方文档,才明白,Beginning with Android 3.2 (API level 13), the "screen size" also changes when the device switches between portrait and landscape orientation,所以要写screenSize。

<activity
            android:name="com.a2bgeek.orientationswitchdemo.MainActivity"
            android:configChanges="orientation|screenSize"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

   3、重写Activity中的onConfigurationChanged方法

完成了三步之后,我们再来旋转屏幕,查看日志:
11-03 21:06:48.551: I/a2bgeek(805): onCreate
11-03 21:06:49.062: I/a2bgeek(805): onStart
11-03 21:06:49.062: I/a2bgeek(805): onResume
11-03 21:07:23.752: I/a2bgeek(805): onConfigurationChanged
11-03 21:07:36.601: I/a2bgeek(805): onConfigurationChanged
11-03 21:29:26.204: I/a2bgeek(805): onConfigurationChanged
可以看到Activity并没有销毁和重建。
这里有个问题要注意,避免重建和销毁的这种方法避免了执行onCreate,也就避免了setContentView,那么布局文件就不会重新加载,也就不会根据横竖屏改变布局了。在这种解决方案下面,如果要切换布局,代码如下:

@Override
	public void onConfigurationChanged(Configuration newConfig) {
		// TODO Auto-generated method stub
		super.onConfigurationChanged(newConfig);
		Log.i(TAG, "onConfigurationChanged");
		if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
			setContentView(R.layout.activity_main);
		} else if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
			setContentView(R.layout.activity_main);
		}
	}

其实不用判断条件,直接setContentView就可以,Android系统会自动使用。但是这样View的状态是无法保持的,因为View状态的恢复是在onRestoreInstanceState中执行的,从日志中我们看到Activity根本没有销毁和重建,那么何来onRestoreInstanceState的执行呢?

Activity中数据的保持

在这部分将会说明Activity在正常的用户使用逻辑中如何保持良好的用户体验。

View状态的保持和数据的保持

1、View状态的保持和临时数据的保持
首先要明确onSaveInstanceState和onRestoreInstanceState的调用条件,首要条件就是不能有onConfigurationChanged,因为这会阻止重建和销毁的过程,也就是说onSaveInstanceState和onRestoreInstanceState是在重建和销毁的过程中调用的,来看一下日志就知道,我还是进行横竖屏切换的操作:
11-04 02:30:42.551: I/a2bgeek(1657): onPause
11-04 02:30:42.622: I/a2bgeek(1657): onSaveInstanceState
11-04 02:30:42.622: I/a2bgeek(1657): onStop
11-04 02:30:42.622: I/a2bgeek(1657): onDestroy
11-04 02:30:42.742: I/a2bgeek(1657): onCreate
11-04 02:30:43.062: I/a2bgeek(1657): onStart
11-04 02:30:43.072: I/a2bgeek(1657): onRestoreInstanceState
11-04 02:30:43.072: I/a2bgeek(1657): onResume
onSaveInstanceState调用的时机是系统有销毁Activity的可能,那么什么情况有这种可能呢?典型的情况由横竖屏切换、锁屏、按下HOME键、长按HOME键展示最近使用列表、切换到其他Activity。
onRestoreInstanceState调用的时机是Activity被系统销毁了,而不是可能被系统销毁了。前述典型的情况里面只有横竖屏切换才会立刻调用onRestoreInstanceState,其他情况其实是Activity处于onStop的状态,只有当Activity被系统销毁而不是人为销毁时才会调用onRestoreInstanceState。什么是被系统销毁?比如内存不够时系统把处于onStop状态的Activity销毁腾出内存给用户正在使用的Activity用。
读者是不是一直对有id的View为什么能够保持状态感到困惑?下面我来回答这个问题。
其实View也是有onSaveInstanceState和onRestoreInstanceState方法的,http://developer.android.com/reference/android/view/View.html#onSaveInstanceState()
工作原理是在View的onSaveInstanceState方法保存一些View有用的数据到一个Parcelable对象,在Activity的onSaveInstanceState中调用View的onSaveInstanceState,把其返回的Parcelable对象用Bundle的putParcelable方法保存在outState中。当系统调用Activit的onRestoreInstanceState时,通过Bundle的getParcelable方法得到Parcelable对象,然后把该Parcelable对象传给View的onRestoreInstanceState,再从View的onRestoreInstanceState中从Parcelable读取保存的数据以便View使用。

@Override
        protected void onSaveInstanceState(Bundle outState) {
		super.onSaveInstanceState(outState);
		Log.i(TAG, "onSaveInstanceState");
		Parcelable savedState = textView1.onSaveInstanceState();
		outState.putParcelable(kTextView1Name, savedState);
	}

	@Override
	protected void onRestoreInstanceState(Bundle savedInstanceState) {
		super.onRestoreInstanceState(savedInstanceState);
		Log.i(TAG, "onRestoreInstanceState");
		SavedState0 savedState = savedInstanceState
				.getParcelable(kTextView1Name);
		textView1.onRestoreInstanceState(savedState);
	}

而几乎所有的View都实现了自己的onSaveInstanceState和onRestoreInstanceState,所以View的状态不太需要开发者来操心。

临时数据的保持其实也是通过Activity的onSaveInstanceState和onRestoreInstanceState方法实现的,那么临时数据指的是什么?临时数据是指在用户的一次使用过程中需要保持的数据,注意是一次使用过程,这个过程的终点是用户人为退出(包括点击返回键、菜单中的退出键、4.0中在最近使用列表中直接滑动关闭)。如果用户始终在使用一个Activity,那么根本不需要考虑这些问题,关键是用户可能进行横竖屏切换、可能接电话、可能锁屏等操作暂时离开当前的Activity,那么就需要为这个Activity保持一些数据包括View的状态。

2、数据的持久化
前面说了什么是临时性数据,那么持久性数据呢?所谓持久是指用户关闭软件后,下次再使用,希望能够保持上次使用的一些数据,比如游戏的存档。持久化的方法就是利用Activity的生命周期,在onPause做数据的存储,在onStart中做数据的恢复,存储方式可以选择数据库、共享文件等方式。这里就不过多赘述了。

对象的保持

这个说实话我没有用过,开发经验还是不够,呵呵。可以看到onRetainNonConfigurationInstance的返回值是一个对象,那么什么时候会用呢?当需要恢复很大的数据对象比如Bitmap,如果使用onSaveInstanceState,会很占资源,因为onSaveInstanceState就不是为大数据对象设计的,这时就可以使用onRetainNonConfigurationInstance。示例代码如下:
@Override
public Object onRetainNonConfigurationInstance() {
    final MyDataObject data = collectMyLoadedData();
    return data;
}

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    final MyDataObject data = (MyDataObject) getLastNonConfigurationInstance();
    if (data == null) {
        data = loadMyData();
    }
    ...
}

onRetainNonConfigurationInstance在五种典型情况下只有在横竖屏切换的情况下才会调用。使用方法和onSaveInstanceState、onRestoreInstanceState一样。

写在最后

写这个系列文章的初衷是总结和归纳,但是在写的过程中发现自己掌握的知识还很肤浅,所以这个系列的每篇文章我都会认真对待,如果有了新的体会,就会更新。

参考资料:

1、http://hubingforever.blog.163.com/blog/static/17104057920106272410547/

2、http://developer.android.com/training/basics/activity-lifecycle/recreating.html

3、http://developer.android.com/guide/topics/resources/runtime-changes.html

抱歉!评论已关闭.