Cómo crear componentes de vistas personalizadas

Prueba el estilo de Compose
Jetpack Compose es el kit de herramientas de IU recomendado para Android. Obtén información para trabajar con diseños en Compose.

Android ofrece un modelo componentizado para compilar la IU que es sofisticado y potente, y que se basa en las clases de diseño fundamentales View y ViewGroup. La plataforma incluye una variedad de subclases View y ViewGroup compiladas previamente (llamadas widgets y diseños, respectivamente) que puedes usar para construir tu IU.

Una lista parcial de widgets disponibles incluye Button, TextView, EditText, ListView, CheckBox, RadioButton, Gallery, Spinner y los AutoCompleteTextView, ImageSwitcher y TextSwitcher, que tienen un propósito más especial.

Entre los diseños disponibles, se encuentran LinearLayout, FrameLayout, RelativeLayout y otros. Para ver más ejemplos, consulta Diseños comunes.

Si ninguno de los widgets o diseños precompilados satisface tus necesidades, puedes crear tu propia subclase de View. Si solo necesitas hacer pequeños ajustes en un widget o diseño existente, puedes subclasificar el widget o el diseño, y anular sus métodos.

Crear tus propias subclases de View te brinda un control preciso sobre el aspecto y la función de un elemento de pantalla. Para tener una idea del control que obtienes con vistas personalizadas, a continuación, se incluyen algunos ejemplos de lo que puedes hacer con ellas:

  • Puedes crear un tipo de View con renderización completamente personalizada, por ejemplo, un "control de volumen", renderizado con gráficos 2D, que se parezca a un control electrónico analógico.
  • Puedes combinar un grupo de componentes de View en un nuevo componente único, quizás para crear algo como un cuadro combinado (una combinación de lista emergente y un campo de texto de entrada libre), un control de selección de panel doble (un panel izquierdo y uno derecho con una lista en la que puedes reasignar un elemento en qué lista), etcétera.
  • Puedes anular la forma en que se renderiza un componente EditText en la pantalla. La app de ejemplo NotePad usa esto para crear una página de bloc de notas alineada.
  • Puedes capturar otros eventos, como presionar teclas, y controlarlos de manera personalizada, como para un juego.

En las siguientes secciones, se explica cómo crear vistas personalizadas y usarlas en tu aplicación. Para obtener información de referencia detallada, consulta la clase View.

El enfoque básico

A continuación, se proporciona una descripción general de alto nivel de lo que necesitas saber para crear tus propios componentes de View:

  1. Extiende una clase o subclase View existente con tu propia clase.
  2. Anula algunos de los métodos de la superclase. Los métodos de la superclase que se deben anular comienzan con on, por ejemplo, onDraw(), onMeasure() y onKeyDown(). Esto es similar a los eventos on en Activity o ListActivity que anulas para el ciclo de vida y otros hooks de funcionalidad.
  3. Usa tu nueva clase de extensión. Cuando termines, puedes usar la clase de extensión nueva en lugar de la vista en la que se basó.

Componentes completamente personalizados

Puedes crear componentes gráficos totalmente personalizados que aparezcan como quieras. Quizás quieras un vUómetro gráfico que se parezca a un viejo medidor analógico, o una vista de texto para cantar en la que una pelota rebota junto a las palabras mientras cantas junto con una máquina de karaoke. Es posible que quieras algo que los componentes integrados no puedan hacer, sin importar cómo los combines.

Afortunadamente, puedes crear componentes que se vean y se comporten de la manera que quieras, limitados solo por tu imaginación, el tamaño de la pantalla y la potencia de procesamiento disponible. Ten en cuenta que tu aplicación podría tener que ejecutarse en algo con mucha menos energía que la estación de trabajo de computadora de escritorio.

Para crear un componente totalmente personalizado, considera lo siguiente:

  • La vista más genérica que puedes extender es View, por lo que, por lo general, debes extenderla para crear tu nuevo supercomponente.
  • Puedes proporcionar un constructor, que puede tomar atributos y parámetros del XML, y puedes usar tus propios atributos y parámetros, como el color y el rango del medidor de VU o el ancho y el amortiguamiento de la aguja.
  • Es probable que desees crear tus propios objetos de escucha de eventos, descriptores de acceso de propiedades y modificadores, así como un comportamiento más sofisticado en tu clase de componente.
  • Es muy probable que desees anular onMeasure() y que debas anular onDraw() si quieres que el componente muestre algo. Si bien ambos tienen un comportamiento predeterminado, el onDraw() predeterminado no hace nada, y el onMeasure() predeterminado siempre establece un tamaño de 100 x 100, algo que probablemente no te convenga.
  • También puedes anular otros métodos on, según sea necesario.

Cómo extender onDraw() y onMeasure()

El método onDraw() entrega un Canvas en el que puedes implementar lo que desees: gráficos 2D, otros componentes estándar o personalizados, texto con estilo o cualquier otra cosa que se te ocurra.

onMeasure() está un poco más involucrado. onMeasure() es una parte crítica del contrato de renderización entre tu componente y su contenedor. Se debe anular onMeasure() para informar de manera eficiente y precisa las mediciones de las partes contenidas. Esto se vuelve un poco más complejo debido a los requisitos de límite del elemento superior (que se pasan al método onMeasure()) y el requisito de llamar al método setMeasuredDimension() con el ancho y la altura medidos una vez que se calculan. Si no llamas a este método desde un método onMeasure() anulado, se generará una excepción en el momento de la medición.

En un nivel alto, la implementación de onMeasure() se verá de la siguiente manera:

  • Se llama al método onMeasure() anulado con especificaciones de ancho y altura, que se tratan como requisitos para las restricciones de las mediciones de ancho y altura que produzcas. Los parámetros widthMeasureSpec y heightMeasureSpec son códigos enteros que representan dimensiones. Puedes encontrar una referencia completa sobre los tipos de restricciones que pueden requerir estas especificaciones en la documentación de referencia, en View.onMeasure(int, int). En esta documentación de referencia, también se explica la operación de medición completa.
  • El método onMeasure() de tu componente calcula el ancho y la altura de medición, que son necesarios para renderizar el componente. Debe intentar mantenerse dentro de las especificaciones pasadas, aunque puede excederlas. En este caso, el elemento superior puede elegir qué hacer, incluido el recorte, el desplazamiento, el lanzamiento de una excepción o la solicitud de onMeasure() que vuelva a intentar, tal vez con diferentes especificaciones de medición.
  • Cuando se calculen el ancho y la altura, llama al método setMeasuredDimension(int width, int height) con las medidas calculadas. De lo contrario, se generará una excepción.

A continuación, se incluye un resumen de otros métodos estándar que el framework requiere en las vistas:

Categoría Métodos Descripción
Creación Constructores Existe una forma del constructor a la que se llama cuando se crea la vista a partir de código y un formulario al que se llama cuando la vista se aumenta desde un archivo de diseño. El segundo formulario analiza y aplica los atributos definidos en el archivo de diseño.
onFinishInflate() Se llama después de que se aumentan una vista y todos sus elementos secundarios desde XML.
Diseño onMeasure(int, int) Se llama para determinar los requisitos de tamaño para esta vista y todos sus elementos secundarios.
onLayout(boolean, int, int, int, int) Se llama cuando esta vista debe asignar un tamaño y una posición a todos sus elementos secundarios.
onSizeChanged(int, int, int, int) Se llama cuando se cambia el tamaño de esta vista.
Dibujo onDraw(Canvas) Se llama cuando la vista debe renderizar su contenido.
Procesamiento de eventos onKeyDown(int, KeyEvent) Se llama cuando se produce un evento de pulsación de tecla.
onKeyUp(int, KeyEvent) Se llama cuando se produce un evento de activación de tecla.
onTrackballEvent(MotionEvent) Se llama cuando se produce un evento de movimiento de la bola de seguimiento.
onTouchEvent(MotionEvent) Se llama cuando se produce un evento de movimiento en la pantalla táctil.
Enfoque onFocusChanged(boolean, int, Rect) Se llama cuando la vista gana o pierde el enfoque.
onWindowFocusChanged(boolean) Se llama cuando la ventana que contiene la vista gana o pierde el enfoque.
Adjuntar onAttachedToWindow() Se llama cuando se adjunta la vista a una ventana.
onDetachedFromWindow() Se llama cuando la vista se separa de la ventana.
onWindowVisibilityChanged(int) Se llama cuando se cambia la visibilidad de la ventana que contiene la vista.

Controles compuestos

Si no quieres crear un componente completamente personalizado, pero quieres crear uno reutilizable que conste de un grupo de controles existentes, lo mejor sería crear un componente compuesto (o control compuesto). En resumen, esto reúne una serie de vistas o controles atómicos más en un grupo lógico de elementos que se pueden tratar como una sola cosa. Por ejemplo, un cuadro combinado puede ser una combinación de un campo EditText de una sola línea y un botón adyacente con una lista emergente adjunta. Si el usuario presiona el botón y selecciona algo de la lista, se propaga el campo EditText, pero también puede escribir algo directamente en EditText si lo prefiere.

En Android, hay otras dos vistas disponibles para hacer esto: Spinner y AutoCompleteTextView. De cualquier manera, este concepto para un cuadro combinado es un buen ejemplo.

Para crear un componente compuesto, haz lo siguiente:

  • Al igual que con un Activity, usa el enfoque declarativo (basado en XML) para crear los componentes contenidos o anímalos de manera programática desde tu código. El punto de partida habitual es un Layout de algún tipo, por lo que debes crear una clase que extienda un Layout. En el caso de un cuadro combinado, puedes usar un LinearLayout con orientación horizontal. Puedes anidar otros diseños en el interior, de modo que el componente compuesto pueda ser arbitrariamente complejo y estructurado.
  • En el constructor de la clase nueva, toma los parámetros que la superclase espere y pásalos primero al constructor de la superclase. Luego, puedes configurar las otras vistas para que las usen en tu componente nuevo. Aquí es donde creas el campo EditText y la lista emergente. Puedes ingresar tus propios atributos y parámetros en el XML que tu constructor puede extraer y usar.
  • De manera opcional, crea objetos de escucha para eventos que puedan generar las vistas contenidas. Un ejemplo es un método de objeto de escucha para que el objeto de escucha de clics en un elemento de la lista actualice el contenido de EditText si se realiza una selección de lista.
  • De manera opcional, crea tus propias propiedades con descriptores de acceso y modificadores. Por ejemplo, permite que se establezca el valor EditText en el componente y se consulte su contenido cuando sea necesario.
  • De manera opcional, anula onDraw() y onMeasure(). Por lo general, esto no es necesario cuando se extiende un Layout, ya que el diseño tiene un comportamiento predeterminado que probablemente funcione bien.
  • De manera opcional, anula otros métodos on, como onKeyDown(), por ejemplo, para elegir determinados valores predeterminados de la lista emergente de un cuadro combinado cuando se presiona una tecla determinada.

Usar un objeto Layout como base para un control personalizado tiene ventajas como las siguientes:

  • Puedes especificar el diseño con los archivos en formato XML declarativos, al igual que con una pantalla de actividad, o bien puedes crear vistas de manera programática y anidarlas en el diseño desde tu código.
  • Los métodos onDraw() y onMeasure(), junto con la mayoría de los demás métodos on, tienen un comportamiento adecuado de modo que no tienes que anularlos.
  • Puedes crear rápidamente vistas compuestas arbitrariamente complejas y reutilizarlas como si fueran un solo componente.

Cómo modificar un tipo de vista existente

Si hay un componente similar al que deseas, puedes extenderlo y anular el comportamiento que quieres cambiar. Puedes hacer todo lo que haces con un componente totalmente personalizado, pero, si comienzas con una clase más especializada en la jerarquía de View, puedes obtener sin costo el comportamiento que deseas.

Por ejemplo, en la app de ejemplo NotePad, se muestran muchos aspectos del uso de la plataforma de Android. Entre ellas, se extiende una vista EditText para crear un bloc de notas alineado. Este no es un ejemplo perfecto, y las APIs para hacerlo podrían cambiar, pero demuestra los principios.

Si aún no lo hiciste, importa la muestra del Bloc de notas a Android Studio o consulta la fuente a través del vínculo proporcionado. En particular, consulta la definición de LinedEditText en el archivo NoteEditor.java.

A continuación, encontrarás algunos aspectos que debes tener en cuenta en este archivo:

  1. La definición

    La clase se define con la siguiente línea:
    public static class LinedEditText extends EditText

    LinedEditText se define como una clase interna dentro de la actividad NoteEditor, pero es pública para que se pueda acceder a ella como NoteEditor.LinedEditText desde fuera de la clase NoteEditor.

    Además, LinedEditText es static, lo que significa que no genera los llamados "métodos sintéticos" que le permiten acceder a los datos de la clase superior. Eso significa que se comporta como una clase separada en lugar de como algo que esté estrechamente relacionado con NoteEditor. Es una forma más limpia de crear clases internas si no necesitan acceder al estado desde la clase externa. Mantiene la clase generada pequeña y permite que se use con facilidad desde otras clases.

    LinedEditText extiende EditText, que es la vista que se personalizará en este caso. Cuando termines, la clase nueva puede reemplazar una vista de EditText normal.

  2. Inicialización de la clase

    Como siempre, se llama primero a la superclase. Este no es un constructor predeterminado, pero es uno parametrizado. El EditText se crea con estos parámetros cuando se aumenta desde un archivo de diseño XML. Por lo tanto, el constructor debe tomarlas y pasarlas también al constructor de la superclase.

  3. Métodos anulados

    En este ejemplo, solo se anula el método onDraw(), pero es posible que debas anular otros cuando crees tus propios componentes personalizados.

    Para esta muestra, anular el método onDraw() te permite pintar las líneas azules en el lienzo de la vista EditText. El lienzo se pasa al método onDraw() anulado. Se llama al método super.onDraw() antes de que finalice. Se debe invocar el método de la superclase. En este caso, invócalo al final después de pintar las líneas que deseas incluir.

  4. Componente personalizado

    Ya tienes el componente personalizado, pero ¿cómo puedes usarlo? En el ejemplo del Bloc de notas, se usa el componente personalizado directamente desde el diseño declarativo. Por lo tanto, observa note_editor.xml en la carpeta res/layout:

    <view xmlns:android="http://schemas.android.com/apk/res/android"
        class="com.example.android.notepad.NoteEditor$LinedEditText"
        android:id="@+id/note"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/transparent"
        android:padding="5dp"
        android:scrollbars="vertical"
        android:fadingEdge="vertical"
        android:gravity="top"
        android:textSize="22sp"
        android:capitalize="sentences"
    />
    

    Se crea el componente personalizado como una vista genérica en el XML, y se especifica la clase con el paquete completo. Se hace referencia a la clase interna que defines con la notación NoteEditor$LinedEditText, que es una forma estándar de hacer referencia a las clases internas en el lenguaje de programación Java.

    Si tu componente de vista personalizada no está definido como una clase interna, puedes declarar el componente de vista con el nombre del elemento XML y excluir el atributo class. Por ejemplo:

    <com.example.android.notepad.LinedEditText
      id="@+id/note"
      ... />
    

    Observa que la clase LinedEditText ahora es un archivo de clase independiente. Cuando la clase está anidada en la clase NoteEditor, esta técnica no funciona.

    Los otros atributos y parámetros de la definición son los que se pasan al constructor del componente personalizado y, luego, se pasan al constructor EditText, por lo que son los mismos parámetros que usas para una vista de EditText. También puedes agregar tus propios parámetros.

Crear componentes personalizados es tan complicado como lo necesites.

Un componente más sofisticado puede anular aún más métodos on e introducir sus propios métodos auxiliares, lo que permite personalizar en gran medida sus propiedades y su comportamiento. El único límite es tu imaginación y lo que necesitas que haga el componente.