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

How to handle screen orientation change when progress dialog and background thread active?

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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();
        }
    };
}

堆:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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处于部分绘制状态。 需要在重新开始工作之前杀死整个应用程序。


编辑:谷歌工程师不推荐这种方法,如Dianne Hackborn(a.k.a。hackbod)在此StackOverflow帖子中所述。查看此博客文章了解更多信息。

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

1
android:configChanges="orientation|screenSize"

所以它看起来像

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

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

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


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

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


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

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

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

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

总而言之,将长时间运行的任务放入IntentService并明智地使用onSaveInstanceState()可以让您有效地跟踪对话并在整个Activity生命周期事件中恢复。活动代码的相关位在下面。你还需要在BroadcastReceiver中使用逻辑来适当地处理Sticky意图,但这超出了这个范围。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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));
    }
}


我遇到了同样的问题。我的活动需要从URL解析一些数据并且速度很慢。所以我创建了一个线程来执行此操作,然后显示进度对话框。我让线程在完成后通过Handler将消息发回UI线程。在Handler.handleMessage中,我从线程获取数据对象(现在就绪)并将其填充到UI。所以它与你的例子非常相似。

经过大量的反复试验后,我发现了一个解决方案。至少现在我可以在线程完成之前或之后的任何时刻旋转屏幕。在所有测试中,对话框都已正确关闭,所有行为都符合预期。

我做了什么如下所示。目标是填充我的数据模型(mDataObject),然后将其填充到UI。应该允许屏幕随时旋转而不会出现意外。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
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设计的"正确"方法 - 他们声称这种"在屏幕旋转期间破坏/重新创建活动"实际上使事情变得更容易,所以我想它不应该太棘手。

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


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

我会提出,如果潜在的问题是程序无法在onDestroy()中生存,那么接受的解决方案只是一种解决方法,使程序出现严重的其他问题和漏洞。请记住,Android框架明确指出,由于您无法控制的情况,您的活动几乎可以随时被销毁。因此,您的活动必须能够以任何理由存在onDestroy()和后续onCreate(),而不仅仅是屏幕方向更改。

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


我的解决方案是扩展ProgressDialog类以获得我自己的MyProgressDialog
我重新定义了show()dismiss()方法以在显示Dialog之前锁定方向,并在Dialog被解除时将其解锁。因此,当显示Dialog并且设备的方向发生变化时,屏幕的方向将一直保持到调用dismiss(),然后屏幕方向会根据传感器值/设备方向而变化。

这是我的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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);
}

}

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

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

1
2
3
4
5
6
7
8
9
10
11
12
<?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方法中执行以下操作

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

然后在一个线程中完成长任务,当完成后,Runnable将内容视图设置为您要用于此活动的实际布局。

例如:

1
2
3
4
5
6
mHandler.post(new Runnable(){

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

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

但是,如果您想使用ProgressDialog,那么这个答案不适合您。


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


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

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

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

1
2
3
4
5
6
7
@Override
protected void onPostExecute(Boolean result)
{    
if (pleaseWaitDialog != null)
            pleaseWaitDialog.dismiss();
[...]
}

这是为了处理后台任务在Activity不可见时完成的情况,因此,onPause()方法已经解除了进度对话框。

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

1
2
private static BackgroundLoginTask backgroundLoginTask;
private static ProgressDialog pleaseWaitDialog;

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

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

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

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

1
2
3
4
5
if ((backgroundLoginTask != null) && (backgroundLoginTask.getStatus() == Status.RUNNING))
        {
           if (pleaseWaitDialog != null)
             pleaseWaitDialog.show();
        }

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

这是整个班级:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
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开发人员,所以请随时发表评论。


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


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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;
        }      
    }
}

我这样做了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
    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 {

                }
            }
        }

    }

你也可以尝试让我知道它对你有用


试图实施jfelectron的解决方案,因为它是"符合'Android方式'事物的这些问题的坚如磐石的解决方案",但它需要一些时间来查找并汇总所提到的所有元素。结束了这个略有不同的,我认为更优雅,解决方案在这里发布完整。

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

活动类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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);
    }
}}

清单文件条目:

在申请部分之前:

1
2
uses-permission android:name="com.mindspiker.Tester.MyService.TEST"
uses-permission android:name="android.permission.BROADCAST_STICKY"

内部申请部分

1
service android:name=".MyService"

我有一个实现,它允许在屏幕方向更改时销毁活动,但仍然会成功破坏重新创建的活动中的对话框。
我使用...NonConfigurationInstance将后台任务附加到重新创建的活动。
普通的Android框架处理重新创建对话框本身,没有任何改变。

我将AsyncTask子类化为"拥有"活动添加字段,以及更新此所有者的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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,我使用onRetainNonConfigurationInstancegetLastNonConfigurationInstance更新了这个字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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引用以释放与其关联的任何内存或其他资源。
  • 在活动被销毁之前清除backgroundtask中的ownerActivity引用,以防它不会立即重新创建。
  • 创建backgroundTask接口和/或集合,以允许从同一个拥有的活动运行不同类型的任务。


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

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

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

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


这是我提出的解决方案:

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


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

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

1
2
3
4
5
6
7
8
9
@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只能执行一次。


由于某种原因,这是一个非常古老的问题。

如果后台任务只需要在活动位于前台时生存,那么"新"解决方案是将后台线程(或者,最好是AsyncTask)托管在保留的片段中,如本开发人员指南和众多问答中所述。 ;如。

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


现在有一种更加独特的方式来处理这些类型的问题。典型的方法是:

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

作为后台进程的任何内容都应该保留在Fragment中(使用Fragment.setRetainInstance()进行设置。这将成为您的'持久数据存储',其中保留您希望保留的任何数据。在方向更改事件之后,此< x1>仍然可以通过FragmentManager.findFragmentByTag()调用以原始状态访问(当你创建它时,你应该给它一个标签而不是ID,因为它没有附加到View)。

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

2.确保您在后台进程和UI之间正确安全地连接:

您必须撤消链接过程。目前你的后台进程将自己附加到View - 而你的View应该将自己附加到后台进程。它更有意义吗? View的操作取决于后台进程,而后台进程不依赖于View。这意味着将链接更改为标准Listener接口。假设您的进程(无论它是什么类 - 无论是AsyncTaskRunnable还是其他类)都定义了OnProcessFinishedListener,当进程完成时,它应该调用该侦听器(如果它存在)。

这个答案是如何做自定义侦听器的简洁描述。

3.无论何时创建UI(包括方向更改),都将UI链接到数据流程中:

现在,您必须担心将后台任务与当前View结构的接口连接起来。如果您正确处理方向更改(不是人们总是建议的configChanges黑客),那么系统将重新创建Dialog。这很重要,这意味着在方向更改时,将调用所有Dialog的生命周期方法。因此,在任何这些方法中(onCreateDialog通常都是一个好地方),您可以进行如下调用:

1
2
3
4
5
6
7
8
DataFragment f = getActivity().getFragmentManager().findFragmentByTag("BACKGROUND_TAG");
if (f != null) {
    f.mBackgroundProcess.setOnProcessFinishedListener(new OnProcessFinishedListener() {
        public void onProcessFinished() {
            dismiss();
        }
    });
 }

请参阅Fragment生命周期以确定侦听器在各个实现中的最佳设置。

这是为此问题中提出的一般问题提供强大而完整的解决方案的一般方法。根据您的具体情况,此答案中可能缺少一些小部件,但这通常是正确处理方向更改事件的最正确方法。


我是一个更新鲜的机器人,我试过这个,它的工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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。
  • 问题是,当旋转屏幕时,书上的每个解决方案都失败了。即使使用AsyncTask类,这也是处理这种情况的正确Android方式。当旋转屏幕时,起始线程正在使用的当前上下文已经消失,并且与显示的对话框混淆。问题始终是Dialog,无论我在代码中添加了多少技巧(将新上下文传递给正在运行的线程,通过旋转保留线程状态等等)。最后的代码复杂性总是巨大的,并且总会出现一些可能出错的问题。

    对我有用的唯一解决方案是Activity / Dialog技巧。它简单而且天才,而且它都是旋转证明:

  • 而不是创建一个Dialog并要求显示它,创建一个已使用android:theme ="@ android:style / Theme.Dialog"在清单中设置的Activity。所以,它看起来像一个对话框。

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

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

  • 在'ActivityDialog'上,使用线程或AsyncTask执行长任务和onRetainNonConfigurationInstance以在旋转屏幕时保存"对话框"状态。

  • 这很快,工作正常。我仍然使用对话框执行其他任务,而AsyncTask则用于不需要在屏幕上显示常量对话框的内容。但是在这种情况下,我总是选择Activity / Dialog模式。

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


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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    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。

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

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

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

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

    1
    2
    3
    4
    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).

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

    1
    2
    3
    preference.AddValue(IS_TASK_NAME_EXECUTED_KEY, true,"boolean");

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

    当我完成后台任务的运行时:

    1
    2
    3
    preference.AddValue(IS_TASK_NAME_EXECUTED_KEY, false,"boolean");

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

    我希望它有所帮助。


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

    我将以下内容作为概念示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    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中的用法很简单

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    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的onPostExecute方法中,我只是在try / catch块(带有空catch)中为进度对话框包装'.dismiss',然后简单地忽略引发的异常。似乎做错了但看起来没有不良影响(至少我后来做的是开始另一个活动,将我长时间运行的查询的结果作为Extra传递)


    我发现并且更容易解决方案在方向改变时处理线程。您可以保留对您的activity / fragment的静态引用,并在对ui执行操作之前验证它是否为null。我建议使用try catch:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
     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;
        }


    }

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

    建立

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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 - 对话

    1
    2
    3
    4
    Dialog dialog = new Dialog(context);
    //set up dialog
    dialog.setContentView(new MyContentView(context));
    dialog.show();

    实现2 - AlertDialog.Builder

    1
    2
    3
    4
    5
    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

    1
    2
    3
    4
    5
    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();