android - rotación - rotacion de pantalla samsung j2
¿Cómo manejar el cambio de orientación de la pantalla cuando el diálogo de progreso y el hilo de fondo están activos? (26)
Mi programa realiza alguna actividad de red en un hilo de fondo. Antes de comenzar, aparece un diálogo de progreso. El diálogo se descarta en el manejador. Todo esto funciona bien, excepto cuando la orientación de la pantalla cambia mientras el cuadro de diálogo está activo (y el hilo de fondo va). En este punto, la aplicación se bloquea o se bloquea, o se encuentra en una etapa extraña donde la aplicación no funciona en absoluto hasta que se eliminan todos los subprocesos.
¿Cómo puedo manejar el cambio de orientación de la pantalla con gracia?
El siguiente código de muestra coincide aproximadamente con lo que hace mi programa real:
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();
}
};
}
Apilar:
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)
He intentado descartar el cuadro de diálogo de progreso en onSaveInstanceState, pero eso evita un bloqueo inmediato. El subproceso de fondo todavía está en curso, y la interfaz de usuario está en un estado parcialmente dibujado. Necesito matar toda la aplicación antes de que vuelva a funcionar.
Cuando cambias de orientación, Android creará una nueva Vista. Es probable que tengas bloqueos porque tu hilo de fondo está intentando cambiar el estado en el anterior. (También puede estar teniendo problemas porque su hilo de fondo no está en el hilo de la interfaz de usuario)
Sugeriría hacer que mHandler sea volátil y actualizarlo cuando cambie la orientación.
Descubrí una solución a esto que todavía no he visto en ningún otro lugar. Puede usar un objeto de aplicación personalizado que sepa si tiene tareas en segundo plano, en lugar de intentar hacer esto en la actividad que se destruye y se recrea al cambiar de orientación. He blogeado sobre esto here .
El problema original percibido era que el código no sobreviviría a un cambio de orientación de la pantalla. Aparentemente, esto se "resolvió" haciendo que el programa manejara el cambio de orientación de la pantalla, en lugar de dejar que el marco de la interfaz de usuario lo hiciera (mediante la llamada a OnDestroy).
Yo diría que si el problema subyacente es que el programa no sobrevivirá en Destruir (), entonces la solución aceptada es solo una solución que deja al programa con otros problemas y vulnerabilidades graves. Recuerde que el marco de Android establece específicamente que su actividad corre el riesgo de ser destruida casi en cualquier momento debido a circunstancias fuera de su control. Por lo tanto, su actividad debe poder sobrevivir a OnDestroy () y subsiguiente a onCreate () por cualquier motivo, no solo un cambio de orientación de la pantalla.
Si va a aceptar los cambios en la orientación de la pantalla de manejo para resolver el problema del OP, debe verificar que otras causas de onDestroy () no generen el mismo error. ¿Eres capaz de hacer esto? Si no es así, me preguntaría si la respuesta "aceptada" es realmente muy buena.
El truco consiste en mostrar / descartar el diálogo dentro de AsyncTask durante onPreExecute / onPostExecute como de costumbre, aunque en caso de cambio de orientación, cree / muestre una nueva instancia del diálogo en la actividad y pase su referencia a la tarea.
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;
}
}
}
Lo he hecho así:
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 {
}
}
}
}
También puedes intentar y hacerme saber que te funciona o no.
Me encontré con el mismo problema. Mi actividad necesita analizar algunos datos de una URL y es lenta. Así que creo un hilo para hacerlo y luego muestro un diálogo de progreso. Dejo que el hilo publique un mensaje de vuelta al hilo de la interfaz de usuario a través del controlador cuando haya terminado. En Handler.handleMessage, obtengo el objeto de datos (listo ahora) del subproceso y lo relleno en la interfaz de usuario. Así que es muy similar a tu ejemplo.
Después de muchas pruebas y errores, parece que encontré una solución. Al menos ahora puedo rotar la pantalla en cualquier momento, antes o después de que el hilo haya terminado. En todas las pruebas, el diálogo está correctamente cerrado y todos los comportamientos son los esperados.
Lo que hice se muestra a continuación. El objetivo es completar mi modelo de datos (mDataObject) y luego rellenarlo en la interfaz de usuario. Debe permitir la rotación de la pantalla en cualquier momento sin sorpresa.
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.
}
}
Eso es lo que funciona para mí. No sé si este es el método "correcto" diseñado por Android; afirman que "destruir / recrear la actividad durante la rotación de la pantalla" en realidad facilita las cosas, así que supongo que no debería ser demasiado complicado.
Déjame saber si ves un problema en mi código. Como se dijo anteriormente, realmente no sé si hay algún efecto secundario.
Me enfrenté a este mismo problema, y se me ocurrió una solución que no se mezclaba con el ProgressDialog y obtenía resultados más rápidos.
Lo que hice fue crear un diseño que tiene una barra de progreso en ella.
<?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>
Luego en el método onCreate haz lo siguiente
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.progress);
}
Luego, haga la tarea larga en un hilo, y cuando termine, haga que Runnable configure la vista de contenido al diseño real que desea utilizar para esta actividad.
Por ejemplo:
mHandler.post(new Runnable(){
public void run() {
setContentView(R.layout.my_layout);
}
});
Esto es lo que hice y descubrí que se ejecuta más rápido que con el ProgressDialog, es menos intrusivo y, en mi opinión, tiene un mejor aspecto.
Sin embargo, si desea utilizar el ProgressDialog, esta respuesta no es para usted.
Mi solución fue extender la clase ProgressDialog
para obtener mi propio MyProgressDialog
.
Redefiní los métodos de show()
y dismiss()
para bloquear la orientación antes de mostrar el Dialog
y desbloquearlo cuando se descarta el Dialog
. Por lo tanto, cuando se muestra el Dialog
y la orientación del dispositivo cambia, la orientación de la pantalla permanece hasta que se llama a la función de dismiss()
, luego la orientación de la pantalla cambia de acuerdo con los valores del sensor / orientación del dispositivo.
Aquí está mi código:
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);
}
}
Mueve la tarea larga a una clase separada. Implementarlo como un patrón sujeto-observador. Siempre que se cree la actividad, regístrese y, al cerrar, cancele el registro con la clase de tarea. La clase de tarea puede usar AsyncTask.
Se me ocurrió una solución sólida para estos problemas que se ajusta a la "forma de Android" de las cosas. Tengo todas mis operaciones de larga duración utilizando el patrón IntentService.
Ese es el intento de transmisión de mis actividades, IntentService hace el trabajo, guarda los datos en la base de datos y luego transmite los intentos de STICKY.
La parte adhesiva es importante, por lo que incluso si la Actividad se detuvo durante el tiempo después de que el usuario inició el trabajo y pierde la transmisión en tiempo real del ItentService, podemos responder y recoger los datos de la Actividad de llamada.
ProgressDialogs
puede trabajar en este patrón muy bien con onSaveInstanceSate()
.
Básicamente, debe guardar un indicador de que tiene un cuadro de diálogo de progreso ejecutándose en el paquete de instancia guardado. NO guarde el objeto de diálogo de progreso porque esto filtrará toda la actividad.
Para tener un identificador persistente en el cuadro de diálogo de progreso, lo guardo como una Referencia débil en el objeto de la aplicación.
Al cambiar la orientación o cualquier otra cosa que haga que la Actividad se detenga (llamada telefónica, el usuario llega a casa, etc.) y luego se reanuda, descarto el diálogo anterior y vuelvo a crear un nuevo diálogo en la Actividad recién creada.
Para los diálogos de progreso indefinido esto es fácil. Para el estilo de la barra de progreso, debe colocar el último progreso conocido en el paquete y la información que esté utilizando localmente en la actividad para realizar un seguimiento del progreso.
Al restaurar el progreso, utilizará esta información para volver a generar la barra de progreso en el mismo estado que antes y luego actualizar según el estado actual de las cosas.
Entonces, para resumir, poner las tareas de larga duración en un IntentService junto con el uso juicioso de onSaveInstanceState()
permite realizar un seguimiento eficiente de los diálogos y restaurarlos durante los eventos del ciclo de vida de la actividad. Los bits relevantes del código de actividad se encuentran a continuación. También necesitarás lógica en tu BroadcastReceiver para manejar apropiadamente los intentos de Sticky, pero eso está fuera del alcance de esto.
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));
}
}
Voy a contribuir con mi enfoque para manejar este problema de rotación. Esto puede no ser relevante para OP, ya que no está usando AsyncTask
, pero tal vez otros lo encuentren útil. Es bastante simple pero parece hacer el trabajo por mí:
Tengo una actividad de inicio de sesión con una clase de AsyncTask
anidada llamada BackgroundLoginTask
.
En mi BackgroundLoginTask
no hago nada fuera de lo normal, excepto para agregar un cheque nulo al llamar ProgressDialog
despido de ProgressDialog
:
@Override
protected void onPostExecute(Boolean result)
{
if (pleaseWaitDialog != null)
pleaseWaitDialog.dismiss();
[...]
}
Esto es para manejar el caso en el que la tarea en segundo plano finaliza mientras la Activity
no está visible y, por lo tanto, el método onPause()
ya ha descartado el diálogo de progreso.
A continuación, en mi clase de Activity
principal, creo AsyncTask
globales estáticos para mi clase de AsyncTask
y mi ProgressDialog
(el AsyncTask
, al estar anidado, puede acceder a estas variables):
private static BackgroundLoginTask backgroundLoginTask;
private static ProgressDialog pleaseWaitDialog;
Esto sirve para dos propósitos: primero, permite que mi Activity
acceda siempre al objeto AsyncTask
incluso desde una actividad nueva, post-rotada. En segundo lugar, permite que mi BackgroundLoginTask
acceda y cierre el ProgressDialog
incluso después de una rotación.
A continuación, agrego esto a onPause()
, lo que hace que el cuadro de diálogo de progreso desaparezca cuando nuestra Activity
está abandonando el primer plano (lo que impide que se produzca un "bloqueo de fuerza")
if (pleaseWaitDialog != null)
pleaseWaitDialog.dismiss();
Finalmente, tengo lo siguiente en mi método onResume()
:
if ((backgroundLoginTask != null) && (backgroundLoginTask.getStatus() == Status.RUNNING))
{
if (pleaseWaitDialog != null)
pleaseWaitDialog.show();
}
Esto permite que el Dialog
de Dialog
vuelva a aparecer después de recrear la Activity
.
Aquí está toda la clase:
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();
}
}
}
}
}
No soy un desarrollador de Android experimentado, así que siéntete libre de comentar.
Edición: los ingenieros de Google no recomiendan este enfoque, como lo describe Dianne Hackborn (también hackbod como hackbod ) en esta publicación de . Echa un vistazo a esta publicación del blog para más información.
Tienes que añadir esto a la declaración de actividad en el manifiesto:
android:configChanges="orientation|screenSize"
por lo que parece
<activity android:label="@string/app_name"
android:configChanges="orientation|screenSize|keyboardHidden"
android:name=".your.package">
La cuestión es que el sistema destruye la actividad cuando se produce un cambio en la configuración. Ver ConfigurationChanges .
Por lo tanto, colocar eso en el archivo de configuración evita que el sistema destruya su actividad. En su lugar, invoca el onConfigurationChanged(Configuration)
.
Esta es mi solución propuesta:
- Mueva la AsyncTask o Thread a un Fragmento retenido, como se explica aquí . Creo que es una buena práctica mover todas las llamadas de la red a fragmentos. Si ya está utilizando fragmentos, uno de ellos podría hacerse responsable de las llamadas. De lo contrario, puede crear un fragmento solo para hacer la solicitud, como lo propone el artículo vinculado.
- El fragmento utilizará una interfaz de escucha para indicar la finalización / falla de la tarea. No tienes que preocuparte por los cambios de orientación allí. El fragmento siempre tendrá el enlace correcto a la actividad actual y el diálogo de progreso se puede reanudar de forma segura.
- Haz que tu diálogo de progreso sea un miembro de tu clase. De hecho deberías hacer eso para todos los diálogos. En el método onPause, debe descartarlos, de lo contrario perderá una ventana en el cambio de configuración. El estado ocupado debe mantenerse por el fragmento. Cuando el fragmento se adjunta a la actividad, puede volver a abrir el cuadro de diálogo de progreso, si la llamada aún se está ejecutando. Se
void showProgressDialog()
puede agregar un método a la interfaz de escucha de fragmentos de actividad para este propósito.
Esta es una pregunta muy antigua que surgió en la barra lateral por alguna razón.
Si la tarea en segundo plano solo necesita sobrevivir mientras la actividad está en primer plano, la solución "nueva" es hospedar el hilo en segundo plano (o, preferiblemente, AsyncTask
) en un fragmento retenido , como se describe en esta guía del desarrollador y en numerosas preguntas y respuestas .
Un fragmento retenido sobrevive si la actividad se destruye por un cambio de configuración, pero no cuando la actividad se destruye en el fondo o en la contraportada. Por lo tanto, la tarea en segundo plano aún se debe interrumpir si isChangingConfigurations()
es falso onPause()
.
Parece demasiado "rápido y sucio" para ser verdad, así que, por favor, señale las fallas, pero lo que encontré funcionó fue ...
Dentro del método onPostExecute de mi AsyncTask, simplemente envolví ''.dismiss'' para el cuadro de diálogo de progreso en un bloque try / catch (con un catch vacío) y luego simplemente ignoré la excepción que se generó. Parece mal hacerlo pero parece que no hay efectos adversos (al menos por lo que estoy haciendo posteriormente, que es comenzar otra actividad pasando el resultado de mi consulta de larga duración como Extra)
Si mantiene dos diseños, todos los subprocesos de la interfaz de usuario deben terminarse.
Si usa AsynTask, entonces puede llamar fácilmente al .cancel()
método dentro del onDestroy()
método de actividad actual.
@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();
}
Para AsyncTask, lea más en la sección "Cancelar una tarea" here .
Actualización: condición agregada para verificar el estado, ya que solo se puede cancelar si está en estado de ejecución. También tenga en cuenta que la AsyncTask solo se puede ejecutar una vez.
Soy más fresco en Android y probé esto y ha funcionado.
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);
}
}
Intenté implementar la solución de jfelectron porque es una " solución sólida para estos problemas que se ajusta a la" forma de Android "de las cosas " pero tomó un tiempo buscar y reunir todos los elementos mencionados. Terminé con esta solución ligeramente diferente, y me parece más elegante, publicada aquí en su totalidad.
Utiliza un IntentService activado desde una actividad para realizar la tarea de ejecución prolongada en un subproceso separado. El servicio devuelve Intentos de transmisión a la actividad que actualiza el diálogo. La Actividad utiliza showDialog (), onCreateDialog () y onPrepareDialog () para eliminar la necesidad de que se pasen datos persistentes en el objeto de la aplicación o en el paquete savedInstanceState. Esto debería funcionar sin importar cómo se interrumpa su aplicación.
Clase de actividad:
public class TesterActivity extends Activity {
private ProgressDialog mProgressDialog;
private static final int PROGRESS_DIALOG = 0;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
Button b = (Button) this.findViewById(R.id.test_button);
b.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
buttonClick();
}
});
}
private void buttonClick(){
clearPriorBroadcast();
showDialog(PROGRESS_DIALOG);
Intent svc = new Intent(this, MyService.class);
startService(svc);
}
protected Dialog onCreateDialog(int id) {
switch(id) {
case PROGRESS_DIALOG:
mProgressDialog = new ProgressDialog(TesterActivity.this);
mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
mProgressDialog.setMax(MyService.MAX_COUNTER);
mProgressDialog.setMessage("Processing...");
return mProgressDialog;
default:
return null;
}
}
@Override
protected void onPrepareDialog(int id, Dialog dialog) {
switch(id) {
case PROGRESS_DIALOG:
// setup a broadcast receiver to receive update events from the long running process
IntentFilter filter = new IntentFilter();
filter.addAction(MyService.BG_PROCESS_INTENT);
registerReceiver(new MyBroadcastReceiver(), filter);
break;
}
}
public class MyBroadcastReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {
if (intent.hasExtra(MyService.KEY_COUNTER)){
int count = intent.getIntExtra(MyService.KEY_COUNTER, 0);
mProgressDialog.setProgress(count);
if (count >= MyService.MAX_COUNTER){
dismissDialog(PROGRESS_DIALOG);
}
}
}
}
/*
* Sticky broadcasts persist and any prior broadcast will trigger in the
* broadcast receiver as soon as it is registered.
* To clear any prior broadcast this code sends a blank broadcast to clear
* the last sticky broadcast.
* This broadcast has no extras it will be ignored in the broadcast receiver
* setup in onPrepareDialog()
*/
private void clearPriorBroadcast(){
Intent broadcastIntent = new Intent();
broadcastIntent.setAction(MyService.BG_PROCESS_INTENT);
sendStickyBroadcast(broadcastIntent);
}}
IntentService Class:
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);
}
}}
Entradas de archivo de manifiesto:
Antes de la sección de aplicación:
uses-permission android:name="com.mindspiker.Tester.MyService.TEST"
uses-permission android:name="android.permission.BROADCAST_STICKY"
dentro de la sección de aplicaciones
service android:name=".MyService"
En estos días hay una manera mucho más clara de manejar este tipo de problemas. El enfoque típico es:
1. Asegúrese de que sus datos estén correctamente separados de la interfaz de usuario:
Todo lo que sea un proceso en segundo plano debe conservarse Fragment
(configúrelo con Fragment.setRetainInstance()
. Esto se convierte en su ''almacenamiento de datos persistentes'' donde se conserva cualquier información basada en los datos que le gustaría conservar. Después del evento de cambio de orientación, esto Fragment
aún será accesible en su original Estado a través de una FragmentManager.findFragmentByTag()
llamada (cuando la creas, debes darle una etiqueta, no una ID, ya que no está adjunta a View
).
Consulte la guía desarrollada de Handling Runtime Changes para obtener información sobre cómo hacerlo correctamente y por qué es la mejor opción.
2. Asegúrese de que está interactuando de forma correcta y segura entre los procesos en segundo plano y su interfaz de usuario:
Debe revertir su proceso de vinculación. En el momento en que su proceso en segundo plano se adjunta a sí mismo View
, en su lugar, View
debería estar adjuntándose a sí mismo en el proceso en segundo plano. Tiene más sentido ¿no? La View
acción de ''depende del proceso en segundo plano, mientras que el proceso en segundo plano no depende de View
.Esto significa cambiar el enlace a una Listener
interfaz estándar . Diga que su proceso (cualquiera sea la clase que sea, ya sea una AsyncTask
, Runnable
o lo que sea) define a OnProcessFinishedListener
, cuando el proceso se realiza, debe llamar a ese oyente si existe.
Esta answer es una buena descripción concisa de cómo hacer escuchas personalizados.
3. Vincule su UI al proceso de datos siempre que se cree la UI (incluidos los cambios de orientación):
Ahora debe preocuparse de interconectar la tarea en segundo plano con cualquiera que sea su View
estructura actual . Si está manejando los cambios de orientación correctamente (no configChanges
siempre lo recomiendan los piratas), entonces Dialog
el sistema los recreará. Esto es importante, significa que en el cambio de orientación, Dialog
se recuperan todos los métodos del ciclo de vida. Entonces, en cualquiera de estos métodos ( onCreateDialog
generalmente es un buen lugar), podría hacer una llamada como la siguiente:
DataFragment f = getActivity().getFragmentManager().findFragmentByTag("BACKGROUND_TAG");
if (f != null) {
f.mBackgroundProcess.setOnProcessFinishedListener(new OnProcessFinishedListener() {
public void onProcessFinished() {
dismiss();
}
});
}
Vea el Ciclo de vida del fragmento para decidir dónde se ajusta mejor el oyente en su implementación individual.
Este es un enfoque general para proporcionar una solución robusta y completa al problema genérico que se plantea en esta pregunta. Probablemente falten algunas piezas menores en esta respuesta, dependiendo de su situación individual, pero este es generalmente el enfoque más correcto para manejar adecuadamente los eventos de cambio de orientación.
He encontrado una solución más fácil para manejar los hilos cuando cambia la orientación. Solo puede mantener una referencia estática a su actividad / fragmento y verificar si es nulo antes de actuar en la interfaz de usuario. Sugiero usar una captura de prueba también:
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;
}
}
La solución más simple y flexible es usar una here con una referencia estática a ProgressBar . Esto proporciona una solución encapsulada y, por lo tanto, reutilizable para los problemas de cambio de orientación. Esta solución me ha servido para realizar diversas tareas asíncronas, incluidas descargas de Internet, comunicación con Services y análisis de sistemas de archivos. La solución ha sido bien probada en múltiples versiones de Android y modelos de teléfonos. Una demostración completa se puede encontrar here con interés específico en DownloadFile.java
Presento lo siguiente como ejemplo de concepto
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();
}
}
}
El uso en una actividad de Android es 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();
}
}
Lo he intentado TODO. Pasé días experimentando. No quería bloquear la actividad de la rotación. Mi escenario fue:
- Un diálogo de progreso que muestra información dinámica al usuario. Por ejemplo: "Conectando al servidor ...", "Descargando datos ...", etc.
- Un hilo haciendo las cosas pesadas y actualizando el diálogo.
- Actualización de la interfaz de usuario con los resultados al final.
El problema era que, al girar la pantalla, todas las soluciones del libro fallaban. Incluso con la clase AsyncTask, que es la forma correcta de Android para hacer frente a estas situaciones. Cuando se gira la pantalla, el contexto actual con el que se está trabajando el hilo de inicio se ha ido y eso confunde con el diálogo que se muestra. El problema siempre fue el diálogo, sin importar cuántos trucos agregué al código (pasando nuevos contextos a la ejecución de hilos, reteniendo los estados de los hilos mediante rotaciones, etc.). La complejidad del código al final siempre fue enorme y siempre había algo que podía salir mal.
La única solución que me funcionó fue el truco Actividad / Diálogo. Es simple y genial y está todo a prueba de rotación:
En lugar de crear un diálogo y pedir que lo muestre, cree una actividad que se haya establecido en el manifiesto con android: theme = "@ android: style / Theme.Dialog". Por lo tanto, sólo parece un diálogo.
Reemplace showDialog (DIALOG_ID) con startActivityForResult (yourActivityDialog, yourCode);
Utilice onActivityResult en la actividad de llamada para obtener los resultados del subproceso en ejecución (incluso los errores) y actualizar la interfaz de usuario.
En su ''ActivityDialog'', use hilos o AsyncTask para ejecutar tareas largas y onRetainNonConfigurationInstance para guardar el estado de "diálogo" cuando gire la pantalla.
Esto es rápido y funciona bien. Sigo usando diálogos para otras tareas y AsyncTask para algo que no requiere un diálogo constante en la pantalla. Pero con este escenario, siempre voy por el patrón de Actividad / Diálogo.
Y no lo intenté, pero incluso es posible bloquear esa Actividad / Diálogo para que no gire cuando el hilo se está ejecutando, lo que acelera las cosas y permite que la Actividad de la llamada gire.
Me enfrenté a la misma situación. Lo que hice fue obtener solo una instancia para mi diálogo de progreso en toda la aplicación.
Primero, creé una clase DialogSingleton para obtener solo una instancia (patrón 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();
}
}
}
Como muestro en esta clase, tengo el diálogo de progreso como atributo. Cada vez que necesito mostrar un diálogo de progreso, obtengo la instancia única y creo un nuevo ProgressDialog.
DialogSingleton.GetInstance().DialogShow(this, "My title here!");
Cuando haya terminado con la tarea en segundo plano, vuelvo a llamar a la instancia única y salgo de su diálogo.
DialogSingleton.GetInstance().DialogDismiss(this);
Guardo el estado de la tarea de fondo en mis preferencias compartidas. Cuando roto la pantalla, pregunto si tengo una tarea en ejecución para esta actividad: (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).
Cuando empiezo a ejecutar una tarea en segundo plano:
preference.AddValue(IS_TASK_NAME_EXECUTED_KEY, true, "boolean");
DialogSingleton.GetInstance().DialogShow(this, "My title here!");
Cuando termine de ejecutar una tarea en segundo plano:
preference.AddValue(IS_TASK_NAME_EXECUTED_KEY, false, "boolean");
DialogSingleton.GetInstance().DialogDismiss(ActivityName.this);
Espero que ayude.
Si crea un fondo Service
que hace todo el trabajo pesado (solicitudes TCP / respuesta, unmarshalling), el View
y Activity
puede ser destruido y re-creado sin fugas ventana o pérdida de datos. Esto permite el comportamiento recomendado de Android, que es destruir una Actividad en cada cambio de configuración (por ejemplo, para cada cambio de orientación).
Es un poco más complejo, pero es la mejor manera de invocar la solicitud del servidor, el procesamiento previo / posterior de los datos, etc.
Incluso puede usar su Service
para poner en cola cada solicitud a un servidor, por lo que es fácil y eficiente manejar esas cosas.
La guía de desarrollo tiene un capítuloServices
completo en .
Si tiene dificultades para detectar eventos de cambio de orientación de un diálogo INDEPENDIENTE DE UNA REFERENCIA DE ACTIVIDAD , este método funciona muy bien. Uso esto porque tengo mi propia clase de diálogo que se puede mostrar en múltiples actividades diferentes, por lo que no siempre sé en qué actividad se muestra. Con este método no es necesario cambiar el AndroidManifest, preocuparse por las referencias de la actividad. y no necesitas un diálogo personalizado (como el que tengo). Sin embargo, sí necesita una vista de contenido personalizada para poder detectar los cambios de orientación utilizando esa vista en particular. Aquí está mi ejemplo:
Preparar
public class MyContentView extends View{
public MyContentView(Context context){
super(context);
}
@Override
public void onConfigurationChanged(Configuration newConfig){
super.onConfigurationChanged(newConfig);
//DO SOMETHING HERE!! :D
}
}
Implementación 1 - Diálogo
Dialog dialog = new Dialog(context);
//set up dialog
dialog.setContentView(new MyContentView(context));
dialog.show();
Implementación 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();
Implementación 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();
Tengo una implementación que permite destruir la actividad en un cambio de orientación de la pantalla, pero aún así destruye el diálogo en la actividad recreada con éxito. Utilizo ...NonConfigurationInstance
para adjuntar la tarea de fondo a la actividad recreada. El marco normal de Android se encarga de recrear el diálogo en sí mismo, nada se cambia allí.
Subcomité AsyncTask agregando un campo para la actividad ''propietaria'' y un método para actualizar este propietario.
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);
}
...
}
En mi clase de actividad, agregué un campo que se backgroundTask
refiere a la tarea de fondo ''propiedad'', y actualizo este campo usando onRetainNonConfigurationInstance
y 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;
}
...
}
Sugerencias para seguir mejorando:
- Borre la
backgroundTask
referencia en la actividad después de que la tarea haya finalizado para liberar cualquier memoria u otros recursos asociados con ella. - Borre la
ownerActivity
referencia en la tarea de fondo antes de que se destruya la actividad en caso de que no se vuelva a crear inmediatamente. - Cree una
BackgroundTask
interfaz y / o colección para permitir que diferentes tipos de tareas se ejecuten desde la misma actividad propietaria.