Cómo crear vistas personalizadas

Este codelab es parte del curso Aspectos avanzados de Android en Kotlin. Aprovecharás al máximo este curso si trabajas con los codelabs de forma secuencial, aunque no es obligatorio. Todos los codelabs del curso se indican en la página de destino de los codelabs de Aspectos avanzados de Android en Kotlin.

Introducción

Android ofrece un gran conjunto de subclases de View, como Button, TextView, EditText, ImageView, CheckBox o RadioButton. Puedes usar estas subclases para crear una IU que permita la interacción del usuario y muestre información en tu app. Si ninguna de las subclases de View satisface tus necesidades, puedes crear una subclase de View conocida como vista personalizada .

Para crear una vista personalizada, puedes extender una subclase View existente (como Button o EditText) o crear tu propia subclase de View. Si extiendes View directamente, puedes crear un elemento interactivo de la IU de cualquier tamaño y forma anulando el método onDraw() para que View lo dibuje.

Después de crear una vista personalizada, puedes agregarla a los diseños de tu actividad de la misma manera en que agregarías un TextView o un Button.

En esta lección, se muestra cómo crear una vista personalizada desde cero extendiendo View.

Conocimientos que ya deberías tener

  • Cómo crear una app con una actividad y ejecutarla con Android Studio

Qué aprenderás

  • Cómo extender View para crear una vista personalizada
  • Cómo dibujar una vista personalizada con forma circular
  • Cómo usar objetos de escucha para controlar la interacción del usuario con la vista personalizada
  • Cómo usar una vista personalizada en un diseño

Actividades

  • Extiende View para crear una vista personalizada.
  • Inicializa la vista personalizada con valores de dibujo y pintura.
  • Anula onDraw() para dibujar la vista.
  • Usa objetos de escucha para proporcionar el comportamiento de la vista personalizada.
  • Agrega la vista personalizada a un diseño.

La app de CustomFanController muestra cómo crear una subclase de vista personalizada extendiendo la clase View. La nueva subclase se llama DialView.

La app muestra un elemento de IU circular que se asemeja a un control de ventilador físico, con ajustes para apagado (0), bajo (1), medio (2) y alto (3). Cuando el usuario presiona la vista, el indicador de selección se mueve a la siguiente posición: 0-1-2-3 y vuelve a 0. Además, si la selección es 1 o superior, el color de fondo de la parte circular de la vista cambia de gris a verde (lo que indica que el ventilador está encendido).

Las vistas son los componentes básicos de la IU de una app. La clase View proporciona muchas subclases, conocidas como widgets de IU, que satisfacen muchas de las necesidades de la interfaz de usuario de una app para Android típica.

Los componentes de IU, como Button y TextView, son subclases que extienden la clase View. Para ahorrar tiempo y esfuerzo de desarrollo, puedes extender una de estas subclases de View. La vista personalizada hereda el aspecto y el comportamiento de su elemento superior, y puedes anular el comportamiento o el aspecto de la apariencia que desees cambiar. Por ejemplo, si extiendes EditText para crear una vista personalizada, la vista actúa como una vista EditText, pero también se puede personalizar para mostrar, por ejemplo, un botón X que borre texto del campo de entrada de texto.

Puedes extender cualquier subclase de View, como EditText, para obtener una vista personalizada. Elige la que más se acerque a lo que quieres lograr. Luego, puedes usar la vista personalizada como cualquier otra subclase View en uno o más diseños como un elemento XML con atributos.

Para crear tu propia vista personalizada desde cero, extiende la clase View. Tu código anula los métodos View para definir la apariencia y la funcionalidad de la vista. La clave para crear tu propia vista personalizada es que eres responsable de dibujar todo el elemento de la IU de cualquier tamaño y forma en la pantalla. Si creas una subclase de una vista existente, como Button, esa clase se encarga del dibujo por ti. (Obtendrás más información sobre el dibujo más adelante en este codelab).

Para crear una vista personalizada, sigue estos pasos generales:

  • Crea una clase de vista personalizada que extienda View o una subclase de View (como Button o EditText).
  • Si extiendes una subclase View existente, anula solo el comportamiento o los aspectos de la apariencia que desees cambiar.
  • Si extiendes la clase View, dibuja la forma de la vista personalizada y controla su apariencia anulando los métodos View, como onDraw() y onMeasure(), en la nueva clase.
  • Agrega código para responder a la interacción del usuario y, si es necesario, vuelve a dibujar la vista personalizada.
  • Usa la clase de vista personalizada como un widget de IU en el diseño XML de tu actividad. También puedes definir atributos personalizados para la vista y, así, proporcionar personalización para la vista en diferentes diseños.

En esta tarea, harás lo siguiente:

  • Crea una app con un ImageView como marcador de posición temporal para la vista personalizada.
  • Extiende View para crear la vista personalizada.
  • Inicializa la vista personalizada con valores de dibujo y pintura.

Paso 1: Crea una app con un ImageView de marcador de posición

  1. Crea una app de Kotlin con el título CustomFanController usando la plantilla Empty Activity. Asegúrate de que el nombre del paquete sea com.example.android.customfancontroller.
  2. Abre activity_main.xml en la pestaña Text para editar el código XML.
  3. Reemplaza el TextView existente por este código. Este texto actúa como una etiqueta en la actividad para la vista personalizada.
<TextView
       android:id="@+id/customViewLabel"
       android:textAppearance="@style/Base.TextAppearance.AppCompat.Display3"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:padding="16dp"
       android:textColor="@android:color/black"
       android:layout_marginStart="8dp"
       android:layout_marginEnd="8dp"
       android:layout_marginTop="24dp"
       android:text="Fan Control"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toTopOf="parent"/>
  1. Agrega este elemento ImageView al diseño. Este es un marcador de posición para la vista personalizada que crearás en este codelab.
<ImageView
       android:id="@+id/dialView"
       android:layout_width="200dp"
       android:layout_height="200dp"
       android:background="@android:color/darker_gray"
       app:layout_constraintTop_toBottomOf="@+id/customViewLabel"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       android:layout_marginLeft="8dp"
       android:layout_marginRight="8dp"
       android:layout_marginTop="8dp"/>
  1. Extrae recursos de dimensión y cadena en ambos elementos de la IU.
  2. Haz clic en la pestaña Diseño. El diseño debería verse así:

Paso 2. Crea tu clase de vista personalizada

  1. Crea una nueva clase de Kotlin llamada DialView.
  2. Modifica la definición de la clase para extender View. Importa android.view.View cuando se te solicite.
  3. Haz clic en View y, luego, en la bombilla roja. Elige Add Android View constructors using '@JvmOverloads'. Android Studio agrega el constructor de la clase View. La anotación @JvmOverloads indica al compilador de Kotlin que genere sobrecargas para esta función que sustituyan los valores predeterminados de los parámetros.
class DialView @JvmOverloads constructor(
   context: Context,
   attrs: AttributeSet? = null,
   defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
  1. Arriba de la definición de la clase DialView, justo debajo de las importaciones, agrega un enum de nivel superior para representar las velocidades del ventilador disponibles. Ten en cuenta que este enum es de tipo Int porque los valores son recursos de cadena en lugar de cadenas reales. Android Studio mostrará errores para los recursos de cadena faltantes en cada uno de estos valores. Corregirás esto en un paso posterior.
private enum class FanSpeed(val label: Int) {
   OFF(R.string.fan_off),
   LOW(R.string.fan_low),
   MEDIUM(R.string.fan_medium),
   HIGH(R.string.fan_high);
}
  1. Debajo de enum, agrega estas constantes. Los usarás como parte del dibujo de los indicadores y las etiquetas del dial.
private const val RADIUS_OFFSET_LABEL = 30      
private const val RADIUS_OFFSET_INDICATOR = -35
  1. Dentro de la clase DialView, define varias variables que necesitas para dibujar la vista personalizada. Importa android.graphics.PointF si se te solicita.
private var radius = 0.0f                   // Radius of the circle.
private var fanSpeed = FanSpeed.OFF         // The active selection.
// position variable which will be used to draw label and indicator circle position
private val pointPosition: PointF = PointF(0.0f, 0.0f)
  • El radius es el radio actual del círculo. Este valor se establece cuando la vista se dibuja en la pantalla.
  • El fanSpeed es la velocidad actual del ventilador, que es uno de los valores de la enumeración FanSpeed. De forma predeterminada, ese valor es OFF.
  • Finalmente, postPosition es un punto X,Y que se usará para dibujar varios de los elementos de la vista en la pantalla.

Estos valores se crean y se inicializan aquí en lugar de cuando se dibuja la vista, para garantizar que el paso de dibujo real se ejecute lo más rápido posible.

  1. También dentro de la definición de la clase DialView, inicializa un objeto Paint con algunos diseños básicos. Importa android.graphics.Paint y android.graphics.Typeface cuando se te solicite. Al igual que con las variables, estos estilos se inicializan aquí para acelerar el paso de dibujo.
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
   style = Paint.Style.FILL
   textAlign = Paint.Align.CENTER
   textSize = 55.0f
   typeface = Typeface.create( "", Typeface.BOLD)
}
  1. Abre res/values/strings.xml y agrega los recursos de cadenas para las velocidades del ventilador:
<string name="fan_off">off</string>
<string name="fan_low">1</string>
<string name="fan_medium">2</string>
<string name="fan_high">3</string>

Una vez que creaste una vista personalizada, debes poder dibujarla. Cuando extiendes una subclase de View, como EditText, esa subclase define la apariencia y los atributos de la vista, y se dibuja en la pantalla. Por lo tanto, no tienes que escribir código para dibujar la vista. En su lugar, puedes anular los métodos del elemento superior para personalizar tu vista.

Si creas tu propia vista desde cero (extendiendo View), eres responsable de dibujar toda la vista cada vez que se actualiza la pantalla y de anular los métodos View que controlan el dibujo. Para diseñar correctamente una vista personalizada que extienda View, debes hacer lo siguiente:

  • Anula el método onSizeChanged() para calcular el tamaño de la vista cuando aparece por primera vez y cada vez que cambia.
  • Anula el método onDraw() para dibujar la vista personalizada con un objeto Canvas diseñado con un objeto Paint.
  • Llama al método invalidate() cuando respondas a un clic del usuario que cambie la forma en que se dibuja la vista para invalidar toda la vista y, de ese modo, forzar una llamada a onDraw() para volver a dibujar la vista.

Se llama al método onDraw() cada vez que se actualiza la pantalla, lo que puede ocurrir muchas veces por segundo. Por motivos de rendimiento y para evitar fallas visuales, debes realizar la menor cantidad posible de trabajo en onDraw(). En particular, no coloques asignaciones en onDraw(), porque las asignaciones pueden conducir a una recolección de elementos no utilizados que puede causar una inestabilidad visual.

Las clases Canvas y Paint ofrecen varias combinaciones de teclas útiles para dibujar:

Aprenderás más sobre Canvas y Paint en un codelab posterior. Para obtener más información sobre cómo Android dibuja vistas, consulta Cómo dibuja vistas Android.

En esta tarea, dibujarás la vista personalizada del controlador del ventilador en la pantalla (el dial, el indicador de posición actual y las etiquetas del indicador) con los métodos onSizeChanged() y onDraw(). También crearás un método asistente, computeXYForSpeed(),,para calcular la posición actual en X e Y de la etiqueta del indicador en el dial.

Paso 1: Calcular las posiciones y dibujar la vista

  1. En la clase DialView, debajo de las inicializaciones, anula el método onSizeChanged() de la clase View para calcular el tamaño del dial de la vista personalizada. Importa kotlin.math.min cuando se solicite.

    Se llama al método onSizeChanged() cada vez que cambia el tamaño de la vista, incluida la primera vez que se dibuja cuando se infla el diseño. Anula onSizeChanged() para calcular las posiciones, las dimensiones y cualquier otro valor relacionado con el tamaño de tu vista personalizada, en lugar de volver a calcularlos cada vez que diseñes. En este caso, usas onSizeChanged() para calcular el radio actual del elemento circular del dial.
override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
   radius = (min(width, height) / 2.0 * 0.8).toFloat()
}
  1. Debajo de onSizeChanged(), agrega este código para definir una función de extensión computeXYForSpeed() para la clase PointF . Importa kotlin.math.cos y kotlin.math.sin cuando se te solicite. Esta función de extensión en la clase PointF calcula las coordenadas X e Y en la pantalla para la etiqueta de texto y el indicador actual (0, 1, 2 o 3), dada la posición FanSpeed actual y el radio del dial. Usarás esto en onDraw().
private fun PointF.computeXYForSpeed(pos: FanSpeed, radius: Float) {
   // Angles are in radians.
   val startAngle = Math.PI * (9 / 8.0)   
   val angle = startAngle + pos.ordinal * (Math.PI / 4)
   x = (radius * cos(angle)).toFloat() + width / 2
   y = (radius * sin(angle)).toFloat() + height / 2
}
  1. Anula el método onDraw() para renderizar la vista en la pantalla con las clases Canvas y Paint. Importa android.graphics.Canvas cuando se solicite. Esta es la anulación de esqueleto:
override fun onDraw(canvas: Canvas) {
   super.onDraw(canvas)
   
}
  1. Dentro de onDraw(), agrega esta línea para establecer el color de pintura en gris (Color.GRAY) o verde (Color.GREEN) según si la velocidad del ventilador es OFF o cualquier otro valor. Importa android.graphics.Color cuando se solicite.
// Set dial background color to green if selection not off.
paint.color = if (fanSpeed == FanSpeed.OFF) Color.GRAY else Color.GREEN
  1. Agrega este código para dibujar un círculo para el dial con el método drawCircle(). Este método usa el ancho y el alto de la vista actual para encontrar el centro y el radio del círculo, y el color de pintura actual. Las propiedades width y height son miembros de la superclase View y señalan las dimensiones actuales de la vista.
// Draw the dial.
canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, paint)
  1. Agrega el siguiente código para dibujar un círculo más pequeño para la marca del indicador de velocidad del ventilador, también con el método drawCircle(). Esta parte usa PointF.Método de extensión computeXYforSpeed() para calcular las coordenadas X e Y del centro del indicador según la velocidad actual del ventilador.
// Draw the indicator circle.
val markerRadius = radius + RADIUS_OFFSET_INDICATOR
pointPosition.computeXYForSpeed(fanSpeed, markerRadius)
paint.color = Color.BLACK
canvas.drawCircle(pointPosition.x, pointPosition.y, radius/12, paint)
  1. Por último, dibuja las etiquetas de velocidad del ventilador (0, 1, 2, 3) en las posiciones adecuadas alrededor del dial. Esta parte del método vuelve a llamar a PointF.computeXYForSpeed() para obtener la posición de cada etiqueta y reutiliza el objeto pointPosition cada vez para evitar asignaciones. Usa drawText() para dibujar las etiquetas.
// Draw the text labels.
val labelRadius = radius + RADIUS_OFFSET_LABEL
for (i in FanSpeed.values()) {
   pointPosition.computeXYForSpeed(i, labelRadius)
   val label = resources.getString(i.label)
   canvas.drawText(label, pointPosition.x, pointPosition.y, paint)
}

El método onDraw() completado se ve de la siguiente manera:

override fun onDraw(canvas: Canvas) {
   super.onDraw(canvas)
   // Set dial background color to green if selection not off.
   paint.color = if (fanSpeed == FanSpeed.OFF) Color.GRAY else Color.GREEN
   // Draw the dial.
   canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, paint)
   // Draw the indicator circle.
   val markerRadius = radius + RADIUS_OFFSET_INDICATOR
   pointPosition.computeXYForSpeed(fanSpeed, markerRadius)
   paint.color = Color.BLACK
   canvas.drawCircle(pointPosition.x, pointPosition.y, radius/12, paint)
   // Draw the text labels.
   val labelRadius = radius + RADIUS_OFFSET_LABEL
   for (i in FanSpeed.values()) {
       pointPosition.computeXYForSpeed(i, labelRadius)
       val label = resources.getString(i.label)
       canvas.drawText(label, pointPosition.x, pointPosition.y, paint)
   }
}

Paso 2: Agrega la vista al diseño

Para agregar una vista personalizada a la IU de una app, debes especificarla como un elemento en el diseño XML de la actividad. Controla su apariencia y comportamiento con atributos de elementos XML, como lo harías con cualquier otro elemento de la IU.

  1. En activity_main.xml, cambia la etiqueta ImageView del dialView a com.example.android.customfancontroller.DialView y borra el atributo android:background. Tanto DialView como el ImageView original heredan los atributos estándar de la clase View, por lo que no es necesario cambiar ninguno de los otros atributos. El nuevo elemento DialView se ve de la siguiente manera:
<com.example.android.customfancontroller.DialView
       android:id="@+id/dialView"
       android:layout_width="@dimen/fan_dimen"
       android:layout_height="@dimen/fan_dimen"
       app:layout_constraintTop_toBottomOf="@+id/customViewLabel"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       android:layout_marginLeft="@dimen/default_margin"
       android:layout_marginRight="@dimen/default_margin"
       android:layout_marginTop="@dimen/default_margin" />
  1. Ejecuta la app. La vista de control del ventilador aparecerá en la actividad.

La tarea final es habilitar tu vista personalizada para que realice una acción cuando el usuario la presione. Cada toque debe mover el indicador de selección a la siguiente posición: apagado-1-2-3 y volver a apagado. Además, si la selección es 1 o superior, cambia el fondo de gris a verde, lo que indica que el ventilador está encendido.

Para habilitar la opción de hacer clic en tu vista personalizada, haz lo siguiente:

  • Establece la propiedad isClickable de la vista en true. Esto permite que tu vista personalizada responda a los clics.
  • Implementa el método performClick() de la clase View para realizar operaciones cuando se haga clic en la vista.
  • Llama al método invalidate(). Esto le indica al sistema Android que llame al método onDraw() para volver a dibujar la vista.

Normalmente, con una vista estándar de Android, implementas OnClickListener() para realizar una acción cuando el usuario hace clic en esa vista. En el caso de una vista personalizada, implementas el método performClick() de la clase View y llamas a super.performClick(). El método performClick() predeterminado también llama a onClickListener(), por lo que puedes agregar tus acciones a performClick() y dejar onClickListener() disponible para que tú o los demás desarrolladores que puedan usar tu vista personalizada la personalicen aún más.

  1. En DialView.kt, dentro de la enumeración FanSpeed, agrega una función de extensión next() que cambie la velocidad actual del ventilador a la siguiente velocidad de la lista (de OFF a LOW, MEDIUM y HIGH, y, luego, de vuelta a OFF). La enumeración completa ahora se ve de la siguiente manera:
private enum class FanSpeed(val label: Int) {
   OFF(R.string.fan_off),
   LOW(R.string.fan_low),
   MEDIUM(R.string.fan_medium),
   HIGH(R.string.fan_high);

   fun next() = when (this) {
       OFF -> LOW
       LOW -> MEDIUM
       MEDIUM -> HIGH
       HIGH -> OFF
   }
}
  1. Dentro de la clase DialView, justo antes del método onSizeChanged(), agrega un bloque init(). Si se establece la propiedad isClickable de la vista como verdadera, se habilita la vista para que acepte la entrada del usuario.
init {
   isClickable = true
}
  1. Debajo de init(),, anula el método performClick() con el siguiente código.
override fun performClick(): Boolean {
   if (super.performClick()) return true

   fanSpeed = fanSpeed.next()
   contentDescription = resources.getString(fanSpeed.label)
  
   invalidate()
   return true
}

Llamada a super.Primero debe ocurrir performClick(), lo que habilita los eventos de accesibilidad y llama a onClickListener().

Las siguientes dos líneas incrementan la velocidad del ventilador con el método next() y establecen la descripción del contenido de la vista en el recurso de cadena que representa la velocidad actual (apagado, 1, 2 o 3).

Por último, el método invalidate() invalida toda la vista, lo que fuerza una llamada a onDraw() para volver a dibujar la vista. Si algo en tu vista personalizada cambia por algún motivo, incluida la interacción del usuario, y el cambio debe mostrarse, llama a invalidate()..

  1. Ejecuta la app. Presiona el elemento DialView para mover el indicador de apagado a 1. El dial debería ponerse de color verde. Con cada toque, el indicador debería moverse a la siguiente posición. Cuando el indicador vuelva a apagarse, el dial debería volver a ponerse de color gris.

En este ejemplo, se muestran los mecanismos básicos para usar atributos personalizados con tu vista personalizada. Defines atributos personalizados para la clase DialView con un color diferente para cada posición del dial del ventilador.

  1. Crea y abre res/values/attrs.xml.
  2. Dentro de <resources>, agrega un elemento de recurso <declare-styleable>.
  3. Dentro del elemento de recurso <declare-styleable>, agrega tres elementos attr, uno para cada atributo, con un name y un format. El format es como un tipo y, en este caso, es color.
<?xml version="1.0" encoding="utf-8"?>
<resources>
       <declare-styleable name="DialView">
           <attr name="fanColor1" format="color" />
           <attr name="fanColor2" format="color" />
           <attr name="fanColor3" format="color" />
       </declare-styleable>
</resources>
  1. Abre el archivo de diseño activity_main.xml.
  2. En DialView, agrega atributos para fanColor1, fanColor2 y fanColor3, y establece sus valores en los colores que se muestran a continuación. Usa app: como prefacio del atributo personalizado (como en app:fanColor1) en lugar de android:, ya que tus atributos personalizados pertenecen al espacio de nombres schemas.android.com/apk/res/your_app_package_name en lugar del espacio de nombres android.
app:fanColor1="#FFEB3B"
app:fanColor2="#CDDC39"
app:fanColor3="#009688"

Para usar los atributos en tu clase DialView, debes recuperarlos. Se almacenan en un AttributeSet, que se entrega a tu clase cuando se crea, si existe. Recuperas los atributos en init y asignas los valores de los atributos a variables locales para el almacenamiento en caché.

  1. Abre el archivo de la clase DialView.kt.
  2. Dentro de DialView, declara variables para almacenar en caché los valores de los atributos.
private var fanSpeedLowColor = 0
private var fanSpeedMediumColor = 0
private var fanSeedMaxColor = 0
  1. En el bloque init, agrega el siguiente código con la función de extensión withStyledAttributes. Proporcionas los atributos y la vista, y configuras tus variables locales. Importar withStyledAttributes también importará la función getColor() correcta.
context.withStyledAttributes(attrs, R.styleable.DialView) {
   fanSpeedLowColor = getColor(R.styleable.DialView_fanColor1, 0)
   fanSpeedMediumColor = getColor(R.styleable.DialView_fanColor2, 0)
   fanSeedMaxColor = getColor(R.styleable.DialView_fanColor3, 0)
}
  1. Usa las variables locales en onDraw()para establecer el color del dial según la velocidad actual del ventilador. Reemplaza la línea en la que se establece el color de pintura (paint.color = if (fanSpeed == FanSpeed.OFF) Color.GRAY else Color.GREEN) por el siguiente código.
paint.color = when (fanSpeed) {
   FanSpeed.OFF -> Color.GRAY
   FanSpeed.LOW -> fanSpeedLowColor
   FanSpeed.MEDIUM -> fanSpeedMediumColor
   FanSpeed.HIGH -> fanSeedMaxColor
} as Int
  1. Ejecuta la app, haz clic en el dial y el parámetro de configuración de color debería ser diferente para cada posición, como se muestra a continuación.

Para obtener más información sobre los atributos de vistas personalizados, consulta Cómo crear una clase de View.

La accesibilidad es un conjunto de técnicas de diseño, implementación y prueba que permiten que todas las personas, incluidas las que tienen discapacidades, puedan usar tu app.

Las discapacidades comunes que pueden afectar el uso de un dispositivo Android por parte de una persona incluyen ceguera, visión reducida, daltonismo, sordera o pérdida de audición, y habilidades motoras restringidas. Cuando desarrollas tus apps teniendo en cuenta la accesibilidad, mejoras la experiencia del usuario no solo para los usuarios con estas discapacidades, sino también para todos los demás.

Android proporciona varias funciones de accesibilidad de forma predeterminada en las vistas de IU estándar, como TextView y Button. Sin embargo, cuando creas una vista personalizada, debes tener en cuenta cómo proporcionará funciones de accesibilidad, como descripciones habladas del contenido en pantalla.

En esta tarea, aprenderás sobre TalkBack, el lector de pantalla de Android, y modificarás tu app para incluir sugerencias y descripciones que se puedan leer para la vista personalizada DialView.

Paso 1. Explora TalkBack

TalkBack es el lector de pantalla integrado de Android. Con TalkBack habilitado, el usuario puede interactuar con su dispositivo Android sin ver la pantalla, ya que Android describe los elementos de la pantalla en voz alta. Los usuarios con discapacidad visual podrían depender de TalkBack para usar tu app.

En esta tarea, habilitarás TalkBack para comprender cómo funcionan los lectores de pantalla y cómo navegar por las apps.

  1. En un dispositivo o emulador Android, navega a Configuración > Accesibilidad > TalkBack.
  2. Presiona el botón de activación Activar/desactivar para activar TalkBack.
  3. Presiona Aceptar para confirmar los permisos.
  4. Confirma la contraseña del dispositivo si se te solicita. Si es la primera vez que ejecutas TalkBack, se iniciará un instructivo. (Es posible que el tutorial no esté disponible en dispositivos más antiguos).
  5. Puede ser útil navegar por el instructivo con los ojos cerrados. Para volver a abrirlo en el futuro, ve a Configuración > Accesibilidad > TalkBack > Configuración > Iniciar instructivo de TalkBack.
  6. Compila y ejecuta la app de CustomFanController, o bien ábrela con el botón Visión general o Recientes de tu dispositivo. Con TalkBack activado, observa que se anuncia el nombre de la app, así como el texto de la etiqueta TextView (“Fan Control”). Sin embargo, si presionas la vista DialView, no se proporciona información sobre el estado de la vista (el parámetro de configuración actual del dial) ni sobre la acción que se realizará cuando presiones la vista para activarla.

Paso 2: Agrega descripciones de contenido para las etiquetas de los diales

Las descripciones de contenido explican el significado y el propósito de las vistas en tu app. Estas etiquetas permiten que los lectores de pantalla, como la función TalkBack de Android, expliquen la función de cada elemento con precisión. En el caso de las vistas estáticas, como ImageView, puedes agregar la descripción del contenido a la vista en el archivo de diseño con el atributo contentDescription. Las vistas de texto (TextView y EditText) usan automáticamente el texto de la vista como la descripción del contenido.

En el caso de la vista de control de ventilador personalizado, debes actualizar de forma dinámica la descripción del contenido cada vez que se haga clic en la vista para indicar el ajuste actual del ventilador.

  1. En la parte inferior de la clase DialView, declara una función updateContentDescription() sin argumentos ni tipo de datos que se muestra.
fun updateContentDescription() {
}
  1. Dentro de updateContentDescription(), cambia la propiedad contentDescription de la vista personalizada al recurso de cadena asociado con la velocidad del ventilador actual (apagado, 1, 2 o 3). Estas son las mismas etiquetas que se usan en onDraw() cuando el dial se dibuja en la pantalla.
fun updateContentDescription() {
   contentDescription = resources.getString(fanSpeed.label)
}
  1. Desplázate hacia arriba hasta el bloque init() y, al final de ese bloque, agrega una llamada a updateContentDescription(). Esto inicializa la descripción del contenido cuando se inicializa la vista.
init {
   isClickable = true
   // ...

   updateContentDescription()
}
  1. Agrega otra llamada a updateContentDescription() en el método performClick(), justo antes de invalidate().
override fun performClick(): Boolean {
   if (super.performClick()) return true
   fanSpeed = fanSpeed.next()
   updateContentDescription()
   invalidate()
   return true
}
  1. Compila y ejecuta la app, y asegúrate de que TalkBack esté activado. Presiona para cambiar el parámetro de configuración de la vista de dial y observa que ahora TalkBack anuncia la etiqueta actual (apagado, 1, 2, 3) y la frase "Presiona dos veces para activar".

Paso 3: Agrega más información para la acción de clic

Podrías detenerte ahí y tu vista sería utilizable en TalkBack. Sin embargo, sería útil que la vista indicara no solo que se puede activar ("Presiona dos veces para activar"), sino también que explicara qué sucederá cuando se active la vista ("Presiona dos veces para cambiar" o "Presiona dos veces para restablecer").

Para ello, agrega información sobre la acción de la vista (en este caso, una acción de clic o toque) a un objeto de información del nodo de accesibilidad, a través de un delegado de accesibilidad. Un delegado de accesibilidad te permite personalizar las funciones relacionadas con la accesibilidad de tu app a través de la composición (en lugar de la herencia).

Para esta tarea, usarás las clases de accesibilidad en las bibliotecas de Android Jetpack (androidx.*) para garantizar la retrocompatibilidad.

  1. En DialView.kt, en el bloque init, establece un delegado de accesibilidad en la vista como un nuevo objeto AccessibilityDelegateCompat. Importa androidx.core.view.ViewCompat y androidx.core.view.AccessibilityDelegateCompat cuando se te solicite. Esta estrategia habilita la mayor cantidad de compatibilidad con versiones anteriores en tu app.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   
})
  1. Dentro del objeto AccessibilityDelegateCompat, anula la función onInitializeAccessibilityNodeInfo() con un objeto AccessibilityNodeInfoCompat y llama al método de la superclase. Importa androidx.core.view.accessibility.AccessibilityNodeInfoCompat cuando se te solicite.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   override fun onInitializeAccessibilityNodeInfo(host: View, 
                            info: AccessibilityNodeInfoCompat) {
      super.onInitializeAccessibilityNodeInfo(host, info)

   }  
})

Cada vista tiene un árbol de nodos de accesibilidad, que puede corresponder o no a los componentes de diseño reales de la vista. Los servicios de accesibilidad de Android navegan por esos nodos para obtener información sobre la vista (como descripciones de contenido que se pueden leer o acciones posibles que se pueden realizar en esa vista). Cuando creas una vista personalizada, es posible que también debas anular la información del nodo para proporcionar información personalizada sobre la accesibilidad. En este caso, anularás la información del nodo para indicar que hay información personalizada para la acción de la vista.

  1. Dentro de onInitializeAccessibilityNodeInfo(), crea un nuevo objeto AccessibilityNodeInfoCompat.AccessibilityActionCompat y asígnalo a la variable customClick. Pasa al constructor la constante AccessibilityNodeInfo.ACTION_CLICK y una cadena de marcador de posición. Importa AccessibilityNodeInfo cuando se te solicite.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   override fun onInitializeAccessibilityNodeInfo(host: View, 
                            info: AccessibilityNodeInfoCompat) {
      super.onInitializeAccessibilityNodeInfo(host, info)
      val customClick = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
         AccessibilityNodeInfo.ACTION_CLICK,
        "placeholder"
      )
   }  
})

La clase AccessibilityActionCompat representa una acción en una vista para fines de accesibilidad. Una acción típica es un clic o un toque, como se usa aquí, pero otras acciones pueden incluir obtener o perder el enfoque, una operación del portapapeles (cortar/copiar/pegar) o desplazarse dentro de la vista. El constructor de esta clase requiere una constante de acción (aquí, AccessibilityNodeInfo.ACTION_CLICK) y una cadena que TalkBack usa para indicar qué es la acción.

  1. Reemplaza la cadena "placeholder" por una llamada a context.getString() para recuperar un recurso de cadena. Para el recurso específico, prueba la velocidad actual del ventilador. Si la velocidad actual es FanSpeed.HIGH, la cadena es "Reset". Si la velocidad del ventilador es otra, la cadena es "Change.". Crearás estos recursos de cadena en un paso posterior.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   override fun onInitializeAccessibilityNodeInfo(host: View, 
                            info: AccessibilityNodeInfoCompat) {
      super.onInitializeAccessibilityNodeInfo(host, info)
      val customClick = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
         AccessibilityNodeInfo.ACTION_CLICK,
        context.getString(if (fanSpeed !=  FanSpeed.HIGH) R.string.change else R.string.reset)
      )
   }  
})
  1. Después del paréntesis de cierre de la definición de customClick, usa el método addAction() para agregar la nueva acción de accesibilidad al objeto de información del nodo.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   override fun onInitializeAccessibilityNodeInfo(host: View, 
                            info: AccessibilityNodeInfoCompat) {
       super.onInitializeAccessibilityNodeInfo(host, info)
       val customClick = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
           AccessibilityNodeInfo.ACTION_CLICK,
           context.getString(if (fanSpeed !=  FanSpeed.HIGH) 
                                 R.string.change else R.string.reset)
       )
       info.addAction(customClick)
   }
})
  1. En res/values/strings.xml, agrega los recursos de cadenas para "Cambiar" y "Restablecer".
<string name="change">Change</string>
<string name="reset">Reset</string>
  1. Compila y ejecuta la app, y asegúrate de que TalkBack esté activado. Observa que la frase "Doble toque para activar" ahora es "Doble toque para cambiar" (si la velocidad del ventilador es inferior a alta o 3) o "Doble toque para restablecer" (si la velocidad del ventilador ya está en alta o 3). Ten en cuenta que el mensaje "Presiona dos veces para…" lo proporciona el servicio de TalkBack.

Descarga el código del codelab terminado.

$  git clone https://github.com/googlecodelabs/android-kotlin-drawing-custom-views


También puedes descargar el repositorio como un archivo ZIP, descomprimirlo y abrirlo en Android Studio.

Download Zip

  • Para crear una vista personalizada que herede el aspecto y el comportamiento de una subclase View, como EditText, agrega una nueva clase que extienda esa subclase y realiza ajustes anulando algunos de los métodos de la subclase.
  • Para crear una vista personalizada de cualquier tamaño y forma, agrega una clase nueva que extienda View.
  • Anula los métodos View, como onDraw(), para definir la forma y la apariencia básica de la vista.
  • Usa invalidate() para forzar un dibujo o un nuevo dibujo de la vista.
  • Para optimizar el rendimiento, asigna variables y asigna los valores necesarios para dibujar y pintar antes de usarlos en onDraw(), como en la inicialización de variables miembro.
  • Anula performClick() en lugar de OnClickListener() en la vista personalizada para proporcionar el comportamiento interactivo de la vista. Esto permite que tú o cualquier otro desarrollador de Android que use tu clase de vista personalizada utilice onClickListener() para proporcionar un comportamiento adicional.
  • Agrega la vista personalizada a un archivo de diseño XML con atributos para definir su apariencia, como lo harías con otros elementos de la IU.
  • Crea el archivo attrs.xml en la carpeta values para definir atributos personalizados. Luego, puedes usar los atributos personalizados para la vista personalizada en el archivo de diseño XML.

Curso de Udacity:

Documentación para desarrolladores de Android:

Videos:

En esta sección, se enumeran las posibles actividades para el hogar para los alumnos que trabajan en este codelab como parte de un curso dirigido por un instructor. Depende del instructor hacer lo siguiente:

  • Si es necesario, asigna una tarea.
  • Comunicarles a los alumnos cómo enviar las actividades para el hogar.
  • Califica las actividades para el hogar.

Los instructores pueden usar estas sugerencias en la medida que quieran y deben asignar cualquier otra actividad para el hogar que consideren apropiada.

Si estás trabajando en este codelab por tu cuenta, usa estas actividades para el hogar para probar tus conocimientos.

Pregunta 1

¿Qué método anulas para calcular las posiciones, las dimensiones y cualquier otro valor cuando se le asigna un tamaño a la vista personalizada por primera vez?

onMeasure()

onSizeChanged()

invalidate()

onDraw()

Pregunta 2

Para indicar que deseas que tu vista se vuelva a dibujar con onDraw(), ¿qué método llamas desde el subproceso de IU después de que cambió un valor de atributo?

▢ onMeasure()

▢ onSizeChanged()

▢ invalidate()

▢ getVisibility()

Pregunta 3

¿Qué método View deberías anular para agregar interactividad a tu vista personalizada?

▢ setOnClickListener()

▢ onSizeChanged()

▢ isClickable()

▢ performClick()

Para obtener vínculos a otros codelabs de este curso, consulta la página de destino de los codelabs de Aspectos avanzados de Android en Kotlin.