Question Comment gérer le changement d'orientation de l'écran lorsque la boîte de dialogue de progression et le fil d'arrière-plan sont actifs?


Mon programme effectue une activité réseau dans un thread d'arrière-plan. Avant de commencer, il affiche une boîte de dialogue de progression. La boîte de dialogue est fermée sur le gestionnaire. Tout fonctionne bien, sauf lorsque l'orientation de l'écran change pendant que le dialogue est actif (et que le thread d'arrière-plan est en cours). À ce stade, l'application se bloque, ou blocages, ou entre dans une étape étrange où l'application ne fonctionne pas du tout jusqu'à ce que tous les threads ont été tués.

Comment gérer l'orientation de l'écran avec élégance?

L'exemple de code ci-dessous correspond grosso modo à ce que fait mon vrai programme:

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

Empiler:

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)

J'ai essayé d'ignorer la boîte de dialogue de progression dans onSaveInstanceState, mais cela évite juste un plantage immédiat. Le thread d'arrière-plan est toujours en cours et l'interface utilisateur est en état partiellement dessiné. Besoin de tuer l'application entière avant qu'il ne recommence à fonctionner.


490
2017-07-10 21:05


origine


Réponses:


Lorsque vous changez d'orientation, Android crée une nouvelle vue. Vous avez probablement des plantages parce que votre thread d'arrière-plan essaie de changer l'état de l'ancien. (Cela peut aussi avoir des problèmes car votre thread d'arrière-plan n'est pas sur le thread d'interface utilisateur)

Je suggère de rendre ce mHandler volatile et de le mettre à jour lorsque l'orientation change.


144
2017-07-12 19:34



Modifier: Les ingénieurs de Google ne recommandent pas cette approche, comme décrit par Dianne Hackborn (a.k.a. hackbod) dans ce StackOverflow post. Check-out ce blog pour plus d'informations.


Vous devez ajouter ceci à la déclaration d'activité dans le manifeste:

android:configChanges="orientation|screenSize"

donc on dirait

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

La question est que le système détruit l'activité lorsqu'un changement de configuration se produit. Voir ConfigurationChangements.

Donc, mettre cela dans le fichier de configuration évite au système de détruire votre activité. Au lieu de cela, il invoque onConfigurationChanged(Configuration) méthode.


256
2018-03-10 16:50



Je suis venu avec une solution solide pour ces problèmes, conforme à la «façon Android». J'ai toutes mes opérations de longue durée en utilisant le modèle IntentService.
C'est mon intention de diffusion d'activités, le service IntentService fait le travail, enregistre les données dans la base de données et diffuse des intentions STICKY.
La partie collante est importante, de sorte que même si l'activité a été interrompue pendant le temps après que l'utilisateur a commencé le travail et qu'il manque la diffusion en temps réel du ItentService, nous pouvons toujours répondre et récupérer les données de l'activité appelante.
ProgressDialogs peut travailler dans ce modèle assez bien avec onSaveInstanceSate().
Fondamentalement, vous devez enregistrer un indicateur indiquant que vous avez une boîte de dialogue de progression exécutée dans le groupe d'instances enregistré. NE sauvegardez PAS l'objet de dialogue de progression, car cela entraînera une fuite de toute l'activité.
 Pour avoir un handle persistant dans la boîte de dialogue de progression, je la stocke en tant que référence faible dans l'objet application.
Lors d'un changement d'orientation ou de toute autre chose qui provoque une pause de l'activité (appel téléphonique, retour de l'utilisateur à la maison, etc.), je rejette l'ancienne boîte de dialogue et recréer une nouvelle boîte de dialogue dans l'activité nouvellement créée.
Pour les dialogues de progression indéfinis, c'est facile. Pour le style de barre de progression, vous devez placer les dernières avancées connues dans le bundle et toutes les informations que vous utilisez localement dans l'activité pour suivre la progression.
Lors de la restauration de la progression, vous utiliserez ces informations pour générer à nouveau la barre de progression dans le même état que précédemment, puis mettre à jour en fonction de l'état actuel des choses.
Donc pour résumer, mettre des tâches de longue durée dans un IntentService couplé à une utilisation juteuse de onSaveInstanceState() vous permet de suivre efficacement les boîtes de dialogue et de les restaurer à travers les événements du cycle de vie de l'activité. Les bits pertinents du code d'activité sont ci-dessous. Vous aurez également besoin de la logique dans votre BroadcastReceiver pour gérer les intentions de Sticky de manière appropriée, mais cela dépasse le cadre de ceci.

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



        }

    }

63
2017-12-19 02:21



J'ai rencontré le même problème. Mon activité doit analyser certaines données à partir d’une URL et elle est lente. Donc, je crée un thread pour le faire, puis affiche un journal de progression. Je laisse le fil poster un message au fil de l'interface utilisateur via Handler quand il est fini. Dans Handler.handleMessage, j'obtiens l'objet de données (prêt maintenant) à partir du thread et le remplis dans l'interface utilisateur. Donc, c'est très similaire à votre exemple.

Après plusieurs essais et erreurs, il semble que j'ai trouvé une solution. Au moins maintenant je peux faire pivoter l'écran à tout moment, avant ou après que le fil soit terminé. Dans tous les tests, le dialogue est correctement fermé et tous les comportements sont comme prévu.

Ce que j'ai fait est montré ci-dessous. L'objectif est de remplir mon modèle de données (mDataObject), puis de le remplir dans l'interface utilisateur. Devrait permettre la rotation de l'écran à tout moment sans surprise.

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;

    public MyHandler(..., Handler h) {...; mHandler = h;} // constructor with handler param
    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.
    }
}

C'est ce qui fonctionne pour moi. Je ne sais pas si c'est la méthode "correcte" telle que conçue par Android - ils prétendent que "détruire / recréer une activité pendant la rotation de l'écran" rend les choses plus faciles, donc je suppose que cela ne devrait pas être trop compliqué.

Faites-moi savoir si vous voyez un problème dans mon code. Comme dit plus haut, je ne sais pas vraiment s'il y a des effets secondaires.


25
2018-02-19 08:42



Ma solution était d'étendre le ProgressDialog classe pour obtenir le mien MyProgressDialog.
J'ai redéfini show() et dismiss() méthodes pour verrouiller l'orientation avant de montrer la Dialog et le déverrouiller quand Dialog est rejeté. Alors quand le Dialog s'affiche et l'orientation de l'appareil change, l'orientation de l'écran reste affichée jusqu'à ce que dismiss() est appelé, puis l'orientation de l'écran change en fonction des valeurs du capteur / de l'orientation du périphérique.

Voici mon code:

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

}

14
2017-08-22 12:30



Le problème initial perçu était que le code ne survivrait pas à un changement d'orientation de l'écran. Apparemment, ceci a été "résolu" en faisant en sorte que le programme gère lui-même le changement d'orientation de l'écran, au lieu de laisser le cadre de l'interface utilisateur le faire (en appelant onDestroy)).

Je dirais que si le problème sous-jacent est que le programme ne survivra pas à onDestroy (), alors la solution acceptée est simplement une solution de contournement qui laisse le programme avec d'autres problèmes et vulnérabilités graves. N'oubliez pas que le framework Android stipule spécifiquement que votre activité risque d'être détruite à tout moment en raison de circonstances indépendantes de votre volonté. Par conséquent, votre activité doit être en mesure de survivre à onDestroy () et à onCreate () ultérieur pour toute raison, et pas seulement à un changement d’orientation de l’écran.

Si vous souhaitez accepter de modifier vous-même l'orientation de l'écran pour résoudre le problème des OP, vous devez vérifier que les autres causes d'onDestroy () ne génèrent pas la même erreur. Es-tu capable de faire ça? Sinon, je me demande si la réponse «acceptée» est vraiment très bonne.


13
2018-05-23 21:21



J'ai rencontré le même problème et j'ai proposé une solution qui n'utilisait pas ProgressDialog et j'obtiens des résultats plus rapides.

Ce que j'ai fait était de créer une mise en page qui a un ProgressBar dedans.

<?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>

Ensuite, dans la méthode onCreate, procédez comme suit

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

Effectuez ensuite la longue tâche dans un thread et, une fois terminé, définissez la vue du contenu sur la disposition réelle que vous souhaitez utiliser pour cette activité.

Par exemple:

mHandler.post(new Runnable(){

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

C’est ce que j’ai fait, et j’ai constaté qu’il était plus rapide que de montrer ProgressDialog et que c’était moins intrusif et que j’avais une meilleure idée de ce que je pensais.

Cependant, si vous souhaitez utiliser ProgressDialog, cette réponse n'est pas pour vous.


8
2017-09-10 14:45



J'ai découvert une solution à ce que je n'ai pas encore vu ailleurs. Vous pouvez utiliser un objet d'application personnalisé qui sait si vous avez des tâches en arrière-plan, au lieu d'essayer de le faire dans l'activité détruite et recréée lors d'un changement d'orientation. J'ai blogué à ce sujet dans ici.


7
2018-05-17 16:20