Custom Views: Vistas compuestas con ViewGroup
Antonio Leiva

Si en un vídeo anterior veíamos cómo hacer Custom Views basadas en una vista existente, hoy vamos a hacer lo mismo para cuando queremos crear vistas compuestas.

Esto quiere decir que a partir de varias vistas, creamos una más grande que luego podemos reutilizar. Además le podemos dar ciertas propiedades y funciones para que podamos modificarla sin tener que saber cómo está creada.

En el vídeo de hoy vamos a ver:

  • Cómo crear una vista compuesta
  • Cómo extraerla a una clase nueva
  • Crear funciones para poder modificar su contenido
  • Cómo usar merge para optimizar el layout

Aquí tienes el vídeo:

Muchas veces nos interesara que varias vistas se junten para crear una sola que podamos tratar como una única unidad de tal forma que podamos reutilizarla en distintas partes de nuestra aplicación para ello lo habitual será extender de una vista compuesta existente de un ViewGroup. Rellenar dentro de ella una vista con los elementos que necesitamos y luego añadirle funciones o métodos con los que podamos modificar la información que tiene.

Vista Compuesta

Crearemos una vista compuesta que contenga una imagen y el titulo debajo se creara en el XML a través del diseñador visual y luego la vamos a extraer para que sea una vista individual.

Añadimos un TextView debajo de nuestra imagen

En el TexView que acabamos de colocar nos iremos a los atributos (haciendo click) sobre el elemento y en el TextAppearance elegiremos el tamaño Medium. A este mismo elemento le agregaremos; un Padding de 8dp el cual sirve para dar espacios internos y nos ayudara a separar un poco el texto de la imagen; Un background de color purple_500; Un textColor: white

Ahora queremos modificar nuestro LinearLayout a través del atributo layout_width y le definimos el valor de wrap_content para que el Aspectratio de la imagen como el TextView se ajusten mejor a la pantalla.

Para completar que se vea más adecuado nos iremos a otro atributo mas que es el layout_gravity y le damos de valor center para que todo nuestro contenido se centre. Como también darle el valor de “Avengers” al TextView en su atributo de tipo text

Crearemos una nueva clase que extenderá de LinearLayout y vamos añadir todos los elementos que hemos creado tanto la imagen como el texto. Nos crearemos una nueva clase que se llame MovieView que extenderá de LinearLayout y que en las recomendaciones que nos aparece cree los constructores por nosotros

import android.content.Context
import android.util.AttributeSet
import android.widget.LinearLayout

class MoviView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : LinearLayout(context, attrs) {
}

Luego en el bloque init que vendría siendo cómo el constructor vamos hacer lo que necesitemos. Seguidamente crearemos un nuevo Layout dentro de nuestro directorio app > res > layout (haciendo click derecho) sobre la carpeta y en el menú que aparece seleccionaremos Layout Reources File dandole el nombre de view_movie. Normalmente lo que se suele hacer en los nombres de layout es poner; de primero el nombre del tipo de elemento que es, ejemplo: view, activiy, etc; y luego lo que representa, en nuestro caso una película (movie).

Por defecto el layout generado por el Android Studio seré de tipo ConstrainLayout y nosotros lo que queremos es que sea de tipo LinearLayout por lo que lo cambiaremos esta vez a travez del XML en el modo code que aparece en la pestaña de la parte superior derecha.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

</LinearLayout>

Luego tomaremos del activity_main una copia de las view de tipo TextView y Cover para pegarlas en nuestro nuevo Layout view_movie

Como podemos observar nuestras los elementos aparecen uno al lado del otro y nosotros lo que queremos es que aparezcan uno debajo del otro por lo que iremos a las propiedades del LinearLayout y en orientation lo asignaremos el valor de vertical.

Ahora recordemos porque pasa este comportamiento con el TextView y el Cover. Sencillo el Android Studio la asigno un valor por defecto a la propiedad layout_height de 1 y esto quiere decir que repartira una porción de la pantalla entre los distintos elementos, por ende quitaremos ese valor y lo dejaremos vacío.

Después a nuestro LinearLayout una vez mas iremos a las propiedades y en el layout_width le daremos el valor de wrap_content

Nuestro siguiente paso es irnos a nuestro MovieView e inflar la vista para ello definimos una variable llamada view que contendrá el LayoutInflater.from() donde le pasaremos el context que estamos recibiendo como argumento y le decimos que inflate() reciba como argumento R.layout.view_movie; luego le pasaremos quien será nuestra vista padre que es this que es el LinearLayout que estamos creando, que se refiere al objeto que esta llamando a esta función; y luego le diríamos el attchToRoot que es el ultimo argumento el lo que hace es que decide si añade las vistas que esta inflando a la vista padre automáticamente o no se lo definimos con el valor true. Nuestro código quedaría de la siguiente manera

package com.misterfront.androiddesdecero

import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout

class MovieView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : LinearLayout(context, attrs) {

    init {
        val view = LayoutInflater
            .from(context)
            .inflate(R.layout.view_movie, this, true)
    }
}

Lo que queremos es guardarnos las vistas internas para luego poder utilizarlas, así que lo que haremos es crear una variable privada cover que será de tipo ImageView y otra variable privada title de tipo TextView.

luego dentro de nuestro constructor (init) le asignaremos a las variables creadas los id de esas vistas.

package com.antonioleiva.androiddesdecero

import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView

class MovieView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : LinearLayout(context, attrs) {

    private val cover: ImageView
    private val title: TextView

    init {
        val view = LayoutInflater
            .from(context)
            .inflate(R.layout.view_movie, this, true)

        cover = findViewById(R.id.cover)
        title = findViewById(R.id.title)
    }
}

Con nuestro código anterior podremos tener una nueva clase Movie que se inicializara con un titulo y una imagen con la que se represente una película. Si en nuestra clase queremos que al momento de crear la case se guarden los valores que pasamos por los argumentos lo que tenemos que hacer colocar la palabra reservada val antes del nombre de nuestra variable en el constructor de la case y ya luego desde afuera podemos acceder a ellas.

class Movie (val title: String, val cover: String) {
}

Una vez hecho lo anterior podemos asignarle una Movie dentro de nuestra clase MovieView y para ello crearemos una función que se llame setMovie() y que tenga una movie por argumento, ya lo que nos quedaria es cargar el titulo y la imagen a nuestra movie.

class MovieView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : LinearLayout(context, attrs) {

    private val cover: ImageView
    private val title: TextView

    init {
        val view = LayoutInflater
            .from(context)
            .inflate(R.layout.view_movie, this, true)

        cover = findViewById(R.id.cover)
        title = findViewById(R.id.title)
    }

    fun setMovie(movie: Movie) {
        title.text = movie.title
        // cover.image = movie.cover
    }
}

En Android no tenemos una forma por defecto de cargar una imagen que venga de una url por ende necesitamos una librería por este motivo comentamos la linea del cover.image = movie.cover.

Ahora nos vamos a la activity_main y decirle que en vez de que cargue lo que teníamos antes, nos cargue nuestra MovieView borrando lo anterior

Luego arrastramos nuestra vista MovieView a nuestro Layout, le agregamos un ID con el valor de movie y le damos a compilar en símbolo del martillo verde

y con esto ya podríamos usar nuestra vista personalizada en vez de crearla cada vez que la necesitemos.

Si ahora nos vamos a la MainActiviy para definir dentro de nuestro onCreate() una variable movie la cual almacenara el nuestro vista y luego utilizaremos esta movie para pasarle la función setMovie para cargar nuestra película

package com.antonioleiva.androiddesdecero

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val movie = findViewById<MovieView>(R.id.movie)
        movie.setMovie(Movie("Batman", "http://"))
    }

}

Si ahora corremos nuestra aplicación veremos que nos mostrara nuestra película con el titulo que le acabamos de definir.

Pero aquí estamos teniendo un pequeño problema que es el siguiente, si nos vamos al Layout Inspector que se encuentra en la parte inferior derecha de nuestro Android Studio. Le decimos que seleccione la aplicación que nosotros estamos creando

para poder ver las capas que estamos teniendo de forma visual seleccionaremos 3D model

Se veria algo así

El problema es que cuando definimos las vistas por XML tenemos que ponerle una vista padre, en nuestro caso es nuestro LinearLayout pero ya el LinearLayout desde donde estamos llamando (activity_main) esta definido por tanto no es necesario tenerlo dos veces. ¿Cómo solucionamos esto? sustituyendo en nuestro view_movie nuestra etiqueta de LinearLayout por merge, este lo que esta diciendo es que cuando se infle la vista esta que hemos identificado como merge sea realmente la que esta inflando la vista en este caso MovieView, entonces se pondría como primer elemento.

Si te fijas el merge esta ignorando algunas cosas, por ejemplo que el LinearLayout sea vertical, esto se debe a que todo lo que pongamos al inicio de nuestro merge lo va a ignorar

Así que hay que ponerlo en otro sitio, para ello es mas sencillo irnos al MovieView y definir una variable orientation que tenga valor VERTICAL

package com.antonioleiva.androiddesdecero

import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView

class MovieView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : LinearLayout(context, attrs) {

    private val cover: ImageView
    private val title: TextView

    init {
        val view = LayoutInflater
            .from(context)
            .inflate(R.layout.view_movie, this, true)

        cover = findViewById(R.id.cover)
        title = findViewById(R.id.title)
        
        orientation = VERTICAL
    }

    fun setMovie(movie: Movie) {
        title.text = movie.title
        // cover.image = movie.cover
    }
}

Para poder ver los cambios debemos compilar.

Por ultimo para que en el diseñador nos permita visualizar de forma correcta como esta representado debemos ir al movie_view y en las propiedades del merge, la propiedad parentTag decirle que tenga el valor de LinearLayout y en la orientation el valor de vertical

Píxeles Independientes de la Densidad (dp)

Los dp son una medida independiente de la densidad de los pixeles, asi que normalmente cuando definas un medida en cualquiera de la interfaz tendremos que usar dp y no pixeles.

Píxeles Escalables (sp)

Para cuando definas el tamaños de los textos.

Quizá también te interese…

Clases y constructores en Kotlin con Android Studio

Clases y constructores en Kotlin con Android Studio

Veremos un repaso las clases y constructores de Kotlin para solventar esas inquietudes que nos surgen a la hora de seguir este curso, que pueden ser como funciona realmente y porque se presentan las clases de ese modo, como es la interacción y el constructor a la hora...

Los 7 mejores cursos online para aprender Android desde cero en 2021

Los 7 mejores cursos online para aprender Android desde cero en 2021

No hay que ser un genio para darse cuenta de que el sector del desarrollo de aplicaciones móviles está en auge y cada vez más gente busca aprender Android para iniciarse en esta profesión. Atraídos, cómo no, por la posibilidad de obtener un empleo estable, (muy) bien...

0 comentarios

Enviar un comentario

Los datos personales que proporciones a través de este formulario quedarán registrados en un fichero de DevExpert, S.L.U., con el fin de gestionar los comentarios que realizas en este blog. La legitimación se realiza a través del consentimiento de la parte interesada. Si no se acepta, no podrás comentar en este blog. Los datos que proporciona solo se utilizan para evitar el correo no deseado y no se usarán para nada más. Puede ejercer los derechos de acceso, rectificación, cancelación y oposición en contacto@devexperto.com.

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Acepto la política de privacidad *