Mon programme effectue des activités de réseau dans un fil d’arrière-plan. Avant de commencer, une boîte de dialogue de progression s’affiche. La boîte de dialogue est fermée sur le gestionnaire. Tout cela fonctionne correctement, sauf lorsque l'orientation de l'écran change alors que la boîte de dialogue est active (et que le fil d'arrière-plan est en cours d'exécution). À ce stade, l'application se bloque ou se trouve dans une impasse, ou entre dans une étape étrange où l'application ne fonctionne pas du tout jusqu'à ce que tous les threads aient été supprimés.
Comment gérer le changement d'orientation de l'écran avec élégance?
L'exemple de code ci-dessous correspond à peu près à ce que mon vrai programme fait:
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é de fermer la boîte de dialogue de progression dans onSaveInstanceState, mais cela empêche simplement un crash immédiat. Le fil de fond est toujours en cours et l'interface utilisateur est partiellement dessinée. Besoin de tuer toute l'application avant qu'elle ne recommence à fonctionner.
Lorsque vous changez d’orientation, Android crée une nouvelle vue. Vous allez probablement avoir des plantages parce que votre thread d'arrière-plan tente de modifier l'état de l'ancien. (Il se peut également que vous rencontriez des problèmes car votre thread d'arrière-plan n'est pas sur le thread d'interface utilisateur)
Je suggèrerais de rendre le mHandler volatil et de le mettre à jour lorsque l'orientation change.
Modifier: Les ingénieurs de Google ne recommandent pas cette approche, telle que décrite par Dianne Hackborn (aka hackbod ) dans ce StackOverflow post . Découvrez cet article de blog pour plus d'informations.
Vous devez ajouter ceci à la déclaration d'activité dans le manifeste:
Android:configChanges="orientation|screenSize"
alors ça ressemble à
<activity Android:label="@string/app_name"
Android:configChanges="orientation|screenSize|keyboardHidden"
Android:name=".your.package">
Le problème est que le système détruit l'activité lorsqu'un changement de configuration se produit. Voir ConfigurationChanges .
Donc, mettre cela dans le fichier de configuration évite au système de détruire votre activité. Au lieu de cela, il appelle la méthode onConfigurationChanged(Configuration)
.
Je suis parvenu à une solution solide pour ces problèmes, conforme à la "méthode Android". Toutes mes opérations de longue durée utilisent le modèle IntentService.
C'est-à-dire que mes activités diffusent des intentions, IntentService fait le travail, enregistre les données dans la base de données, puis diffuse des intentions collantes. La partie collante est importante, de sorte que même si l'activité a été suspendue pendant le temps après que l'utilisateur a lancé le travail et manque la diffusion en temps réel de IntentService, nous pouvons toujours répondre et récupérer les données de l'activité appelante. ProgressDialog
s peut très bien fonctionner avec ce motif avec onSaveInstanceState()
.
En gros, vous devez enregistrer un indicateur indiquant qu'une boîte de dialogue de progression est en cours d'exécution dans le groupe d'instances sauvegardé. Ne pas enregistrer l'objet de boîte de dialogue de progression, car toute l'activité sera perdue. Pour avoir un handle persistant à la boîte de dialogue de progression, je la stocke comme une référence faible dans l'objet d'application. En cas de changement d'orientation ou de tout autre facteur entraînant la suspension de l'activité (appel téléphonique, l'utilisateur clique sur son domicile, etc.), puis la reprenant, je ferme l'ancienne boîte de dialogue et recrée une nouvelle boîte de dialogue dans l'activité nouvellement créée.
Ceci est facile pour les dialogues de progrès indéfinis. Pour le style de barre de progression, vous devez mettre la dernière progression connue dans l'ensemble et les informations que vous utilisez localement dans l'activité pour suivre l'évolution. 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 la mettre à jour en fonction de l'état actuel des choses.
En résumé, le fait d'intégrer des tâches de longue durée dans un service IntentService associé à une utilisation judicieuse de onSaveInstanceState()
vous permet de suivre efficacement les dialogues et de les restaurer tout au long des événements du cycle de vie de l'activité. Les bits pertinents du code d'activité sont ci-dessous. Vous aurez également besoin de logique dans votre BroadcastReceiver pour gérer les intentions de Sticky de manière appropriée, mais cela dépasse le cadre de la présente.
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));
}
}
J'ai rencontré le même problème. Mon activité doit analyser certaines données d'une URL et c'est lent. Je crée donc un fil pour le faire, puis affiche un dialogue de progression. Je laisse le fil poster un message sur le fil d'interface utilisateur via Handler
quand il est terminé. Dans Handler.handleMessage
, j'obtiens l'objet de données (prêt maintenant) à partir du thread et le remplis dans l'interface utilisateur. Cela ressemble donc beaucoup à votre exemple.
Après beaucoup d'essais et d'erreurs, il semble que j'ai trouvé une solution. Au moins maintenant, je peux faire pivoter l'écran à tout moment, avant ou après la fin du fil. Dans tous les tests, la boîte de dialogue est correctement fermée et tous les comportements sont conformes aux attentes.
Ce que j'ai fait est montré ci-dessous. Le but est de remplir mon modèle de données (mDataObject
), puis de le renseigner dans l'interface utilisateur. Doit 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;
// 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.
}
}
C'est ce qui fonctionne pour moi. Je ne sais pas si c'est la méthode "correcte" conçue par Android - ils prétendent que cette "activité de destruction/recréation pendant la rotation de l'écran" rend les choses plus faciles, alors je suppose que cela ne devrait pas être le cas trop délicat.
Faites-moi savoir si vous voyez un problème dans mon code. Comme dit ci-dessus, je ne sais pas vraiment s'il y a un effet secondaire.
Le problème perçu à l’origine était que le code ne survivrait pas à un changement d’orientation de l’écran. Apparemment, cela a été "résolu" en laissant le programme gérer le changement d'orientation de l'écran lui-même, au lieu de laisser le cadre d'interface utilisateur le faire (via l'appelant onDestroy).
Je soutiendrais que si le problème sous-jacent est que le programme ne survivra pas à onDestroy (), la solution acceptée n’est alors qu’une solution de contournement qui laisse au programme de graves problèmes et vulnérabilités. N'oubliez pas que le cadre Android indique spécifiquement que votre activité risque d'être détruite presque à tout moment en raison de circonstances indépendantes de votre volonté. Par conséquent, votre activité doit pouvoir survivre à onDestroy () et ensuite à onCreate () pour une raison quelconque, pas seulement un changement d'orientation de l'écran.
Si vous acceptez de gérer vous-même les modifications d'orientation de l'écran pour résoudre le problème du PO, vous devez vérifier que les autres causes de onDestroy () n'entraînent pas la même erreur. Êtes-vous capable de faire ça? Sinon, je me demanderais si la réponse "acceptée" est vraiment une très bonne réponse.
Ma solution était d'étendre la classe ProgressDialog
pour obtenir mon propre MyProgressDialog
.
J'ai redéfini les méthodes show()
et dismiss()
pour verrouiller l'orientation avant d'afficher la Dialog
et la déverrouiller lorsque Dialog
est ignoré. Ainsi, lorsque la Dialog
est affichée et que l'orientation de l'appareil change, l'orientation de l'écran reste jusqu'à l'appel de dismiss()
, l'orientation de l'écran change ensuite en fonction des valeurs de capteur/de l'appareil.
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);
}
}
J'ai rencontré le même problème et j'ai proposé une solution qui n'invoquait pas l'utilisation de ProgressDialog et j'ai obtenu des résultats plus rapides.
Ce que j'ai fait est de créer une mise en page qui contient une barre de progression.
<?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);
}
Ensuite, effectuez la tâche longue dans un fil, puis cliquez sur Runnable pour définir l’affichage du contenu sur la présentation 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 est plus rapide que de montrer ProgressDialog, que c’est moins intrusif et que j’ai une meilleure apparence, à mon avis.
Toutefois, si vous souhaitez utiliser ProgressDialog, cette réponse ne vous convient pas.
Je vais contribuer mon approche à la gestion de ce problème de rotation. Cela n’est peut-être pas pertinent pour OP car il n’utilise pas AsyncTask
, mais d’autres le trouveront peut-être utile. C'est assez simple mais cela semble faire le travail pour moi:
J'ai une activité de connexion avec une classe AsyncTask
imbriquée appelée BackgroundLoginTask
.
Dans mon BackgroundLoginTask
je ne fais rien d’extraordinaire, si ce n’est pour ajouter un contrôle nul en appelant le renvoi de ProgressDialog
:
@Override
protected void onPostExecute(Boolean result)
{
if (pleaseWaitDialog != null)
pleaseWaitDialog.dismiss();
[...]
}
Cela permet de gérer le cas où la tâche en arrière-plan se termine alors que Activity
n'est pas visible et que, par conséquent, le dialogue de progression a déjà été ignoré par la méthode onPause()
.
Ensuite, dans ma classe Activity
parente, je crée des descripteurs statiques globaux pour ma classe AsyncTask
et ma ProgressDialog
(la AsyncTask
, étant imbriquée, peut accéder à ces variables):
private static BackgroundLoginTask backgroundLoginTask;
private static ProgressDialog pleaseWaitDialog;
Cela sert à deux choses: premièrement, cela permet à ma Activity
d'accéder toujours à l'objet AsyncTask
même à partir d'une nouvelle activité post-rotation. Deuxièmement, cela permet à ma BackgroundLoginTask
d’accéder à la ProgressDialog
et de la supprimer même après une rotation.
Ensuite, j’ajoute ceci à onPause()
, provoquant la disparition de la boîte de dialogue de progression lorsque notre Activity
quitte le premier plan (ce qui empêche l’affreux crash de "force fermer"):
if (pleaseWaitDialog != null)
pleaseWaitDialog.dismiss();
Enfin, dans ma méthode onResume()
:
if ((backgroundLoginTask != null) && (backgroundLoginTask.getStatus() == Status.RUNNING))
{
if (pleaseWaitDialog != null)
pleaseWaitDialog.show();
}
Cela permet à la Dialog
de réapparaître une fois la Activity
recréée.
Voici la classe entière:
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();
}
}
}
}
}
Je ne suis en aucun cas un développeur expérimenté Android, alors n'hésitez pas à commenter.
J'ai découvert une solution à ce problème que je n'ai pas encore vue ailleurs. Vous pouvez utiliser un objet d'application personnalisé qui sait si des tâches en arrière-plan sont en cours, au lieu d'essayer de le faire dans l'activité détruite et recréée lors du changement d'orientation. J'ai blogué à ce sujet dans ici .
Je l'ai fait comme ça:
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 {
}
}
}
}
Vous pouvez également essayer de me faire savoir que cela fonctionne pour vous ou non
Déplacez la tâche longue dans une classe séparée. Implémentez-le en tant que modèle sujet-observateur. Chaque fois que l'activité est créée, enregistrez et fermez-la en annulant l'enregistrement avec la classe de tâches. La classe de tâche peut utiliser AsyncTask.
L'astuce consiste à afficher/fermer la boîte de dialogue dans AsyncTask pendant onPreExecute/onPostExecute comme d'habitude, mais en cas de changement d'orientation, créez/affichez une nouvelle instance de la boîte de dialogue dans l'activité et transmettez sa référence à la tâche.
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;
}
}
}
J'ai une implémentation qui permet de détruire l'activité lors d'un changement d'orientation de l'écran, mais détruit tout de même le dialogue dans l'activité recréée avec succès. J'utilise ...NonConfigurationInstance
pour associer la tâche en arrière-plan à l'activité recréée. Le cadre normal Android gère la recréation du dialogue lui-même, rien n'y est modifié.
J'ai sous-classé AsyncTask en ajoutant un champ pour l'activité 'propriétaire' et une méthode pour mettre à jour ce propriétaire.
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);
}
...
}
Dans ma classe d'activité, j'ai ajouté un champ backgroundTask
faisant référence à la tâche d'arrière-plan 'appartenant à' et je le met à jour à l'aide de onRetainNonConfigurationInstance
et getLastNonConfigurationInstance
.
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;
}
...
}
Suggestions d'amélioration
backgroundTask
dans l'activité une fois la tâche terminée pour libérer la mémoire ou les autres ressources qui lui sont associées.ownerActivity
dans la tâche d’arrière-plan avant que l’activité ne soit détruite, au cas où elle ne serait pas recréée immédiatement.BackgroundTask
pour permettre à différents types de tâches de s'exécuter à partir de la même activité propriétaire.J'ai essayé d'implémenter la solution de jfelectron car c'est une " solution solide comme un roc à ces problèmes qui est conforme à la 'Voie Android' des choses "mais il a fallu un certain temps pour rechercher et rassembler tous les éléments mentionnés. Nous avons fini avec cette solution légèrement différente, et je pense plus élégante, publiée ici dans son intégralité.
Utilise un IntentService tiré d'une activité pour exécuter la tâche longue sur un thread séparé. Le service renvoie des intentions de diffusion collantes à l’activité qui met à jour la boîte de dialogue. L'activité utilise showDialog (), onCreateDialog () et onPrepareDialog () pour éliminer le besoin de données persistantes transmises à l'objet d'application ou au bundle savedInstanceState. Cela devrait fonctionner quelle que soit la manière dont votre application est interrompue.
Classe d'activité:
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);
}}
Classe IntentService:
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);
}
}}
Entrées du fichier manifeste:
avant la section d'application:
uses-permission Android:name="com.mindspiker.Tester.MyService.TEST"
uses-permission Android:name="Android.permission.BROADCAST_STICKY"
section d'application intérieure
service Android:name=".MyService"
Si vous créez un arrière-plan Service
qui fait tout le travail lourd (demandes/réponses tcp, unmarshalling), les View
et Activity
peuvent être détruits et recréés sans fuite de fenêtre ni perte de données. Cela permet le comportement recommandé Android, qui consiste à détruire une activité à chaque changement de configuration (par exemple, à chaque changement d'orientation).
C'est un peu plus complexe, mais c'est le meilleur moyen d'invoquer une requête serveur, un pré/post-traitement de données, etc.
Vous pouvez même utiliser votre Service
pour mettre chaque requête en file d'attente sur un serveur, ce qui simplifie et accélère le traitement de ces choses.
Le guide de développement a un complet chapitre sur Services
.
Si vous conservez deux dispositions, tous les threads d'interface utilisateur doivent être terminés.
Si vous utilisez AsynTask, vous pouvez facilement appeler la méthode .cancel()
à l'intérieur de la méthode onDestroy()
de l'activité en cours.
@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();
}
Pour AsyncTask, consultez la section "Annulation d’une tâche" à la section ici .
Mise à jour: Condition supplémentaire permettant de vérifier l'état, car elle ne peut être annulée que si elle est en cours d'exécution. Notez également que la AsyncTask ne peut être exécutée qu'une fois.
Voici ma solution proposée:
void showProgressDialog()
peut être ajoutée à l'interface du programme d'écoute fragment-activity à cette fin.Je suis un plus frais dans Android et j'ai essayé cela et cela a fonctionné.
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);
}
}
J'ai tout essayé. Passé des jours à expérimenter. Je ne voulais pas empêcher l'activité de tourner. Mon scénario était:
Le problème était que, lors de la rotation de l'écran, chaque solution du livre échouait. Même avec la classe AsyncTask, qui est la manière correcte de traiter Android. Lors de la rotation de l'écran, le contexte actuel avec lequel le thread de départ travaille est parti et le champ de dialogue affiché s'affiche. Le problème résidait toujours dans le dialogue, quel que soit le nombre de trucs ajoutés au code (passage de nouveaux contextes aux threads en cours d'exécution, conservation de l'état des threads par rotations, etc.). La complexité du code à la fin était toujours énorme et il y avait toujours quelque chose qui pouvait mal tourner.
La seule solution qui a fonctionné pour moi a été l'astuce Activité/Dialogue. C'est simple et génial et tout est en rotation:
Au lieu de créer un dialogue et de le montrer, créez une activité définie dans le manifeste avec Android: theme = "@ Android: style/Theme.Dialog". Donc, cela ressemble à un dialogue.
Remplacez showDialog (DIALOG_ID) par startActivityForResult (yourActivityDialog, votre code);
Utilisez onActivityResult dans l'activité appelante pour obtenir les résultats du thread en cours d'exécution (même les erreurs) et mettre à jour l'interface utilisateur.
Sur votre 'ActivityDialog', utilisez des threads ou AsyncTask pour exécuter des tâches longues et onRetainNonConfigurationInstance pour enregistrer l'état "dialogue" lors de la rotation de l'écran.
C'est rapide et fonctionne bien. J'utilise toujours des boîtes de dialogue pour d'autres tâches et l'AsyncTask pour quelque chose qui n'exige pas de dialogue constant à l'écran. Mais avec ce scénario, je choisis toujours le modèle Activité/Dialogue.
Et je ne l'ai pas essayé, mais il est même possible d'empêcher la rotation de cette activité/de ce dialogue lorsque le thread est en cours d'exécution, ce qui accélère les choses tout en permettant à l'activité d'appel d'effectuer une rotation.
De nos jours, il existe une manière beaucoup plus distincte de traiter ces types de problèmes. L'approche typique est:
Tout ce qui est un processus en arrière-plan doit être conservé dans un Fragment
(à définir avec Fragment.setRetainInstance()
. Ceci devient votre 'stockage de données persistant' où toutes les données que vous souhaitez conserver sont conservées. Après le changement d'orientation événement, cette Fragment
sera toujours accessible dans son état d'origine via un appel FragmentManager.findFragmentByTag()
(lorsque vous le créez, vous devez lui attribuer une balise et non un ID car il n'est pas attaché à une View
).
Reportez-vous au développé) du guide --- (Gestion des modifications d'exécution pour savoir comment le faire correctement et pourquoi il s'agit de la meilleure option.
Vous devez inverser votre processus de création de liens. Au moment où votre processus d'arrière-plan s'attache à un View
- votre View
devrait plutôt s'attacher au processus d'arrière-plan. C'est plus logique non? L'action de View
dépend du processus d'arrière-plan, alors que le processus d'arrière-plan ne dépend pas de View
, ce qui signifie que le lien est remplacé par une interface standard Listener
. Dites que votre processus (quelle que soit la classe - qu’il s’agisse d’une AsyncTask
, Runnable
ou d’une chose quelconque) définit un OnProcessFinishedListener
, lorsque le processus est terminé, il doit appeler cet écouteur s’il existe.
This answer est une description concise de Nice sur la façon de créer des écouteurs personnalisés.
Maintenant, vous devez vous préoccuper de l’interfaçage de la tâche d’arrière-plan avec votre structure actuelle View
. Si vous gérez correctement vos changements d’orientation (pas le configChanges
hack que les gens recommandent toujours), votre Dialog
sera recréé. par le système. Ceci est important, cela signifie que lors du changement d'orientation, toutes vos méthodes de cycle de vie de Dialog
sont rappelées. Donc, dans l’une quelconque de ces méthodes (onCreateDialog
est généralement un bon endroit), vous pouvez effectuer un appel comme suit:
DataFragment f = getActivity().getFragmentManager().findFragmentByTag("BACKGROUND_TAG");
if (f != null) {
f.mBackgroundProcess.setOnProcessFinishedListener(new OnProcessFinishedListener() {
public void onProcessFinished() {
dismiss();
}
});
}
Reportez-vous à la section cycle de vie d'un fragment pour déterminer où le réglage de l'auditeur convient le mieux à votre implémentation individuelle.
Ceci est une approche générale pour fournir une solution robuste et complète au problème générique posé dans cette question. Selon votre scénario, il manque probablement quelques éléments mineurs dans cette réponse, mais il s'agit généralement de l'approche la plus appropriée pour gérer correctement les événements de changement d'orientation.
C'est une très vieille question qui a été soulevée dans l'encadré pour une raison quelconque.
Si la tâche en arrière-plan doit uniquement survivre tant que l'activité est au premier plan, la "nouvelle" solution consiste à héberger le fil en arrière-plan (ou, de préférence, AsyncTask
) dans un conservé. fragment , comme décrit dans ce guide du développeur et nombreuses questions/réponses .
Un fragment conservé survit si l'activité est détruite pour un changement de configuration, mais pas lorsque l'activité est détruite en arrière-plan ou dans la pile arrière. Par conséquent, la tâche en arrière-plan doit toujours être interrompue si isChangingConfigurations()
est false dans onPause()
.
J'ai fait face à la même situation. Ce que j'ai fait est d'obtenir une seule instance pour mon dialogue de progression dans l'application entière.
Tout d'abord, j'ai créé une classe DialogSingleton pour obtenir une seule instance (modèle Singleton)
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();
}
}
}
Comme je le montre dans cette classe, j'ai le dialogue de progression en tant qu'attribut. Chaque fois que je dois afficher une boîte de dialogue de progression, je récupère l'instance unique et crée un nouveau ProgressDialog.
DialogSingleton.GetInstance().DialogShow(this, "My title here!");
Lorsque j'ai terminé la tâche en arrière-plan, je rappelle l'instance unique et ferme sa boîte de dialogue.
DialogSingleton.GetInstance().DialogDismiss(this);
Je sauvegarde le statut de la tâche en arrière-plan dans mes préférences partagées. Lorsque je fais pivoter l'écran, je demande si une tâche est en cours d'exécution pour cette activité: (onCreate)
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).
Lorsque je commence à exécuter une tâche en arrière-plan:
preference.AddValue(IS_TASK_NAME_EXECUTED_KEY, true, "boolean");
DialogSingleton.GetInstance().DialogShow(this, "My title here!");
Quand j'ai fini d'exécuter une tâche en arrière-plan:
preference.AddValue(IS_TASK_NAME_EXECUTED_KEY, false, "boolean");
DialogSingleton.GetInstance().DialogDismiss(ActivityName.this);
J'espère que ça aide.
j'ai trouvé et une solution plus facile à gérer les threads lorsque l'orientation change. Vous pouvez simplement garder une référence statique à votre activité/fragment et vérifier si sa valeur est nulle avant d'agir sur l'interface utilisateur. Je suggère d'utiliser un essai essayer aussi:
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;
}
}
Cela semble beaucoup trop "rapide et sale" pour être vrai, alors veuillez signaler les défauts, mais ce que j'ai trouvé marchait, c'était ...
Dans la méthode onPostExecute de mon AsyncTask, j'ai simplement enveloppé le '.dismiss' pour la boîte de dialogue de progression dans un bloc try/catch (avec un crochet vide), puis j'ai simplement ignoré l'exception qui a été déclenchée. Cela semble faux, mais il ne semble pas y avoir d'effet néfaste (du moins pour ce que je fais par la suite, qui consiste à démarrer une autre activité en transmettant le résultat de ma longue requête en tant qu'Extra).
La solution la plus simple et la plus flexible consiste à utiliser un AsyncTask avec une référence statique à ProgressBar . Ceci fournit une solution encapsulée et donc réutilisable aux problèmes de changement d’orientation. Cette solution m’a bien servi pour diverses tâches asynchrones, notamment les téléchargements sur Internet, la communication avec Services et les analyses du système de fichiers. La solution a été bien testée sur plusieurs versions Android et modèles de téléphones. Une démo complète peut être trouvée ici avec un intérêt spécifique pour DownloadFile.Java
Je présente ce qui suit comme exemple de concept
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();
}
}
}
Utilisation dans un Android L'activité est simple
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();
}
}
Si vous avez du mal à détecter les événements de changement d'orientation d'un dialogue INDÉPENDANT D'UNE RÉFÉRENCE D'ACTIVITÉ, cette méthode fonctionne très bien. J'utilise ceci parce que j'ai ma propre classe de dialogue qui peut être affichée dans plusieurs activités différentes, de sorte que je ne sais pas toujours en quelle activité il est affiché. et vous n'avez pas besoin d'un dialogue personnalisé (comme je l'ai). Cependant, vous avez besoin d'une vue de contenu personnalisée pour pouvoir détecter les modifications d'orientation à l'aide de cette vue particulière. Voici mon exemple:
public class MyContentView extends View{
public MyContentView(Context context){
super(context);
}
@Override
public void onConfigurationChanged(Configuration newConfig){
super.onConfigurationChanged(newConfig);
//DO SOMETHING HERE!! :D
}
}
Dialog dialog = new Dialog(context);
//set up dialog
dialog.setContentView(new MyContentView(context));
dialog.show();
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();
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();