当前位置:  开发笔记 > 编程语言 > 正文

当进度对话框和后台线程激活时,如何处理屏幕方向更改?

如何解决《当进度对话框和后台线程激活时,如何处理屏幕方向更改?》经验,为你挑选了9个好方法。

我的程序在后台线程中执行一些网络活动.在开始之前,它会弹出一个进度对话框.该对话框在处理程序上被关闭.这一切都很好,除非在对话框启动时屏幕方向发生变化(后台线程正在运行).此时,应用程序崩溃或死锁,或进入一个奇怪的阶段,在应用程序完全无法工作之前,直到所有线程都被杀死.

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

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

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.(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> 小智..:

编辑:谷歌工程师不推荐这种方法,正如Dianne Hackborn(又名hackbod)在这篇StackOverflow帖子中所描述的那样.查看此博客文章了解更多信息.


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

android:configChanges="orientation|screenSize"

所以它看起来像


问题是当配置发生变化时,系统会破坏活动.请参阅ConfigurationChanges.

因此,将其放在配置文件中可以避免系统破坏您的活动.相反,它会调用该onConfigurationChanged(Configuration)方法.


这不是一个可接受的解决方案.它掩盖了真正的问题.
这似乎是我期望的行为.但是,文档表明活动被销毁"因为任何应用程序资源(包括布局文件)都可以根据任何配置值进行更改.因此,处理配置更改的唯一安全方法是重新检索所有资源".除了`orientation`之外,还有更多的原因可以改变配置:`keyboardHidden`(我已经编辑了wiki的答案),`uiMode`(例如,进入或退出汽车模式;夜间模式改变),我现在想知道这实际上是一个很好的答案.
这绝对是最好的解决方案; 因为它只是旋转布局(你首先想到的行为).一定要把android:configChanges ="orientation | keyboardHidden"(因为手机有横向键盘)
请不要在这里遵循这种方法.DDosAttack是完全正确的.想象一下,您正在为下载或其他需要很长时间的其他内容创建进度对话框.作为用户,您不会继续参与该活动并盯着它.你会切换到主屏幕或其他应用程序,如游戏或电话可能进来或其他资源饥饿,最终会破坏你的活动.然后是什么?你正面临着同样的老问题,这个问题并没有用那个巧妙的小技巧来解决.当用户回来时,活动将再次重新创建.
有效,但Google不推荐.
为什么Google讨厌这个解决方案?他们自己使用AdView:
@Nobu这并没有改变切换和退回的解决方案的问题.它只是不会在方向更改或hw键盘/开合时重新启动活动.当然,如果你有长时间运行的后台操作,那么你必须以不同的方式处理它(并且有不同的方法来做到这一点,并在任务栏中显示通知等等),但这是一个不同的问题.并且通常如果用户回来,活动将不会被重新创建,除非它由于某种原因被android杀死,然后你无需担心必须重新填充你的字段.
谷歌祝福或不,这是一个很棒的解决方案!没有更多泄露的窗口,没有不需要的onPause/Resume调用,状态数据仍然有效,甚至解决了我一直遇到的其他一些问题.谢谢你的好消息.
尽管Google讨厌这种解决方案,但它的效果非常好.在旋转上重新创建一个Activity的愚蠢令人难以置信,使得所有内容都难以处理20倍.对旧上下文的单一引用会得到异常,很难正确处理.这是我经过大量测试后最终采用的解决方案,我确保在onDestroy上做了一些非常大量的清理,以防止内存泄漏,这就是......
这会阻止UI更改布局吗?
如果为横向和纵向设置不同的布局,这个解决方案会杀死操作系统所做的布局处理,因为它不会重新创建活动,因此您将被卡在活动开始的布局中.
不要那样做.来自doc:"注意:自己处理配置更改会使使用备用资源变得更加困难,因为系统不会自动为您应用它们.当您必须避免由于配置而重新启动时,应该将此技术视为最后的手段更改,不建议用于大多数应用程序."

2> haseman..:

当您切换方向时,Android将创建一个新的视图.您可能正在崩溃,因为您的后台线程正在尝试更改旧的状态.(它可能也有问题,因为你的后台线程不在UI线程上)

我建议使mHandler易变,并在方向改变时更新它.


您可能已经确定了崩溃的原因.我摆脱了崩溃,但我仍然没有想出如何以可靠的方式将UI恢复到方向改变之前的状态.但你的回答让我前进,所以把它作为答案.
最近玩了它,我可以传递你的应用改变方向时你会得到一个新的活动.(您还可以获得新视图)如果您尝试更新旧视图,您将获得异常,因为旧视图的应用程序上下文无效(您的旧活动)您可以通过传入myActivity.getApplicationContext()来解决这个问题.而不是指向活动本身的指针.
当方向发生变化时,您应该在活动中获得onStart.实质上,您必须使用旧数据重新配置视图.所以我建议从进度条请求数字状态udpates并在你获得新的'onStart'时重建一个新的视图如果你得到一个新的活动我不记得,但是一些搜索文档应该有所帮助.
@Nepster是的我也在想这个.如果有人解释了不稳定性,那将会很棒.

3> 小智..:

我想出了一个坚如磐石的解决方案来解决这些问题,这些解决方案符合"Android方式"的要求.我使用IntentService模式进行了所有长时间运行的操作.

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

基本上,您需要保存一个标志,您在已保存的实例包中运行了一个进度对话框.不要保存进度对话框对象,因为这会泄漏整个Activity.要拥有进度对话框的持久句柄,我将其存储为应用程序对象中的弱引用.在方向更改或导致活动暂停的任何其他内容(电话呼叫,用户点击回家等)然后恢复时,我会关闭旧对话框并在新创建的活动中重新创建一个新对话框.

对于无限期的进度对话,这很容易.对于进度条样式,您必须在捆绑中放置最后的已知进度以及您在活动中本地使用的任何信息以跟踪进度.在恢复进度时,您将使用此信息以与以前相同的状态重新生成进度条,然后根据事物的当前状态进行更新.

总而言之,将长时间运行的任务放入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.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.show(AddAccount.this, "", logingon, true));
    }
}



4> 小智..:

我遇到了同样的问题.我的活动需要从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设计的"正确"方法 - 他们声称这种"在屏幕旋转期间破坏/重新创建活动"实际上使事情变得更容易,所以我想它不应该太棘手.

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



5> gymshoe..:

最初的感知问题是代码无法在屏幕方向更改中存活.显然,通过让程序自己处理屏幕方向更改而不是让UI框架执行它(通过调用onDestroy)来"解决"这个问题.

我想提出的是,如果根本问题是,该方案将无法生存的onDestroy(),然后接受的解决方案就是这样留下严重的其他问题和漏洞的程序的解决方法.请记住,Android框架明确指出,由于您无法控制的情况,您的活动几乎可以随时被销毁.因此,您的活动必须能够以任何理由存在onDestroy()和后续onCreate(),而不仅仅是屏幕方向更改.

如果您要自己接受处理屏幕方向更改以解决OP的问题,则需要验证onDestroy()的其他原因不会导致相同的错误.你能做到吗?如果没有,我会质疑"接受"的答案是否真的是一个非常好的答案.



6> 小智..:

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

}



7> Pzanno..:

我遇到了同样的问题,我想出了一个没有使用ProgressDialog进行访问的解决方案,我得到了更快的结果.

我所做的是创建一个包含ProgressBar的布局.





然后在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,那么这个答案不适合您.



8> Heikki Toivo..:

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



9> Anders..:

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

我有一个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;

这有两个目的:首先,它允许我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
    {       
       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开发人员,所以请随时发表评论.

推荐阅读
黄晓敏3023
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有