Как справиться с изменением ориентации экрана при активном диалоге прогресса и фоновом потоке? - PullRequest
505 голосов
/ 11 июля 2009

Моя программа выполняет сетевую активность в фоновом потоке. Перед началом работы появляется диалоговое окно прогресса. Диалог закрывается на обработчике. Это все работает нормально, за исключением случаев, когда ориентация экрана изменяется, когда диалоговое окно открыто (и фоновая нить идет). В этот момент приложение либо вылетает, либо блокируется, либо попадает в странную стадию, когда приложение вообще не работает, пока все потоки не будут уничтожены.

Как можно изящно справиться с изменением ориентации экрана?

Пример кода ниже примерно соответствует тому, что делает моя настоящая программа:

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

Stack:

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, но это только предотвращает немедленный сбой. Фоновый поток все еще продолжается, а пользовательский интерфейс находится в частично отрисованном состоянии. Необходимо убить все приложение, прежде чем оно снова заработает.

Ответы [ 26 ]

257 голосов
/ 10 марта 2010

Редактировать: Инженеры Google не рекомендуют этот подход, как описано Дайанной Хэкборн (aka hackbod ) в этом сообщении StackOverflow , Проверьте этот блог для получения дополнительной информации.


Вы должны добавить это к объявлению активности в манифесте:

android:configChanges="orientation|screenSize"

так выглядит

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

Дело в том, что система уничтожает активность, когда происходит изменение конфигурации. См. Изменения конфигурации .

Таким образом, размещение этого в файле конфигурации позволяет избежать разрушения вашей системы. Вместо этого он вызывает метод onConfigurationChanged(Configuration).

146 голосов
/ 12 июля 2009

Когда вы меняете ориентацию, Android создаст новый вид. Вы, вероятно, получаете сбои, потому что ваш фоновый поток пытается изменить состояние старого. (Это может также быть проблема, потому что ваш фоновый поток не находится в потоке пользовательского интерфейса)

Я бы предложил сделать этот mHandler энергозависимым и обновить его при изменении ориентации.

66 голосов
/ 19 декабря 2010

Я придумал надежное решение для этих проблем, которое соответствует «Android Way» вещей. Все мои длительные операции выполняются с использованием шаблона IntentService.

То есть, моя деятельность передает намерения, IntentService выполняет работу, сохраняет данные в БД и затем передает липкие намерения. Важная часть важна, так что даже если действие было приостановлено в течение времени после того, как пользователь начал работу и пропускает трансляцию в реальном времени от IntentService, мы все равно можем ответить и забрать данные из вызывающего действия. ProgressDialog s может очень хорошо работать с этим шаблоном с 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));
    }
}
26 голосов
/ 19 февраля 2010

Я встретил ту же проблему. Моя деятельность должна анализировать некоторые данные из URL, и это медленно. Поэтому я создаю поток для этого, а затем показываю диалог прогресса. Я позволил ветке опубликовать сообщение обратно в ветку пользовательского интерфейса через Handler после его завершения. В Handler.handleMessage я получаю объект данных (готовый сейчас) из потока и заполняю его для пользовательского интерфейса. Так что это очень похоже на ваш пример.

После долгих проб и ошибок похоже, что я нашел решение. По крайней мере, теперь я могу поворачивать экран в любой момент, до или после завершения потока. Во всех тестах диалоговое окно правильно закрывается, и все поведение соответствует ожидаемому.

То, что я сделал, показано ниже. Цель состоит в том, чтобы заполнить мою модель данных (mDataObject) и затем заполнить ее для пользовательского интерфейса. Должен позволять поворот экрана в любой момент без удивления.

class MyActivity {

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

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

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

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

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

class MyThread extends Thread {
    Handler mHandler;
    MyDataObject mDataObject;

    // 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 - они утверждают, что эта операция «уничтожить / воссоздать во время поворота экрана» на самом деле облегчает ситуацию, поэтому я думаю, что это не должно быть слишком хитрым.

Дайте мне знать, если вы видите проблему в моем коде. Как сказано выше, я не знаю, есть ли побочный эффект.

14 голосов
/ 24 мая 2011

Первоначальная проблема заключалась в том, что код не выдержал изменения ориентации экрана. По-видимому, это было «решено», когда программа сама обрабатывала изменение ориентации экрана, а не позволяла каркасу пользовательского интерфейса (через вызов onDestroy)).

Я бы сказал, что если основная проблема заключается в том, что программа не выживет в Destroy (), то принятое решение - это просто обходной путь, который оставляет программу с другими серьезными проблемами и уязвимостями. Помните, что платформа Android специально заявляет, что ваша деятельность может быть уничтожена практически в любое время из-за обстоятельств, не зависящих от вас. Следовательно, ваша деятельность должна быть в состоянии выжить по любой причине (onDestroy () и последующим onCreate ()), а не только по изменению ориентации экрана.

Если вы собираетесь самостоятельно обрабатывать изменения ориентации экрана для решения проблемы OP, вам необходимо убедиться, что другие причины onDestroy () не приводят к той же ошибке. Вы можете сделать это? Если нет, я бы спросил, действительно ли «принятый» ответ очень хороший.

14 голосов
/ 22 августа 2012

Мое решение состояло в том, чтобы расширить класс 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 голосов
/ 10 сентября 2010

Я столкнулся с той же проблемой, и я нашел решение, которое не вызывало использование 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, тогда этот ответ не для вас.

7 голосов
/ 13 августа 2011

Я собираюсь внести свой вклад в решение этой проблемы ротации. Это может не относиться к OP, так как он не использует AsyncTask, но, возможно, другие найдут его полезным. Это довольно просто, но мне кажется, что эта работа мне подходит:

У меня есть логин с вложенным классом AsyncTask с именем BackgroundLoginTask.

В моем BackgroundLoginTask я не делаю ничего необычного, кроме добавления нулевой проверки при вызове ProgressDialog dismiss:

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

Это для обработки случая, когда фоновая задача завершается, когда Activity не виден, и, следовательно, диалоговое окно прогресса уже было закрыто методом onPause().

Затем в моем родительском Activity классе я создаю глобальные статические дескрипторы для моего AsyncTask класса, и мой ProgressDialog (будучи вложенным, AsyncTask может обращаться к этим переменным):

private static BackgroundLoginTask backgroundLoginTask;
private static ProgressDialog pleaseWaitDialog;

Это служит двум целям: во-первых, оно позволяет моему Activity всегда получать доступ к объекту AsyncTask даже после нового, пост-ротационного действия. Во-вторых, он позволяет BackgroundLoginTask получать доступ к ProgressDialog и отклонять его даже после поворота.

Затем я добавляю это к onPause(), в результате чего диалоговое окно прогресса исчезает, когда наш Activity покидает передний план (предотвращая этот ужасный сбой принудительного закрытия):

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

Наконец, у меня в методе onResume() есть следующее:

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

Это позволяет Dialog вновь появляться после воссоздания Activity.

Вот весь класс:

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

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

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

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

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

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

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

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

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

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

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

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

       }

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

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

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

    }
}

Я ни в коем случае не опытный разработчик Android, поэтому не стесняйтесь комментировать.

7 голосов
/ 17 мая 2010

Я нашел решение для этого, которого я еще нигде не видел. Вы можете использовать пользовательский объект приложения, который знает, выполняются ли фоновые задачи, вместо того, чтобы пытаться делать это в действии, которое уничтожается и воссоздается при изменении ориентации. Я писал об этом в здесь .

4 голосов
/ 27 июня 2011

Хитрость заключается в том, чтобы показать / закрыть диалоговое окно в AsyncTask во время onPreExecute / onPostExecute как обычно, хотя в случае изменения ориентации создайте / покажите новый экземпляр диалога в действии и передайте его ссылку на задачу.

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;
        }       
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...