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 en secuencia, pero no es obligatorio. Todos los codelabs del curso se detallan en la página de destino de Codelabs avanzados de Android en Kotlin.

Introducción

Android ofrece un gran conjunto de subclases 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 View se ajusta a tus necesidades, puedes crear una subclase 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, podrás crear un elemento de IU interactivo de cualquier tamaño y forma. Para ello, anula el método onDraw() para que View dibuje.

Después de crear una vista personalizada, puedes agregarla a tus diseños de actividad de la misma manera en que agregarías una 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 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 CustomFanController demuestra cómo crear una subclase de vista personalizada extendiendo la clase View. La nueva subclase se llama DialView.

La app muestra un elemento circular de la IU que se asemeja a un control de ventilador físico, con opciones de 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 nuevamente 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 la alimentación del ventilador está encendida).

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

Los componentes básicos de la 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 aspecto de la apariencia que quieras cambiar. Por ejemplo, si extiendes EditText para crear una vista personalizada, la vista actúa como una vista de EditText, pero también podría personalizarse para mostrar, por ejemplo, un botón X que borra el texto del campo de entrada de texto.

Puedes ampliar cualquier subclase View, como EditText, para obtener una vista personalizada; elige la que esté más cerca de 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 encargará de dibujarla. (Más adelante en este codelab, obtendrás más información sobre cómo dibujar).

Para crear una vista personalizada, siga estos pasos generales:

  • Crea una clase de vistas personalizada que extienda View o extienda una subclase 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. Para ello, anula los métodos View, como onDraw() y onMeasure(), en la clase nueva.
  • 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, a fin de proporcionar una personalización para la vista en diferentes diseños.

En esta tarea, hará 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 marcador de posición de ImageView

  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 de 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 strings y dimensiones en ambos elementos de la IU.
  2. Haz clic en la pestaña Design. El diseño debería verse de la siguiente manera:

Paso 2. Cree su clase personalizada de vista

  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. Selecciona Add Android View constructors using '@JvmOverloads'. Android Studio agrega el constructor desde la clase View. La anotación @JvmOverloads le indica al compilador de Kotlin que genere sobrecargas para esta función que sustituyen los valores de parámetros predeterminados.
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 una enum de nivel superior para representar las velocidades de ventilador disponibles. Ten en cuenta que este enum es de tipo Int porque los valores son recursos de string en lugar de strings reales. Android Studio mostrará errores para los recursos de strings faltantes en cada uno de estos valores; los corregirás 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. Las usarás como parte del dibujo de los indicadores de marcado y las etiquetas.
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)
  • La radius es el radio actual del círculo. Este valor se establece cuando la vista se dibuja en la pantalla.
  • La 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.
  • Por último, postPosition es un punto X, Y que se usará para dibujar varios elementos de la vista en la pantalla.

Estos valores se crean e 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 varios estilos básicos. Importa android.graphics.Paint y android.graphics.Typeface cuando se te solicite. Como antes, con las variables, estos estilos se inicializan aquí para ayudar a 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 strings 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 creas una vista personalizada, debes poder dibujarla. Cuando extiendes una subclase View como EditText, esa subclase define la apariencia y los atributos de la vista, y se dibuja en la pantalla. En consecuencia, no es necesario escribir código para dibujar la vista. Puedes anular métodos del elemento superior para personalizar tu vista.

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

  • Calcula el tamaño de la vista cuando aparece por primera vez y, cada vez que se modifique el tamaño de esa vista, anula el método onSizeChanged().
  • Anula el método onDraw() para dibujar la vista personalizada usando un objeto Canvas con un estilo Paint.
  • Llama al método invalidate() cuando se responda a un clic de usuario que cambia la forma en que se dibuja la vista para invalidar toda la vista. De esta manera, se fuerza una llamada a onDraw() para volver a dibujarla.

Se llama al método onDraw() cada vez que se actualiza la pantalla, lo que puede ser muchas veces por segundo. Por motivos de rendimiento y a fin de evitar fallas visuales, debes hacer el menor trabajo posible en onDraw(). En particular, no coloques asignaciones en onDraw(), ya que estas podrían generar una recolección de elementos no utilizados que podría provocar inestabilidades visuales.

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

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

En esta tarea, dibujarás la vista personalizada del control 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 auxiliar, computeXYForSpeed(),, para calcular la posición X,Y actual de la etiqueta del indicador en el dial.

Paso 1: Calcula las posiciones y dibuja 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 el diseño aumenta. Anula onSizeChanged() para calcular posiciones, dimensiones y cualquier otro valor relacionado con tu tamaño de vista personalizada, en lugar de volver a calcularlos cada vez que dibujes. En este caso, usarás 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 a fin de 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, Y en la pantalla para la etiqueta de texto y el indicador actual (0, 1, 2 o 3), según la posición y el radio actuales de FanSpeed. 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 del esqueleto:
override fun onDraw(canvas: Canvas) {
   super.onDraw(canvas)
   
}
  1. Dentro de onDraw(), agrega esta línea para configurar el color de la 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 a fin de dibujar un círculo para el dial con el método drawCircle(). Este método usa el ancho y la altura actuales para encontrar el centro del círculo, el radio del círculo y el color de la pintura actual. Las propiedades width y height son miembros de la superclase View e indican 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 a fin de 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.computeXYforSpeed() para calcular las coordenadas X e Y del centro de indicadores,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() completo 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: Cómo agregar 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 los atributos de elementos XML, como lo harías con cualquier otro elemento de la IU.

  1. En activity_main.xml, cambia la etiqueta ImageView para 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 así:
<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. Tu vista de control de ventilador aparece en la actividad.

La última tarea consiste en habilitar tu vista personalizada para que realice una acción cuando el usuario presione la vista. Cada vez que presiones el indicador de selección, deberás moverlo a la siguiente posición: apagado, 1 y 2, y viceversa. Además, si la selección es 1 o mayor, cambia el fondo de gris a verde, lo que indica que el ventilador está encendido.

Para habilitar la vista personalizada en la que se puede hacer clic, sigue estos pasos:

  • Establece la propiedad isClickable de la vista en true. Esto permite que su vista personalizada responda a los clics.
  • Implementa la clase View performClick() para realizar operaciones cuando se hace 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.

Por lo general, con una vista estándar de Android, implementas OnClickListener() para realizar una acción cuando el usuario hace clic en esa vista. Para una vista personalizada, implementa el método performClick() de la clase View y llama a super.performClick(). El método performClick() predeterminado también llama a onClickListener(), de modo que puedas agregar tus acciones a performClick() y dejar onClickListener() disponible para que tú o bien otros desarrolladores que lo usan puedan ver tu personalización.

  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, 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 configura la propiedad isClickable de la vista como verdadera, se permite que esa vista acepte las entradas 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
}

La llamada a super.performClick() debe ocurrir primero, lo que habilita los eventos de accesibilidad y llama a onClickListener().

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

Por último, el método invalidate() invalida toda la vista, lo que obliga a 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 se debe mostrar el cambio, llama a invalidate()..

  1. Ejecuta la app. Presiona el elemento DialView para desactivar el indicador 1. El dial se pondrá de color verde. Con cada toque, el indicador debería moverse a la siguiente posición. Cuando el indicador vuelva a estar desactivado, el dial debe volver a ser gris.

En este ejemplo, se muestra la mecánica básica de usar atributos personalizados con tu vista personalizada. Debes definir 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 recursos <declare-styleable>.
  3. Dentro del elemento del recurso <declare-styleable>, agrega tres elementos attr, uno para cada atributo, con name y 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 el prefacio para el 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 de a 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 en el momento de su creación, 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 clase DialView.kt.
  2. Dentro del DialView, declara las variables para almacenar en caché los valores del atributo.
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. Usted proporciona los atributos y la vista, y configura sus variables locales. Si importas withStyledAttributes, también se 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 de onDraw() para establecer el color del dial según la velocidad actual del ventilador. Reemplaza la línea en la que se estableció el color de la 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 la 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 vista personalizada, consulta Cómo crear una clase de vista.

La accesibilidad es un conjunto de técnicas de diseño, implementación y pruebas que permiten que todos los usuarios, incluidas las personas con discapacidad, 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. Desarrollar tus apps con la accesibilidad en mente mejora la experiencia de los usuarios no solo para los usuarios con estas discapacidades, sino también para todos los demás usuarios.

Android proporciona varias funciones de accesibilidad de forma predeterminada en las vistas de IU estándar, como TextView y Button. Sin embargo, cuando crees una vista personalizada, debes tener en cuenta cómo esa vista personalizada proporcionará funciones accesibles, como descripciones por voz 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 de habla y descripciones para la vista personalizada de DialView.

Paso 1. Explora TalkBack

TalkBack es el lector de pantalla incorporado de Android. Con TalkBack habilitado, el usuario puede interactuar con su dispositivo Android sin ver la pantalla, porque 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 de Android, navega a Configuración > Accesibilidad > TalkBack.
  2. Presiona el botón de activación Activado/Desactivado para activar TalkBack.
  3. Presiona Aceptar para confirmar los permisos.
  4. Si se te solicita, confirma la contraseña de tu dispositivo. Si es la primera vez que ejecutas TalkBack, se iniciará un instructivo. (Es posible que el instructivo no esté disponible en dispositivos más antiguos).
  5. Puede ser útil explorar el instructivo con los ojos cerrados. Para volver a abrir el instructivo en el futuro, navega 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 Recientes o Recientes del dispositivo. Cuando TalkBack esté activado, observa que se anuncia el nombre de la app y el texto de la etiqueta TextView (Control de ventilador). Sin embargo, si presionas la vista DialView, no se leerá información sobre el estado de la vista (la configuración actual del dial) ni sobre la acción que se llevará a cabo cuando la presiones para activarla.

Paso 2: Agregue descripciones de contenido para las etiquetas de marcación

Las descripciones de contenido describen 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 exactitud. En el caso de las vistas estáticas, como ImageView, puedes agregar la descripción de 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 en la vista como descripción de contenido.

En la vista de control de ventilador personalizado, debes actualizar de forma dinámica la descripción del contenido cada vez que se hace clic en ella para indicar la configuración 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 strings asociado con la velocidad actual del ventilador (desactivada, 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 hasta el bloque init() y, al final del bloque, agrega una llamada a updateContentDescription(). De esta manera, se 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 la configuración de la vista de marcado y observa que, ahora que TalkBack anuncia la etiqueta actual (desactivada, 1, 2 y 3), así como la frase, presiona dos veces para activar la opción.

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

Si te detienes allí, la vista se podrá usar en TalkBack. Pero sería útil que tu vista no solo indicara que puede activarse (presionar dos veces para activar), sino también explicar qué ocurrirá cuando se active la vista (presiona dos veces para cambiar). O presiona dos veces para restablecer."

Para ello, debes agregar información sobre la acción de la vista (aquí, un clic o una acción de presión) a un objeto de información de nodo de accesibilidad, por medio de un delegado de accesibilidad. Un delegado de accesibilidad te permite personalizar las funciones relacionadas con la accesibilidad de tu app mediante la composición (en lugar de la herencia).

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

  1. En DialView.kt, en el bloque init, configura un delegado de accesibilidad en la vista como un objeto AccessibilityDelegateCompat nuevo. Importa androidx.core.view.ViewCompat y androidx.core.view.AccessibilityDelegateCompat cuando se te solicite. Esta estrategia permite la mayor retrocompatibilidad 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 superpuesto. 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 estos nodos para encontrar información sobre la vista (como descripciones de contenido hablado o posibles acciones que se pueden realizar en esa vista). Cuando crea una vista personalizada, es posible que también deba anular la información del nodo a fin de proporcionar información personalizada para ofrecer 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 string 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 con fines de accesibilidad. Una acción típica es un clic o un toque, como se usa aquí, pero otras acciones pueden incluir ganar o perder el enfoque, una operación del portapapeles (cortar/copiar/pegar) o desplazarse dentro de la vista. El constructor para esta clase requiere una constante de acción (aquí, AccessibilityNodeInfo.ACTION_CLICK) y una string que se usa en TalkBack para indicar cuál es la acción.

  1. Reemplaza la string "placeholder" por una llamada a context.getString() para recuperar un recurso de strings. Para el recurso específico, prueba la velocidad actual del ventilador. Si actualmente la velocidad es FanSpeed.HIGH, la string es "Reset". Si la velocidad del ventilador es cualquier otra cosa, la string es "Change." En este paso, crearás estos recursos de string.
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 de los paréntesis de cierre de la definición customClick, usa el método addAction() a fin de agregar la nueva acción de accesibilidad al objeto de información de 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 strings para "Change" y "Reset".
<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 "Presiona dos veces para activar" ahora es dos veces (si la velocidad del ventilador es inferior o alta) o dos veces para restablecerla (si la velocidad está alta o 3). Ten en cuenta que el servicio de TalkBack proporciona la solicitud "Presiona dos veces para...".

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 la apariencia y el comportamiento de una subclase View, como EditText, agrega una clase nueva que extienda esa subclase y haz ajustes anulando algunos de los métodos de esta.
  • Para crear una vista personalizada de cualquier tamaño y forma, agrega una nueva clase 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 el dibujo o el rediseño de la vista.
  • Para optimizar el rendimiento, asigna variables y asigna los valores necesarios para dibujar y pintar antes de usarlas en onDraw(), como en la inicialización de variables de miembro.
  • Anula performClick() en lugar de OnClickListener() a la vista personalizada para proporcionar el comportamiento interactivo de la vista. Esto permite que tú o cualquier otro desarrollador de Android que pueda usar tu clase de vista personalizada use 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 tareas para los alumnos que trabajan con este codelab como parte de un curso que dicta un instructor. Depende del instructor hacer lo siguiente:

  • Si es necesario, asigna la tarea.
  • Informa a los alumnos cómo enviar los deberes.
  • Califica las tareas.

Los instructores pueden usar estas sugerencias lo poco o lo que quieran, y deben asignar cualquier otra tarea que consideren apropiada.

Si estás trabajando en este codelab por tu cuenta, usa estas tareas para poner a prueba tus conocimientos.

Question 1

Para calcular las posiciones, las dimensiones y cualquier otro valor cuando se asigna un tamaño a la vista personalizada, ¿qué método se debe anular?

onMeasure()

onSizeChanged()

invalidate()

onDraw()

Question 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 cambia un valor de atributo?

▢ onMeasure()

▢ onSizeChanged()

▢ invalidate()

▢ getVisibility()

Question 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 Codelabs avanzados de Android en Kotlin.