Как справиться с изменением ориентации экрана при активном диалоге прогресса и фоновом потоке? - 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 ]

1 голос
/ 20 ноября 2014

В наши дни есть гораздо более четкий способ решения подобных проблем. Типичный подход:

1. Убедитесь, что ваши данные правильно отделены от пользовательского интерфейса:

Все, что является фоновым процессом, должно храниться в Fragment (установите это значение с помощью Fragment.setRetainInstance(). Это становится вашим «постоянным хранилищем данных», где хранятся все данные на основе, которые вы хотели бы сохранить. После события изменения ориентации этот Fragment по-прежнему будет доступен в своем первоначальном состоянии через вызов FragmentManager.findFragmentByTag() (когда вы создаете его, вы должны присвоить ему тег , а не ID , поскольку он не присоединен к View) .

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

2. Убедитесь, что вы правильно и безопасно взаимодействуете между фоновыми процессами и вашим пользовательским интерфейсом:

Вы должны отменить процесс связывания. В данный момент ваш фоновый процесс присоединяется к View - вместо этого View должен присоединиться к фоновому процессу. Это имеет больше смысла, верно? Действие View зависит от фонового процесса, тогда как фоновый процесс не зависит от View. Это означает изменение ссылки на стандартный интерфейс Listener. Скажем, ваш процесс (независимо от того, какой это класс - будь то AsyncTask, Runnable или любой другой) определяет OnProcessFinishedListener, когда процесс завершится, он должен вызвать этот слушатель, если он существует.

Этот ответ является хорошим кратким описанием того, как сделать пользовательских слушателей.

3. Свяжите свой пользовательский интерфейс с процессом обработки данных при каждом создании пользовательского интерфейса (включая изменения ориентации):

Теперь вам нужно беспокоиться о сопряжении фоновой задачи с текущей структурой View. Если вы правильно обрабатываете изменения (не всегда рекомендуют хакеры configChanges), то ваш Dialog будет воссоздан системой. Это важно, это означает, что при изменении ориентации все ваши методы жизненного цикла Dialog будут вызваны. Таким образом, в любом из этих методов (onCreateDialog, как правило, хорошее место), вы можете сделать вызов, подобный следующему:

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

См. Жизненный цикл Fragment , чтобы узнать, где настройка слушателя лучше всего подходит для вашей индивидуальной реализации.

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

1 голос
/ 07 марта 2010

Я пробовал ВСЕ. Провел дни экспериментов. Я не хотел блокировать вращение активности. Мой сценарий был:

  1. Диалог прогресса, показывающий динамическую информацию для пользователя. Например: "Подключение к серверу ...", "Загрузка данных ..." и т. Д.
  2. Поток, выполняющий тяжелые вещи и обновляющий диалог
  3. Обновление интерфейса с результатами в конце.

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

Единственное решение, которое сработало для меня, это трюк «Активность / Диалог» Это просто и гениально, и все это доказательство вращения:

  1. Вместо создания диалога и запроса на его показ, создайте действие, которое было установлено в манифесте с помощью android: theme = "@ android: style / Theme.Dialog". Так что это просто похоже на диалог.

  2. Заменить showDialog (DIALOG_ID) на startActivityForResult (yourActivityDialog, yourCode);

  3. Используйте onActivityResult в вызывающем Activity для получения результатов из выполняющегося потока (даже ошибок) и обновления пользовательского интерфейса.

  4. В вашем «ActivityDialog» используйте потоки или AsyncTask для выполнения длинных задач и onRetainNonConfigurationInstance для сохранения «диалогового» состояния при повороте экрана.

Это быстро и отлично работает. Я все еще использую диалоги для других задач и AsyncTask для чего-то, что не требует постоянного диалога на экране. Но в этом сценарии я всегда использую шаблон «Активность / Диалог».

И я этого не пробовал, но даже можно заблокировать вращение этого Activity / Dialog во время работы потока, ускорения и одновременного вращения вызывающего Activity.

0 голосов
/ 18 ноября 2015

Если вы боретесь с обнаружением событий изменения ориентации в диалоговом окне НЕЗАВИСИМО ОТ ССЫЛКИ НА ДЕЯТЕЛЬНОСТЬ , этот метод работает потрясающе хорошо. Я использую это, потому что у меня есть свой собственный класс диалога, который может отображаться в нескольких различных действиях, поэтому я не всегда знаю, в каком действии он отображается. С этим методом вам не нужно изменять AndroidManifest, беспокойтесь о ссылках на действия, и вам не нужен пользовательский диалог (как у меня). Тем не менее, вам нужен пользовательский вид контента, чтобы вы могли обнаружить изменения ориентации, используя этот конкретный вид. Вот мой пример:

Настройка

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();
0 голосов
/ 02 октября 2015

Я нашел и более простое решение для обработки потоков при изменении ориентации. Вы можете просто сохранить статическую ссылку на вашу деятельность / фрагмент и проверить, имеет ли оно значение null, прежде чем действовать в интерфейсе. Я предлагаю также использовать попытку 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;
    }


}
0 голосов
/ 10 мая 2013

Самое простое и гибкое решение - использовать AsyncTask со статической ссылкой на ProgressBar . Это обеспечивает инкапсулированное и, следовательно, многоразовое решение проблем изменения ориентации. Это решение хорошо мне помогло для решения различных асинхронных задач, включая загрузку через Интернет, обмен данными с 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 Активность проста

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 голосов
/ 10 ноября 2010

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

В методе onPostExecute моего AsyncTask я просто обернул '.dismiss' для диалогового окна прогресса в блоке try / catch (с пустым catch) и затем просто проигнорировал возникшее исключение. Кажется, что это неправильно, но похоже, что нет никаких вредных последствий (по крайней мере, для того, что я делаю впоследствии, чтобы начать другое действие, передаваемое в результате моего длительного выполнения запроса в качестве дополнительного)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...