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
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 subclaseView
(comoButton
oEditText
). - 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étodosView
, comoonDraw()
yonMeasure()
, 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
- Crea una app de Kotlin con el título
CustomFanController
usando la plantilla"Empty Activity". Asegúrate de que el nombre del paquete seacom.example.android.customfancontroller
. - Abre
activity_main.xml
en la pestaña Text para editar el código XML. - 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"/>
- 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"/>
- Extrae recursos de strings y dimensiones en ambos elementos de la IU.
- 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
- Crea una nueva clase de Kotlin llamada
DialView
. - Modifica la definición de la clase para extender
View
. Importaandroid.view.View
cuando se te solicite. - Haz clic en
View
y, luego, en la bombilla roja. Selecciona Add Android View constructors using '@JvmOverloads'. Android Studio agrega el constructor desde la claseView
. 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) {
- Arriba de la definición de la clase
DialView
, justo debajo de las importaciones, agrega unaenum
de nivel superior para representar las velocidades de ventilador disponibles. Ten en cuenta que esteenum
es de tipoInt
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);
}
- 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
- Dentro de la clase
DialView
, define varias variables que necesitas para dibujar la vista personalizada. Importaandroid.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ónFanSpeed
. De forma predeterminada, ese valor esOFF
. - 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.
- También dentro de la definición de la clase
DialView
, inicializa un objetoPaint
con varios estilos básicos. Importaandroid.graphics.Paint
yandroid.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)
}
- 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 objetoCanvas
con un estiloPaint
. - 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 aonDraw()
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:
- Dibuja el texto con
drawText()
. Para especificar el tipo de letra, llama asetTypeface()
y el color del texto llamando asetColor()
. - Dibuja formas básicas con
drawRect()
,drawOval()
ydrawArc()
. Llama asetStyle()
para cambiar si las formas están rellenas, contorneadas o ambas. - Dibuja mapas de bits con
drawBitmap()
.
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
- En la clase
DialView
, debajo de las inicializaciones, anula el métodoonSizeChanged()
de la claseView
para calcular el tamaño del dial de la vista personalizada. Importakotlin
.math.min
cuando se solicite
Se llama al métodoonSizeChanged()
cada vez que cambia el tamaño de la vista, incluida la primera vez que se dibuja cuando el diseño aumenta. AnulaonSizeChanged()
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ásonSizeChanged()
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()
}
- Debajo de
onSizeChanged()
, agrega este código a fin de definir una función de extensióncomputeXYForSpeed()
para la clasePointF
. Importakotlin.math.cos
ykotlin.math.sin
cuando se te solicite. Esta función de extensión en la clasePointF
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 deFanSpeed
. Usarás esto enonDraw().
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
}
- Anula el método
onDraw()
para renderizar la vista en la pantalla con las clasesCanvas
yPaint
. Importaandroid.graphics.Canvas
cuando se solicite. Esta es la anulación del esqueleto:
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
}
- 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 esOFF
o cualquier otro valor. Importaandroid.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
- 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 propiedadeswidth
yheight
son miembros de la superclaseView
e indican las dimensiones actuales de la vista.
// Draw the dial.
canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, paint)
- 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 usaPointF
.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)
- 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 objetopointPosition
cada vez para evitar asignaciones. UsadrawText()
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.
- En
activity_main.xml
, cambia la etiquetaImageView
paradialView
acom.example.android.customfancontroller.DialView
y borra el atributoandroid:background
. TantoDialView
como elImageView
original heredan los atributos estándar de la claseView
, por lo que no es necesario cambiar ninguno de los otros atributos. El nuevo elementoDialView
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" />
- 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 entrue
. 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étodoonDraw()
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.
- En
DialView.kt
, dentro de la enumeraciónFanSpeed
, agrega una función de extensiónnext()
que cambie la velocidad actual del ventilador a la siguiente velocidad de la lista (deOFF
aLOW
,MEDIUM
yHIGH
y, luego, aOFF
). 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
}
}
- Dentro de la clase
DialView
, justo antes del métodoonSizeChanged()
, agrega un bloqueinit()
. Si se configura la propiedadisClickable
de la vista como verdadera, se permite que esa vista acepte las entradas del usuario.
init {
isClickable = true
}
- Debajo de
init(),
, anula el métodoperformClick()
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().
.
- 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.
- Crea y abre
res/values/attrs.xml
. - Dentro de
<resources>
, agrega un elemento de recursos<declare-styleable>
. - Dentro del elemento del recurso
<declare-styleable>
, agrega tres elementosattr
, uno para cada atributo, conname
yformat
. Elformat
es como un tipo y, en este caso, escolor
.
<?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>
- Abre el archivo de diseño
activity_main.xml
. - En
DialView
, agrega atributos parafanColor1
,fanColor2
yfanColor3
, y establece sus valores en los colores que se muestran a continuación. Usaapp:
como el prefacio para el atributo personalizado (como enapp:fanColor1
) en lugar deandroid:
, ya que tus atributos personalizados pertenecen al espacio de nombresschemas.android.com/apk/res/
your_app_package_name
en lugar de aandroid
.
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é.
- Abre el archivo de clase
DialView.kt
. - 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
- En el bloque
init
, agrega el siguiente código con la función de extensiónwithStyledAttributes
. Usted proporciona los atributos y la vista, y configura sus variables locales. Si importaswithStyledAttributes
, también se importará la funcióngetColor()
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)
}
- 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
- 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.
- En un dispositivo o emulador de Android, navega a Configuración > Accesibilidad > TalkBack.
- Presiona el botón de activación Activado/Desactivado para activar TalkBack.
- Presiona Aceptar para confirmar los permisos.
- 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).
- 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.
- 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 etiquetaTextView
(Control de ventilador). Sin embargo, si presionas la vistaDialView
, 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.
- En la parte inferior de la clase
DialView
, declara una funciónupdateContentDescription()
sin argumentos ni tipo de datos que se muestra.
fun updateContentDescription() {
}
- Dentro de
updateContentDescription()
, cambia la propiedadcontentDescription
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 enonDraw()
cuando el dial se dibuja en la pantalla.
fun updateContentDescription() {
contentDescription = resources.getString(fanSpeed.label)
}
- Desplázate hasta el bloque
init()
y, al final del bloque, agrega una llamada aupdateContentDescription()
. De esta manera, se inicializa la descripción del contenido cuando se inicializa la vista.
init {
isClickable = true
// ...
updateContentDescription()
}
- Agrega otra llamada a
updateContentDescription()
en el métodoperformClick()
, justo antes deinvalidate()
.
override fun performClick(): Boolean {
if (super.performClick()) return true
fanSpeed = fanSpeed.next()
updateContentDescription()
invalidate()
return true
}
- 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.
- En
DialView.kt
, en el bloqueinit
, configura un delegado de accesibilidad en la vista como un objetoAccessibilityDelegateCompat
nuevo. Importaandroidx.core.view.ViewCompat
yandroidx.core.view.AccessibilityDelegateCompat
cuando se te solicite. Esta estrategia permite la mayor retrocompatibilidad en tu app.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
})
- Dentro del objeto
AccessibilityDelegateCompat
, anula la funciónonInitializeAccessibilityNodeInfo()
con un objetoAccessibilityNodeInfoCompat
y llama al método superpuesto. Importaandroidx.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.
- Dentro de
onInitializeAccessibilityNodeInfo()
, crea un nuevo objetoAccessibilityNodeInfoCompat.AccessibilityActionCompat
y asígnalo a la variablecustomClick
. Pasa al constructor la constanteAccessibilityNodeInfo.ACTION_CLICK
y una string de marcador de posición. ImportaAccessibilityNodeInfo
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.
- Reemplaza la string
"placeholder"
por una llamada acontext.getString()
para recuperar un recurso de strings. Para el recurso específico, prueba la velocidad actual del ventilador. Si actualmente la velocidad esFanSpeed.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)
)
}
})
- Después de los paréntesis de cierre de la definición
customClick
, usa el métodoaddAction()
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)
}
})
- En
res/values/strings.xml
, agrega los recursos de strings para "Change" y "Reset".
<string name="change">Change</string>
<string name="reset">Reset</string>
- 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.
- Para crear una vista personalizada que herede la apariencia y el comportamiento de una subclase
View
, comoEditText
, 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
, comoonDraw()
, 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 deOnClickListener
() 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 useonClickListener()
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 carpetavalues
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:
- Cómo crear vistas personalizadas
@JvmOverloads
- Componentes personalizados
- Cómo Android dibuja las vistas
onMeasure()
onSizeChanged()
onDraw()
Canvas
Paint
drawText()
setTypeface()
setColor()
drawRect()
drawOval()
drawArc()
drawBitmap()
setStyle()
invalidate()
- Ver
- Eventos de entrada
- Pintura
- Biblioteca de extensiones de Kotlin android-ktx
withStyledAttributes
- Documentación de Android KTX
- Blog de anuncio original de Android KTX
- Cómo hacer que las vistas personalizadas sean más accesibles
AccessibilityDelegateCompat
AccessibilityNodeInfoCompat
AccessibilityNodeInfoCompat.AccessibilityActionCompat
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.