当进度对话框和后台线程处于活动状态时,如何处理屏幕方向变化?


524

我的程序在后台线程中执行一些网络活动。在开始之前,它会弹出一个进度对话框。该对话框在处理程序上关闭。这一切都很好,除非对话框打开(并且背景线程正在运行)时屏幕方向改变。此时,该应用程序要么崩溃,死锁,要么进入怪异的阶段,直到所有线程被杀死,该应用程序才完全无法运行。

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

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

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处于部分绘制状态。需要终止整个应用程序,然后才能再次开始工作。


1
考虑到您收到的答案,您应该更改已接受的答案,以求最好,不是吗?
rds


3
所有人,对此问题都有很好的解释和可能的解决方案。浏览http://blog.doityourselfandroid.com/2010/11/14/handling-progress-dialogs-and-screen-orientation-changes/ Lemme知道是否有帮助。
arcamax 2011年

2
这篇博客文章中,关于如何在屏幕方向上保留异步后台任务有相当完整的解释。一探究竟!
Adrian Monk 2014年

只需在清单中将android:configChanges =“ orientation | screenSize”设置为Activity。它将停止android重新创建其活动
Jawad Zeb 2015年

Answers:


155

当您切换方向时,Android将创建一个新的视图。您可能会崩溃,因为您的后台线程试图更改旧线程的状态。(由于您的后台线程不在UI线程上,因此可能也会遇到麻烦)

我建议使mHandler易失,并在方向更改时对其进行更新。


14
您可能已经查明了崩溃的原因。我摆脱了崩溃,但是我仍然没有想出如何以可靠的方式将UI恢复到方向更改之前的状态。但是您的回答使我前进,因此将其授予答案。
Heikki Toivonen

4
方向更改时,您应该在活动中获得一个onStart。本质上,您必须使用旧数据重新配置视图。因此,我建议您从进度条中请求数字状态补全,并在获得新的“ onStart”时重建新视图,如果您也有新活动,我记不清了,但是从文档中进行一些搜索应该会有所帮助。
haseman

6
最近玩过它,我可以继续说下去,当您的应用更改方向时,您确实获得了新的活动。(您还将获得一个新视图)如果尝试更新旧视图,则将获得异常,因为旧视图具有无效的应用程序上下文(您的旧活动)。您可以通过传入myActivity.getApplicationContext()来解决此问题。而不是活动本身的指针。
haseman

1
有人可以解释在这方面的挥发性使用/收益
贾瓦德·瑞伯

2
@Nepster是的,我也对此感到疑惑。如果有人对挥发物做出解释,那将是很棒的。
RestInPeace 2015年

261

编辑: Google工程师不推荐这种方法,正如该StackOverflow帖子中的Dianne Hackborn(aka hackbod)所述。查看此博客文章以获取更多信息。


您必须将其添加到清单中的活动声明中:

android:configChanges="orientation|screenSize"

所以看起来

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

问题在于,当配置发生更改时,系统会破坏活动。请参阅ConfigurationChanges

因此,将其放入配置文件可避免系统破坏您的活动。而是调用该onConfigurationChanged(Configuration)方法。


21
这绝对是最好的解决方案。因为它只是旋转布局(您首先期望的行为)。只需确保放入android:configChanges =“ orientation | keyboardHidden”(因为具有横向键盘的手机即可)
nikib3ro 2010年

24
这似乎是我期望的行为。但是,文档表明该活动已被破坏,“因为任何应用程序资源(包括布局文件)都可以基于任何配置值进行更改。因此,处理配置更改的唯一安全方法是重新获取所有资源”。此外orientation,还有许多更改配置的原因:(keyboardHidden我已经编辑了Wiki答案),uiMode(例如,进入或退出汽车模式;夜间模式更改),等等。我现在想知道这是否实际上是一个好答案。
rds

116
这不是可接受的解决方案。它只是掩盖了真正的问题。
rf43 2011年

18
可行,但Google不推荐。
Ed Burnette

21
请不要在这里遵循这种方法。DDosAttack是完全正确的。想象一下,您正在为下载或需要很长时间的其他操作创建进度对话框。作为用户,您不会停留在该活动上并凝视它。您将切换到主屏幕或其他应用程序,例如游戏或打来的电话,或者其他资源匮乏的资源,这些资源最终会破坏您的活动。然后呢?您正面临着一个古老的问题,而这个巧妙的小技巧无法解决。用户回来后,​​将重新创建该活动。
tiguchi 2012年

68

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

也就是说,我的活动广播了意图,IntentService完成了工作,将数据保存在数据库中,然后广播了粘性意图。粘性部分很重要,因此,即使在用户启动工作之后的活动期间暂停了活动,并且错过了IntentService的实时广播,我们仍然可以响应并从调用活动中获取数据。ProgressDialog使用可以很好地使用此模式onSaveInstanceState()

基本上,您需要保存一个标志,表明已保存的实例束中正在运行一个进度对话框。 不要保存进度对话框对象,因为这将泄漏整个活动。为了持久地处理进度对话框,我将其作为弱引用存储在应用程序对象中。在方向更改或任何其他导致“活动”暂停(电话,用户打回家等)然后继续进行的操作后,我关闭了旧对话框,并在新创建的“活动”中重新创建了一个新对话框。

对于不确定的进度对话框,这很容易。对于进度条样式,您必须将最后一个已知的进度以及您在本地使用的所有信息放入捆绑包中,以跟踪进度。恢复进度时,您将使用此信息以与以前相同的状态重新生成进度栏,然后根据事物的当前状态进行更新。

综上所述,将长期运行的任务与IntentService一起使用以及明智地使用,onSaveInstanceState()可以使您有效地跟踪对话框,然后在Activity生命周期事件中进行还原。活动代码的相关位如下。您还需要在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));
    }
}

似乎是一个不错的解决方案
Derekyy 2015年

“我使用IntentService模式进行所有长时间运行的操作。” 这不是一个完美的解决方案,因为它就像是用大炮射出麻雀和大量样板代码,有关更多内容,您可以观看 youtube.com/watch?v=NJsq0TU0qeg
Kamil Nekanowicz

28

我遇到了同样的问题。我的活动需要从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
非常感谢!提示onRetainNonConfigurationInstance()getLastNonConfigurationInstance()帮助我解决了我的问题。竖起大拇指!
sven

15

最初认为的问题是代码无法在屏幕方向更改中幸免。显然,这是通过让程序处理屏幕方向更改本身来“解决”的,而不是让UI框架执行此操作(通过调用onDestroy)。

我要提出的是,如果潜在的问题是程序无法在onDestroy()上生存,那么公认的解决方案只是一种变通办法,它会使程序面临其他严重的问题和漏洞。请记住,Android框架明确指出,由于无法控制的情况,您的活动几乎随时都有被销毁的风险。因此,由于任何原因,您的活动必须能够生存onDestroy()和随后的onCreate(),而不仅仅是屏幕方向变化。

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


14

我的解决方案是扩展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);
}

}

8

我遇到了同样的问题,我想出了一个解决方案,该解决方案没有使用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,那么此答案不适合您。


该解决方案在一个简单的用例中很优雅,但有缺点。您需要重建完整的内容视图。setContentView(R.layout.my_layout);还不够 你需要将所有的听众,复位数据等
RDS

@rds你是对的。这实际上只是一个简单案例的解决方案,或者如果您需要在显示视图之前在onCreate方法中进行一些繁重的工作。
2011年

我不太明白。可以像通常那样在onCreate()中设置侦听器,而可以在run()中设置它们。我在这里想念什么吗?
代码诗人

7

我发现了一个我在其他地方都没有见过的解决方案。您可以使用一个自定义的应用程序对象,该对象知道您是否要执行后台任务,而不是尝试在定向更改后被销毁并重新创建的活动中执行此操作。我在博客上讲述这个在这里


1
创建自定义Application通常用于维护全局应用程序状态。我并不是说这行不通,但似乎过于复杂了。从文档“通常不需要子类化应用程序”。我在很大程度上喜欢sonxurxo的回答。
rds

7

我将为解决这一轮换问题做出自己的贡献。这可能与OP不相关,因为他没有使用OP AsyncTask,但也许其他人会发现它很有用。这很简单,但似乎可以为我完成工作:

我有一个AsyncTask名为的嵌套类的登录活动BackgroundLoginTask

在我中,BackgroundLoginTask我不会做任何异常的事情,除了在调用时会添加null检查ProgressDialog

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

这是为了处理后台任务完成而Activity看不见的情况,因此进度对话框已经被进程关闭了。onPause()方法。

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

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开发人员,请随时发表评论。


1
有趣!特别是对于那些使用AsyncTask的人。刚刚尝试了您的解决方案,它似乎通常可以工作。有一个问题:ProgressDialog似乎仍在旋转后不久终止,而ProgressDialog仍处于活动状态。我将四处逛逛,看看发生了什么,以及如何解决。但是我不再遇到那些崩溃了!
Scott Biggs 2012年

1
找到了解决方法。看来这里的问题是静态的ProgressDialog。当旋转中断ProgressDialog时,有时会在新的Activity中重新启动后调用其.dismiss()方法。通过使每个Activity创建一个ProgressDialog,我们确保不会将新的ProgressDialog与旧的Activity一起杀死。我还确保在每次关闭ProgressDialog时都将其设置为null(以帮助进行垃圾回收)。因此,我们在这里有一个解决方案!为使用AsyncTask的人加油!
Scott Biggs 2012年

4

将漫长的任务移到单独的课程。将其实现为主题观察者模式。每当创建活动时,就注册该任务,同时关闭该任务类的注销。Task类可以使用AsyncTask。


1
我看不出有什么帮助。您能否更详细地说明这如何避免出现的问题。
Heikki Toivonen

1
正如Haseman所说的那样,它可以防止后端访问UI元素,并且我们可以将UI与后端分开,后端在单独的线程中运行,即使在屏幕重新定向并向后端任务注册和注销后再更新状态后,它仍可以继续运行。我使用此方法解决的真实示例是,我有一个下载任务,将其移至单独的线程中,无论何时创建线程,我都向其注册/注销。
Vinay,2009年

好的,我正在重新讨论这个问题,但我认为我仍然不完全理解这个答案。假设我们有一个主要活动,即启动一个AsyncTask来执行长时间运行的网络操作,我们不想在屏幕方向更改期间中断它。我看不到新活动如何向旧活动开始的AsyncTask发送消息。你能举一个例子吗?
Heikki Toivonen

@Heikki,我的实现低于您的意思吗?
beetstra 2011年

4

技巧是照常在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;
        }       
    }
}

4

我这样做是这样的:

    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 {

                }
            }
        }

    }

您也可以尝试让我知道它是否适合您


开发人员的页面上可能根本无法执行onDestroy代码: “注意上表中的“可杀死”列-对于那些标记为可杀死的方法,在该方法返回托管活动的进程后,该方法可能会被杀死。系统在任何时间都不会执行其代码的另一行
ilomambo 2012年

我坚信“ onRetainNonConfigurationInstance()”是用于此类情况的方法...纽约市工作
Nitin Bansal 2012年

我正面临类似的问题,因为Sachin Gurnani是否使用静态声明解决了我的问题。stackoverflow.com/questions/12058774/...
史蒂芬杜

2

如果你创建一个后台Service,做所有繁重(TCP请求/响应,解组)中,View并且Activity可以被摧毁并重新创建无泄漏窗口或丢失数据。这允许Android建议的行为,即在每次配置更改(例如,每次方向更改)时销毁一个Activity

它稍微复杂一点,但是它是调用服务器请求,数据预处理/后处理等的最佳方法。

您甚至可以将Service每个请求排队到服务器,因此可以轻松高效地处理这些事情。

开发指南中有完整的章节Services


A Service比A AsyncTask要做的更多,但在某些情况下可能是更好的方法。不一定更好,是吗?话虽这么说,我不明白这是如何解决ProgressDialog从main泄漏的问题Activity。您在哪里实例化ProgressDialog?您在哪里解雇它?
rds

2

我有一个实现,可以在更改屏幕方向时销毁活动,但仍然可以成功销毁重新创建的活动中的对话框。我...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引用“所拥有的” 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中的引用,以防万一它不会立即被重新创建。
  • 创建一个BackgroundTask接口和/或集合,以允许不同类型的任务从同一拥有活动中运行。

2

如果维护两个布局,则应终止所有UI线程。

如果使用AsynTask,则可以轻松地.cancel()onDestroy()当前活动的方法内部调用method 。

@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只能执行一次。


2

尝试实施jfelectron的解决方案,因为它是“ 这些问题的坚如磐石的解决方案,符合“ Android方式”,但是花了一些时间来查找和汇总提到的所有要素。最终,这个解决方案略有不同,我认为是更优雅的解决方案。

使用从活动触发的IntentService在单独的线程上执行长时间运行的任务。该服务将粘性广播意向发送回活动,以更新对话框。该活动使用showDialog(),onCreateDialog()和onPrepareDialog()消除了在应用程序对象或saveInstanceState捆绑包中传递持久数据的需要。无论您的应用程序如何中断,这都应该起作用。

活动类别:

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"

2

这是我建议的解决方案:

  • 移动的AsyncTask或线程所保持的片段,如解释在这里。我相信将所有网络调用移至片段是一个好习惯。如果您已经在使用片段,则可以使其中之一负责调用。否则,您可以按照链接文章的建议创建仅用于执行请求的片段。
  • 该片段将使用侦听器接口来信号通知任务完成/失败。您不必担心那里的方向改变。该片段将始终具有指向当前活动的正确链接,并且可以安全地恢复进度对话框。
  • 使进度对话框成为班级成员。实际上,您应该对所有对话框执行此操作。在onPause方法中,您应该关闭它们,否则您将泄漏有关配置更改的窗口。片段应保持繁忙状态。将片段附加到活动后,如果调用仍在运行,则可以再次打开进度对话框。void showProgressDialog()为此,可以将一种方法添加到片段活动侦听器接口。

完美的解决方案,但不明白为什么这个答案比其他答案黯然失色!!!
blackkara '17

2

我面临着同样的情况。我所做的就是在整个应用程序中只为我的进度对话框获得一个实例。

首先,我创建了一个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);

希望对您有所帮助。


2

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

如果后台任务仅在活动处于前台时才需要生存,则“新”解决方案是将后台线程(或最好是AsyncTask)托管在保留的片段中,如本开发人员指南大量问答中所述

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


2

我在android上比较新鲜,我尝试了一下,而且效果很好。

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

1

我尝试了一切。花了几天的时间进行实验。我不想阻止活动旋转。我的情况是:

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

问题是,旋转屏幕时,书上的每个解决方案都失败了。即使使用AsyncTask类,这也是处理这种情况的正确Android方法。旋转屏幕时,启动线程正在使用的当前Context已消失,并且与显示的对话框混淆。问题始终是对话框,无论我向代码中添加了多少技巧(将新的上下文传递给正在运行的线程,通过旋转保留线程状态等)。最后的代码复杂度总是很大,总有可能出错。

对我唯一有效的解决方案是“活动/对话框”技巧。它简单而又天才,并且可以旋转:

  1. 与其创建一个对话框并要求显示它,不如创建一个在清单中使用android:theme =“ @ android:style / Theme.Dialog”设置的Activity。因此,它看起来就像一个对话框。

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

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

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

这样速度很快,效果很好。我仍然将对话框用于其他任务,将AsyncTask用于不需要屏幕上持续对话的对象。但是在这种情况下,我总是选择“活动/对话框”模式。

而且,我没有尝试过,但是甚至可以在线程运行时阻止Activity / Dialog旋转,从而加快执行速度,同时允许调用Activity旋转。


不错,除了必须通过参数传递参数,该参数IntentObjectAsyncTask
rds

@Rui我去年也使用了这种方法。现在我想到这也是不正确的。如果这是“解决”此问题的方法,为什么Google甚至会有对话框?我看到的问题是,如果从ActivityA打开ActivityB(Theme.Dialog),则ActivityA在Activity堆栈中向下移动,因此在必要时被OS标记为已准备好终止。因此,如果您运行的过程很漫长,并且显示出某种虚假的进度“对话”,并且花费的时间太长,并且内存不足,那么... ActivityA被杀死,进度完成也没有任何回报。
rf43 2011年

1

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

1.确保您的数据已与UI正确分开:

任何作为后台进程的东西都应该保留Fragment(将其设置为Fragment.setRetainInstance()。这将成为您的“永久数据存储”,您想要保留的任何数据都将保留在其中。在方向更改事件发生后,Fragment仍可以在原始状态下访问通过FragmentManager.findFragmentByTag()调用状态(创建时,您应该给它一个标签而不是ID,因为它没有附加到View)。

有关正确执行此操作以及为什么它是最佳选择的信息,请参见处理运行时更改开发指南。

2.确保在后台进程和用户界面之间正确安全地进行接口:

你必须 撤消链接过程。目前,您的后台进程将自己附加到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();
        }
    });
 }

有关确定侦听器最适合您的单个实现的位置,请参见片段生命周期

这是为该问题中提出的一般问题提供可靠而完整的解决方案的通用方法。根据您的个人情况,此答案中可能遗漏了一些小片段,但这通常是正确处理方向更改事件的最正确方法。


1

我已经找到了一种在方向改变时处理线程的简便解决方案。您可以仅保留对您的活动/片段的静态引用,并在对ui进行操作之前验证其是否为null。我建议也使用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;
    }


}

1

如果您正在努力检测对话框的方向更改事件,则独立于活动参考,那么此方法将非常有效。我之所以使用它,是因为我有自己的对话框类,可以在多个不同的Activity中显示,因此我并不总是知道它在哪个Activity中显示。使用这种方法,您无需更改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.Builder

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

1

这是我面对的解决方案: 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"

Google不推荐这种方法。

第二种方法: 在活动的onCreate()方法上,如果的值不为null ,则需要DialogFragment通过重新构建ProgressDialogFragment带有标题和消息的方法来保留您的内容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");
      }
  }
}

0

似乎太“快而脏”了,所以请指出缺陷,但我发现有效的方法是...

在AsyncTask的onPostExecute方法中,我仅将进度对话框的'.dismiss'包装在try / catch块中(带有空catch),然后忽略所引发的异常。似乎做错了,但似乎没有任何不良影响(至少对于我随后要做的事情是,它开始另一个活动,并将我长期运行的查询的结果作为Extra传递)


您是说Android平台告诉窗口泄漏时实际上没有内存泄漏吗?
rds

我的进度对话框是活动类的成员变量,因此我假设当活动被破坏并重新创建时,将对其进行垃圾收集并且没有泄漏。我错了吗?
西蒙(Simon)

是的,我认为这是错误的。如您所说,的Activity引用了Dialog。更改配置后,第一个配置将Activity被销毁,这意味着所有字段均设置为null。但是底层WindowManager也有引用Dialog(因为它尚未被消除)。新的Activity尝试创建一个新的Dialog(in preExecute()),并且窗口管理器引发致命异常,阻止您这样做。确实,如果这样做的话,将无法彻底销毁Dialog因此而保留对initial的引用Activity。我对吗?
rds

0

最简单,最灵活的解决方案是将AsyncTask与对ProgressBar的静态引用一起使用。这为方向改变问题提供了一种封装的,因此可重复使用的解决方案。该解决方案非常适合执行各种异步任务,包括Internet下载,与Services进行通信以及文件系统扫描。该解决方案已经在多个Android版本和手机型号上进行了良好的测试。在此处可以找到完整的演示,并且对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();
    }
}

0

当您更改方向时,Android会终止该活动并创建新的活动。我建议对Rx java使用改造。自动处理崩溃。

改造电话时使用这些方法。

.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.