Cómo brindar compatibilidad con diferentes densidades de píxeles

Los dispositivos Android no solo tienen diferentes tamaños de pantalla (teléfonos celulares, tablets, TVs, etc.), sino que también tienen pantallas con píxeles de distintos tamaños. Un dispositivo puede tener 160 píxeles por pulgada, mientras que otro dispositivo cabe en 480 píxeles en el mismo espacio. Si no tienes en cuenta estas variaciones en la densidad de píxeles, el sistema podría escalar las imágenes, lo que podría generar imágenes borrosas, o bien que las imágenes aparezcan con un tamaño incorrecto.

En esta página, se muestra cómo puedes diseñar tu app para que sea compatible con diferentes densidades de píxeles mediante el uso de unidades de medida independientes de la resolución y el suministro de recursos de mapas de bits alternativos para cada densidad de píxeles.

Mira el siguiente video para obtener una descripción general de estas técnicas.

Para obtener más información sobre el diseño de elementos de íconos, consulta los lineamientos para íconos de material design.

Cómo utilizar píxeles independientes de la densidad

Evita usar píxeles para definir distancias o tamaños. Definir dimensiones con píxeles es un problema, ya que las diferentes pantallas tienen distintas densidades de píxeles. Por lo tanto, la misma cantidad de píxeles corresponde a diferentes tamaños físicos en distintos dispositivos.

Una imagen que muestra dos ejemplos de pantallas de dispositivos con diferentes densidades
Figura 1: Dos pantallas del mismo tamaño pueden tener una cantidad diferente de píxeles.

Para conservar el tamaño visible de la IU en pantallas con diferentes densidades, diseña la IU con píxeles independientes de la densidad (dp) como unidad de medida. Un dp es una unidad de píxeles virtual que es aproximadamente igual a un píxel en una pantalla de densidad media (160 dpi o la densidad "de referencia"). Android traduce este valor a la cantidad apropiada de píxeles reales para cada densidad.

Ten en cuenta los dos dispositivos de la figura 1. Una vista de 100 píxeles de ancho aparece mucho más grande en el dispositivo de la izquierda. Una vista definida para tener 100 dp de ancho aparece del mismo tamaño en ambas pantallas.

En cambio, cuando defines tamaños de texto, puedes usar píxeles escalables (sp) como unidades. De forma predeterminada, la unidad de sp tiene el mismo tamaño que la de un dp, pero cambia de tamaño en función del tamaño de texto que prefiere el usuario. Nunca uses sp para los tamaños del diseño.

Por ejemplo, para especificar el espaciado entre dos vistas, usa dp:

<Button android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/clickme"
    android:layout_marginTop="20dp" />

Para especificar el tamaño del texto, usa sp:

<TextView android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textSize="20sp" />

Cómo convertir unidades de dp a unidades de píxeles

En algunos casos, debes expresar las dimensiones en dp y, luego, convertirlas en píxeles. La conversión de unidades dp a píxeles de pantalla es la siguiente:

px = dp * (dpi / 160)

Nota: Nunca codifiques esta ecuación para calcular píxeles. En su lugar, usa TypedValue.applyDimension(), que convierte muchos tipos de dimensiones (dp, sp, etc.) en píxeles.

Imagina una app en la que un gesto de desplazamiento o lanzamiento se reconozca después de que el dedo del usuario haya movido al menos 16 píxeles. En una pantalla de referencia, el dedo del usuario debe mover 16 pixels / 160 dpi, que equivale a 1/10 de pulgada (o 2.5 mm), antes de que se reconozca el gesto.

En un dispositivo con una pantalla de alta densidad (240 dpi), el dedo del usuario debe mover 16 pixels / 240 dpi, que equivale a 1/15 de pulgada (o 1.7 mm). La distancia es mucho más corta y, por lo tanto, la app parece más sensible para el usuario.

Para solucionar este problema, expresa el umbral de gestos en el código en dp y, luego, conviértelo a píxeles reales. Por ejemplo:

Kotlin

// The gesture threshold expressed in dp
private const val GESTURE_THRESHOLD_DP = 16.0f

private var gestureThreshold: Int = 0

// Convert the dps to pixels, based on density scale
gestureThreshold = TypedValue.applyDimension(
  COMPLEX_UNIT_DIP,
  GESTURE_THRESHOLD_DP + 0.5f,
  resources.displayMetrics).toInt()

// Use gestureThreshold as a distance in pixels...

Java

// The gesture threshold expressed in dp
private final float GESTURE_THRESHOLD_DP = 16.0f;

// Convert the dps to pixels, based on density scale
int gestureThreshold = (int) TypedValue.applyDimension(
  COMPLEX_UNIT_DIP,
  GESTURE_THRESHOLD_DP + 0.5f,
  getResources().getDisplayMetrics());

// Use gestureThreshold as a distance in pixels...

El campo DisplayMetrics.density especifica el factor de escala que se usa para convertir unidades dp a píxeles según la densidad de píxeles actual. En una pantalla de densidad media, DisplayMetrics.density equivale a 1.0 y en una de densidad alta, a 1.5. En una pantalla de densidad extraalta, equivale a 2.0 y en una de densidad baja, a 0.75. TypedValue.applyDimension() usa esta cifra para obtener la cantidad real de píxeles de la pantalla actual.

Cómo usar valores de configuración escalados previamente

Puedes usar la clase ViewConfiguration para acceder a las distancias, las velocidades y los tiempos comunes que usa el sistema Android. Por ejemplo, la distancia en píxeles que usa el framework como umbral de desplazamiento se puede obtener con getScaledTouchSlop():

Kotlin

private val GESTURE_THRESHOLD_DP = ViewConfiguration.get(myContext).scaledTouchSlop

Java

private final int GESTURE_THRESHOLD_DP = ViewConfiguration.get(myContext).getScaledTouchSlop();

Los métodos en ViewConfiguration que comienzan con el prefijo getScaled garantizarán que se muestre un valor en píxeles que se muestren de forma correcta, independientemente de la densidad de píxeles actual.

Optar por gráficos vectoriales

Una alternativa a crear varias versiones específicas de densidad de una imagen es crear un solo gráfico vectorial. Estos crean una imagen mediante XML para definir rutas y colores, en lugar de usar mapas de bits de píxeles. Por lo tanto, los gráficos vectoriales pueden escalar a cualquier tamaño sin artefactos de escalamiento, aunque, por lo general, son mejores para ilustraciones como íconos y no para fotografías.

Los gráficos vectoriales suelen proporcionarse como archivos SVG (gráficos vectoriales escalables), pero Android no admite este formato, por lo que debes convertirlos al formato de elementos de diseño vectorial de Android.

Puedes convertir un archivo SVG a un elemento de diseño vectorial mediante Vector Asset Studio de Android Studio de la siguiente manera:

  1. En la ventana Project, haz clic con el botón derecho en el directorio res y selecciona New > Vector Asset.
  2. Selecciona Archivo local (SVG, PSD).
  3. Ubica el archivo que quieras importar y realiza las modificaciones necesarias.

    Imagen que muestra cómo importar SVG en Android Studio
    Figura 2: Importación de un SVG con Android Studio.

    Es posible que observes algunos errores en la ventana Asset Studio que indican que los elementos de diseño vectoriales no admiten algunas propiedades del archivo. Esto no te impide importar el archivo; se ignoran las propiedades no compatibles.

  4. Presiona Siguiente.

  5. En la siguiente pantalla, confirma el conjunto de orígenes en donde quieras ubicar el archivo de tu proyecto y haz clic en Finalizar.

    Debido a que un elemento de diseño vectorial se puede usar en todas las densidades de píxeles, este archivo se incluye en el directorio de elementos de diseño predeterminado, como se muestra en la siguiente jerarquía. No es necesario usar directorios específicos de densidad.

    res/
      drawable/
        ic_android_launcher.xml
    

Para obtener más información sobre cómo crear gráficos vectoriales, lee la documentación de elementos de diseño vectoriales.

Cómo proporcionar mapas de bits alternativos

Para proporcionar una buena calidad gráfica en dispositivos con diferentes densidades de píxeles, proporciona varias versiones de cada mapa de bits en tu app (una para cada intervalo de densidad, en la resolución correspondiente). De lo contrario, Android debe escalar tu mapa de bits para que ocupe el mismo espacio visible en cada pantalla, lo que genera artefactos de escalamiento, como difuminado.

Una imagen que muestra los tamaños relativos de mapas de bits en diferentes tamaños de densidad
Figura 3: Tamaños relativos de mapas de bits en intervalos de diferentes densidades.

Hay muchos intervalos de densidad disponibles para usar en tus apps. En la tabla 1, se describen los diferentes calificadores de configuración disponibles y a qué tipos de pantallas se aplican.

Tabla 1: Calificadores de configuración para diferentes densidades de píxeles

Calificador de densidad Descripción
ldpi Recursos para pantallas de densidad baja (ldpi) (120 dpi)
mdpi Recursos para pantallas de densidad media (mdpi) (~160 dpi) Esa es la densidad de referencia.
hdpi Recursos para pantallas de densidad alta (hdpi) (~240 dpi)
xhdpi Recursos para pantallas de densidad muy alta (xhdpi) (~320 dpi)
xxhdpi Recursos para pantallas de densidad muy, muy alta (xxhdpi) (~480 dpi)
xxxhdpi Recursos para usos de densidad extremadamente alta (xxxhdpi) (~640 dpi)
nodpi Recursos para todas las densidades. Estos son recursos independientes de la densidad. El sistema no escala recursos etiquetados con este calificador, independientemente de la densidad de la pantalla actual.
tvdpi Recursos para pantallas entre mdpi y hdpi; aproximadamente, 213 dpi. Este no se considera un grupo de densidad "principal". Se usa principalmente para televisiones, y la mayoría de las apps no lo necesitan; con recursos mdpi y hdpi es suficiente para la mayoría de las apps, y el sistema los ajusta según corresponda. Si resulta necesario proporcionar recursos tvdpi, colócalos con un factor de 1.33 * mdpi. Por ejemplo, una imagen de 100 x 100 píxeles para pantallas mdpi es de 133 x 133 píxeles para tvdpi.

Si deseas crear elementos de diseño alternativos de mapa de bits para diferentes densidades, sigue la relación de escalamiento 3:4:6:8:12:16 entre las seis densidades principales. Por ejemplo, si tienes un elemento de diseño de mapa de bits de 48 x 48 píxeles para pantallas de densidad media, los tamaños son los siguientes:

  • 36 x 36 (0.75x) para densidad baja (ldpi)
  • 48 x 48 (referencia de 1.0x) para densidad media (mdpi)
  • 72 x 72 (1.5x) para densidad alta (hdpi)
  • 96 x 96 (2.0x) para densidad muy alta (xhdpi)
  • 144 x 144 (3.0x) para densidad muy muy alta (xxhdpi)
  • 192 x 192 (4.0x) para densidad extremadamente alta (xxxhdpi)

Coloca los archivos de imagen generados en el subdirectorio correspondiente en res/:

res/
  drawable-xxxhdpi/
    awesome_image.png
  drawable-xxhdpi/
    awesome_image.png
  drawable-xhdpi/
    awesome_image.png
  drawable-hdpi/
    awesome_image.png
  drawable-mdpi/
    awesome_image.png

Luego, cada vez que hagas referencia a @drawable/awesomeimage, el sistema seleccionará el mapa de bits apropiado en función de los DPI de la pantalla. Si no proporcionas un recurso específico de densidad para ese valor, el sistema ubicará la siguiente mejor coincidencia y la ajustará para que se ajuste a la pantalla.

Sugerencia: Si tienes recursos de elementos de diseño que no quieres que el sistema escale, como cuando realizas algunos ajustes en la imagen durante el tiempo de ejecución, colócalos en un directorio con el calificador de configuración nodpi. Los recursos con este calificador se consideran independientes de la densidad y el sistema no los escala.

Para obtener más información sobre otros calificadores de configuración y la manera en que Android selecciona los recursos apropiados para la configuración de pantalla actual, consulta la Descripción general de recursos de la app.

Cómo agregar íconos de apps en los directorios mipmap

Al igual que con otros elementos de mapas de bits, debes proporcionar versiones específicas de densidad del ícono de la app. Sin embargo, algunos selectores de aplicaciones muestran el ícono de la app hasta un 25% más grande que lo que requiere el bucket de densidad del dispositivo.

Por ejemplo, si el bucket de densidad de un dispositivo es xxhdpi y el ícono de app más grande que proporcionas es en drawable-xxhdpi, el selector de apps lo escalará, lo que hará que se vea menos nítido.

Para evitar esto, coloca todos los íconos de tu app en los directorios mipmap en lugar de en los directorios drawable. A diferencia de los directorios drawable, todos los directorios mipmap se retienen en el APK, incluso si compilas APK específicos de densidad. Esto permite que las apps de selector elijan el ícono con mejor resolución para que se muestre en la pantalla principal.

res/
  mipmap-xxxhdpi/
    launcher_icon.png
  mipmap-xxhdpi/
    launcher_icon.png
  mipmap-xhdpi/
    launcher_icon.png
  mipmap-hdpi/
    launcher_icon.png
  mipmap-mdpi/
    launcher_icon.png

En el ejemplo anterior de un dispositivo xxhdpi, puedes proporcionar un ícono de selector de mayor densidad en el directorio mipmap-xxxhdpi.

Para obtener lineamientos de diseño de íconos, consulta Íconos del sistema.

Si quieres obtener ayuda para crear íconos de apps, consulta Cómo crear íconos de apps con Image Asset Studio.

Sugerencias para problemas de densidad poco comunes

En esta sección, se describe la manera en que Android realiza ajustes para mapas de bits en diferentes densidades de píxeles y cómo puedes controlar aún más la manera en que se dibujan los mapas de bits en diferentes densidades. A menos que tu app manipule gráficos o que tengas problemas para ejecutar la app en diferentes densidades de píxeles, puedes ignorar esta sección.

Para comprender mejor la manera de admitir varias densidades cuando se manipulan gráficos durante el tiempo de ejecución, debes conocer la manera en que el sistema ayuda a garantizar la escala adecuada para los mapas de bits. Esto se hace de las siguientes maneras:

  1. Escalamiento previo de los recursos, como los elementos de diseño del mapa de bits

    Según la densidad de la pantalla actual, el sistema usa cualquier recurso específico de densidad de tu app. Si los recursos no están disponibles en la densidad correcta, el sistema carga los recursos predeterminados y los aumenta o reduce según sea necesario. El sistema supone que los recursos predeterminados (los de un directorio sin calificadores de configuración) están diseñados para la densidad de píxeles de referencia (mdpi) y cambia el tamaño de esos mapas de bits al tamaño apropiado para la densidad de píxeles actual.

    Si solicitas las dimensiones de un recurso escalado previamente, el sistema muestra valores que representan las dimensiones después del escalamiento. Por ejemplo, un mapa de bits con un diseño de 50 x 50 píxeles para una pantalla mdpi se ajusta a 75 x 75 píxeles en una pantalla hdpi (si no hay un recurso alternativo para hdpi), y el sistema informa el tamaño como tal.

    Hay algunas situaciones en las que tal vez no te convenga que Android escale un recurso previamente. La forma más sencilla de evitar el ajuste previo es colocar el recurso en un directorio de recursos con el calificador de configuración nodpi. Por ejemplo:

    res/drawable-nodpi/icon.png

    Cuando el sistema usa el mapa de bits icon.png de esta carpeta, no lo escala según la densidad del dispositivo actual.

  2. Ajuste de escala automático de dimensiones y coordenadas de píxeles

    Puedes inhabilitar las dimensiones y las imágenes con ajuste de escala previo si configuras android:anyDensity en "false" en el manifiesto, o bien, de manera programática para Bitmap, estableciendo inScaled en "false". En este caso, el sistema escala automáticamente cualquier coordenada absoluta de píxeles y valor de dimensión de píxeles en el momento de la representación. Hace esto para garantizar que los elementos de pantalla definidos por píxeles se muestren aproximadamente en el mismo tamaño físico que puedan mostrarse en la densidad de píxeles de referencia (mdpi). El sistema controla este escalamiento de forma transparente en la app y, luego, informa las dimensiones de píxeles ajustados a la app, en lugar de las dimensiones de píxeles físicos.

    Por ejemplo, supongamos que un dispositivo tiene una pantalla de alta densidad WVGA, que tiene 480 x 800 y aproximadamente el mismo tamaño que una pantalla HVGA tradicional, pero ejecuta una app que inhabilitó el ajuste previo. En este caso, el sistema "mienta" a la app cuando consulta las dimensiones de pantalla e informa 320 x 533, la traducción de mdpi aproximada para la densidad de píxeles.

    Luego, cuando la app realiza operaciones de dibujo, como la invalidación de un rectángulo de (10, 10) a (100, 100), el sistema transforma las coordenadas ajustándolas a la cantidad adecuada y, en realidad, invalida la región (15,15) a (150, 150). Esta discrepancia puede generar un comportamiento inesperado si tu app manipula directamente el mapa de bits ajustado, pero esto se considera un intercambio razonable para garantizar el mejor rendimiento posible de la app. Si te encuentras con esta situación, lee Cómo convertir unidades dp en unidades de píxeles.

    Por lo general, no se inhabilita el ajuste de escala previo. La mejor manera de admitir varias pantallas es seguir las técnicas básicas que se describen en esta página.

Si tu app manipula mapas de bits o interactúa de manera directa con píxeles en la pantalla de alguna otra manera, es posible que debas realizar pasos adicionales para admitir diferentes densidades de píxeles. Por ejemplo, si respondes a gestos táctiles contando la cantidad de píxeles que recorre un dedo, debes usar los valores de píxeles independientes de la densidad adecuados, en lugar de píxeles reales. Sin embargo, puedes convertir valores de dp a px.

Cómo probar la app en todas las densidades de píxeles

Prueba tu app en varios dispositivos con diferentes densidades de píxeles para asegurarte de que la IU se escale correctamente. Cuando sea posible, realiza pruebas en un dispositivo físico. Si no tienes acceso a dispositivos físicos de todas las densidades de píxeles, usa Android Emulator.

Si quieres realizar pruebas en dispositivos físicos, pero no quieres comprarlos, puedes usar Firebase Test Lab para acceder a dispositivos en el centro de datos de Google.