La Vista previa para desarrolladores de Android 11 ya está disponible. Pruébala y comparte tus comentarios.

Componentes de vistas personalizadas

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. Para comenzar, la plataforma incluye una variedad de subclases de View y ViewGroup precompiladas, llamadas widgets y diseños, respectivamente, que puedes usar para compilar tu IU.

Una lista parcial de widgets disponibles incluye Button; TextView; EditText; ListView; CheckBox; RadioButton; Gallery; Spinner; y, para objetivos más especiales, AutoCompleteTextView, ImageSwitcher y TextSwitcher.

Entre los diseños disponibles, se encuentran LinearLayout, FrameLayout, RelativeLayout y otros. Para ver más ejemplos, consulta Objetos de diseño 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.

La creación de 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 encontrarás algunos ejemplos de lo que podrías hacer con ellas:

  • Podrías crear un tipo de vista con un renderizado completamente personalizado, como un "control de volumen" renderizado con gráficos 2D y que se asemeje a un control electrónico analógico.
  • Podrías combinar un grupo de componentes de View en un nuevo componente único, por ejemplo, para hacer algo como un ComboBox (una combinación de lista emergente y campo de texto de entrada libre), un control de selector de doble panel (paneles izquierdo y derecho con una lista en cada uno, donde puedes reasignar qué elemento hay en cada lista), y así sucesivamente.
  • Podrías anular la forma en que se renderiza en la pantalla un componente EditText (en el instructivo del Bloc de notas, se usa esta función para crear una página de bloc de notas alineada).
  • Podrías capturar otros eventos, como presionar teclas, y controlarlos de alguna manera personalizada (como para un juego).

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

El enfoque básico

A continuación, presentamos una descripción general de alto nivel de lo que debes saber para comenzar a 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 otras trampas de funcionalidad.
  3. Usa tu nueva clase de extensión. Una vez completada, puedes usar la nueva clase de extensión en lugar de la vista en la que está basada.

Sugerencia: Es posible definir las clases de extensión como clases internas dentro de las actividades que las usan. Esto es útil porque controla el acceso a ellas, pero no es necesario (quizás desees crear una nueva vista pública para un uso más amplio en tu app).

Componentes totalmente personalizados

Puedes usar componentes totalmente personalizados para crear componentes gráficos que tengan el aspecto que desees. Por ejemplo, un vúmetro gráfico con el aspecto de un viejo medidor analógico, o una vista de texto para cantar a coro en la que una pelota rebota sobre las palabras para que puedas cantar junto con una máquina de karaoke. De cualquier manera, te conviene crear algo que los componentes integrados no hagan, sin importar la combinación que uses.

Por suerte, puedes crear componentes que tengan el aspecto y el comportamiento que quieras. Los únicos límites serán tu imaginación, el tamaño de la pantalla y la potencia de procesamiento disponible (recuerda que tu app deberá ejecutarse en un dispositivo con mucho menos potencia que la estación de trabajo de escritorio).

Para crear un componente totalmente personalizado:

  1. Como era de esperar, la vista más genérica que puedes extender es View, por lo que, en general, comenzarás extendiéndola para crear tu nuevo supercomponente.
  2. Puedes suministrar un constructor que pueda tomar atributos y parámetros del XML, y también puedes consumir tus propios atributos y parámetros (por ejemplo, el color y el rango del vúmetro, o el ancho y el amortiguamiento de la aguja, etc.).
  3. Tal vez deseas crear tus propios objetos de escucha de eventos, o modificadores y métodos de acceso de propiedades, y, posiblemente, un comportamiento más sofisticado en tu clase de componente.
  4. Es muy probable que desees anular onMeasure() y que debas anular onDraw() si deseas que el componente muestre alguna información. Si bien ambos tienen un comportamiento predeterminado, el onDraw() predeterminado no realizará ninguna acción, y el onMeasure() predeterminado siempre establecerá un tamaño de 100 x 100, que quizás no sea el que deseas.
  5. También es posible anular otros métodos on..., según sea necesario.

Extiende onDraw() y onMeasure()

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

Nota: Esto no se aplica a los gráficos 3D. Si deseas usar gráficos 3D, debes extender SurfaceView, en lugar de Ver, y dibujar desde un subproceso separado. Consulta el ejemplo de GLSurfaceViewActivity para obtener más detalles.

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 los límites de la vista superior (que se pasan al método onMeasure()) y por el requisito de llamada al método setMeasuredDimension(), donde se miden el ancho y la altura una vez que se calcularon. Si no puedes llamar a este método desde un método onMeasure() anulado, el resultado será una excepción en el momento de la medición.

En un nivel alto, la implementación de onMeasure() tiene un aspecto como el siguiente:

  1. Se utiliza el método onMeasure() anulado con especificaciones de ancho y altura (parámetros widthMeasureSpec y heightMeasureSpec, ambos son códigos enteros que representan dimensiones) que se deben tratar como requisitos para las restricciones de las medidas de ancho y altura que deberías producir. Si buscas una referencia completa sobre el tipo de restricciones que pueden requerir estas especificaciones, consulta la documentación de referencia en View.onMeasure(int, int) (en esta documentación de referencia, también se explica de manera muy clara toda la operación de medición).
  2. El método onMeasure() de tu componente debe calcular el ancho y la altura de medición que se requerirán para renderizar el componente. Se debería mantener dentro de las especificaciones aprobadas, aunque puede optar por superarlas (en este caso, la vista superior puede elegir qué hacer, como recortar, desplazarse, lanzar una excepción o pedirle al onMeasure() que intente nuevamente, tal vez con diferentes especificaciones de medición).
  3. Una vez que se calculan el ancho y la altura, se debe llamar al método setMeasuredDimension(int width, int height) con las medidas calculadas. De lo contrario, se generará una excepción.

Aquí encontrarás un resumen de algunos de los otros métodos estándares que el marco 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 se aumenta la vista desde un archivo de diseño. El segundo formulario debe analizar y aplicar cualquier atributo definido en el archivo de diseño.
onFinishInflate() Se llama después de que se aumentaron 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 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 tecla nuevo.
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 de 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 se separa la vista de su ventana.
onWindowVisibilityChanged(int) Se llama cuando cambia la visibilidad de la ventana que contiene la vista.

Controles compuestos

Si no deseas crear un componente totalmente personalizado, pero buscas crear uno reutilizable que conste de un grupo de controles existentes, crear un componente compuesto (o control compuesto) puede ser lo adecuado. En pocas palabras, así se crea una serie de más controles (o vistas) atómicos en un grupo lógico de elementos que se pueden tratar como una unidad. Por ejemplo, se puede considerar un cuadro combinado como una combinación de un campo EditText de una sola línea y un botón adyacente con una lista emergente adjunta. Si presionas el botón y seleccionas algo de la lista, se propaga el campo EditText, pero el usuario también puede escribir algo directamente en EditText si lo prefiere.

En Android, en realidad, hay otras dos vistas disponibles para hacer esto: Spinner y AutoCompleteTextView, pero el concepto de cuadro combinado es un ejemplo fácil de entender.

Para crear un componente compuesto:

  1. El punto de partida habitual es un diseño de algún tipo, por lo que debes crear una clase que amplíe un diseño. Por ejemplo, en el caso de un cuadro combinado, podemos usar un LinearLayout con orientación horizontal. Recuerda que se pueden anidar otros diseños en el interior, por lo que el componente compuesto puede ser complejo y estructurado de una manera arbitraria. Ten en cuenta que, al igual que con una actividad, puedes usar el enfoque declarativo (basado en XML) para crear los componentes contenidos o puedes anidarlos de manera programática desde el código.
  2. En el constructor para la clase nueva, toma los parámetros que la superclase espera y pásalos primero al constructor de la superclase. Luego, puedes configurar las otras vistas para usar dentro del nuevo componente. Aquí deberías crear el campo EditText y la PopupList. Ten en cuenta que también puedes introducir tus propios atributos y parámetros en el XML, los que tu constructor puede extraer y usar.
  3. También puedes crear objetos de escucha para eventos que tus vistas contenidas puedan generar. Por ejemplo, un método de escucha para que el objeto de escucha List Item Click actualice el contenido del campo EditText si se realiza una selección de lista.
  4. También puedes crear tus propias propiedades con modificadores y accesos. Por ejemplo, permite que se establezca inicialmente el valor de EditText en el componente y se consulte su contenido cuando sea necesario.
  5. En el caso de extender un diseño, no necesitas anular los métodos onDraw() y onMeasure(), ya que el diseño tendrá un comportamiento predeterminado que probablemente funcionará bien. Sin embargo, puedes anularlos si es necesario.
  6. Puedes anular otros métodos on..., como onKeyDown(), para elegir ciertos valores predeterminados de la lista emergente de un cuadro combinado cuando se presiona una tecla determinada.

Para resumir, el uso de un diseño como base para un control personalizado tiene varias ventajas, entre las que se incluyen las siguientes:

  • Puedes especificar el diseño utilizando los archivos XML declarativos de la misma manera que con una pantalla de actividad o puedes crear vistas de forma programática y anidarlas en el diseño desde tu código.
  • Es posible que los métodos onDraw() y onMeasure() (más la mayoría de los otros métodos on...) tengan un comportamiento adecuado, de modo que no tengas que anularlos.
  • Al final, puedes crear con mucha rapidez vistas complejas arbitrarias y reutilizarlas como si fueran un solo componente.

Cómo modificar un tipo de vista existente

Existe una opción aún más fácil para crear una vista personalizada que es útil en ciertas circunstancias. Si hay un componente que ya es muy similar al que deseas, solo debes extenderlo y anular el comportamiento que deseas cambiar. Puedes hacer todo lo que harías con un componente totalmente personalizado, pero, si comienzas con una clase más especializada en la jerarquía de vistas, también puedes obtener un comportamiento gratuito que probablemente haga justo lo que deseas.

A modo de ejemplo, la app del Bloc de notas demuestra muchos aspectos del uso de la plataforma de Android. Entre ellos, está la extensión de una vista de EditText para crear un bloc de notas alineado. Este no es un ejemplo perfecto, y las API para esta función podrían cambiar; de todas maneras, se demuestran los principios.

Si aún no lo hiciste, importa el ejemplo del Bloc de notas a Android Studio (o solo consulta la fuente usando el vínculo proporcionado). En particular, ten en cuenta la definición de LinedEditText en el archivo NoteEditor.java.

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

  1. La definición

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

    • Se define LinedEditText como una clase interna dentro de la actividad NoteEditor, pero es pública para que se pueda acceder como NoteEditor.LinedEditText desde fuera de la clase NoteEditor si se desea.
    • 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, lo que a su vez significa que se comporta como una clase separada, en lugar de un elemento estrechamente relacionado con NoteEditor . Esta es una forma más limpia de crear clases internas si no necesitan acceso al estado desde la clase externa. Además, mantiene la clase generada pequeña y permite que se use fácilmente desde otras clases.
    • Extiende EditText, que es la vista que elegimos personalizar en este caso. Cuando terminemos, la nueva clase podrá reemplazar a una vista de EditTextnormal.
  2. Inicialización de la clase

    Como siempre, se llama primero a la superclase. Además, este no es un constructor predeterminado, sino uno con parámetros. Se crea EditText con estos parámetros cuando se aumenta desde un archivo de diseño XML. Por lo tanto, nuestro constructor debe tomarlos y pasarlos también al constructor de la superclase.

  3. Métodos anulados

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

    Para esta muestra, anular el método onDraw() nos permite pintar las líneas azules en el lienzo de la vista EditText (se pasa el lienzo al método anulado). Se llama al método super.onDraw() antes de que finalice. Se debe invocar el método de la superclase y, en este caso, lo haremos al final, después de pintar las líneas que queremos incluir.

  4. Cómo usar el componente personalizado

    Ahora tenemos nuestro componente personalizado, pero ¿cómo podemos usarlo? En el ejemplo del Bloc de notas, se usa el componente personalizado directamente desde el diseño declarativo. Por lo tanto, debes echar un vistazo a 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. Ten en cuenta también que la clase interna que definimos se identifica usando la notación NoteEditor$LinedEditText, que es una forma estándar de referirse a las clases internas en el lenguaje de programación Java.

      Si tu componente de vista personalizado no está definido como una clase interna, tienes la opción de 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. Si la clase está anidada en la clase NoteEditor, esta técnica no funcionará.

    • 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 de EditText, por lo que son los mismos parámetros que usarías en una vista de EditText. Ten en cuenta que también puedes agregar tus propios parámetros, lo que analizaremos a continuación.

Y eso es todo. Está claro que este es un caso simple, pero ese es el punto: crear componentes personalizados es tan complicado como lo desees.

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