如何处理屏幕方向变化时,进度对话框和背景线程活动?

我的程序在后台线程中执行一些网络活动。在开始之前,它会弹出一个进度对话框。该对话框在处理程序上被解除。这一切都很好,除了当对话框打开时屏幕方向发生变化(背景线程正在运行)。此时,应用程序要么崩溃,要么死锁,要么进入一个奇怪的阶段,在所有线程被杀死之前,应用程序根本无法工作。

我如何处理屏幕方向的变化优雅?

下面的示例代码大致匹配我的实际程序:

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处于部分绘制状态。需要在它重新开始工作之前杀死整个应用程序。

244299 次浏览

将长任务移动到单独的类中。将其实现为主题-观察者模式。每当创建活动时,注册并在关闭时注销任务类。任务类可以使用AsyncTask。

当你切换方向时,Android会创建一个新的View。你可能会崩溃,因为你的后台线程试图改变旧线程的状态。(它也可能有麻烦,因为你的后台线程不在UI线程上)

我建议将mHandler设为volatile,并在方向改变时更新它。

我也遇到过同样的问题。我的活动需要从一个URL解析一些数据,它很慢。所以我创建了一个线程来完成这个任务,然后显示一个进度对话框。当线程完成时,我让线程通过Handler将消息发送回UI线程。在Handler.handleMessage中,我从线程中获得数据对象(现在已经准备好了),并将其填充到UI。这和你的例子很相似。

经过反复试验,我似乎找到了解决办法。至少现在我可以在任何时候旋转屏幕,在线程完成之前或之后。在所有测试中,对话框都是正确关闭的,所有行为都符合预期。

我所做的如下所示。目标是填充我的数据模型(mDataObject),然后将其填充到UI。应该允许屏幕旋转在任何时候没有意外。

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;


// constructor with handler param
public MyHandler(..., Handler h) {
...
mHandler = h;
}


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.
}
}

这对我来说很有效。我不知道这是否是Android设计的“正确”方法——他们声称这种“在屏幕旋转期间破坏/重建活动”实际上使事情变得更简单,所以我猜这应该不是太棘手。

如果您在我的代码中发现问题,请告诉我。如上所述,我真的不知道是否有任何副作用。

我什么都试过了。花了几天时间做实验。我不想阻止活动旋转。我的设想是:

  1. 向用户显示动态信息的进度对话框。例如:“连接到服务器…”,“下载数据…”,等等。
  2. 一个线程做繁重的工作,更新对话框
  3. 在最后用结果更新UI。

问题是,当旋转屏幕时,书上的每个解都失败了。甚至使用AsyncTask类,这是Android处理这种情况的正确方式。当旋转屏幕时,启动线程正在处理的当前Context消失了,这打乱了正在显示的对话框。问题始终在于对话框,无论我在代码中添加了多少技巧(将新上下文传递给正在运行的线程,通过旋转保持线程状态等等)。最后的代码总是非常复杂,总是会有一些地方出错。

对我来说唯一有效的解决方案就是“活动/对话”技巧。这很简单,很天才,而且都是旋转证明:

  1. 而不是创建一个对话框并要求显示它,创建一个活动,已经设置在manifest中android:theme="@android:style/ theme .Dialog"。它看起来就像一个对话框。

  2. 将showDialog(DIALOG_ID)替换为startActivityForResult(yourActivityDialog, yourCode);

  3. 在调用Activity中使用onActivityResult从执行线程获得结果(甚至是错误)并更新UI。

  4. 在你的“ActivityDialog”,使用线程或AsyncTask执行长任务和onRetainNonConfigurationInstance保存“对话”状态时旋转屏幕。

这是快速和工作良好。我仍然使用对话框的其他任务和AsyncTask的一些东西,不需要一个固定的对话框在屏幕上。但在这种情况下,我总是使用活动/对话框模式。

而且,我没有尝试它,但它甚至可以阻止活动/对话框旋转,当线程运行时,加速事情,同时允许调用活动旋转。

编辑:谷歌工程师不推荐这种方法,正如Dianne Hackborn (a.k.a hackbod)在< >强StackOverflow邮报》< / >强中所描述的那样。查看这篇博文了解更多信息。


你必须把这个添加到manifest中的activity声明中:

android:configChanges="orientation|screenSize"

看起来是这样的

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

问题是,当配置发生更改时,系统将破坏活动。看到# EYZ0。

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

我发现了一个我还没有在其他地方见过的解决方案。你可以使用一个自定义应用程序对象,它知道你是否有后台任务在进行,而不是试图在活动中做这件事,在方向改变时被销毁和重新创建。我在在这里上写过这篇博客。

我也遇到了同样的问题,我想出了一个不使用ProgressDialog的解决方案,我得到了更快的结果。

我所做的是创建一个布局,其中有一个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将内容视图设置为您想要用于此活动的实际布局。

例如:

mHandler.post(new Runnable(){


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

这就是我所做的,我发现它比显示ProgressDialog运行得更快,在我看来,它的侵入性更小,外观更好。

然而,如果你想要使用ProgressDialog,那么这个答案不适合你。

看起来太“快速和肮脏”了,所以请指出缺点,但我发现有用的是……

在我的AsyncTask的onPostExecute方法中,我简单地包装了`。try/catch块中的进度对话框(带有空捕获)的Dismiss ',然后简单地忽略所引发的异常。似乎是错误的,但似乎没有不良影响(至少对于我随后所做的事情,这是启动另一个活动,将我的长时间运行查询的结果传递为一个额外)

对于这些问题,我想出了一个坚如磐石的解决方案,它符合“Android方式”。我使用IntentService模式进行所有长时间运行的操作。

也就是说,我的活动广播意图,IntentService做的工作,在DB中保存数据,然后广播黏糊糊的意图。粘性部分很重要,即使Activity在用户启动工作后暂停,并且错过了来自IntentService的实时广播,我们仍然可以响应并从调用Activity中获取数据。ProgressDialogs可以很好地与onSaveInstanceState()一起使用此模式。

基本上,您需要保存一个标志,表明您在保存的实例包中运行了一个进度对话框。保存进度对话框对象,因为这会泄露整个活动。要获得进度对话框的持久句柄,我将其存储为应用程序对象中的弱引用。在方向改变或任何其他导致活动暂停(电话,用户点击家等),然后恢复,我解散旧的对话框,并在新创建的活动中重新创建一个新的对话框。

对于无限进度对话框来说,这很容易。对于进度条样式,您必须将最后一个已知的进度放在bundle中,并将您在活动中本地使用的任何信息用于跟踪进度。在恢复进度时,您将使用此信息以与之前相同的状态重新生成进度条,然后根据当前状态进行更新。

总之,将长时间运行的任务放到IntentService中,再加上明智地使用onSaveInstanceState(),可以让您高效地跟踪对话框,并在活动生命周期事件中恢复对话框。活动代码的相关部分如下。您还需要BroadcastReceiver中的逻辑来适当地处理Sticky意图,但这超出了本文的范围。

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

如果你创建一个后台Service来完成所有繁重的工作(tcp请求/响应,解组),ViewActivity可以被销毁并重新创建,而不会泄漏窗口或丢失数据。这允许Android推荐的行为,这是在每个配置更改时销毁一个Activity(例如。对于每个方向变化)。

它有点复杂,但它是调用服务器请求、数据预处理/后处理等的最佳方式。

您甚至可以使用Service将每个请求排队到服务器,这样就可以轻松有效地处理这些事情。

开发指南有一个完整的关于Services的章节

我有一个实现,允许活动在屏幕方向改变时被销毁,但仍然成功地破坏了重新创建的活动中的对话框。 我使用...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,指的是“拥有的”后台任务,我使用onRetainNonConfigurationInstancegetLastNonConfigurationInstance更新这个字段。

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接口和/或集合,以允许不同类型的任务从相同的活动中运行。

最初的问题是代码无法在屏幕方向改变时存活下来。显然,这是“解决”的程序处理屏幕方向变化本身,而不是让UI框架做它(通过调用onDestroy)。

我会提出,如果潜在的问题是程序无法在onDestroy()上存活,那么被接受的解决方案只是一个工作区,它会给程序留下严重的其他问题和漏洞。记住,Android框架特别声明,你的活动几乎在任何时候都有被销毁的风险,这是由于你无法控制的情况。因此,你的活动必须能够生存onDestroy()和后续的onCreate()出于任何原因,而不仅仅是屏幕方向的变化。

如果你要接受自己处理屏幕方向变化来解决OP的问题,你需要验证onDestroy()的其他原因不会导致相同的错误。你能做到吗?如果不是,我会质疑这个“公认的”答案是否真的是一个很好的答案。

诀窍是在onPreExecute/onPostExecute期间像往常一样在AsyncTask中显示/解散对话框,尽管在方向改变的情况下,在活动中创建/显示一个对话框的新实例,并将其引用传递给任务。

public class MainActivity extends Activity {
private Button mButton;
private MyTask mTask = null;


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


MyTask task = (MyTask) getLastNonConfigurationInstance();
if(task != null){
mTask = task;
mTask.mContext = this;
mTask.mDialog = ProgressDialog.show(this, "", "", true);
}


mButton = (Button) findViewById(R.id.button1);
mButton.setOnClickListener(new View.OnClickListener(){
public void onClick(View v){
mTask = new MyTask(MainActivity.this);
mTask.execute();
}
});
}




@Override
public Object onRetainNonConfigurationInstance() {
String str = "null";
if(mTask != null){
str = mTask.toString();
mTask.mDialog.dismiss();
}
Toast.makeText(this, str, Toast.LENGTH_SHORT).show();
return mTask;
}






private class MyTask extends AsyncTask<Void, Void, Void>{
private ProgressDialog mDialog;
private MainActivity mContext;




public MyTask(MainActivity context){
super();
mContext = context;
}




protected void onPreExecute() {
mDialog = ProgressDialog.show(MainActivity.this, "", "", true);
}


protected void onPostExecute(Void result) {
mContext.mTask = null;
mDialog.dismiss();
}




@Override
protected Void doInBackground(Void... params) {
SystemClock.sleep(5000);
return null;
}
}
}

如果你维护两个布局,所有的UI线程都应该终止。

如果你使用AsynTask,那么你可以很容易地在当前活动的onDestroy()方法中调用.cancel()方法。

@Override
protected void onDestroy (){
removeDialog(DIALOG_LOGIN_ID); // remove loading dialog
if (loginTask != null){
if (loginTask.getStatus() != AsyncTask.Status.FINISHED)
loginTask.cancel(true); //cancel AsyncTask
}
super.onDestroy();
}

对于AsyncTask,请在在这里的“取消任务”部分阅读更多信息。

< >强更新: 增加了检查状态的条件,因为只有当它处于运行状态时才能取消。 还要注意AsyncTask只能执行一次

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

我有一个带有嵌套的AsyncTask类的登录活动,称为BackgroundLoginTask

在我的BackgroundLoginTask中,我没有做任何不寻常的事情,除了在调用ProgressDialog的dismiss时添加一个空检查:

@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;

这有两个目的:首先,它允许我的Activity始终访问AsyncTask对象,即使是从一个新的、旋转后的活动。其次,它允许我的BackgroundLoginTask访问和解散ProgressDialog,即使在旋转之后。

接下来,我将此添加到onPause(),导致进度对话框在Activity离开前台时消失(防止丑陋的“强制关闭”崩溃):

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


}
}

我绝不是一个经验丰富的Android开发人员,所以请随意发表评论。

尝试实现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"

我是这样做的:

    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 {


}
}
}


}

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

我的解决方案是扩展ProgressDialog类来获得我自己的MyProgressDialog.

我重新定义了show()dismiss()方法,在显示Dialog之前锁定方向,并在Dialog被取消时将其解锁。

因此,当显示Dialog并且设备的方向改变时,屏幕的方向保持不变,直到调用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);
}
}

最简单、最灵活的解决方案是使用AsyncTask和对ProgressBar的静态引用。这为方向更改问题提供了一个封装的、因此可重用的解决方案。这个解决方案在各种异步任务(包括互联网下载、与服务通信和文件系统扫描)中发挥了很好的作用。该解决方案已经在多个安卓版本和手机型号上进行了良好的测试。完整的演示可以在在这里中找到,并对DownloadFile.java特别感兴趣

下面是一个概念示例

public class SimpleAsync extends AsyncTask<String, Integer, String> {
private static ProgressDialog mProgressDialog = null;
private final Context mContext;


public SimpleAsync(Context context) {
mContext = context;
if ( mProgressDialog != null ) {
onPreExecute();
}
}


@Override
protected void onPreExecute() {
mProgressDialog = new ProgressDialog( mContext );
mProgressDialog.show();
}


@Override
protected void onPostExecute(String result) {
if ( mProgressDialog != null ) {
mProgressDialog.dismiss();
mProgressDialog = null;
}
}


@Override
protected void onProgressUpdate(Integer... progress) {
mProgressDialog.setProgress( progress[0] );
}


@Override
protected String doInBackground(String... sUrl) {
// Do some work here
publishProgress(1);
return null;
}


public void dismiss() {
if ( mProgressDialog != null ) {
mProgressDialog.dismiss();
}
}
}

在Android Activity中的使用很简单

public class MainActivity extends Activity {
DemoServiceClient mClient = null;
DownloadFile mDownloadFile = null;


@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate( savedInstanceState );
setContentView( R.layout.main );
mDownloadFile = new DownloadFile( this );


Button downloadButton = (Button) findViewById( R.id.download_file_button );
downloadButton.setOnClickListener( new View.OnClickListener() {
@Override
public void onClick(View view) {
mDownloadFile.execute( "http://www.textfiles.com/food/bakebred.txt");
}
});
}


@Override
public void onPause() {
super.onPause();
mDownloadFile.dismiss();
}
}

这是我提出的解决方案:

  • 移动AsyncTask或线程到一个保留的片段,如在这里所述。我认为将所有网络调用都移动到片段是一个很好的实践。如果您已经在使用片段,那么可以让其中一个片段负责调用。否则,您可以创建一个片段来完成请求,正如链接的文章所建议的那样。
  • 片段将使用监听器接口来指示任务完成/失败。你不必担心方向的变化。片段将始终具有到当前活动的正确链接,并且可以安全地恢复进度对话框。
  • 让你的进度对话框成为你的类的成员。事实上,您应该对所有对话框都这样做。在onPause方法中,您应该取消它们,否则您将泄漏配置更改的窗口。繁忙状态应该由片段保持。当片段附加到活动时,如果调用仍在运行,您可以再次调出进度对话框。为此,可以将void showProgressDialog()方法添加到片段活动侦听器接口。

如今,有一种更加独特的方式来处理这类问题。典型的方法是:

1. 确保你的数据与用户界面正确分离:

任何后台进程都应该在保留的Fragment中(设置为Fragment.setRetainInstance())。这就成为了你的“持久数据存储”,你想要保留的任何数据都会被保留。在方向改变事件之后,这个Fragment仍然可以通过FragmentManager.findFragmentByTag()调用以其原始状态访问(当你创建它时,你应该给它一个标签不是ID,因为它没有附加到View)。

请参阅处理运行时更改开发指南,了解如何正确执行此操作,以及为什么这是最佳选择。

2. 确保你在后台进程和你的UI之间正确和安全的接口:

你必须反向你的链接过程。此时,你的后台进程将自己附加到View -相反,你的View应该将自己附加到后台进程。这样更有道理,对吧?View的操作依赖于后台进程,而后台进程不依赖于View。这意味着将链接更改为标准的Listener接口。假设你的进程(不管它是什么类-无论是AsyncTaskRunnable或其他)定义了一个OnProcessFinishedListener,当进程完成时,它应该调用该侦听器(如果它存在)。

这个回答非常简洁地描述了如何定制监听器。

3.每当UI被创建(包括方向的改变)时,将你的UI链接到数据过程中:

现在,您必须考虑如何将后台任务与当前View结构连接起来。如果你正在处理朝向改变正确(而不是人们经常推荐的configChanges黑客),那么你的Dialog将被系统重新创建。这很重要,它意味着在方向改变时,所有Dialog的生命周期方法都将被召回。所以在任何这些方法中(onCreateDialog通常是一个好地方),你可以像下面这样调用:

DataFragment f = getActivity().getFragmentManager().findFragmentByTag("BACKGROUND_TAG");
if (f != null) {
f.mBackgroundProcess.setOnProcessFinishedListener(new OnProcessFinishedListener() {
public void onProcessFinished() {
dismiss();
}
});
}

请参阅片段的生命周期,以确定侦听器的设置最适合您的个人实现。

这是一种通用方法,用于为本问题中问到的一般问题提供健壮而完整的解决方案。根据您的具体情况,这个答案中可能还缺少一些细节,但这通常是正确处理方向更改事件的最正确方法。

我是一个新手在机器人,我尝试了这个,它是有效的。

public class loadTotalMemberByBranch extends AsyncTask<Void, Void,Void> {
ProgressDialog progressDialog = new ProgressDialog(Login.this);
int ranSucess=0;
@Override
protected void onPreExecute() {
// TODO Auto-generated method stub
super.onPreExecute();
progressDialog.setTitle("");
progressDialog.isIndeterminate();
progressDialog.setCancelable(false);
progressDialog.show();
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);


}
@Override
protected Void doInBackground(Void... params) {
// TODO Auto-generated method stub


return null;
}
@Override
protected void onPostExecute(Void result) {
// TODO Auto-generated method stub
super.onPostExecute(result);
progressDialog.dismiss();
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
}
}

我发现了一个更容易的解决方案来处理线程时,方向改变。你可以只保留一个静态引用到你的活动/片段,并在对ui进行操作之前验证它是否为空。我建议使用try catch:

 public class DashListFragment extends Fragment {
private static DashListFragment ACTIVE_INSTANCE;


@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);


ACTIVE_INSTANCE = this;


new Handler().postDelayed(new Runnable() {
public void run() {
try {
if (ACTIVE_INSTANCE != null) {
setAdapter(); // this method do something on ui or use context
}
}
catch (Exception e) {}




}
}, 1500l);


}


@Override
public void onDestroy() {
super.onDestroy();


ACTIVE_INSTANCE = null;
}




}

如果你正在努力检测对话框独立于活动引用的方向变化事件,这个方法会非常有效。我使用这个是因为我有自己的对话框类,可以在多个不同的活动中显示,所以我不总是知道它在哪个活动中显示。有了这个方法,你不需要改变AndroidManifest,担心Activity引用,你不需要一个自定义对话框(就像我有)。但是,您确实需要一个自定义内容视图,以便使用该特定视图检测方向变化。以下是我的例子:

设置

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

我也遇到过同样的情况。我所做的就是在整个应用程序中只获取一个进度对话框的实例。

首先,我创建了一个DialogSingleton类来只获得一个实例(Singleton模式)

public class DialogSingleton
{
private static Dialog dialog;


private static final Object mLock = new Object();
private static DialogSingleton instance;


private DialogSingleton()
{


}


public static DialogSingleton GetInstance()
{
synchronized (mLock)
{
if(instance == null)
{
instance = new DialogSingleton();
}


return instance;
}
}


public void DialogShow(Context context, String title)
{
if(!((Activity)context).isFinishing())
{
dialog = new ProgressDialog(context, 2);


dialog.setCanceledOnTouchOutside(false);


dialog.setTitle(title);


dialog.show();
}
}


public void DialogDismiss(Context context)
{
if(!((Activity)context).isFinishing() && dialog.isShowing())
{
dialog.dismiss();
}
}
}

正如我在这个类中所展示的,我将进度对话框作为属性。每次我需要显示一个进度对话框时,我都会获得唯一的实例并创建一个新的ProgressDialog。

DialogSingleton.GetInstance().DialogShow(this, "My title here!");

当我完成后台任务时,我再次调用惟一实例并关闭它的对话框。

DialogSingleton.GetInstance().DialogDismiss(this);

我将后台任务状态保存在共享首选项中。当我旋转屏幕时,我问我是否有这个活动的任务运行:(onCreate)

if(Boolean.parseBoolean(preference.GetValue(IS_TASK_NAME_EXECUTED_KEY, "boolean").toString()))
{
DialogSingleton.GetInstance().DialogShow(this, "Checking credentials!");
} // preference object gets the info from shared preferences (my own implementation to get and put data to shared preferences) and IS_TASK_NAME_EXECUTED_KEY is the key to save this flag (flag to know if this activity has a background task already running).

当我开始运行后台任务时:

preference.AddValue(IS_TASK_NAME_EXECUTED_KEY, true, "boolean");


DialogSingleton.GetInstance().DialogShow(this, "My title here!");

当我完成运行后台任务:

preference.AddValue(IS_TASK_NAME_EXECUTED_KEY, false, "boolean");


DialogSingleton.GetInstance().DialogDismiss(ActivityName.this);

我希望这能有所帮助。

这是一个非常古老的问题,出于某种原因出现在侧栏上。

如果后台任务只需要在活动处于前台时存活,那么“新的”解决方案是将后台线程(或者,最好是AsyncTask)驻留在保留片段中,如开发人员指南众多Q&所述。

如果活动因配置更改而被销毁,则保留的片段将存活,但当活动在后台或后台堆栈中被销毁时,则保留。因此,如果onPause()中的isChangingConfigurations()为false,后台任务仍然应该中断。

这是我面对它时的解决方案: ProgressDialog不是Fragment的子类,所以我的自定义类“ProgressDialogFragment”可以扩展DialogFragment,以保持配置更改时显示的对话框
import androidx.annotation.NonNull;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.os.Bundle;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;


/**
* Usage:
* To display the dialog:
*     >>> ProgressDialogFragment.showProgressDialogFragment(
*              getSupportFragmentManager(),
*              "fragment_tag",
*              "my dialog title",
*              "my dialog message");
*
* To hide the dialog
*     >>> ProgressDialogFragment.hideProgressDialogFragment();
*/




public class ProgressDialogFragment extends DialogFragment {


private static String sTitle, sMessage;
private static ProgressDialogFragment sProgressDialogFragment;


public ProgressDialogFragment() {
}


private ProgressDialogFragment(String title, String message) {
sTitle = title;
sMessage = message;
}




@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return ProgressDialog.show(getActivity(), sTitle, sMessage);
}


public static void showProgressDialogFragment(FragmentManager fragmentManager, String fragmentTag, String title, String message) {
if (sProgressDialogFragment == null) {
sProgressDialogFragment = new ProgressDialogFragment(title, message);
sProgressDialogFragment.show(fragmentManager, fragmentTag);


} else { // case of config change (device rotation)
sProgressDialogFragment = (ProgressDialogFragment) fragmentManager.findFragmentByTag(fragmentTag); // sProgressDialogFragment will try to survive its state on configuration as much as it can, but when calling .dismiss() it returns NPE, so we have to reset it on each config change
sTitle = title;
sMessage = message;
}


}


public static void hideProgressDialogFragment() {
if (sProgressDialogFragment != null) {
sProgressDialogFragment.dismiss();
}
}
}

挑战是保留对话标题&消息时屏幕 当它们重置为默认空字符串时旋转,尽管对话框仍然显示

有两种方法可以解决这个问题:

< >强第一种方法: 使利用对话框的活动在清单文件中的配置更改期间保留状态:

android:configChanges="orientation|screenSize|keyboardHidden"

谷歌不喜欢此方法。

< >强第二种方法: 在活动的onCreate()方法上,你需要通过重新构建ProgressDialogFragment来保留DialogFragment的标题&如果savedInstanceState不为空:

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_deal);


if (savedInstanceState != null) {
ProgressDialogFragment saveProgressDialog = (ProgressDialogFragment) getSupportFragmentManager()
.findFragmentByTag("fragment_tag");
if (saveProgressDialog != null) {
showProgressDialogFragment(getSupportFragmentManager(), "fragment_tag", "my dialog title", "my dialog message");
}
}
}
当你改变方向时,Android会杀死那个活动并创建新的活动。 我建议使用Rx java的retrofit。

在改造调用时使用这些方法。

< p > <强> < em > .subscribeOn (Schedulers.io ()) .observeOn (AndroidSchedulers.mainThread ()) < / em > < /强> < / p >