Android Dev Summit, October 23-24: two days of technical content, directly from the Android team. Sign-up for livestream updates.

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

Los dispositivos Android no solo vienen con pantallas de diferentes tamaños (teléfonos celulares, tablets, TV, etc.), sino que sus pantallas también tienen píxeles de distintos tamaños. Es decir, hay dispositivos que tienen 160 píxeles por pulgada cuadrada y otros que adaptan 480 píxeles en el mismo espacio. Si no tienes en cuenta estas variaciones de densidad de píxeles, es posible que el sistema escale tus imágenes (y se vean borrosas) o que estas se muestren en el tamaño incorrecto.

En esta página, se explica cómo diseñar tu app para que admita diferentes densidades de píxeles mediante las unidades de medida independientes de la resolución y los recursos de mapa 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 cómo diseñar elementos de íconos reales, consulta los lineamientos sobre íconos de material design.

Cómo utilizar píxeles independientes de la densidad

Lo primero que debes evitar es 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 puede corresponder a diferentes tamaños físicos en distintos dispositivos.

Figura 1: Dos pantallas del mismo tamaño pueden tener diferentes cantidades de píxeles

Para que el tamaño de tu IU se mantenga visible en pantallas con distintas densidades, debes diseñar tu IU con píxeles independientes de la densidad (dp) como unidad de medida. Un dp es una unidad de píxel virtual que prácticamente equivale a un píxel en una pantalla de densidad media (160 dpi; la densidad "de referencia"). Android traduce este valor a la cantidad apropiada de píxeles reales para cada densidad.

Por ejemplo, observa los dos dispositivos en la figura 1. Si tuvieras que establecer que una vista tenga "100 píxeles" de ancho, se vería mucho más grande en el dispositivo del lado izquierdo. Por lo tanto, debes usar "100dp" para garantizar que se muestre con el mismo tamaño en ambas pantallas.

Sin embargo, al definir tamaños de texto, debes usar píxeles escalables (sp) como unidades (pero nunca para los tamaños del diseño). De manera predeterminada, la unidad de sp tiene el mismo tamaño que la de dp, pero se cambia en función del tamaño de texto que prefiere el usuario.

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

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

Para especificar tamaño de texto, usa siempre 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, necesitarás expresar las dimensiones en dp y, luego, convertirlas a píxeles. La conversión de unidades dp a píxeles de pantalla es simple:

px = dp * (dpi / 160)

Imagina una app en la que un gesto de desplazamiento o lanzamiento se reconozca después de que el dedo del usuario haya recorrido al menos 16 píxeles. En una pantalla de referencia, el dedo del usuario debe desplazarse 16 pixels / 160 dpi, lo que equivale a un décimo de pulgada (o 2.5 mm) para que se reconozca el gesto. En un dispositivo con una pantalla de alta densidad (240 dpi), el usuario debe desplazarse 16 pixels / 240 dpi, lo que equivale a un quinceavo de pulgada (o 1.7 mm). La distancia es mucho más corta y, por lo tanto, la app le resulta más sensible al usuario.

Para solucionar este problema, el umbral del gesto debe expresarse en dp en el código y, luego, convertirse a píxeles reales. Por ejemplo:

Kotlin

    // The gesture threshold expressed in dp
    private const val GESTURE_THRESHOLD_DP = 16.0f
    ...
    private var mGestureThreshold: Int = 0
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Get the screen's density scale
        val scale: Float = resources.displayMetrics.density
        // Convert the dps to pixels, based on density scale
        mGestureThreshold = (GESTURE_THRESHOLD_DP * scale + 0.5f).toInt()

        // Use mGestureThreshold as a distance in pixels...
    }
    

Java

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

    // Get the screen's density scale
    final float scale = getResources().getDisplayMetrics().density;
    // Convert the dps to pixels, based on density scale
    mGestureThreshold = (int) (GESTURE_THRESHOLD_DP * scale + 0.5f);

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

En el campo DisplayMetrics.density, se especifica el factor de escalamiento que debes usar para convertir unidades de dp a píxeles, de acuerdo con la densidad de píxeles actual. En una pantalla de densidad media, DisplayMetrics.density equivale a 1.0; en una de densidad alta, equivale a 1.5; en una de densidad muy alta, equivale a 2.0; y en una de densidad baja, equivale a 0.75. Esta cifra es el factor por el cual debes multiplicar las unidades de dp a fin de obtener la cantidad real de píxeles para 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 marco de trabajo como umbral de desplazamiento se puede obtener con getScaledTouchSlop():

Kotlin

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

Java

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

Se garantiza que los métodos en ViewConfiguration que comienzan con el prefijo getScaled muestran un valor en píxeles que se verá de manera correcta independientemente de la densidad de píxeles actual.

Cómo proporcionar mapas de bits alternativos

Para proporcionar gráficos de buena calidad en dispositivos con diferentes densidades de píxeles, debes brindar 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 deberá escalar tu mapa de bits para que ocupe el mismo espacio visible en cada pantalla, lo que generará artefactos de escalamiento como imágenes borrosas.

Figura 2: Tamaños relativos de mapas de bits en diferentes tamaños de densidad

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; esta 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; de aproximadamente 213 dpi. Este no se considera un grupo de densidad "principal". Se usa más que nada para televisiones, y la mayoría de las apps no lo necesitarán ya que con recursos mdpi y hdpi será suficiente; el sistema los escalará según corresponda. Si crees que es necesario proporcionar recursos tvdpi, debes ajustar su tamaño con un factor de 1.33*mdpi. Por ejemplo, una imagen de 100 x 100 píxeles para pantallas mdpi debe ser de 133 x 133 píxeles para tvdpi.

Si deseas crear elementos de diseño alternativos de mapa de bits para diferentes densidades, debes seguir la proporció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 diferentes deben ser 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)

Luego, ubica los archivos de imágenes generados en el subdirectorio correspondiente en res/ y el sistema seleccionará el archivo correcto automáticamente en función de la densidad de píxeles del dispositivo en el que se ejecuta tu app:

    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
    

De esta manera, cada vez que hagas referencia a @drawable/awesomeimage, el sistema seleccionará el mapa de bits apropiado en función de la densidad de la pantalla. Si no proporcionas un recurso específico de densidad para ese valor, el sistema seleccionará la mejor opción y la escalará para que se adapte a la pantalla.

Sugerencia: Si tienes algunos recursos de elementos de diseño que el sistema no debería escalar nunca (tal vez porque tú mismo realizas ajustes en la imagen durante el tiempo de ejecución), debes ubicarlos 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 Cómo proporcionar recursos.

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

Al igual que todos los otros elementos de mapas de bits, debes proporcionar versiones específicas de densidad del ícono de tu app. Sin embargo, los selectores de apps muestran el ícono de tu app como máximo un 25% más grande que lo que requiere el intervalo de densidad del dispositivo.

Por ejemplo, si el intervalo 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á y hará que se vea menos nítido. Por lo tanto, debes proporcionar un ícono de selector de mayor densidad en el directorio mipmap-xxxhdpi para que el selector pueda usar el elemento xxxhdpi en su lugar.

Como el ícono de tu app puede escalarse hacia arriba de esta manera, debes ubicar todos los íconos en directorios mipmap en lugar de drawable. A diferencia del directorio drawable, todos los directorios mipmap se retienen en el APK si compilas APK específicos de densidad. De esta manera, el selector de apps puede elegir 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
    

Para obtener los lineamientos de diseño de íconos, consulta la guía del material sobre íconos.

Para obtener información sobre cómo crear íconos de apps, consulta Cómo crear íconos de apps con Image Asset Studio.

Cómo usar gráficos vectoriales en su lugar

Como alternativa a la creación de varias versiones específicas de densidad de una imagen, puedes crear un gráfico vectorial. Estos crean una imagen mediante XML para definir rutas y colores, en lugar de usar mapas de bits de píxeles. Los gráficos vectoriales pueden escalarse en cualquier tamaño sin artefactos de escalamiento, aunque suelen funcionar mejor para ilustraciones como íconos, no así para fotos.

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

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

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

    Figura 3: Se importa un archivo SVG con Android Studio

    Es posible que aparezcan algunos errores en la ventana de Asset Studio que indiquen que algunas propiedades del archivo no son compatibles con los elementos de diseño vectoriales. Sin embargo, estos no impedirán que realices la importación, ya que las propiedades no compatibles simplemente se ignoran.

  4. Haz clic en Siguiente.

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

    Como un elemento de diseño vectorial puede usarse en todas las densidades de píxeles, este archivo se ubica en el directorio de elementos de diseño predeterminado (no necesitas 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, consulta la documentación sobre Elementos de diseño vectoriales.

Sugerencias para problemas de densidad poco comunes

En esta sección, se proporciona una descripción adicional sobre cómo Android realiza el escalamiento para mapas de bits en diferentes densidades de píxeles y cómo puedes controlar la manera en que los mapas de bits se dibujan en diferentes densidades. A menos que tu app manipule gráficos o que tengas problemas para ejecutarla en diferentes densidades de píxeles, puedes ignorar esta sección.

Para entender mejor la manera de admitir varias densidades cuando manipulas gráficos durante el tiempo de ejecución, debes tener claro que el sistema ayuda a garantizar la escala apropiada para los mapas de bits de las siguientes maneras:

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

    En función de la densidad de la pantalla actual, el sistema usa cualquier recurso específico de densidad de tu app. Si no hay recursos disponibles en la densidad correcta, el sistema carga los recursos predeterminados y los escala hacia arriba o hacia abajo, según corresponda. El sistema asume 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 ajustará esos mapas de bits al tamaño adecuado 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 escala 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 quizá no te convenga que Android escale un recurso previamente. La manera más sencilla de evitar el escalamiento previo es incluir 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. Escalamiento automático de dimensiones y coordenadas de píxeles

    Puedes inhabilitar el escalamiento previo de imágenes y dimensiones si estableces android:anyDensity en "false" en el manifiesto, o de manera programática para un Bitmap si defines 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. De esta manera, garantiza que los elementos de pantalla definidos por píxeles se muestren aproximadamente en el mismo tamaño físico que tendrían 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 escalados a la app, en lugar de las dimensiones de píxeles físicos.

    Por ejemplo, supongamos que un dispositivo tiene una pantalla de densidad alta WVGA, que tiene una resolución de 480 x 800 y aproximadamente el mismo tamaño que una pantalla HVGA tradicional, pero ejecuta una app con escalamiento previo inhabilitado. En este caso, el sistema le "mentirá" a la app cuando esta consulte las dimensiones de la pantalla y le informará que es de 320 x 533 (la traducción de mdpi aproximada para la densidad de píxeles). Entonces, cuando la app realice las operaciones de dibujo, como la invalidación del rectángulo de (10, 10) a (100, 100), el sistema transformará las coordenadas al realizar el escalamiento correspondiente y, luego, invalidará la región (15, 15) a (150, 150). Esta discrepancia puede generar un comportamiento inesperado si tu app manipula de manera directa el mapa de bits escalado, pero se considera como un intercambio razonable para que el rendimiento de las apps sea el máximo posible. Si tienes este problema, consulta Cómo convertir unidades de dp a unidades de píxeles.

    Por lo general, no se debe inhabilitar el escalamiento previo. La mejor manera para admitir varias pantallas es seguir las técnicas básicas que se describen en este documento.

Si tu app manipula mapas de bits o interactúa de manera directa con píxeles en la pantalla de alguna otra manera, tal vez debas realizar pasos adicionales para admitir diferentes densidades de píxeles. Por ejemplo, si respondes a gestos táctiles al contar 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. Puedes convertir valores de dp a px fácilmente.

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

Es importante que pruebes tu app en varios dispositivos con diferentes densidades de píxeles para asegurarte de que tu IU se escale correctamente. Es fácil probarla en un dispositivo físico, pero también puedes hacerlo en el emulador de Android si no tienes acceso a dispositivos físicos de todas las densidades de píxeles.

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