我的程序在后台线程中执行一些网络活动。在开始之前,它会弹出一个进度对话框。该对话框在处理程序上被解除。这一切都很好,除了当对话框打开时屏幕方向发生变化(背景线程正在运行)。此时,应用程序要么崩溃,要么死锁,要么进入一个奇怪的阶段,在所有线程被杀死之前,应用程序根本无法工作。
我如何处理屏幕方向的变化优雅?
下面的示例代码大致匹配我的实际程序:
public class MyAct extends Activity implements Runnable {
public ProgressDialog mProgress;
// UI has a button that when pressed calls send
public void send() {
mProgress = ProgressDialog.show(this, "Please wait",
"Please wait",
true, true);
Thread thread = new Thread(this);
thread.start();
}
public void run() {
Thread.sleep(10000);
Message msg = new Message();
mHandler.sendMessage(msg);
}
private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
mProgress.dismiss();
}
};
}
栈:
E/WindowManager( 244): Activity MyAct has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView@433b7150 that was originally added here
E/WindowManager( 244): android.view.WindowLeaked: Activity MyAct has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView@433b7150 that was originally added here
E/WindowManager( 244): at android.view.ViewRoot.<init>(ViewRoot.java:178)
E/WindowManager( 244): at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:147)
E/WindowManager( 244): at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:90)
E/WindowManager( 244): at android.view.Window$LocalWindowManager.addView(Window.java:393)
E/WindowManager( 244): at android.app.Dialog.show(Dialog.java:212)
E/WindowManager( 244): at android.app.ProgressDialog.show(ProgressDialog.java:103)
E/WindowManager( 244): at android.app.ProgressDialog.show(ProgressDialog.java:91)
E/WindowManager( 244): at MyAct.send(MyAct.java:294)
E/WindowManager( 244): at MyAct$4.onClick(MyAct.java:174)
E/WindowManager( 244): at android.view.View.performClick(View.java:2129)
E/WindowManager( 244): at android.view.View.onTouchEvent(View.java:3543)
E/WindowManager( 244): at android.widget.TextView.onTouchEvent(TextView.java:4664)
E/WindowManager( 244): at android.view.View.dispatchTouchEvent(View.java:3198)
我已经尝试在onSaveInstanceState中取消进度对话框,但这只是防止了立即崩溃。背景线程仍在运行,UI处于部分绘制状态。需要在它重新开始工作之前杀死整个应用程序。
我尝试执行jfelectron的解决方案,因为它是“解决这些问题的坚如磐石的解决方案,符合‘Android方式’”,但我花了一些时间来查找并整合所有提到的元素。最终得到了这个略有不同的,我认为更优雅的解决方案,完整地贴在这里。
使用从活动中触发的IntentService在单独的线程上执行长时间运行的任务。该服务向更新对话框的活动发出sticky Broadcast intent。Activity使用showDialog(), onCreateDialog()和onPrepareDialog()来消除在应用程序对象或savedInstanceState包中传递持久化数据的需要。无论应用程序如何中断,这都应该工作。
活动类:
public class TesterActivity extends Activity {
private ProgressDialog mProgressDialog;
private static final int PROGRESS_DIALOG = 0;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
Button b = (Button) this.findViewById(R.id.test_button);
b.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
buttonClick();
}
});
}
private void buttonClick(){
clearPriorBroadcast();
showDialog(PROGRESS_DIALOG);
Intent svc = new Intent(this, MyService.class);
startService(svc);
}
protected Dialog onCreateDialog(int id) {
switch(id) {
case PROGRESS_DIALOG:
mProgressDialog = new ProgressDialog(TesterActivity.this);
mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
mProgressDialog.setMax(MyService.MAX_COUNTER);
mProgressDialog.setMessage("Processing...");
return mProgressDialog;
default:
return null;
}
}
@Override
protected void onPrepareDialog(int id, Dialog dialog) {
switch(id) {
case PROGRESS_DIALOG:
// setup a broadcast receiver to receive update events from the long running process
IntentFilter filter = new IntentFilter();
filter.addAction(MyService.BG_PROCESS_INTENT);
registerReceiver(new MyBroadcastReceiver(), filter);
break;
}
}
public class MyBroadcastReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {
if (intent.hasExtra(MyService.KEY_COUNTER)){
int count = intent.getIntExtra(MyService.KEY_COUNTER, 0);
mProgressDialog.setProgress(count);
if (count >= MyService.MAX_COUNTER){
dismissDialog(PROGRESS_DIALOG);
}
}
}
}
/*
* Sticky broadcasts persist and any prior broadcast will trigger in the
* broadcast receiver as soon as it is registered.
* To clear any prior broadcast this code sends a blank broadcast to clear
* the last sticky broadcast.
* This broadcast has no extras it will be ignored in the broadcast receiver
* setup in onPrepareDialog()
*/
private void clearPriorBroadcast(){
Intent broadcastIntent = new Intent();
broadcastIntent.setAction(MyService.BG_PROCESS_INTENT);
sendStickyBroadcast(broadcastIntent);
}}
IntentService经济舱:
public class MyService extends IntentService {
public static final String BG_PROCESS_INTENT = "com.mindspiker.Tester.MyService.TEST";
public static final String KEY_COUNTER = "counter";
public static final int MAX_COUNTER = 100;
public MyService() {
super("");
}
@Override
protected void onHandleIntent(Intent intent) {
for (int i = 0; i <= MAX_COUNTER; i++) {
Log.e("Service Example", " " + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
Intent broadcastIntent = new Intent();
broadcastIntent.setAction(BG_PROCESS_INTENT);
broadcastIntent.putExtra(KEY_COUNTER, i);
sendStickyBroadcast(broadcastIntent);
}
}}
清单文件条目:
应用前部分:
uses-permission android:name="com.mindspiker.Tester.MyService.TEST"
uses-permission android:name="android.permission.BROADCAST_STICKY"
应用程序内部部分
service android:name=".MyService"
If you're struggling with detecting orientation change events of a dialog INDEPENDENT OF AN ACTIVITY REFERENCE, this method works excitingly well. I use this because I have my own dialog class that can be shown in multiple different Activities so I don't always know which Activity it's being shown in. With this method you don't need to change the AndroidManifest, worry about Activity references, and you don't need a custom dialog (as I have). You do need, however, a custom content view so you can detect the orientation changes using that particular view. Here's my example:
设置
public class MyContentView extends View{
public MyContentView(Context context){
super(context);
}
@Override
public void onConfigurationChanged(Configuration newConfig){
super.onConfigurationChanged(newConfig);
//DO SOMETHING HERE!! :D
}
}
实现1 -对话
Dialog dialog = new Dialog(context);
//set up dialog
dialog.setContentView(new MyContentView(context));
dialog.show();
实现2 - AlertDialog。构建器
AlertDialog.Builder builder = new AlertDialog.Builder(context);
//set up dialog builder
builder.setView(new MyContentView(context)); //Can use this method
builder.setCustomTitle(new MycontentView(context)); // or this method
builder.build().show();
实现3 - ProgressDialog / AlertDialog
ProgressDialog progress = new ProgressDialog(context);
//set up progress dialog
progress.setView(new MyContentView(context)); //Can use this method
progress.setCustomTitle(new MyContentView(context)); // or this method
progress.show();
我的解决方案是扩展ProgressDialog类来获得我自己的MyProgressDialog。
我重新定义了show()和dismiss()方法来在显示对话框之前锁定方向,并在对话框被解散时将其解锁。
因此,当显示对话框并且设备的方向发生变化时,屏幕的方向保持不变,直到调用dismiss(),然后屏幕方向根据传感器值/设备方向发生变化。
这是我的代码:
public class MyProgressDialog extends ProgressDialog {
private Context mContext;
public MyProgressDialog(Context context) {
super(context);
mContext = context;
}
public MyProgressDialog(Context context, int theme) {
super(context, theme);
mContext = context;
}
public void show() {
if (mContext.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT)
((Activity) mContext).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
else
((Activity) mContext).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
super.show();
}
public void dismiss() {
super.dismiss();
((Activity) mContext).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
}
}
我有一个实现,允许活动在屏幕方向改变上被销毁,但仍然在重新创建的活动中成功地销毁对话框。
我用…NonConfigurationInstance将后台任务附加到重新创建的活动。
正常的Android框架处理重新创建对话框本身,没有任何改变。
我子类化了AsyncTask,为“拥有”活动添加了一个字段,以及一个更新这个所有者的方法。
class MyBackgroundTask extends AsyncTask<...> {
MyBackgroundTask (Activity a, ...) {
super();
this.ownerActivity = a;
}
public void attach(Activity a) {
ownerActivity = a;
}
protected void onPostExecute(Integer result) {
super.onPostExecute(result);
ownerActivity.dismissDialog(DIALOG_PROGRESS);
}
...
}
在我的活动类中,我添加了一个字段backgroundTask引用'owned' backgroundTask,我使用onRetainNonConfigurationInstance和getLastNonConfigurationInstance更新这个字段。
class MyActivity extends Activity {
public void onCreate(Bundle savedInstanceState) {
...
if (getLastNonConfigurationInstance() != null) {
backgroundTask = (MyBackgroundTask) getLastNonConfigurationInstance();
backgroundTask.attach(this);
}
}
void startBackgroundTask() {
backgroundTask = new MyBackgroundTask(this, ...);
showDialog(DIALOG_PROGRESS);
backgroundTask.execute(...);
}
public Object onRetainNonConfigurationInstance() {
if (backgroundTask != null && backgroundTask.getStatus() != Status.FINISHED)
return backgroundTask;
return null;
}
...
}
进一步改进建议:
在任务完成后,清除活动中的backgroundTask引用以释放与之关联的所有内存或其他资源。
在活动被销毁之前,清除后台任务中的ownerActivity引用,以防它不会立即被重新创建。
创建一个BackgroundTask接口和/或集合,以允许不同类型的任务从相同的活动中运行。