Saltar a contenido

ELEMENTOS DRAWABLES

Los elementos drawables, cuya traducción literal sería "elementos dibujables", son un tipo de recurso gráfico que puede ser mostrado en pantalla. Se pueden definir tanto en XML como de forma programática mediante código Kotlin, sin embargo, en general se recomienda definirlos en XML. La definición en XML genera elementos dibujables optimizados, en formato vectorial que se verán bien en cualquier tamaño de pantalla, y que además nos permitirán aprovechar los modificadores de la carpeta de recursos (por ejemplo, para que el drawable cambie dependiendo del tamaño o de la orientación de la pantalla).

Podemos utilizar los drawables tanto desde XML como desde código Kotlin. En XML se pueden incluir dentro de los layouts (por ejemplo referenciando el nombre del drawable externo desde los atributos android:drawable, android:background o android:icon) o también dentro de otro recurso tipo drawable (para crear un drawable a partir de otros drawables). En código Kotlin podemos referenciarlos mediante métodos de la API como por ejemplo getDrawable(int).

Los drawables nos permiten personalizar el aspecto de las aplicaciones, por ejemplo cambiar la apariencia de un botón o de cualquier vista, cambiar el fondo, crear formas, gradientes, animaciones, etc. Entre los diferentes tipos de drawables que podemos utilizar encontramos los de tipo básico (definen un drawable a partir de primitivas) y los compuestos (formados a partir de otros drawables), estos son:

Drawables básicos:

  • Color: Rellena el lienzo o el área indicada de un determinado color.
  • Gradiente: Rellena el lienzo o el área indicada con un gradiente de color.
  • Forma (shape): Permite definir primitivas geométricas básicas como por ejemplo rectángulos u óvalos.
  • Imagen (bitmap): Las imágenes también se consideran drawables, por lo que se podrán utilizar de la misma forma que el resto.
  • Nine-patch: Tipo especial de imagen PNG que al ser estirada solo se escala su parte central, pero no su marco.

Drawables compuestos:

  • Lista de capas (layer list): Es un drawable que contiene otros drawables, donde cada uno especifica la posición en la que se ubica dentro de la capa.
  • Lista de estados (state list): Este drawable puede mostrar diferentes drawables según el estado en el que se encuentre el elemento sobre el que se aplica. Por ejemplo, podemos hacer que un botón se muestre de forma distinta según su estado: normal, presionado, inhabilitado, etc.
  • Lista de niveles (level list): Similar al anterior, pero en este caso cada item tiene asignado un valor numérico (nivel) o rango de valores dentro del cual se mostrará. Por lo tanto, permite cambiar el elemento a mostrar dependiendo del valor de la vista sobre la que se aplique.
  • Animación: Define una animación por fotogramas.
  • Transición (transition): Nos permite mostrar una transición de un drawable a otro mediante un fundido.
  • Inserción (inset): Ubica un drawable dentro de otro en la posición especificada.
  • Recorte (clip): Realiza el recorte de un drawable.
  • Escala (scale): Cambia el tamaño de un drawable.

Todos los drawables derivan de la clase Drawable. Esto nos permite utilizarlos de la misma forma, independientemente del tipo del que se trate. A continuación vamos a ver en detalle los distintos tipos de drawables, para qué valen y cómo tenemos que utilizarlos.

COLORES

Los ColorDrawables son los drawables más sencillos, nos permiten definir un color y asignarle un nombre. Se pueden utilizar en cualquier lugar donde se espere un drawable o un color, tanto dentro de otros drawables, como en un layout o desde código Kotlin. Por ejemplo podemos establecer el fondo de un botón o el color del texto de un TextView.

Se definen como un recurso XML dentro del fichero res/values/colors.xml (aunque puede tener otro nombre). Este fichero contendrá una etiqueta <resources> como raíz y una serie de etiquetas <color> para definir los colores. Para cada color indicaremos su nombre mediante el atributo name y el color como valor de la etiqueta, por ejemplo:

<resources>
    <color name="rojo">#FF0000</color>
    <color name="verde">#00FF00</color>
    <color name="azul">#0000FF</color>
</resources>

El valor del color se especifica empezando por el carácter numeral (#) seguido del valor RGB en hexadecimal y opcionalmente el canal alfa. Para este valor se soportan los siguientes formatos: #RGB, #ARGB y #RRGGBB, #AARRGGBB.

Para referenciar un color desde XML tenemos que usar la notación @color/color_name:

<TextView android:textColor="@color/rojo"
          android:text="¡Hola mundo!"/>

También lo podemos referenciar desde código Kotlin mediante R.color.color_name, por ejemplo:

val color = ContextCompat.getColor(context, R.color.rojo);

FORMAS

La etiqueta shape permite crear drawables con formas simples, como por ejemplo un rectángulo o un óvalo, e indicar sus dimensiones, fondo, etc. Cada forma se define en un fichero XML separado dentro de la carpeta /res/drawable/. Como etiqueta raíz del XML tenemos que usar <shape> e indicar mediante su atributo shape el tipo de figura que queremos dibujar. Los tipos que podemos elegir son:

  • rectangle: para formas rectangulares.
  • line: para crear una línea horizontal.
  • oval: para definir formas circulares u ovaladas.
  • ring: para figuras en forma de anillo. Nos permitirá definir el radio interno y la anchura del anillo mediante los atributos innerRadius y thickness, respectivamente. También es posible establecer estos valores en función de la anchura del drawable por medio de los atributos innerRadiusRatio y thicknessRatio.

Dentro de la etiqueta <shape> podemos añadir las siguientes etiquetas para configurar la forma:

  • <stroke>: Permite configurar el borde. Mediante los atributos width y color podemos indicar la anchura y color, y con dashWidth y dashGap podemos crear una línea punteada.
  • <corners>: Permite crear rectángulos con bordes redondeados.
  • <padding>: Define el espaciado interior, desde el borde hasta el contenido.
  • <solid>: mediante su atributo color podremos establecer el color de fondo de la figura.

Opcionalmente podemos utilizar también la etiqueta <size> para definir el tamaño (width y height) de la forma. Sin embargo, esto creará una figura de tamaño fijo que no se adaptará a los distintos tamaños de pantalla o de las vistas sobre las que se aplique. Por lo que solo se recomienda su uso cuando queramos que el tamaño de la forma no cambie.

A continuación se incluye un ejemplo en el que se define una forma rectangular con color de fondo azul, borde rojo de 20dp de grosor, esquinas redondeadas y un espaciado interior de 10dp:

<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <solid android:color="#0000ff"/>
    <stroke android:color="#ff0000" android:width="20dp"/>
    <corners android:radius="20dp"/>
    <padding android:left="10dp"
             android:top="10dp"
             android:right="10dp"
             android:bottom="10dp"/>
</shape>

En este código también podríamos haber hecho referencia a los colores que hemos definido previamente como recursos:

<solid android:color="@color/azul"/>
<stroke android:color="@color/rojo" android:width="20dp"/>

En la siguiente imagen se puede ver el resultado obtenido:

Ejemplo de drawable: forma rectangular

Uso del elemento shape

El nombre del fichero XML se utilizará como identificador del recurso drawable para poder acceder a él. Si por ejemplo guardamos el código del ejemplo anterior dentro del fichero rectangulo.xml, podremos hacer referencia a él desde XML mediante @drawable/rectangulo:

<TextView android:background="@drawable/rectangulo"
          android:layout_height="wrap_content"
          android:layout_width="wrap_content" />

O desde código Kotlin usando R.drawable.rectangulo, por ejemplo:

val rec = ContextCompat.getDrawable(context, R.drawable.rectangulo)

GRADIENTES

Mediante la etiqueta gradient podemos dibujar un gradiente de color dentro de una figura. Esta etiqueta requiere al menos el uso de los atributos startColor y endColor para especificar los colores entre los que se va a realizar la transición. También podemos usar el atributo centerColor para establecer el color intermedio y realizar un gradiente entre tres colores.

Además, con el atributo type podemos cambiar el tipo de gradiente:

  • linear: es el tipo por defecto. Muestra una transición lineal desde startColor a endColor, con un ángulo que puede ser definido mediante el atributo angle.
  • radial: dibuja un gradiente circular en el que se se produce una transición desde startColor en la parte más externa a endColor en el centro. Requiere el uso del atributo gradientRadius para indicar el radio del círculo a través del cual se producirá la transición. También es posible utilizar de manera opcional los atributos centerX y centerY para desplazar el centro del círculo. El atributo gradientRadius está definido en píxeles, por lo que es recomendable definir un drawable de este estilo diferente para cada posible resolución de pantalla.
  • sweep: el gradiente se muestra en forma de barrido en la parte exterior de la figura sobre la que se aplique (normalmente un anillo).

El siguiente código muestra un ejemplo de como definir un gradiente lineal para un rectángulo:

<shape xmlns:android="http://schemas.android.com/apk/res/android"
       android:shape="rectangle">
       <gradient android:type="linear"
                 android:startColor="#ffffff"
                 android:endColor="#ffffff"
                 android:centerColor="#000000"
                 android:angle="45"/>
</shape>

En la siguiente figura se muestra el resultado de tres tipos de gradientes: lineal (el del código de ejemplo), radial y de tipo barrido o sweep.

Ejemplos de drawable a partir de gradientes

IMÁGENES

Otra forma de definir drawables es mediante el uso de imágenes normales de tipo .png (formato preferido), .jpg (aceptable) o .gif (desaconsejado). Al situar una imagen de uno de estos tipos dentro de la carpeta de drawables (/res/drawable o alguna con modificador, por ejemplo /res/drawable-hdpi) se transformará automáticamente en un recurso con el mismo nombre que podrá ser tratado igual que cualquier otro drawable. Por ejemplo, si guardamos en dicho directorio la imagen titulo.png, podremos acceder a ella desde XML mediante @drawable/titulo (sin la extensión):

<ImageView android:layout_height="wrap_content"
           android:layout_width="wrap_content"
           android:src="@drawable/titulo" />

O también desde código Kotlin mediante R.drawable.titulo:

val d = ContextCompat.getDrawable(context, R.drawable.titulo)

Por este motivo es importante que el nombre de la imagen sea un nombre válido de variable.

Bitmaps

En el ejemplo anterior se ha visto cómo cargar una imagen en un Drawable, pero es posible que nos interese trabajar directamente con la clase Bitmap si queremos acceder o modificar el contenido de la imagen.

Para crear un bitmap a partir de un recurso tipo imagen utilizaremos la clase BitmapFactory. Dentro de ella tenemos varios métodos con prefijo decode que nos permiten leer las imágenes de diferentes formas: de un array de bytes en memoria, de un flujo de entrada, de un fichero, de una URL o de un recurso de la aplicación. Por ejemplo, la imagen titulo.png del ejemplo anterior podemos leerla como Bitmap de la siguiente forma:

val imagen = BitmapFactory.decodeResource(resources, R.drawable.titulo)

Donde la variable imagen será de tipo Bitmap y resources devuelve los recursos de la aplicación.

Los bitmaps pueden ser mutables o inmutables según si permiten modificar el valor de sus píxeles o no, respectivamente. Si el bitmap se crea a partir de un array de píxeles, de un recurso con la imagen (como es el caso) o de otro bitmap, tendremos un bitmap inmutable. Si creamos el bitmap vacío especificando su altura y su anchura, entonces será mutable. También podemos conseguir un bitmap mutable haciendo una copia de un bitmap existente mediante el método copy, e indicando que queremos que el bitmap resultante sea mutable.

Para crear un bitmap, ya sea vacío, a partir de un array de píxeles o a partir de otro bitmap, disponemos de una serie de métodos estáticos llamados createBitmap dentro de la clase Bitmap.

Al crear un bitmap a partir de otro podremos realizar diferentes transformaciones, como escalado, rotación, etc. Además, es importante que cuando no se vaya a utilizar más el bitmap se libere la memoria que ocupa mediante su método recycle.

IMÁGENES NINE-PATCH

La forma más flexible que tenemos para definir el aspecto de un componente es mediante una imagen propia, ya que podemos editarla mediante un programa externo y definirla como queramos. Sin embargo, encontramos el problema de que los componentes normalmente no tendrán siempre el mismo tamaño, sino que Android los estirará según su contenido y según los parámetros del layout. Esto es un problema ya que al estirar la imagen veremos que esta se deforma, dando un aspecto terrible a nuestra aplicación. En la siguiente figura se muestra un ejemplo en el que se ha aplicado la misma imagen como fondo a dos botones, el primer botón se ve bien porque tiene el mismo tamaño que la imagen, pero el segundo, al ser mucho más ancho, provoca que la imagen se deforme:

Efecto de imagen estirada

Para solucionar esto Android cuenta con un tipo especial de imágenes PNG llamadas nine-patch (y que tienen la extensión .9.png). Las imágenes nine-patch se dividen en nueve regiones o parches, cada uno de los cuales tiene un comportamiento distinto a la hora de estirar o escalar la imagen: la parte central se puede estirar en cualquier dirección, las esquinas no se estiran, y los bordes sólo se estiran en su misma dirección (horizontal para los bordes superior e inferior, y vertical para los bordes izquierdo y derecho). A continuación se muestra un ejemplo de dicha división:

Parches de la imagen

Si transformamos la imagen anterior a tipo nine-patch y definimos sus nueve regiones, podremos comprobar que al aplicarla de nuevo como fondo del mismo botón se solucionará el problema, mostrándose de forma correcta independientemente del tamaño:

Aplicación de nine-patch a un botón

Transformar una imagen a nine-patch

Para transformar una imagen a nine-patch en primer lugar tenemos que cambiar su extensión de .png a .9.png. En Android Studio podemos hacer esto desde la misma plataforma, simplemente apretamos botón derecho sobre un drawable tipo .png y elegimos la opción "Create 9-patch file...". A continuación, si hacemos doble clic sobre la imagen .9.png generada podremos acceder a un editor como el de la siguiente imagen:

Herramienta draw9patch

En este editor, arrastrando con el ratón sobre la superficie de la imagen, tenemos que añadir una serie de píxeles en su borde para marcar los diferentes patches (el borde negro que se puede observar en la figura de ejemplo). La fila superior de píxeles negros y la columna izquierda indican las zonas de la imagen que son flexibles y que se pueden estirar. Al marcar estas dos zonas el sistema ya puede definir los nueve parches necesarios.

Opcionalmente podemos especificar en la fila inferior y en la columna derecha la zona de contenido (en la figura de ejemplo se puede ver que esa zona es de menor tamaño que la definida para los 9 patches). Por ejemplo, si utilizamos la imagen como fondo de un botón, esta será la zona donde se ubicará el texto del botón. Marcando la casilla Show content veremos en la columna derecha de la herramienta una previsualización de esta zona de contenido.

A la hora de referenciar la imagen como recurso se tendrá que eliminar toda la extensión (.9.png). Por ejemplo, la imagen fondo.9.png tendríamos que referenciarla como R.drawable.fondo.

Es importante que eliminemos o renombremos la imagen original para que no dé error (ya que su identificador de recurso sería el mismo) y asegurarnos de que solo se utilice la versión nine-patch.

LISTA DE ESTADOS

El componente llamado state list o selector permite definir un drawable mediante XML que mostrará diferentes aspectos dependiendo de su estado. Por ejemplo, si lo aplicamos sobre un botón, nos permitirá asignar una apariencia distinta dependiendo de si el botón ha sido pulsado, ha recibido el foco o si está en estado normal.

Este componente se define en un XML formado por la etiqueta <selector> como raíz y una serie de <items> para indicar el drawable a dibujar en cada estado. En cada item tendremos que definir mediante sus atributos los estados para los que se aplica. Algunos de los atributos que podemos usar son: state_pressed, state_focused, state_hovered, state_selected, state_checkable, state_checked, state_enabled o state_activated. Por ejemplo, podemos especificar los estados de un botón (pulsado, seleccionado y normal) de la siguiente forma:

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- presionado -->
    <item android:state_pressed="true"
          android:drawable="@drawable/boton_pressed" />
    <!-- seleccionado -->
    <item android:state_focused="true"
          android:drawable="@drawable/boton_selected" />
    <!-- normal: no seleccionado ni presionado -->
    <item android:drawable="@drawable/boton_normal" />
</selector>

Como se puede ver en el ejemplo, se ha dejado el último item como estado por defecto (sin asignarle ningún estado). Los drawables especificados para cada estado pueden ser de cualquier tipo (por ejemplo imágenes, nine-patch o formas definidas en XML).

Para utilizarlo, igual que en los otros casos, tendremos que usar como identificador de recurso el nombre del fichero XML. Por ejemplo si lo guardamos como estados_boton.xml en la carpeta de drawables, podremos acceder a él desde XML usando @drawable/estados_boton y desde código Kotlin usando R.drawable.estados_boton.

LISTA DE NIVELES

La lista de niveles (level-list) es muy similar a la lista de estados, pero en este caso los diferentes drawables a mostrar se especifican para una serie de rangos de valores numéricos. Por ejemplo, al aplicarlos sobre un SeekBar podremos cambiar su apariencia dependiendo del valor del mismo. A continuación se muestra un ejemplo:

<level-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:minLevel="0" android:maxLevel="50"
          android:drawable="@drawable/fondo1"/>
    <item android:minLevel="51" android:maxLevel="100"
          android:drawable="@drawable/fondo2"/>
</level-list>

LISTA DE CAPAS

La etiqueta layer-list permite crear un drawable a partir de la composición por capas de otros drawables. En el XML que lo define tenemos que incluir layer-list como etiqueta raíz y, dentro de esta, añadir las distintas capas utilizando la etiqueta item. En el atributo drawable de cada item especificaremos el drawable que queremos que se muestre en esa capa. Por ejemplo:

<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/imagen_fondo"/>
    <item android:drawable="@drawable/imagen_intermedia"/>
    <item android:drawable="@drawable/imagen_superior"/>
</layer-list>

Los elementos se añadirán en orden, quedando el primer item en el fondo de la imagen (en el ejemplo anterior imagen_fondo quedaría detrás del resto de capas).

Por defecto todas las capas se colocarán en la misma posición, empezando en el borde superior izquierdo, por lo que si tienen el mismo tamaño se ocultarán unas a otras. Si queremos desplazarlas para colocar las figura de cada capa donde queramos, podemos usar los atributos top, bottom, left y right para indicar un offset o margen en píxeles desde el borde de cada item. Además también podemos especificar su ancho y su alto mediante width y height o su alineación mediante el atributo gravity.

ANIMACIÓN POR FOTOGRAMAS

Este tipo de drawable permite definir una animación en XML a partir de una secuencia de fotogramas. Cada fotograma se compondrá de un drawable (que puede ser de cualquiera de los tipos que hemos visto) y una duración en milisegundos (especificada en el atributo duration). A continuación se muestra un ejemplo:

<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
        android:oneshot="false">
  <item android:drawable="@drawable/spr1" android:duration="200" />
  <item android:drawable="@drawable/spr2" android:duration="200" />
  <item android:drawable="@drawable/spr3" android:duration="200" />
</animation-list>

La propiedad oneshot indica si la animación se va a reproducir solo una vez (true) o en bucle infinito (false).

Posteriormente podremos asignar la animación como si de un drawable normal se tratara. Tendremos que usar como identificador de recurso el nombre del fichero XML, si por ejemplo lo hemos guardado como animacion.xml (dentro de la carpeta de drawables), podremos acceder desde XML usando @drawable/animacion y desde código Kotlin con R.drawable.animacion.

Si asignamos la animación desde código Kotlin tendremos que iniciar la reproducción manualmente mediante el método start. Cuando la queramos detener podremos usar el método stop. A continuación se incluye un ejemplo:

val iv = findViewById(R.id.miImageView) as ImageView
iv.setBackgroundResource(R.drawable.animacion)
val anim = iv.background as AnimationDrawable
anim.start();

El método start no se puede llamar desde onCreate, ya que en ese momento el drawable todavía no está vinculado a la vista. Si lo que queremos es que se ponga en marcha nada más cargarse la actividad, el lugar idóneo para invocarlo es en onWindowFocusChanged.

Para asignar la animación desde XML y que se inicie la reproducción automáticamente, sin necesidad de escribir código Kotlin para hacerlo, podemos utilizar un ProgressBar y asignar la animación mediante su atributo indeterminateDrawable.

Definición programática

También podemos definir la animación de forma programática como se muestra a continuación:

val f1 = ContextCompat.getDrawable(context, R.drawable.spr1)!!
val f2 = ContextCompat.getDrawable(context, R.drawable.spr2)!!
val f3 = ContextCompat.getDrawable(context, R.drawable.spr3)!!

val animFotogramas = AnimationDrawable()
animFotogramas.addFrame(f1, 200)
animFotogramas.addFrame(f2, 200)
animFotogramas.addFrame(f3, 200)

animFotogramas.isOneShot = false

TRANSICIÓN, INSERCIÓN, RECORTE Y ESCALA

Además de todos los tipos que hemos visto, también podemos definir drawables en XML para crear:

  • Transiciones: Tipo sencillo de animación que realiza una transición de un drawable a otro mediante un fundido. La definición en XML usa como raíz la etiqueta transition con dos subnodos item para especificar los drawables de la transición.
  • Inserción: Ubica un drawable dentro de otro en la posición especificada. En el XML tenemos que usar solamente la etiqueta inset indicando el drawable y los márgenes hasta la vista que lo contenga.
  • Recorte: Realiza el recorte de un drawable. El XML consistiría en una única etiqueta clip indicando el drawable y los atributos de recorte.
  • Escala: Permite cambiar el tamaño de un drawable. En el XML solo tendríamos que incluir la etiqueta scale con la referencia al drawable y los valores a aplicar para el escalado.

DRAWABLES DESDE CÓDIGO KOTLIN

Supongamos que tenemos un ImageView con identificador "visor" y un drawable de nombre rectangulo.xml. Para asignar el drawable al ImageView normalmente lo haríamos directamente en el XML mediante el atributo android:src = "@drawable/rectangulo" en la definición del ImageView.

Pero también podemos hacer esta asignación desde código Kotlin. Por ejemplo, podríamos obtener una referencia a dicha vista y mostrar en ella nuestro rectángulo de la siguiente forma:

val visor: ImageView = findViewById(R.id.visor) as ImageView
visor.setImageResource(R.drawable.rectangulo)

Otra alternativa para mostrarlo es obtener primero el objeto Drawable y posteriormente incluirlo en el ImageView:

val rec = ContextCompat.getDrawable(context, R.drawable.rectangulo )
visor.setImageDrawable(rec)

Todas las primitivas que hemos visto para definir drawables también se pueden crear de forma programática. En el paquete android.graphics.drawable.shapes podemos encontrar clases que encapsulan diferentes formas geométricas. Por ejemplo, para crear el rectángulo lo podríamos hacer de la siguiente forma:

val rec = RectShape()
val sd = ShapeDrawable(rec)
sd.paint.color = Color.RED
sd.intrinsicWidth = 100
sd.intrinsicHeight = 50

visor.setImageDrawable(sd)

Pero como ya hemos indicado, y salvo en casos especiales, se recomienda definir los drawables en XML dentro de las carpetas de recursos. De esta forma, además de ser optimizados, podremos hacer uso de los modificadores (sufijos) de estas carpetas.