Custom Views en Android: Extendiendo una vista existente
Antonio Leiva

En el vídeo de hoy te explico qué son las Custom Views o vistas personalizadas, y qué necesitas saber para implementarlas.

Vamos a ver a fondo un ejemplo que a pesar de ser relativamente sencillo, requiere de bastantes pasos para entenderlo por completo.

Te explicaré:

  • Qu√© es una Custom View
  • C√≥mo definir una Custom View que extienda de una View ya existente
  • C√≥mo modificar su comportamiento
  • C√≥mo incluirla en nuestros dise√Īos
  • C√≥mo a√Īadir propiedades personalizadas directamente en XML
  • C√≥mo recuperar esos valores por c√≥digo

Aquí tienes el vídeo:

Ya hemos visto a lo largo de los videos anteriores que tenemos una serie de vistas proporcionadas por el framework de Android que nos ayudan a crear las pantallas con la estructura que nosotros necesitemos, pero habr√° ocasiones en las que esas vistas no se ajusten a lo que necesitemos o porque nos interese agrupar un conjunto de vistas para crear una nueva. Vamos a necesitar crear lo que se denomina Custom Views (vistas personalizadas).

Lo que queremos hacer es un par de vistas personalizadas. La primera vista es una imagen.

La imagen anterior es un grid, dividida por filas y columnas, donde le hemos dicho que haya 3 elementos por fila, o sea, tres columnas. Por tanto en función de como sea la pantalla estos elementos serán mas anchos o mas estrechos y no queremos que la imagen crezca mas en alto que en ancho o vice versa, queremos que lo haga de forma proporcional.

Vamos a crear una Custom view con ImageView un poco variado. Esta es una de las opciones que tenemos cuando creamos una vista personalizada que nos basemos en una vista que ya existe y le adaptemos cosas. El básico y esencial sería basarnos en la vista View que es como un lienzo en blanco, y luego modificar ciertas cosas. Nosotros vamos a ir a un nivel de abstracción un poco mas arriba y vamos a sobrescribir el comportamiento habitual que tiene el ImageView.

Lo primero que haremos es crear un archivo que se va a llamar AspectRatioImageView yendo a:

No olvidar que la carpeta se llamara distinto dependiendo del nombre que se le haya dado al momento de crear el proyecto en los pasos anteriores.

ahora crearemos una clase con el nombre del archivo y lo que tenemos que hacer es que extienda de ImageView, es decir, que coja todas las propiedades y funcionalidad de ImageView y que la podamos usar en nuestra clase. Se extiende colocando dos puntos y seguido el nombre de la clase que deseamos extender:

Esto nos va a obligara llamar a un constructor padre, es decir, la forma en la que definimos como se construye este objeto o sino crear varios constructores. Una cosa importante es que cuando estamos trabajando con la librería de AppCompat, que por defecto la vas a usar siempre si estas usando una de las plantillas de Android Studio. En vez de extender de la variante del framework de Android Studio utilicemos la de AppCompat.

Nos recomendara que es lo mas sencillo para usar los constructores y quedaría de la siguiente manera. Con esto ya nos da todas las propiedades que puede necesitar un ImageView o en general cualquier vista cuando la estamos creando. La principal y si la creamos por código es el contexto (context) que es básicamente una acumulación de información que le sirve a la vista para ver como pintarse, que tema usar, este tipo de cosas . El resto solo son necesarias si estamos creando esta vista desde XML. Cuando se crea la vista por el XML algunas de las cosas que definimos se pasaran por el constructor.

Luego de esto tenemos que definir cual va hacer el AspectRatio que vamos a utilizar. Lo podemos poner como una propiedad privada de tipo Float e iniciaremos con el valor de 1. El tipo Float ser√≠a un numero en coma flotante que lo que quiere decir es que puede tener decimales, de tal forma que el AspectRatio pueda ser 1×1.5, esto quiere decir que por cada 1 de ancho necesita 1.5 de alto.
Lo siguiente que necesitaríamos hacer para nuestra imagen es sobrescribir onMeasure (sin centrarte en el código que esta escrito sino en las cosas que puedas hacer). El día que necesitas hacer algo parecido vas necesitar informarte mucho mejor en como funcionan las Custom View porque dependen mucho de lo que quieras hacer, también conlleva matemática implicada, es relativamente complejo.

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    }

Lo que har√≠amos es sobrescribir la funci√≥n onMeasure, esta funci√≥n se llama cuando estamos pintando una pantalla y queremos saber cual es el tama√Īo final que va a tener esa vista, para dec√≠rselo a las vistas padre, pasara por este m√©todo, adem√°s nos dar√° informaci√≥n de cual es la configuraci√≥n que tiene esa vista, las vistas padre, definir√° cierta informaci√≥n para que puedas definir con mayor fiabilidad cual es el tama√Īo que le vas a dar a tus vista. A veces podr√°s utilizar estos argumentos que vienen por parametro en la funci√≥n onMeasure, los cuales est√°n relacionados con el Match_Parent o el Wrap_Content o si le asignamos un tama√Īo especifico nos vendr√° identificado aqu√≠ tambi√©n. Dejaremos que la vista coja el tama√Īo que le corresponder√≠a en funci√≥n de todo el resto de configuraci√≥n y luego modificarle la altura con el AspectRatio en comparaci√≥n con el ancho. As√≠ que vamos a coger el tama√Īo de la propia property de la clase measureWidth, como tambi√©n el alto. Estos son la altura y la anchura una vez se ha medido y luego de esto tenemos que comprobar que si ambos valores son iguales a cero, hara que el codigo de nuestra funci√≥n no se ejecute porque todav√≠a no se ha terminado de medir la vista y no tenemos los datos para calcular los datos en funci√≥n del ancho.

class AspectRatioImageView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {
    var ratio: Float = 1f

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        var width = measuredWidth
        var height = measuredHeight

        if (width == 0 && height == 0) {
            return
        }
    }
}

El siguiente paso es comprobar si alguna de las dos no es cero, cual de ellas es distinto de cero. Porque por ejemplo si el ancho es cero quiere decir que no tiene información suficiente para calcularla y que lo que estamos intentando es calcular la anchura en función de la altura, si es el caso contrario y tenemos el ancho debemos calcular en funcion del ancho el alto.

class AspectRatioImageView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {
    private var ratio: Float = 1f

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        var width = measuredWidth
        var height = measuredHeight

        if (width == 0 && height == 0) {
            return
        }

        if(width > 0){
            height = (width*ratio).toInt()
        } else if(height > 0){
            width = (height/ratio).toInt()
        }
    }
}

Ya al final para decirle que hemos decido modificar el alto y el ancho con respecto a lo que nos había dado nuestra clase padre WidthMeasureSpec tenemos que llamar a setMeasuredDimension() y como argumentos le pasamos el width y height.

setMeasuredDimension(width, height)

Ahora nos vamos a ir a la activity_main.xml y borraremos todas las vistas que teniamos, luego vamos a√Īadir una View de tipo AspectRatioImageView. Para a√Īadir nuestra vista personalizada tendremos que hacerlo por codigo. Al escribir las iniciales de la vista que queremos llamar el auto completador nos mostrara cual es, de la siguiente manera.

Tendiendo definida nuestra View ahora le daremos un ancho de 200dp, para el alto lo definiremos como wrap_content y también le asigaremos un id con el nombre de cover.

<com.antonioleiva.androiddesdecero.AspectRatioImageView
        android:id="@+id/cover"
        android:layout_width="200dp"
        android:layout_height="wrap_content"/>

Ahora con el id podemos trabajar nuestra vista desde el archivo MainActivity.kt y nos quedara algo asi:

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

        var cover = findViewById<AspectRatioImageView>(R.id.cover)
        cover.ratio = 1.5f
    }
}

Ya tenemos nuestra vista creada pero no estamos cargando ninguna imagen en ella, ¬ŅComo podemos hacerlo? A√Īadiendo un recurso dentro de nuestra libreria de Resources para que esa imagen pueda ser cargada dentro del ImageView.

Podemos buscar una caratula en google y descargarla. Ya con la caratula descargada que no es mas que una imagen .jpg la vamos arrastrar dentro de nuestro proyecto a la carpeta app > res> drawable.

Ahora con nuestra imagen dentro de nuestros recurso podemos ir a nuestro activity_main.xml y seleccionando nuestra vista iremos a las propiedades en busca de una que se llama srcCompat y elegiremos la imagen que hayamos puesto en nuestra carpeta drawable.

Si nos fijamos bien, nuestra imagen no ocupa el cuadro completo, solo ocupa el tama√Īo de la imagen y tenemos otra opci√≥n dentro de los ImageView que es el scaleType y que por defecto su valor sera centerInside, nosotros usaremos el valor de centerCrop que nos permite utilizar todo el ancho y en el alto lo que se salga lo recortara. Al ejecutarlo deberiamos poder ver algo como esto:

Cuando tenemos una vista perzonalizada le podemos definir atributos propios, para ello nos vamos a app > res > values y creamos un archivo attrs.xml y dentro del mismo podemos a√Īadir atributos que definan el estilo de nuestra vista AspectRatioView de la siguente manera

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="AspectRatioImageView">
        <attr name="ratio" format="float" />
    </declare-styleable>
</resources>

Al tener definidos nuestros atributos vamos a nuestro activity_main.xml y agregamos ese atributo que acabo de crear dentro de nuestra vista AspectRatioImageView, el nombre sera ratio y le ponemos nuestro valor 1.5

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <com.antonioleiva.androiddesdecero.AspectRatioImageView
        android:id="@+id/cover"
        android:layout_width="200dp"
        android:layout_height="wrap_content"
        app:ratio="1.5"
        app:srcCompat="@drawable/avengers" />
</LinearLayout>

Pare recibir este dato dentro de nuestra vista se hara mediante el contrusctor que se define como init dentro de nuestra clase AspectRatioImageView.kt

class AspectRatioImageView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {
    var ratio: Float = 1f

    init {
        val a = context.obtainStyledAttributes(attrs, R.styleable.AspectRatioImageView)
        ratio = a.getFloat(R.styleable.AspectRatioImageView_ratio, 1f)
        a.recycle()
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        var width = measuredWidth
        var height = measuredHeight

        if (width == 0 && height == 0) {
            return
        }

        if(width > 0){
            height = (width*ratio).toInt()
        } else if(height > 0){
            width = (height/ratio).toInt()
        }

        setMeasuredDimension(width, height)
    }
}

Luego de esto podremos ver los cambios en el dise√Īador

Quiz√° tambi√©n te interese…

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 Antonio Leiva Gordillo, 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 *