VISITAS:

lunes, 22 de octubre de 2012

ANDROID: Soporte para múltiples pantallas

Conceptos

  • Screen size (tamaño de pantalla): Es es tamaño físico de la pantalla, medido en pulgadas en su diagonal. Android agrupa todos los tamaños de pantalla en cuatro grupos: small, normal, large, extra-large.
  • Screen density (densidad de pantalla): Es la cantidad de pixels en un área física de la pantalla, dpi (dots per inch). Android agrupa las densidades de pantalla en cuatro grupos: low, medium, high y extra-high.
  • Orientation (orientación): Es la orientación de la pantalla desde el punto de vista del usuario. Puede ser landscape (paisaje, más ancho que alto) o portrait (retrato, más alto que ancho). La orientación puede cambiar en runtime cuando el usuario rota el dispositivo.
  • Resolution (resolución): Es el número total de pixels en la pantalla. Las aplicaciones no deberían tratar directamente con la resolución, sino con el tamaño de la pantalla y con la densidad de pixels (según los grupos de tamaños y resoluciones).
  • Density-independent pixels (dp, pixels independientes de la densidad): Es una unidad virtual de pixels que se usa cuando se definen layuts de UI. Se utiliza para expresar las dimensiones y/o posiciones de una forma independiente de la densidad. Un dp es equivalente a un pixel físico en una pantalla de 160 dpi, perteneciente al grupo de densidad medium. En runtime, el sistema maneja el escalado de los dp según sea necesario, para adaptarlos a la densidad real de la pantalla que se esté usando. La conversión de dp a pixels es muy sencilla: px = dp * dpi / 160. Siempre se deben utilizar dp cuando se definen las dimensiones y las posiciones en la UI para asegurar la visualización correcta en pantallas con diferentes densidades.

Rangos de pantallas soportados

Para simplificar el diseño de interfaces de usuario para múltiples tipos de pantallas, Android agrupa los rangos de tamaños y densidades de pantalla en:
  • Tamaños: small, normal, large, extra-large

  • Densidades: ldpi, mdpi, hdpi, xhdpi

La configuración base de pantalla es size=normal y density=medium.
Cuando se diseñan interfaces de usuario, se descubre que cada diseño requiere una cantidad mínima de espacio. Este tamaño mínimo se expresa en unidades dp.
Para optimizar una aplicación se suelen definir diferentes diseños para los diferentes tamaños y para las diferentes densidades. Normalmente se definen layouts alternativos para los distintos tamaños de pantalla (small, normal, large, xlarge) y bitmaps alternativos para las distintas densidades (ldpi, mdpi, hdpi, xhdpi). En runtime, Android utiliza los recursos más adecuados para cada caso.

Ejemplo: Múltiples densidades de pantalla

Imaginemos 3 dispositivos: A, B y C con las siguientes resoluciones y tamaños:
  • Dispositivo A: 6 x 8 pulgadas (diagonal = 10 pulgadas), 6 x 8 pixels. Este dispositivo tiene una densidad de 1 dpi
  • Dispositivo B: 6 x 8 pulgadas (diagonal = 10 pulgadas), 12 x 16 pixels. Este dispositivo tiene una densidad de 2 dpi
  • Dispositivo C: 6 x 8 pulgadas (diagonal = 10 pulgadas), 24 x 32 pixels. Este dispositivo tiene una densidad de 4 dpi
Los tres dispositivos tienen el mismo tamaño físico. Sin embargo, cada uno tiene una densidad de pixels distinta. El dispositivo A se podría considerar de baja densidad (ldpi), el B de densidad media (mdpi) y el C de alta densidad (hdpi). Nota: Esto es un ejemplo para comprender bien el problema con números pequeños; en la realidad una densidad media mdpi sería de unos 160 dpi.
Si ponemos un botón en la posición 0,0 de tamaño 4 x 4 pixels en cada uno de los dispositivos, obtendríamos los siguientes resultados:
  • En el dispositivo A, el botón ocuparía 4 x 4 pixels, o sea, 4 x 4 pulgadas.
  • En el dispositivo B, el botón ocuparía igualmente 4 x 4 pixels, pero en este caso serían 2 x 2 pulgadas, o sea, sería la mitad de grande que el mismo botón en el dispositivo A.
  • En el dispositivo C, el botón ocuparía los mismos 4 x 4 pixels, pero en este caso, el tamaño sería de 1 x 1 pulgada.
Como se puede observar, si expresamos el tamaño del  botón en pixels, tendremos un tamaño distinto en cada dispositivo, según la densidad de cada uno de estos. Siendo más grande cuanto menor es la densidad. Además, en cada caso, el espacio sobrante para otras vistas es distinto en cada dispositivo.

Vamos a colocar un botón en la posición 0,0 en los tres dispositivos. Pero ahora vamos a expresar el tamaño del botón en dp, no en pixels. Las dimensiones del botón serán 4 x 4 dp.
Antes de nada tenemos que definir la densidad de pixels de referencia (baseline). En los dispositivos android, esta densidad es de 160 dpi. Sin embargo en nuestro ejemplo vamos a considerar 2 dpi como la densidad de referencia. Entonces, si expresamos los tamaños en dp, el sistema android tendrá que calcular los pixels reales en función de la densidad de pixels del dispositivo. La fórmula, como vimos en el apartado anterior es: px = dp x dpi / 2
  • En el dispositivo A, el botón ocuparía 4 x 1 / 2 = 2 pixels, o sea, 2 pulgadas.
  • En el dispositivo B, el botón ocuparía 4 x 2 / 2 = 4 pixels, que también corresponden a 2 pulgadas.
  • En el dispositivo C, el botón ocuparía 4 x 4 / 2 = 8 pixels, 2 pulgadas igualmente.

Ahora el botón ocupa el mismo tamaño en todos los dispositivos y por tanto el espacio sobrante para otras vistas es lógicamente el mismo en todos los dispositivos.

Se considera que una aplicación es independiente de la densidad cuando preserva los tamaños físicos de los elementos de UI cuando se visualiza en pantallas de distintas densidades. Esta condición es muy importante, porque de no cumplirse, un elemento tal como un botón aparecerá más grande en pantallas de baja densidad y más pequeño en pantallas de alta densidad.

Como conclusión: Si expresamos las posiciones y tamaños en dp, resolvemos el problema que surge con dispositivos de distintas densidades de pixels.

Sin embargo, en el caso de los bitmaps, aunque expresemos su tamaño en dp, el sistema android intentará escalar el bitmap al tamaño físico adecuado. Pero este escalado a veces da como resultado imágenes borrosas y/o pixeladas. Esto se debe a que el escalado de un bitmap produce los siguientes efectos:

  • Ampliación: la imagen se pixela debido a que cada pixel de la imagen tiene que ocupar 2 o más pixels (dependiendo del factor de ampliación)
  • Reducción: la imagen se emborrona debido a que desaparecen pixels de la imagen (para reducir su tamaño)

Para evitar este problema con los bitmaps, es conveniente proporcionar una versión de cada bitmap para las distintas densidades. Se suele utilizar el siguiente criterio:

  • Bitmap para mdpi: 100%
  • Bitmap para hdpi: 150%
  • Bitmap para xhdpi: 200%
  • Bitmap para ldpi: 75%

Esto significa que si tenemos un bitmap de por ejemplo 100x100 pixels para mdpi, entonces deberíamos proporcionar otra versión del mismo bitmap a 150x150 pixels para hdpi, otra versión a 200x200 pixels para xhdpi y otra versión de 75x75 pixels para ldpi. Físicamente, todos se mostrarán con el mismo tamaño, pero no aparecerán los efectos de pixelado (en hdpi y xhdpi) ni de imagen borrosa (en ldpi).

El siguiente problema que se nos plantea es con dispositivos de distinto tamaño físico.

Soporte de múltiples tamaños y densidades de pantalla

Por defecto, Android renderiza el layout de la aplicación escalando bitmaps y layouts en función de la densidad y el tamaño de la pantalla. Sin embargo, esto puede no ser suficiente. A veces los programadores tienen que hacer algo también:

  • Declarar en el manifest los tamaños de pantalla que soporta la aplicación: de esta forma nos aseguramos que la aplicación sólo se ejecutará en los dispositivos con las pantallas soportadas. Para ello se utiliza el tag supports-screens  del fichero manifest.
  • Proporcionar diferentes layouts para diferentes tamaños de pantalla. Por defecto, Android re-escala el layout de la aplicación para encajarlo en el tamaño de pantalla actual. Esto funciona bien en algunos casos, pero no siempre. Por ejemplo, en una pantalla grande (como la de una tablet) suele ser interesante reorganizar los elementos aprovechando el mayor espacio disponible. Los layouts para los distintos tamaños de pantalla se colocan en los directorios: layout-small, layout-normal, layout-large, layout-xlarge
  • Proporcionar diferentes bitmaps para distintas densidades: Por defecto, Android escala los bitmaps de forma que tengan el tamaño deseado en función de la densidad del dispositivo. Por ejemplo, si sólo proporcionamos un bitmap para mdpi, cuando la aplicación se ejecute en hdpi, el sistema agrandará el bitmap. Y cuando se ejecute en ldpi, entonces el sistema reducirá el bitmap. Estos escalados pueden producir pixelado e imágenes borrosas. Para evitar estos fenómenos, lo mejor es proporcionar bitmaps para los distintos grupos de densidades. Estas versiones se colocarán en los directorios drawable-ldpi, drawable-mdpi, drawable-hdpi y drawable-xhdpi. Cuando no existe versión de un bitmap para una densidad determinada, el sistema busca la versión que más se acerque a la densidad del dispositivo actual.
El formato para los directorios de layouts y bitmaps es:

        resource-qualifier

El resource puede ser drawable o layout.
El qualifier puede ser:

  • Tamaño: small, normal, large, xlarge
  • Densidad: ldpi, mdpi, hdpi, xhdpi
  • Orientación: land, port
Los qualifiers se pueden concatenar con un guión. Por ejemplo, layout-small-port para un layout en pantallas de tamaño small y orientación portrait.
Si no se usa qualifier se supone que es para el  valor por defecto: normal y mdpi.

Diseño de layouts y drawables alternativos

Los tipos de recursos alternativos que se deberían crear dependen de las necesidades de la aplicación. Normalmente:
  • para los layouts se proporcionan los qualifiers para tamaño (small, normal, large, xlarge) y para orientación (land, port)
  • para los bitmaps se proporcionan los qualifiers para densidad (ldpi, mdpi, hdpi, xhdpi)

Layouts alternativos

Cuando se prueba la aplicación en diversos tamaños de pantalla y densidades es cuando se detecta la necesidad o no de proporcionar layouts alternativos. Por ejemplo:
  • Probando en una pantalla size=small, se descubre que  no caben todos los componentes (por ejemplo, una fila de botones). En este caso se debería proporcionar un layout para pantallas small que ajuste el tamaño y/o posición de los botones.
  • Probando en una pantalla size=xlarge, se descubre que sobra mucho espacio. En este caso se debería proporcionar un layout para pantallas xlarge con un rediseño de los componentes para aprovechar el espacio en una pantalla grande. Si dejamos que el sistema redimensione los componentes para adaptarse a una pantalla grande, el usuario tendrá una mala experiencia con la aplicación. Es mucho mejor hacer un diseño especial para pantallas grandes.
  • Probando en orientation=land, se descubre que algunos elementos que estaban en la parte inferior de la pantalla no aparecen.
En resumen, hay que diesñar para size=normal, y luego probar la aplicación en:
  • size=small
  • size=large
  • orientation=land
  • orientation=port

Drawables alternativos

Todas las aplicaciones deberían tener drawables alternativos para las diferentes densidades. El drawable más importante es el icono launcher, que debe mostrarse perfectamente en todas las densidades.
Los bitmaps deberían seguir las siguientes escalas:

ldpi > mdpi > hdpi > xhdpi
3 > 4 > 6 > 8
75% > 100% > 150% > 200%

Consejos y buenas prácticas

El objetivo de soportar múltiples pantallas es crear aplicaciones que funcionen adecuadamente en todos los grupos de tamaños, orientaciones y densidades que soporta Android.

1. Usar wrap_content, match_parent o dp para dimensiones

Se deben utilizar estos valores para los atributos layout_width y layout_height
Igualmente, se debe utilizar dp para el tamaño de los textos.

2. No usar pixels en el código de la aplicación

Los métodos del API de Android que obtienen dimensiones y posiciones retornan los valores en pixels.

3. No utilizar AbsoluteLayout

AbsoluteLayout obliga a utilizar posiciones fijas en las vistas hijas, lo cual provocará que las aplicaciones no funcionen en múltiples configuraciones de pantallas.
Se debería utilizar RelativeLayout, el cual usa posiciones relativas para colocar a sus vistas hijas.



8 comentarios:

  1. Muy bueno, altamente esclarecedor. Gracias.

    ResponderEliminar
    Respuestas
    1. Hola Federico.
      Me alegro que te haya servido.
      Este es uno de los temas que más me costó entender y aplicar en Android.

      Eliminar
  2. Me has solucionado mi problema, he visto más post pero el tuyo esta muy bien explicado y comprendi más a la hora de programar para distintas pantallas, muchas gracias por compartir.

    ResponderEliminar
  3. Muy bueno el aporte, me ha aclarado muchas dudas aunque sigo sin dar solución a mi problema porque en mi caso no puedo modificar el layout, es decir, tango un menú principal compuesto por 9 imágenes (como un grid 3x3). He incluido las imagenes con distintos tamaños en las carpetas ldpi, mdpi,...., y funciona bien en casi todos los casos, pero hay algunos en los que no. Por ejemplo, en las tablet con resoluciones bajas las imagenes aparecen muy pequeñas

    ResponderEliminar
  4. Me as despejado muchísima dudas, la verdad tener que lidiar con tanta cantidad de pantallas no es fácil sino no entiendes el funcionamiento de este. muchas gracias por todo.

    ResponderEliminar
  5. Amigo lei su articulo y todavia estoy en ese proceso de entender esa parte de las imagenes.. estoy haciendo una aplicaion tipo nicho y estoy sufriendo para las imagenes rectangulares que tengo. Agradeceria su ayuda..

    ResponderEliminar