Una de las particularidades del desarrollo de aplicaciones destinadas a dispositivos móviles Android es que el usuario puede cambiar la orientación del dispositivo(vertical-portrait/horizontal-landscape) por lo que nuestras apps se podrán visualizar de dos formas distintas en un mismo dispositivo. Puesto que ambos formatos son muy distintos, si queremos que nuestra app pueda aprovechar al máximo las posibilidades de cada formato deberemos tener dos versiones de la interfaz gráfica (y que serán más si queremos dar un buen soporte tanto a smartphones como a tablets). Veamos un ejemplo sencillo con una app que me encanta:
Se puede apreciar en este ejemplo cómo la interfaz cambia para utilizar mejor el espacio disponible en cada formato (obsérvese la ubicación de los botones). Las otras opciones posibles, y que personalmente creo que devalúa la percepción de calidad de una app, son forzar a que la app siempre se muestre en el mismo formato (algo modificable) o bien permitir la rotación de la interfaz pero sin modificarla y dejando que el SO realize el ajuste y reescalado pertinente.Este es el siguiente caso:
En este artículo se va mostrar cómo podemos gestionar las rotaciones de pantallas y hacer que nuestras apps respondan al cambio.
Entorno de pruebas:
Requisitos: Conocimientos básicos de Android SDK.
Aplicación de pruebas
Como es habitual, vamos a crear un proyecto de pruebas como base para el artículo. La versión actual del plugin de ADT ha mejorado notablemente su asistente de creación de proyectos, pero nosotros sólo necesitamos un proyecto para Android 2.2 que de entrada tendrá una Activity y un layout con un formulario con dos campos y un ListView. Este proyecto «base» tiene la siguiente estructura:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" > <TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:layout_marginTop="5dp" android:text="@string/field1" android:textSize="15sp" /> <EditText android:id="@+id/editText1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignTop="@+id/textView1" android:layout_toRightOf="@+id/textView1" android:ems="10" android:hint="@string/field1"/> <TextView android:id="@+id/textView2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_below="@+id/editText1" android:text="@string/field2" android:textSize="15sp"/> <EditText android:id="@+id/editText2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignLeft="@+id/editText1" android:layout_alignTop="@+id/textView2" android:ems="10" android:hint="@string/field2"/> <ListView android:id="@android:id/list" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_below="@+id/editText2" > </ListView> </RelativeLayout>
package com.danielme.blog.android.displayrotationdemo; import android.os.Bundle; import android.widget.ArrayAdapter; import android.app.ListActivity; public class MainActivity extends ListActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); String[] array = new String[10]; for (int i = 0; i < 10;i++) { array[i] = System.currentTimeMillis() + ""; } super.setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, array)); } }
Funcionamiento «natural» de Android frente a las rotaciones
Al rotar la pantalla, Android reinicia la Activity (onpause–>onstop–>onresume–>ondestroy–>oncreate–>onstart) para aplicar la nueva configuración (en realidad este comportamiento se produce al cambiar la pantalla, el teclado o el idioma). Lo verdaderamente interesante es el hecho de que, de forma totalmente automáticamente y transparente, Android aplicará el layout correspondiente a la nueva orientación de pantalla en el caso de que hayamos definido un layout para cada formato de pantalla. Si sólo tenemos uno, simplemente se aplicará el mismo y se adaptará al nuevo formato. Por ejemplo si se rota el proyecto (Ctrl+F11):
La convención habitual consiste en ubicar dentro del directorio /res/layout las interfaces en formato vertical o portrait y, opcionalmente, en el directorio /res/layout-land, esos layout con el mismo nombre pero adaptados a una presentación en horizontal o landscape. En ambos casos el layout deberá tener el mismo nombre (p.ej. activity_main.xml) para que el cambio sea automático. Obviamente si sólo tenemos una versión de un layout se aplicará siempre la misma.
La organización de los layout en directorios siguiendo las convenciones del sistema es simple pero potente porque permite que una misma app tenga sus interfaces adaptadas a múltiples dispositivos y formatos de visualización. Por ejemplo, en /res/layout-sw600dp/ colocaremos los layout específicos para los dispositivos que tengan al menos 600dp en su dimensión más pequeña tal y como se recomienda para las tablets de 7″.
Como ejemplo, vamos a añadir al proyecto el nuevo layout /res/layout-land/activity_main.xml, que no es más que el layout actual pero con los campos de texto ocupando una sola línea de forma que con la pantalla en horizontal se aproveche mejor el espacio:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" > <EditText android:id="@+id/editText1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:ems="10" android:hint="@string/field1" /> <EditText android:id="@+id/editText2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:ems="10" android:hint="@string/field2" /> </LinearLayout> <ListView android:id="@android:id/list" android:layout_width="match_parent" android:layout_height="wrap_content" > </ListView> </LinearLayout>
Si ejecutamos nuevamente la app, los layout se aplican de forma automática sin modificación alguna de nuestro código.
Este comportamiento por defecto de Android frente al cambio de orientación plantea un problema en nuestro ejemplo: al reiniciarse la Activity se vuelven a obtener los datos del ListView. Esto puede suponer un problema en el caso de que esos datos sean costosos de obtener, por ejemplo provienen directamente de un servicio web, o bien que el ListView sea paginado y el usuario haya cargado más datos antes de rotar la pantalla. Afortunadamente, Android nos proporciona dos opciones a la hora de afrontar este problema, que consiste básicamente en invocar un método antes de ejecutar onDestroy para que guarde los datos que se quieran conservar:
-
Sobreescribir el método onSaveInstanceState(Bundle bundle) y guardar los datos que queramos conservar en el Bundle. Este bundle lo recibimos en el método onCreate(Bundle bundle), por lo que podemos recuperar los datos el crearse de nuevo la Activity y actuar en consecuencia. Esta técnica es práctica para conservar parámetros, pero si lo que queremos recuperar es un objeto la documentación oficial recomienda usar el siguiente método por ser más eficiente.
-
Sobreescribir el método onRetainNonConfigurationInstance() y hacer que devuelva el objeto que queramos conservar. Para recuperarlo una vez se haya «reiniciado» la Activity se invocará getLastNonConfigurationInstance().
Nota: este método está deprecated desde Honeycomb, desde esa versión lo recomendado es usar Fragments y, al cambiar la orientación, “reciclarlos” (se indica en el onCreate de la Activity con la sentencia setRetainInstance(true);).Este aspecto es susceptible de ser tratado en una futura revisión del presente artículo.
Vamos a aplicar la segunda estrategia para conservar los datos que muestra el ListView:
package com.danielme.blog.android.displayrotationdemo; import android.os.Bundle; import android.widget.ArrayAdapter; import android.app.ListActivity; /** * * @author Daniel Medina <http://danielme.com> * */ public class MainActivity extends ListActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); String[] array = null; Object restore = getLastNonConfigurationInstance(); if (restore != null) { array = (String[]) restore; } else { array = new String[10]; for (int i = 0; i < 10;i++) { array[i] = System.currentTimeMillis() + ""; } } super.setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, array)); } @Override public Object onRetainNonConfigurationInstance() { String[] array = new String[getListAdapter().getCount()]; for (int i=0;i < getListAdapter().getCount();i++) { array[i] = (String) getListAdapter().getItem(i); } return array; } }
Si ejecutamos el proyecto, veremos como ahora el contenido del ListView se conserva al rotar.
Por último, un breve apunte sobre los Dialogs. Si mostramos uno cuando rotamos la pantalla, este se cierra y aparentemente no pasa nada pero si nos fijamos en el LogCat observaremos un excepción de este estilo:
android.view.WindowLeaked: Activity com.danielme.blog.android.displayrotationdemo.DefaultActivity has leaked window
Esto es debido a que se ha destruído la Activity a la que pertenece el Dialog y este ha quedado «huérfano». Para evitar este error, tendremos que hacer un dismiss al rotar en el caso de que mostremos un Dialog, por ejemplo en el método onRetainNonConfigurationInstance o en el onPause. Si queremos que el Dialog se vuelva a mostrar tras la rotación, tendríamos que indicárselo al onCreate por ejemplo enviándole un parámetro desde onSaveInstanceState.
Hágalo usted mismo
Existe la posibilidad de evitar este comportamiento por defecto de Android frente a los cambios dinámicos de configuración, simplemente añadiendo en el Manifest a la activity la opción
android:configChanges="orientation|keyboardHidden"
Mucho cuidado con esto!! para que esta técnica funcione si usamos para nuestra app un target superior a Gingerbread (10) hay que usar la siguiente sentencia (y el proyecto sólo compilará contra un API igual o superior a ese target):
android:configChanges="orientation|keyboardHidden|screenSize"
Con esto estamos indicando que el cambio de configuración lo gestionará directamente la app, por lo que la Activity no se reinicia y se invoca el método onConfigurationChanged(Configuration newConfig) que deberemos sobreeescribir para realizar todos los cambios que queramos al cambiarse la orientación del dispositivo.Obsérvese que siguiendo esta estrategia NO se cambia automáticamente el layout en el caso de que se hayan definido layout distintos, por lo que si fuera necesario no quedaría más remedio que hacer estos cambios programáticamente.
Así pues, esta estrategia, que al igual que todo lo que no sea respetar el ciclo de vida de la Activity está desaconsejado por Google, es apropiada para el caso en el que no se proporcione un layout distinto para cada orientación ya que al no reiniciarse la Activity no tendremos que preocuparnos por mantener el estado de la interfaz, en nuestro caso el ListView.Vamos a comprobarlo con la app de ejemplo y añadimos el método onConfigurationChanged, copiándolo directamente de la documentación oficial 🙂
@Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); // Checks the orientation of the screen if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { Toast.makeText(this, "landscape", Toast.LENGTH_SHORT).show(); } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT){ Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show(); } }
Se puede comprobar que, efectivamente, no se cambia el layout y el ListView conserva los datos, así como que se ha ejecutado onConfigurationChanged:
Bloquear la rotación
Por último, comentar que existe la posibilidad de forzar que una Activity siempre se muestre con la misma orientación, aunque como ya he comentado existen apps que permiten evitar estas restricciones. El bloqueo se puede hacer en el manifest en la definición de la activity:
android:screenOrientation="portrait"
o bien
android:screenOrientation="landscape"
Si hiciera falta, también se puede programáticamente:
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
Por mucho que giremos nuestro dispositivo, no cambiara nada ni se invocará método alguno:
Demo completa en GitHub
Se encuentra disponible en GitHub una demo completa de todo lo visto en el presente tutorial. Para más información sobre cómo utilizar GitHub, consultar este artículo.
También se puede descargar directamente el .apk:
Sí señor. Llevo 1 año reciclándome con Android. Tengo un listado de 200 favoritos en el navegador clasificados. Y nunca había encontrado tu página. Y encima en español! xD
Buenísimo el artículo. Sobre todo por el tema de convervar los datos con la rotación. Siempre me supuso un quebradero de cabeza hasta ahora.
Gracias! 😉
Pues si que es un articulo completo, has despejado todas mis dudas sobre el tema de orientación y cambios de pantalla. Muchas gracias. Me subscribo a tu feed automáticamente.
Saludos
Thank you for this. I’m really looking forward to reading the chapter on Honeycomb and how to use Fragments to handle configuration changes.
If you need to save more complex or several sets of data, do you still use onRetainNonConfigurationInstance() ?
When using fragments you can call setRetainInstance(true).
Me da error al crear dos xml con el mismo nombre
Cuando creas el segundo, no le des directamente a Finish. Dale a Next, elige en Avaible Qualifiers -> Otientation y pulsa el botón ->. Te aparecerá en Choosen Qualifiers. Verás que te aparece un desplegable de Screen Orientation. Elige landscape, y de esta forma se te creará en la carpeta correspondiente y con el mismo nombre.
Buen apunte para crear los layouts, aunque personalmente me gusta hacer las cosas a mano y prescindir de los asistentes.
Hola, tengo una Android novo Spark de 9.7″. La aplicación de la cámara original funcionaba excelente pero ahora toma todas las fotos y videos rotados a la izquierda. No sé cómo solucionarlo. Si está en posición plana sobre una mesa los iconos se ven bien pero si la levanto un poco se voltean y empieza a tomar todo chueco. Bajé el programa de Ultimate rotation y nada. Bajé otra app de cámara (la ics) y tampoco se solucionó el problema. De plano ¿Tendré que resetearla de fábrica? Ojalá tengas alguna idea de lo que podría hacer antes de llegar a esto. Aquí screen shots de cómo se ve. Heeeeelp!!! https://www.dropbox.com/s/wf79l1mamqr5uh8/2013_10_03_13.jpg
Pues ni idea, es bastante extraño lo que te está pasando. Me temo que quizás no te quede más remedio que flshearlo.
Sí, eso temí. Ni hablar. Gracias por tu artículo (muy bueno en verdad) y por tu respuesta 😉
gracia hermano me ha servido de mucho ya que logre corregir un error que me daba al rotar la pantalla no acostumbro a dejar comentarios pero me has salvado la verdad lo mereces.. muy bueno y fácil de entender el tutorial.
Que tal buen día, una pregunta, tengo una pantalla bloqueada en vertical, pero lanza una actividad que esta bloqueada en horizontal, el problema que tengo es que cuando regresa a la primera actividad se «relanza» y me reinicia la actividad, hay alguna manera de correguir esto? 😦
ola oigan tengo la carpeta layout-land pero no m toma los xml de esa carpeta, me rota las mismas vistas de la carpeta layout y se muestra muy feo, hice todos los pasos como se muestran arriba pero asi me los mostro si alguien sabe como digan porfas 😀 Gracias
hola , amigo la verdad no entiendo muy bien eso y me gustaría me ayudaras a rotar una apk.
Wow!!! Qué maravilla, concuerdo con algunos de los usuarios, es totalmente dificil encontrar un sitio en español y con soluciones variadas para android, de verdad SE AGRADECE bastante!!! Estoy a punto de llorar de la emoción :’)
Muchas gracias!!!