android-activity - 进度对话框和后台线程时, 如何处理屏幕方向更改是否有效?

  显示原文与译文双语对照的内容

我的程序在后台线程中执行一些网络 Activity 。 开始之前,它会弹出一个进度对话框。 对话框上的对话框被关闭。 这一切都正常,除非屏幕方向改变,而对话框是( 后台线程正在运行) 。 此时应用要么崩溃,要么死锁,要么进入一个奇怪的阶段,应用程序根本无法工作,直到所有线程都被杀死。

如何正确处理屏幕方向变化?

下面的示例代码大致符合我的实际程序:


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中关闭进度对话框,但这只是防止立即崩溃。 后台线程仍在运行,并且用户界面处于部分绘制状态。 需要先杀死整个应用程序,然后才能再次运行。

时间:

编辑: 谷歌工程师不推荐这种方法,因为由Dianne描述 Hackborn ( a 。k 。a 。 在这种 StackOverflow发布, hackbod ). 于更多information,检出相关 这个博客帖子.


你必须将它添加到清单中的Activity 声明中:


android:configChanges="orientation|screenSize"

所以看起来


<activity android:label="@string/app_name" 
 android:configChanges="orientation|screenSize|keyboardHidden" 
 android:name=".your.package">

问题是当配置发生改变时,系统会破坏 Activity 。 参见 ConfigurationChanges web 。

所以将它放在配置文件中避免了系统破坏你的Activity 。 相反,它调用 onConfigurationChanged(Configuration) 方法。

我提出了一个rock-solid解决方案,这些问题符合'安卓方式'的。 我使用IntentService模式拥有所有长时间运行的操作。
这就是我的活动广播意图,IntentService做这项工作,把数据保存在数据库中,然后广播 STICKY 。
粘胶的部分是很重要的,这样,即使系统中的作业暂停 Activity 后期间所累积的用户发起工作期间,错过了这实时广播从ItentService我们仍然能够响应和拿起从调用 Activity的数据。
ProgressDialogs 可以很好地使用 onSaveInstanceSate()
基本上,你需要保存一个在已经保存的实例包中运行的进度对话框。 不保存进度对话框对象,因为这会泄漏整个 Activity 。
要对进度对话框有一个持久的句柄,我将它作为一个Weak-reference存储在应用程序对象中。
在方向更改或者任何导致 Activity 暂停( 电话,用户点击主页等) 然后继续的事情上,我关闭旧对话框并在新创建的Activity 中重新创建一个新对话框。
对于不确定的进度对话框,这很容易。 对于进度栏样式,你必须将最后已知的进度放在包中,以及你在 Activity 本地使用的任何信息来跟踪进度。
在恢复进度时,你将使用这里信息来将进度栏与之前的状态相同,然后基于当前状态进行更新。
小结一下,放长时间运行的任务变成一个IntentService加上 juidcious onSaveInstanceState() 使你可以有效地跟踪对话框和还原然后穿过 Activity的生命周期事件的使用。 Activity 代码的相关位如下。 你还需要你的BroadcastReceiver中的逻辑来适当地处理棘手的意图,但这超出了这个范围。


 public void doSignIn(View view) {
 waiting=true;
 AppClass app=(AppClass) getApplication();
 String logingon=getString(R.string.signon);
 app.Dialog=new WeakReference<ProgressDialog>(ProgressDialog.show(AddAccount.this,"", logingon, true));
..}


@Override
 protected void onSaveInstanceState(Bundle saveState) {
 super.onSaveInstanceState(saveState);
 saveState.putBoolean("waiting",waiting);
 }

 @Override
 public void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 if(savedInstanceState!=null) {
 restoreProgress(savedInstanceState);

 }
...}


private void restoreProgress(Bundle savedInstanceState) {
 waiting=savedInstanceState.getBoolean("waiting");
 if (waiting) {
 AppClass app=(AppClass) getApplication();
 ProgressDialog refresher=(ProgressDialog) app.Dialog.get();
 refresher.dismiss();
 String logingon=getString(R.string.signon);
 app.Dialog=new WeakReference<ProgressDialog>(ProgressDialog.show(AddAccount.this,"", logingon, true));



 }

 }

我遇到了同样的问题。 我的Activity 需要解析一个URL中的一些数据,而且速度很慢。 所以我创建了一个线程,然后显示一个 progressdialog 。 当线程完成时,我让线程通过处理程序将一个msg投递回UI线程。 在处理程序handleMessage中,从线程获取数据对象( 立即准备就绪) 并将它的填充到用户界面。 所以它与你的例子非常相似。

经过多次尝试和错误后,我似乎找到了一个解决方案。 至少现在我可以在线程完成之前或者之后旋转屏幕。 在所有测试中,对话框被正确关闭,所有的行为都是一样的。

我所做的如下所示。 目标是填充我的数据模型( mDataObject ),然后将它的填充到用户界面。 在任何时候都应该允许屏幕旋转。


class MyActivity {

private MyDataObject mDataObject = null;
private static MyThread mParserThread = null;//static, or make it singleton

OnCreate() {
. . .
 Object retained = this.getLastNonConfigurationInstance();
 if(retained!= null) {
//data is already completely obtained before config change
//by my previous self.
//no need to create thread or show dialog at all
 mDataObject = (MyDataObject) retained;
 populateUI();
 } else if(mParserThread!= null && mParserThread.isAlive()){
//note: mParserThread is a static member or singleton object.
//config changed during parsing in previous instance. swap handler
//then wait for it to finish.
 mParserThread.setHandler(new MyHandler());
 } else {
//no data and no thread. likely initial run
//create thread, show dialog
 mParserThread = new MyThread(..., new MyHandler());
 mParserThread.start();
 showDialog(DIALOG_PROGRESS);
 }
}

//http://android-developers.blogspot.com/2009/02/faster-screen-orientation-change.html
public Object onRetainNonConfigurationInstance() {
//my future self can get this without re-downloading
//if it's already ready.
 return mDataObject;
}

//use Activity.showDialog instead of ProgressDialog.show
//so the dialog can be automatically managed across config change
@Override
protected Dialog onCreateDialog(int id) {
//show progress dialog here
}

//inner class of MyActivity
private class MyHandler extends Handler {
 public void handleMessage(msg) {
 mDataObject = mParserThread.getDataObject();
 populateUI();
 dismissDialog(DIALOG_PROGRESS);
 }
}
}

class MyThread extends Thread {
 Handler mHandler;
 MyDataObject mDataObject;

 public MyHandler(..., Handler h) {...; mHandler = h;}//constructor with handler param
 public void setHandler(Handler h) { mHandler = h; }//for handler swapping after config change
 public MyDataObject getDataObject() { return mDataObject; }//return data object (completed) to caller
 public void run() {
 mDataObject = new MyDataObject();
//do the lengthy task to fill mDataObject with data
 lengthyTask(mDataObject);
//done. notify activity
 mHandler.sendEmptyMessage(0);//tell activity: i'm ready. come pick up the data.
 }
}

这就是我的工作。 我不知道这是否是"正确"--所设计的方法他们声称这个"在屏幕旋转过程中销毁/重新创建 Activity"让事情变得更加简单,所以我猜它应该不会太复杂。

如果我的代码中有问题,请告诉我。 就像上面所说我并不真正知道是否有副作用。

我使用ProgressDialog和得到更快results,我面临了同样的问题,我想出了一个解决方案,并没有

我所做的是创建一个布局,其中包含一个 ProgressBar 。


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<ProgressBar
 android:id="@+id/progressImage"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:layout_centerInParent="true"
/>
</RelativeLayout>

然后在onCreate方法中执行以下操作


public void onCreate(Bundle icicle) {
 super.onCreate(icicle);
 setContentView(R.layout.progress);
}

然后在一个线程中执行长任务,当完成时,让一个Runnable将内容视图设置为你想要用于这里 Activity的真实布局。

例如:


mHandler.post(new Runnable(){

public void run() {
 setContentView(R.layout.my_layout);
 } 
});

这就是我所做的,我发现它比显示ProgressDialog运行速度快,而且在我看来,它的侵入性更小。

但是,如果你想使用 ProgressDialog,那么这个答案不是针对你的。

我的解决方案是扩展 ProgressDialog 类以获得我自己的MyProgressDialog
我重新定义了 show()dismiss() 方法来在显示 Dialog 之前锁定方向,并在 Dialog 关闭时将它的解锁。 所以当 Dialog 仿真来展示设备的方向改变时,屏幕的显示方向会一直存在,直到调用 dismiss() 时,然后根据 sensor-values/device-orientation screen-orientation更改。

这是我的代码:


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);
}

}

我将贡献我的方法来处理这个旋转问题。 这可能与OP不相关,因为他没有使用 AsyncTask,但也许其他人会发现它有用。 这很简单,但它似乎为我做了工作:

我有一个登录 Activity,它有一个嵌套的AsyncTask 类,名为 BackgroundLoginTask

在我的BackgroundLoginTask 中,除了在调用 ProgressDialog 取消时添加空检查外,我不做任何普通的检查:


@Override
protected void onPostExecute(Boolean result)
{ 
if (pleaseWaitDialog!= null)
 pleaseWaitDialog.dismiss();
[...]
}

这是处理后台任务在 Activity 不可见时完成的情况,因此,进程对话框已经被 onPause() 方法取消。

接下来,在我的父 Activity 类中,我创建了 AsyncTask 类和 ProgressDialog ( 嵌套的AsyncTask 可以访问这些变量)的全局静态句柄:


private static BackgroundLoginTask backgroundLoginTask;
private static ProgressDialog pleaseWaitDialog;

在一个新的,post-rotated这有两种用途:even. AsyncTask 首先,它使得我的Activity 一般能访问的对象 第二,它允许我的BackgroundLoginTask 在旋转后访问和关闭 ProgressDialog

接下来,我将这个添加到 onPause() 中,使得当我们的Activity 离开前台( 阻止那些丑陋的"强制关闭"crash ) 时,进度对话框消失:


 if (pleaseWaitDialog!= null)
 pleaseWaitDialog.dismiss();

最后,在我的onResume() 方法中,我有以下内容:


if ((backgroundLoginTask!= null) && (backgroundLoginTask.getStatus() == Status.RUNNING))
 {
 if (pleaseWaitDialog!= null)
 pleaseWaitDialog.show();
 }

这允许 Dialog 在重新创建 Activity 后重新出现。

下面是整个类:


public class NSFkioskLoginActivity extends NSFkioskBaseActivity {
 private static BackgroundLoginTask backgroundLoginTask;
 private static ProgressDialog pleaseWaitDialog;
 private Controller cont;

//This is the app entry point.
/** Called when the activity is first created. */
 @Override
 public void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);

 if (CredentialsAvailableAndValidated())
 {
//Go to main menu and don't run rest of onCreate method.
 gotoMainMenu();
 return;
 }
 setContentView(R.layout.login);
 populateStoredCredentials(); 
 }

//Save current progress to options when app is leaving foreground
 @Override
 public void onPause()
 {
 super.onPause();
 saveCredentialsToPreferences(false);
//Get rid of progress dialog in the event of a screen rotation. Prevents a crash.
 if (pleaseWaitDialog!= null)
 pleaseWaitDialog.dismiss();
 }

 @Override
 public void onResume()
 {
 super.onResume();
 if ((backgroundLoginTask!= null) && (backgroundLoginTask.getStatus() == Status.RUNNING))
 {
 if (pleaseWaitDialog!= null)
 pleaseWaitDialog.show();
 }
 }

/**
 * Go to main menu, finishing this activity
 */
 private void gotoMainMenu()
 {
 startActivity(new Intent(getApplicationContext(), NSFkioskMainMenuActivity.class));
 finish();
 }

/**
 * 
 * @param setValidatedBooleanTrue If set true, method will set CREDS_HAVE_BEEN_VALIDATED to true in addition to saving username/password.
 */
 private void saveCredentialsToPreferences(boolean setValidatedBooleanTrue)
 {
 SharedPreferences settings = getSharedPreferences(APP_PREFERENCES, MODE_PRIVATE);
 SharedPreferences.Editor prefEditor = settings.edit();
 EditText usernameText = (EditText) findViewById(R.id.editTextUsername);
 EditText pswText = (EditText) findViewById(R.id.editTextPassword);
 prefEditor.putString(USERNAME, usernameText.getText().toString());
 prefEditor.putString(PASSWORD, pswText.getText().toString());
 if (setValidatedBooleanTrue)
 prefEditor.putBoolean(CREDS_HAVE_BEEN_VALIDATED, true);
 prefEditor.commit();
 }

/**
 * Checks if user is already signed in
 */
 private boolean CredentialsAvailableAndValidated() {
 SharedPreferences settings = getSharedPreferences(APP_PREFERENCES,
 MODE_PRIVATE);
 if (settings.contains(USERNAME) && settings.contains(PASSWORD) && settings.getBoolean(CREDS_HAVE_BEEN_VALIDATED, false) == true)
 return true; 
 else
 return false;
 }

//Populate stored credentials, if any available
 private void populateStoredCredentials()
 {
 SharedPreferences settings = getSharedPreferences(APP_PREFERENCES,
 MODE_PRIVATE);
 settings.getString(USERNAME,"");
 EditText usernameText = (EditText) findViewById(R.id.editTextUsername);
 usernameText.setText(settings.getString(USERNAME,""));
 EditText pswText = (EditText) findViewById(R.id.editTextPassword);
 pswText.setText(settings.getString(PASSWORD,""));
 }

/**
 * Validate credentials in a seperate thread, displaying a progress circle in the meantime
 * If successful, save credentials in preferences and proceed to main menu activity
 * If not, display an error message
 */
 public void loginButtonClick(View view)
 {
 if (phoneIsOnline())
 {
 EditText usernameText = (EditText) findViewById(R.id.editTextUsername);
 EditText pswText = (EditText) findViewById(R.id.editTextPassword);
//Call background task worker with username and password params
 backgroundLoginTask = new BackgroundLoginTask();
 backgroundLoginTask.execute(usernameText.getText().toString(), pswText.getText().toString());
 }
 else
 {
//Display toast informing of no internet access
 String notOnlineMessage = getResources().getString(R.string.noNetworkAccessAvailable);
 Toast toast = Toast.makeText(getApplicationContext(), notOnlineMessage, Toast.LENGTH_SHORT);
 toast.show();
 }
 }

/**
 * 
 * Takes two params: username and password
 *
 */
 public class BackgroundLoginTask extends AsyncTask<Object, String, Boolean>
 { 
 private Exception e = null;

 @Override
 protected void onPreExecute()
 {
 cont = Controller.getInstance();
//Show progress dialog
 String pleaseWait = getResources().getString(R.string.pleaseWait);
 String commWithServer = getResources().getString(R.string.communicatingWithServer);
 if (pleaseWaitDialog == null)
 pleaseWaitDialog= ProgressDialog.show(NSFkioskLoginActivity.this, pleaseWait, commWithServer, true);

 }

 @Override
 protected Boolean doInBackground(Object... params)
 {
 try {
//Returns true if credentials were valid. False if not. Exception if server could not be reached.
 return cont.validateCredentials((String)params[0], (String)params[1]);
 } catch (Exception e) {
 this.e=e;
 return false;
 }
 }

/**
 * result is passed from doInBackground. Indicates whether credentials were validated.
 */
 @Override
 protected void onPostExecute(Boolean result)
 {
//Hide progress dialog and handle exceptions
//Progress dialog may be null if rotation has been switched
 if (pleaseWaitDialog!= null)
 {
 pleaseWaitDialog.dismiss();
 pleaseWaitDialog = null;
 }

 if (e!= null)
 {
//Show toast with exception text
 String networkError = getResources().getString(R.string.serverErrorException);
 Toast toast = Toast.makeText(getApplicationContext(), networkError, Toast.LENGTH_SHORT);
 toast.show();
 }
 else
 {
 if (result == true)
 {
 saveCredentialsToPreferences(true);
 gotoMainMenu();
 }
 else
 {
 String toastText = getResources().getString(R.string.invalidCredentialsEntered);
 Toast toast = Toast.makeText(getApplicationContext(), toastText, Toast.LENGTH_SHORT);
 toast.show();
 } 
 }
 }

 }
}

我绝对不是一位经验丰富的安卓开发者,所以欢迎你发表评论。

我已经这样做了:


 package com.palewar;
 import android.app.Activity;
 import android.app.ProgressDialog;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Message;

 public class ThreadActivity extends Activity {


 static ProgressDialog dialog;
 private Thread downloadThread;
 final static Handler handler = new Handler() {

 @Override
 public void handleMessage(Message msg) {

 super.handleMessage(msg);

 dialog.dismiss();

 }

 };

 protected void onDestroy() {
 super.onDestroy();
 if (dialog!= null && dialog.isShowing()) {
 dialog.dismiss();
 dialog = null;
 }

 }

/** Called when the activity is first created. */
 @Override
 public void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.main);

 downloadThread = (Thread) getLastNonConfigurationInstance();
 if (downloadThread!= null && downloadThread.isAlive()) {
 dialog = ProgressDialog.show(ThreadActivity.this,"",
"Signing in...", false);
 }

 dialog = ProgressDialog.show(ThreadActivity.this,"",
"Signing in.. .", false);

 downloadThread = new MyThread();
 downloadThread.start();
//processThread();
 }

//Save the thread
 @Override
 public Object onRetainNonConfigurationInstance() {
 return downloadThread;
 }


 static public class MyThread extends Thread {
 @Override
 public void run() {

 try {
//Simulate a slow network
 try {
 new Thread().sleep(5000);
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 handler.sendEmptyMessage(0);

 } finally {

 }
 }
 }

 }

你也可以尝试让我知道它对你是否有效

...