Android 旋转屏幕--处理Activity与AsyncTask的最佳解决方案
一、概述
运行时变更就是设备在运行时发生变化(例如屏幕旋转、键盘可用性及语言)。发生这些变化,Android会重启Activity,这时就需要保存activity的状态及与activity相关的任务,以便恢复activity的状态。
为此,google提供了三种解决方案:
- 对于少量数据: 通过
onSaveInstanceState()
,保存有关应用状态的数据。 然后在onCreate()
或onRestoreInstanceState()
期间恢复 Activity 状态。 - 对于大量数据:用
Fragment
保留需要回复的对象。 - 自行处理配置变更,不重启Activity。
下面会逐一介绍三种情况,其实保存一些变量对象很简单,难的是当Activity创建异步线程去加载数据时,旋转屏幕时,怎么保存线程的状态。比如,在线程的加载过程中,旋转屏幕,就会存在问题:此时数据没有完成加载,onCreate重新启动时,会再次启动线程;而上个线程可能还在运行,并且可能会更新已经不存在的控件,造成错误。下面会一一解决这些问题。本文较长,主要是代码多,可以先下载demo,源码下载:http://download.csdn.net/detail/jycboy/9720486对比着看。
二、使用onSaveInstanceState,onRestoreInstanceState
代码如下:
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
|
/**
* 使用onSaveInstanceState,onRestoreInstanceState;
* 在这里不考虑没有加载完毕,就旋转屏幕的情况。
* @author 超超boy
*
*/
public
class
SavedInstanceStateActivity
extends
ListActivity
{
private
static
final
String TAG =
"MainActivity"
;
private
ListAdapter mAdapter;
private
ArrayList<String> mDatas;
private
DialogFragment mLoadingDialog;
private
LoadDataAsyncTask mLoadDataAsyncTask;
@Override
public
void
onCreate(Bundle savedInstanceState)
{
super
.onCreate(savedInstanceState);
Log.e(TAG,
"onCreate"
);
initData(savedInstanceState);
}
/**
* 初始化数据
*/
private
void
initData(Bundle savedInstanceState)
{
if
(savedInstanceState !=
null
)
mDatas = savedInstanceState.getStringArrayList(
"mDatas"
);
if
(mDatas ==
null
)
{
mLoadingDialog =
new
LoadingDialog();
mLoadingDialog.show(getFragmentManager(),
"LoadingDialog"
);
mLoadDataAsyncTask =
new
LoadDataAsyncTask();
mLoadDataAsyncTask.execute();
//mLoadDataAsyncTas
}
else
{
initAdapter();
}
}
/**
* 初始化适配器
*/
private
void
initAdapter()
{
mAdapter =
new
ArrayAdapter<String>(
SavedInstanceStateActivity.
this
,
android.R.layout.simple_list_item_1, mDatas);
setListAdapter(mAdapter);
}
@Override
protected
void
onRestoreInstanceState(Bundle state)
{
super
.onRestoreInstanceState(state);
Log.e(TAG,
"onRestoreInstanceState"
);
}
@Override
//在这里保存数据,好用于返回
protected
void
onSaveInstanceState(Bundle outState)
{
super
.onSaveInstanceState(outState);
Log.e(TAG,
"onSaveInstanceState"
);
outState.putSerializable(
"mDatas"
, mDatas);
}
/**
* 模拟耗时操作
*
* @return
*/
private
ArrayList<String> generateTimeConsumingDatas()
{
try
{
Thread.sleep(
3000
);
}
catch
(InterruptedException e)
{ e.printStackTrace();
}
return
new
ArrayList<String>(Arrays.asList(
"通过Fragment保存大量数据"
,
"onSaveInstanceState保存数据"
,
"getLastNonConfigurationInstance已经被弃用"
,
"RabbitMQ"
,
"Hadoop"
,
"Spark"
));
}
private
class
LoadDataAsyncTask
extends
AsyncTask<Void, Void, Void>
{
@Override
protected
Void doInBackground(Void... params)
{
mDatas = generateTimeConsumingDatas();
return
null
;
}
@Override
protected
void
onPostExecute(Void result)
{
mLoadingDialog.dismiss();
initAdapter();
}
}
@Override
protected
void
onDestroy()
{
Log.e(TAG,
"onDestroy"
);
super
.onDestroy();
}
}
|
界面为一个ListView,onCreate中启动一个异步任务去加载数据,这里使用Thread.sleep模拟了一个耗时操作;当用户旋转屏幕发生重新启动时,会onSaveInstanceState中进行数据的存储,在onCreate中对数据进行恢复,免去了不必要的再加载一遍。
运行结果:
12-24 20:13:41.814 1994-1994/? E/MainActivity: onCreate
12-24 20:13:46.124 1994-1994/? E/MainActivity: onSaveInstanceState
12-24 20:13:46.124 1994-1994/? E/MainActivity: onDestroy
12-24 20:13:46.154 1994-1994/? E/MainActivity: onCreate
12-24 20:13:46.164 1994-1994/? E/MainActivity: onRestoreInstanceState
当正常加载数据完成之后,用户不断进行旋转屏幕,log会不断打出:onSaveInstanceState->onDestroy->onCreate->onRestoreInstanceState,验证Activity重新启动,但是我们没有再次去进行数据加载。
如果在加载的时候,进行旋转,则会发生错误,异常退出(退出原因:dialog.dismiss()时发生NullPointException,因为与当前对话框绑定的FragmentManager为null,在这里这个不是关键)。
效果图:
三、使用Fragment保留对象,恢复数据
果重启 Activity 需要恢复大量数据、重新建立网络连接或执行其他密集操作,依靠系统通过onSaveInstanceState()
回调为您保存的 Bundle
,可能无法完全恢复 Activity 状态,因为它并非设计用于携带大型对象(例如位图),而且其中的数据必须先序列化,再进行反序列化,这可能会消耗大量内存并使得配置变更速度缓慢。 在这种情况下,如果 Activity 因配置变更而重启,则可通过保留 Fragment
来减轻重新初始化 Activity 的负担。此片段可能包含对您要保留的有状态对象的引用。
当 Android 系统因配置变更而关闭 Activity 时,不会销毁您已标记为要保留的 Activity 的片段。 您可以将此类片段添加到 Activity 以保留有状态的对象。
要在运行时配置变更期间将有状态的对象保留在片段中,请执行以下操作:
- 扩展
Fragment
类并声明对有状态对象的引用。 - 在创建片段后调用
setRetainInstance(boolean)
。 - 将片段添加到 Activity。
- 重启 Activity 后,使用
FragmentManager
检索片段。
例如,按如下方式定义片段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public
class
RetainedFragment
extends
Fragment {
// data object we want to retain
private
MyDataObject data;
// this method is only called once for this fragment
@Override
public
void
onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState);
// retain this fragment
setRetainInstance(
true
);
}
public
void
setData(MyDataObject data) {
this
.data = data;
}
public
MyDataObject getData() {
return
data;
}
}
|
注意:尽管您可以存储任何对象,但是切勿传递与 Activity
绑定的对象,例如,Drawable
、Adapter
、View
或其他任何与 Context
关联的对象。否则,它将使Activity无法被回收造成内存泄漏。(泄漏资源意味着应用将继续持有这些资源,但是无法对其进行垃圾回收,因此可能会丢失大量内存)
下面举一个实际的例子:
1.RetainedFragment
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public
class
RetainedFragment
extends
Fragment
{
// data object we want to retain
private
Bitmap data;
// this method is only called once for this fragment
@Override
public
void
onCreate(Bundle savedInstanceState)
{
super
.onCreate(savedInstanceState);
// retain this fragment
setRetainInstance(
true
);
}
public
void
setData(Bitmap data)
{
this
.data = data;
}
public
Bitmap getData()
{
return
data;
}
}
|
只是保持Bitmap对象的引用,你可以用Fragment保存多个对象。
2.FragmentRetainDataActivity:
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
|
public
class
FragmentRetainDataActivity
extends
Activity
{
private
static
final
String TAG =
"FragmentRetainData"
;
private
RetainedFragment dataFragment;
private
DialogFragment mLoadingDialog;
private
ImageView mImageView;
private
Bitmap mBitmap;
BitmapWorkerTask bitmapWorkerTask;
@Override
public
void
onCreate(Bundle savedInstanceState)
{
super
.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.e(TAG,
"onCreate"
);
// find the retained fragment on activity restarts
FragmentManager fm = getFragmentManager();
dataFragment = (RetainedFragment) fm.findFragmentByTag(
"data"
);
// create the fragment and data the first time
if
(dataFragment ==
null
)
{
// add the fragment
dataFragment =
new
RetainedFragment();
fm.beginTransaction().add(dataFragment,
"data"
).commit();
}
// the data is available in dataFragment.getData()
mBitmap = dataFragment.getData();
initView();
}
/**
* 初始化控件
*/
private
void
initView()
{
mImageView = (ImageView) findViewById(R.id.id_imageView);
if
(mBitmap !=
null
)
mImageView.setImageBitmap(mBitmap);
//图片为空时,加载图片;有时候即使dataFragment!=null时,图片也不一定就加载完了,比如在加载的过程中,旋转屏幕,此时图片就没有加载完
else
{
mLoadingDialog =
new
LoadingDialog();
mLoadingDialog.show(getFragmentManager(),
"LOADING_DIALOG"
);
bitmapWorkerTask =
new
BitmapWorkerTask(
this
);
bitmapWorkerTask.execute(
"http://images2015.cnblogs.com/blog/747969/201612/747969-20161222164357995-1098775233.jpg"
);
}
}
/**
* 异步下载图片的任务。
* 设置成静态内部类是为了防止内存泄漏
* @author guolin
*/
private
static
class
BitmapWorkerTask
extends
AsyncTask<String, Void, Bitmap> {
//图片的URL地址
private
String imageUrl;
//保存外部activity的弱引用
private
WeakReference<Context> weakReference;
public
BitmapWorkerTask(Context context) {
weakReference =
new
WeakReference<>(context);
}
@Override
protected
Bitmap doInBackground(String... params) {
imageUrl = params[
0
];
//为了演示加载过程,阻塞2秒
try
{Thread.sleep(
2000
);
}
catch
(InterruptedException e)
{ e.printStackTrace();
}
return
downloadUrlToStream(imageUrl);
}
@Override
protected
void
onPostExecute(Bitmap bitmap) {
super
.onPostExecute(bitmap);
if
(bitmap !=
null
){
FragmentRetainDataActivity retainDataActivity= (FragmentRetainDataActivity) weakReference.get();
//调用回调方法
retainDataActivity.onLoaded(bitmap);
}
}
/**
* 建立HTTP请求,并获取Bitmap对象。
* 修改了下
* @param urlString
* 图片的URL地址
* @return 解析后的Bitmap对象
*/
private
Bitmap downloadUrlToStream(String urlString) {
HttpURLConnection urlConnection =
null
;
Bitmap bitmap =
null
;
try
{
final
URL url =
new
URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
if
(urlConnection.getResponseCode()==HttpURLConnection.HTTP_OK){
//连接成功
InputStream is = urlConnection.getInputStream();
bitmap = BitmapFactory.decodeStream(is);
is.close();
return
bitmap;
}
else
{
return
null
;
}
}
catch
(
final
IOException e) {
e.printStackTrace();
}
finally
{
if
(urlConnection !=
null
) {
urlConnection.disconnect();
}
}
return
null
;
}
}
//加载完毕的回掉
public
void
onLoaded(Bitmap bitmap){
mBitmap = bitmap;
mLoadingDialog.dismiss();
mImageView.setImageBitmap(mBitmap);
// load the data from the web
dataFragment.setData(mBitmap);
Log.e(TAG,
"onLoaded"
);
}
public
void
onPause(){
super
.onPause();
Log.e(TAG,
"onPause"
);
if
(getFragmentManager() !=
null
&& mLoadingDialog !=
null
)
mLoadingDialog.dismiss();
}
@Override
public
void
onDestroy()
{
super
.onDestroy();
Log.e(TAG,
"onDestroy"
);
if
(bitmapWorkerTask !=
null
)
bitmapWorkerTask.cancel(
true
);
// store the data in the fragment
dataFragment.setData(mBitmap);
}
}
|
这里边用BitmapWorkerTask异步下载图片,downloadUrlToStream封装了下载图片的代码;
BitmapWorkerTask用弱引用保持外部Activity对象防止内存泄漏,下载完毕后用onLoaded回调方法更新UI。
通过检查dataFragment、mBitmap判断是否已经加载过,加载过直接用就可以。
效果图:
在gif里可以看到,如果未加载完毕就旋转屏幕,它会重新启动异步线程去下载,这种效果并不好,我们会在最后解决这个问题。
四、自行处理配置变更
如果应用在特定配置变更期间无需更新资源,并且因性能限制您需要尽量避免重启,则可声明 Activity 将自行处理配置变更,这样可以阻止系统重启 Activity。
要声明由 Activity 处理配置变更,需设置清单文件manifest:
1
2
3
|
<
activity
android:name=".MyActivity"
android:configChanges="orientation|keyboardHidden"
android:label="@string/app_name">
|
"orientation"
和 "keyboardHidden"
,分别用于避免因屏幕方向和可用键盘改变而导致重启)。您可以在该属性中声明多个配置值,方法是用管道 |
字符分隔这些配置值。
注意:API 级别 13 或更高版本的应用时,若要避免由于设备方向改变而导致运行时重启,则除了 "orientation"
值以外,您还必须添加 "screenSize"
值。 也就是说,您必须声明 android:configChanges="orientation|screenSize"
。
当其中一个配置发生变化时,MyActivity
不会重启。相反,MyActivity
会收到对 onConfigurationChanged()
的调用。向此方法传递Configuration
对象指定新设备配置。您可以通过读取 Configuration
中的字段,确定新配置,然后通过更新界面中使用的资源进行适当的更改。
例如,以下 onConfigurationChanged()
实现检查当前设备方向:
1
2
3
4
5
6
7
8
9
10
11
|
@Override
public
void
onConfigurationChanged(Configuration newConfig) {
super
.onConfigurationChanged(newConfig);
// Checks the orientation of the screen
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();
}
}
|
示例代码:
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
|
public
class
ConfigChangesTestActivity
extends
ListActivity
{
private
static
final
String TAG =
"MainActivity"
;
private
ListAdapter mAdapter;
private
ArrayList<String> mDatas;
private
DialogFragment mLoadingDialog;
private
LoadDataAsyncTask mLoadDataAsyncTask;
@Override
public
void
onCreate(Bundle savedInstanceState)
{
super
.onCreate(savedInstanceState);
Log.e(TAG,
"onCreate"
);
initData(savedInstanceState);
}
/**
* 初始化数据
*/
private
void
initData(Bundle savedInstanceState)
{
mLoadingDialog =
new
LoadingDialog();
mLoadingDialog.show(getFragmentManager(),
"LoadingDialog"
);
mLoadDataAsyncTask =
new
LoadDataAsyncTask();
mLoadDataAsyncTask.execute();
}
/**
* 初始化适配器
*/
private
void
initAdapter()
{
mAdapter =
new
ArrayAdapter<String>(ConfigChangesTestActivity.
this
,
android.R.layout.simple_list_item_1, mDatas);
setListAdapter(mAdapter);
}
/**
* 模拟耗时操作
*
* @return
*/
private
ArrayList<String> generateTimeConsumingDatas()
{
try
{
Thread.sleep(
2000
);
}
catch
(InterruptedException e)
{
}
return
new
ArrayList<String>(Arrays.asList(
"通过Fragment保存大量数据"
,
"onSaveInstanceState保存数据"
,
"getLastNonConfigurationInstance已经被弃用"
,
"RabbitMQ"
,
"Hadoop"
,
"Spark"
));
}
/**
* 当配置发生变化时,不会重新启动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();
}
}
private
class
LoadDataAsyncTask
extends
AsyncTask<Void, Void, Void>
{
@Override
protected
Void doInBackground(Void... params)
{
mDatas = generateTimeConsumingDatas();
return
null
;
}
@Override
protected
void
onPostExecute(Void result)
{
mLoadingDialog.dismiss();
initAdapter();
}
}
@Override
protected
void
onDestroy()
{
Log.e(TAG,
"onDestroy"
);
super
.onDestroy();
}
}
|
这种方法使用简单,在回调方法onConfigurationChanged执行你的操作就可以,不会重启Activity。
但是自行处理配置变可能导致备用资源的使用更为困难,因此不到万不得已不用,大部分情况推荐用Fragment。
效果图:
五、旋转屏幕时如果任务线程未执行完,如何保存恢复
解决上边提到的问题,在未加载完毕的情况下,旋转屏幕,保存任务线程,重启activity不重新执行异步线程。那么他的难点就是保存任务线程,不销毁它,用上边三个方法都可以实现,推荐用1,2种方法。下边就主要介绍用第二种方法Fragment,那么你也肯定想到了,用Fragment保存AsyncTask就可以了。
代码如下:
1. OtherRetainedFragment
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
|
/**
* 保存对象的Fragment
* @author 超超boy
*
*/
public
class
OtherRetainedFragment
extends
Fragment
{
// data object we want to retain
// 保存一个异步的任务
private
MyAsyncTask data;
// this method is only called once for this fragment
@Override
public
void
onCreate(Bundle savedInstanceState)
{
super
.onCreate(savedInstanceState);
// retain this fragment
setRetainInstance(
true
);
}
public
void
setData(MyAsyncTask data)
{
this
.data = data;
}
public
MyAsyncTask getData()
{
return
data;
}
|
和之前差不多就是保存对象是MyAsyncTask。
2.MyAsyncTask:
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
|
public
class
MyAsyncTask
extends
AsyncTask<Void, Void, Void>
{
//保存外部activity的弱引用
private
WeakReference<Context> weakReference;
public
MyAsyncTask(Context context) {
weakReference =
new
WeakReference<>(context);
}
/**
* 是否完成
*/
private
boolean
isCompleted;
/**
* 进度框
*/
private
LoadingDialog mLoadingDialog;
private
List<String> items;
/**
* 开始时,显示加载框
*/
@Override
protected
void
onPreExecute()
{
mLoadingDialog =
new
LoadingDialog();
FixProblemsActivity activity = (FixProblemsActivity) weakReference.get();
if
(activity !=
null
)
mLoadingDialog.show(activity.getFragmentManager(),
"LOADING"
);
}
/**
* 加载数据
*/
@Override
protected
Void doInBackground(Void... params)
{
items = loadingData();
return
null
;
}
/**
* 加载完成回调当前的Activity
*/
@Override
protected
void
onPostExecute(Void unused)
{
isCompleted =
true
;
notifyActivityTaskCompleted();
if
(mLoadingDialog !=
null
)
mLoadingDialog.dismiss();
}
public
List<String> getItems()
{
return
items;
}
private
List<String> loadingData()
{
try
{
Thread.sleep(
5000
);
}
catch
(InterruptedException e)
{
}
return
new
ArrayList<String>(Arrays.asList(
"通过Fragment保存大量数据"
,
"onSaveInstanceState保存数据"
,
"getLastNonConfigurationInstance已经被弃用"
,
"RabbitMQ"
,
"Hadoop"
,
"Spark"
));
}
/**
* 设置Activity,因为Activity会一直变化
*
* @param activity
*/
public
void
setActivity(Context activity)
{
if
(activity ==
null
){
mLoadingDialog.dismiss();
return
;
}
weakReference =
new
WeakReference<>(activity);
// 设置为当前的Activity
FixProblemsActivity fActivity = (FixProblemsActivity) weakReference.get();
// 开启一个与当前Activity绑定的等待框
if
(activity !=
null
&& !isCompleted)
{
mLoadingDialog =
new
LoadingDialog();
mLoadingDialog.show(fActivity.getFragmentManager(),
"LOADING"
);
}
// 如果完成,通知Activity
if
(isCompleted)
{
notifyActivityTaskCompleted();
}
}
/**
* 在Activity不可见时,关闭dialog
*/
public
void
dialogDismiss(){
if
(mLoadingDialog !=
null
){
mLoadingDialog.dismiss();
}
}
private
void
notifyActivityTaskCompleted()
{
if
(
null
!= weakReference.get())
{
((FixProblemsActivity) weakReference.get()).onTaskCompleted();
}
}
}
|
主要在onCreate方法中执行一些逻辑判断,如果没有开启任务(第一次进入),开启任务;如果已经开启了,调用setActivity(this);
在onPause中关闭dialog,防止内存泄漏。
设置了等待5秒,足够旋转三四个来回了~~~~可以看到虽然在不断的重启,但是丝毫不影响任务的运行和加载框的显示~~~~
效果图:
Activity之概览屏幕(Overview Screen)
概览屏幕
概览屏幕(也称为最新动态屏幕、最近任务列表或最近使用的应用)是一个系统级别 UI,其中列出了最近访问过的 Activity 和任务。 用户可以浏览该列表并选择要恢复的任务,也可以通过滑动清除任务将其从列表中移除。 对于 Android 5.0 版本(API 级别 21),包含不同文档的同一 Activity 的多个实例可能会以任务的形式显示在概览屏幕中。前边是文档中说的,比较虚,下面来一个gif你就懂啦。
可以看到左边的任务视图是两个重叠在一块的,这也就是概览屏幕。
通常,您应该允许系统定义任务和 Activity 在概览屏幕中的显示方法,并且无需修改此行为。不过,应用可以确定 Activity 在概览屏幕中的显示方式和时间。 您可以使用 ActivityManager.AppTask
类来管理任务,使用 Intent
类的 Activity 标志来指定某 Activity 添加到概览屏幕或从中移除的时间。 此外,您也可以使用 <activity>
属性在清单文件中设置该行为。
将任务添加到概览屏幕
通过使用 Intent
类的标志添加任务,您可以更好地控制某文档在概览屏幕中打开或重新打开的时间和方式。 使用 <activity>
属性时,您可以选择始终在新任务中打开文档,或选择对文档重复使用现有任务。
使用 Intent 标志添加任务
为 Activity 创建新文档时,可调用 ActivityManager.AppTask
类的 startActivity()
方法,以向其传递启动 Activity 的 Intent。 要插入逻辑换行符以便系统将 Activity 视为新任务显示在概览屏幕中,可在启动 Activity 的 Intent
的 addFlags()
方法中传递 FLAG_ACTIVITY_NEW_DOCUMENT
标志。
注:FLAG_ACTIVITY_NEW_DOCUMENT
标志取代了 FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET
标志,后者自 Android 5.0(API 级别 21)起已弃用。
如果在创建新文档时设置 FLAG_ACTIVITY_MULTIPLE_TASK
标志,则系统始终会以目标 Activity 作为根创建新任务。此设置允许同一文档在多个任务中打开。以下代码演示了主 Activity 如何执行此操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public
void
createNewDocument(View view) {
final
Intent newDocumentIntent = newDocumentIntent();
if
(useMultipleTasks) {
newDocumentIntent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
}
startActivity(newDocumentIntent);
}
private
Intent newDocumentIntent() {
boolean
useMultipleTasks = mCheckbox.isChecked();
final
Intent newDocumentIntent =
new
Intent(
this
, NewDocumentActivity.
class
);
newDocumentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
newDocumentIntent.putExtra(KEY_EXTRA_NEW_DOCUMENT_COUNTER, incrementAndGet());
return
newDocumentIntent;
}
private
static
int
incrementAndGet() {
Log.d(TAG,
"incrementAndGet(): "
+ mDocumentCounter);
return
mDocumentCounter++;
}
}
|
注:使用 FLAG_ACTIVITY_NEW_DOCUMENT
标志启动的 Activity 必须具有在清单文件中设置的 android:launchMode="standard"
属性值(默认)。
当主 Activity 启动新 Activity 时,系统会搜遍现有任务,看看是否有任务的 Intent 与 Activity 的 Intent 组件名称和 Intent 数据相匹配。 如果未找到任务或者 Intent 包含 FLAG_ACTIVITY_MULTIPLE_TASK
标志,则会以该 Activity 作为其根创建新任务。如果找到的话,则会将该任务转到前台并将新 Intent 传递给 onNewIntent()
。新 Activity 将获得 Intent 并在概览屏幕中创建新文档,如下例所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@Override
protected
void
onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState);
setContentView(R.layout.activity_new_document);
mDocumentCount = getIntent()
.getIntExtra(DocumentCentricActivity.KEY_EXTRA_NEW_DOCUMENT_COUNTER,
0
);
mDocumentCounterTextView = (TextView) findViewById(
R.id.hello_new_document_text_view);
setDocumentCounterText(R.string.hello_new_document_counter);
}
@Override
protected
void
onNewIntent(Intent intent) {
super
.onNewIntent(intent);
/* If FLAG_ACTIVITY_MULTIPLE_TASK has not been used, this activity
is reused to create a new document.
*/
setDocumentCounterText(R.string.reusing_document_counter);
}
|
使用 Activity 属性添加任务
此外,Activity 还可以在其清单文件中指定始终通过使用 <activity>
属性 android:documentLaunchMode
进入新任务。 此属性有四个值,会在用户使用该应用打开文档时产生以下效果:
-
"
-
该 Activity 会对文档重复使用现有任务。这与
不设置
FLAG_ACTIVITY_MULTIPLE_TASK
标志、但设置FLAG_ACTIVITY_NEW_DOCUMENT
标志所产生的效果相同,如上文的 使用 Intent 标志添加任务中所述。
"
-
该 Activity 为文档创建新任务,即便文档已打开也是如此。使用此值与同时设置
FLAG_ACTIVITY_NEW_DOCUMENT
和FLAG_ACTIVITY_MULTIPLE_TASK
标志所产生的效果相同。
"
- 该 Activity 不会为文档创建新任务。概览屏幕将按其默认方式对待此 Activity:为应用显示单个任务,该任务将从用户上次调用的任意 Activity 开始继续执行。 "
-
该 Activity 不会为文档创建新任务。设置此值会替代
FLAG_ACTIVITY_NEW_DOCUMENT
和FLAG_ACTIVITY_MULTIPLE_TASK
标志的行为(如果在 Intent 中设置了其中一个标志),并且概览屏幕将为应用显示单个任务,该任务将从用户上次调用的任意 Activity 开始继续执行。
intoExisting
"
always
"
none”
"
never
"
注:对于除 none
和 never
以外的值,必须使用 launchMode="standard"
定义 Activity。如果未指定此属性,则使用 documentLaunchMode="none"
。
移除任务
默认情况下,在 Activity 结束后,文档任务会从概览屏幕中自动移除。 您可以使用 ActivityManager.AppTask
类、Intent
标志或 <activity>
属性替代此行为。
通过将 <activity>
属性 android:excludeFromRecents
设置为 true
,您可以始终将任务从概览屏幕中完全排除。
您可以通过将 <activity>
属性 android:maxRecents
设置为整型值,设置应用能够包括在概览屏幕中的最大任务数。默认值为 16。达到最大任务数后,最近最少使用的任务将从概览屏幕中移除。 android:maxRecents
的最大值为 50(内存不足的设备上为 25);小于 1 的值无效。
使用 AppTask 类移除任务
在于概览屏幕创建新任务的 Activity 中,您可以通过调用 finishAndRemoveTask()
方法指定何时移除该任务以及结束所有与之相关的 Activity。
1
2
3
4
5
6
7
|
private
Intent newDocumentIntent() {
final
Intent newDocumentIntent =
new
Intent(
this
, NewDocumentActivity.
class
);
newDocumentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT |
android.content.Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS);
newDocumentIntent.putExtra(KEY_EXTRA_NEW_DOCUMENT_COUNTER, incrementAndGet());
return
newDocumentIntent;
}
|
要达到同样的效果,请将 <activity>
属性 android:autoRemoveFromRecents
设置为 false
。文档 Activity 的默认值为 true
,常规 Activity 的默认值为 false
。如前所述,使用此属性替代 FLAG_ACTIVITY_RETAIN_IN_RECENTS
标志。
demo的代码
1.DocumentCentricActivity.java:
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
|
package
com.example.documentcentricapps;
import
android.app.Activity;
import
android.content.Intent;
import
android.os.Build;
import
android.os.Bundle;
import
android.os.PersistableBundle;
import
android.util.Log;
import
android.view.View;
import
android.widget.CheckBox;
public
class
DocumentCentricActivity
extends
Activity {
private
final
static
String TAG =
"DocumentCentricActivity"
;
public
final
static
String KEY_EXTRA_NEW_DOCUMENT_COUNTER =
"KEY_EXTRA_NEW_DOCUMENT_COUNTER"
;
private
static
int
mDocumentCounter =
0
;
private
CheckBox mCheckbox;
@Override
protected
void
onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState);
setContentView(R.layout.activity_document_centric_main);
mCheckbox = (CheckBox) findViewById(R.id.multiple_task_checkbox);
}
@Override
public
void
onPostCreate(Bundle savedInstanceState, PersistableBundle persistentState) {
super
.onPostCreate(savedInstanceState, persistentState);
// Restore state from PersistableBundle
if
(persistentState !=
null
) {
if
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mDocumentCounter = persistentState.getInt(KEY_EXTRA_NEW_DOCUMENT_COUNTER);
}
}
}
@Override
public
void
onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) {
/*
To maintain activity state across reboots the system saves and restore critical information for
all tasks and their activities. Information known by the system includes the activity stack order,
each task’s thumbnails and each activity’s and task's Intents. For Information that cannot be retained
because they contain Bundles which can’t be persisted a new constrained version of Bundle,
PersistableBundle is added. PersistableBundle can store only basic data types. To use it
in your Activities you must declare the new activity:persistableMode attribute in the manifest.
*/
if
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
outPersistentState.putInt(KEY_EXTRA_NEW_DOCUMENT_COUNTER, mDocumentCounter);
}
super
.onSaveInstanceState(outState, outPersistentState);
}
public
void
createNewDocument(View view) {
boolean
useMultipleTasks = mCheckbox.isChecked();
final
Intent newDocumentIntent = newDocumentIntent();
if
(useMultipleTasks) {
/*
When {@linkIntent#FLAG_ACTIVITY_NEW_DOCUMENT} is used with {@link Intent#FLAG_ACTIVITY_MULTIPLE_TASK}
the system will always create a new task with the target activity as the root. This allows the same
document to be opened in more than one task.
*/
newDocumentIntent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
}
startActivity(newDocumentIntent);
}
/**
* Returns an new {@link Intent} to start {@link NewDocumentActivity} as a new document in
* overview menu.
*
* To start a new document task {@link Intent#FLAG_ACTIVITY_NEW_DOCUMENT} must be used. The
* system will search through existing tasks for one whose Intent matches the Intent component
* name and the Intent data. If it finds one then that task will be brought to the front and the
* new Intent will be passed to onNewIntent().
*
* Activities launched with the NEW_DOCUMENT flag must be created with launchMode="standard".
*/
private
Intent newDocumentIntent() {
final
Intent newDocumentIntent =
new
Intent(
this
, NewDocumentActivity.
class
);
newDocumentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
newDocumentIntent.putExtra(KEY_EXTRA_NEW_DOCUMENT_COUNTER, incrementAndGet());
return
newDocumentIntent;
}
private
static
int
incrementAndGet() {
Log.d(TAG,
"incrementAndGet(): "
+ mDocumentCounter);
return
mDocumentCounter++;
}
}
|
2.NewDocumentActivity.java:
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
|
public
class
NewDocumentActivity
extends
Activity {
private
TextView mDocumentCounterTextView;
private
int
mDocumentCount;
@Override
protected
void
onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState);
setContentView(R.layout.activity_new_document);
mDocumentCount = getIntent()
.getIntExtra(DocumentCentricActivity.KEY_EXTRA_NEW_DOCUMENT_COUNTER,
0
);
mDocumentCounterTextView = (TextView) findViewById(
R.id.hello_new_document_text_view);
setDocumentCounterText(R.string.hello_new_document_counter);
}
@Override
protected
void
onNewIntent(Intent intent) {
super
.onNewIntent(intent);
/* If {@link Intent#FLAG_ACTIVITY_MULTIPLE_TASK} has not been used this Activity
will be reused.
*/
setDocumentCounterText(R.string.reusing_document_counter);
}
public
void
onRemoveFromOverview(View view) {
// It is good pratice to remove a document from the overview stack if not needed anymore.
if
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
finishAndRemoveTask();
}
}
public
void
setDocumentCounterText(
int
resId) {
mDocumentCounterTextView
.setText(String.format(getString(resId), String.valueOf(mDocumentCount)));
}
}
|
上边只是主要的代码,完整的demo下载:http://download.csdn.net/detail/jycboy/9715602
本文转载自:http://www.cnblogs.com/jycboy/p/overview_screen.html 超超boy