Cómo hacer que las vistas personalizadas sean más accesibles

Si tu aplicación requiere un componente de vistas personalizadas, debes hacer que la vista sea más accesible. Los siguientes pasos pueden mejorar la accesibilidad de tu vista personalizada, como se describe en esta página:

  • Maneja los clics del controlador direccional.
  • Implementa métodos de API de accesibilidad.
  • Envía objetos AccessibilityEvent específicos a tu vista personalizada.
  • Completa AccessibilityEvent y AccessibilityNodeInfo para tu vista.

Maneja los clics del controlador direccional

En la mayoría de los dispositivos, si haces clic en una vista que usa un controlador direccional, se envía un KeyEvent con KEYCODE_DPAD_CENTER a la vista que está en primer plano en ese momento. Todas las vistas estándar de Android manejan KEYCODE_DPAD_CENTER de manera correcta. Si creas un control de View personalizado, asegúrate de que este evento tenga el mismo efecto que presionar la vista en la pantalla táctil.

Tu control personalizado debe tratar el evento KEYCODE_ENTER de la misma manera que a KEYCODE_DPAD_CENTER. De esta manera, es más fácil para los usuarios interactuar con un teclado completo.

Implementa métodos de API de accesibilidad

Los eventos de accesibilidad son mensajes sobre las interacciones de los usuarios con los componentes de la interfaz visual de tu app. Estos mensajes se manejan mediante los servicios de accesibilidad, que usan la información en estos eventos para generar comentarios y solicitudes adicionales. Los métodos de accesibilidad forman parte de las clases View y View.AccessibilityDelegate. Los métodos son los siguientes:

dispatchPopulateAccessibilityEvent()
El sistema llama a este método cuando tu vista personalizada genera un evento de accesibilidad. La implementación predeterminada de este método llama a onPopulateAccessibilityEvent() para esta vista y, luego, al método dispatchPopulateAccessibilityEvent() para cada elemento secundario de esta vista.
onInitializeAccessibilityEvent()
El sistema llama a este método para obtener información adicional sobre el estado de la vista, además del contenido de texto. Si tu vista personalizada ofrece controles de interacción más allá de un simple TextView o Button, anula este método y establece la información adicional sobre tu vista, como el tipo de campo de contraseña, el tipo de casilla de verificación o los estados que proporcionan interacción del usuario o comentarios al evento, utilizando este método. Si anulas este método, llama a su superimplementación y solo modifica las propiedades que la superclase no haya establecido.
onInitializeAccessibilityNodeInfo()
Este método proporciona servicios de accesibilidad con información sobre el estado de la vista. La implementación estándar de View tiene un conjunto estándar de propiedades de vistas, pero si tu vista personalizada proporciona control de interacción más allá de un simple TextView o Button, anula este método y establece la información adicional sobre tu vista en el objeto AccessibilityNodeInfo controlado por este método.
onPopulateAccessibilityEvent()
Este método define la solicitud de texto hablado del AccessibilityEvent para tu vista. También se llama a este método si la vista es un elemento secundario de una vista que genera un evento de accesibilidad.
onRequestSendAccessibilityEvent()
El sistema llama a este método cuando un elemento secundario de tu vista genera un AccessibilityEvent. Este paso permite que la vista superior modifique el evento de accesibilidad con información adicional. Implementa este método solo si tu vista personalizada puede tener vistas secundarias y si la vista superior puede proporcionar información de contexto al evento de accesibilidad que es útil para los servicios de accesibilidad.
sendAccessibilityEvent()
El sistema llama a este método cuando un usuario realiza una acción en una vista. El evento se clasifica con un tipo de acción del usuario, por ejemplo, TYPE_VIEW_CLICKED. En general, debes enviar un AccessibilityEvent siempre que cambie el contenido de tu vista personalizada.
sendAccessibilityEventUnchecked()
Este método se usa cuando el código de llamada necesita ocuparse de forma directa de verificar la habilitación de las funciones de accesibilidad en el dispositivo (AccessibilityManager.isEnabled()). Si implementas este método, realiza la llamada como si la accesibilidad estuviese habilitada, independientemente de la configuración del sistema. Por lo general, no es necesario implementar este método para una vista personalizada.

Para admitir la accesibilidad, anula e implementa los métodos de accesibilidad anteriores directamente en tu clase de vistas personalizadas.

Como mínimo, implementa los siguientes métodos de accesibilidad para tu clase de vistas personalizadas:

  • dispatchPopulateAccessibilityEvent()
  • onInitializeAccessibilityEvent()
  • onInitializeAccessibilityNodeInfo()
  • onPopulateAccessibilityEvent()

Para saber más sobre la implementación de estos métodos, consulta la sección sobre carga de información para eventos de accesibilidad.

Envía eventos de accesibilidad

Según las características específicas de tu vista personalizada, es posible que necesites enviar objetos AccessibilityEvent en momentos diferentes o para eventos que no se manejan con la implementación predeterminada. La clase View proporciona una implementación predeterminada para estos tipos de eventos:

En general, debes enviar un AccessibilityEvent siempre que cambie el contenido de tu vista personalizada. Por ejemplo, si implementas una barra de control deslizante personalizada que le permite al usuario seleccionar un valor numérico cuando presiona las teclas de flecha hacia la izquierda o la derecha, tu vista personalizada debe emitir un evento TYPE_VIEW_TEXT_CHANGED cada vez que cambie el valor del control deslizante. En la siguiente muestra de código, se ilustra el uso del método sendAccessibilityEvent() para informar este evento.

Kotlin

override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
    return when(keyCode) {
        KeyEvent.KEYCODE_DPAD_LEFT -> {
            currentValue--
            sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED)
            true
        }
        ...
    }
}

Java

@Override
public boolean onKeyUp (int keyCode, KeyEvent event) {
    if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
        currentValue--;
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
        return true;
    }
    ...
}

Propaga eventos de accesibilidad

Cada AccessibilityEvent tiene un conjunto de propiedades obligatorias que describe el estado de la vista en un determinado momento. Estas propiedades incluyen datos como el nombre de clase de la vista, la descripción del contenido y el estado verificado. Las propiedades específicas obligatorias para cada tipo de evento se describen en la documentación de referencia de AccessibilityEvent.

La implementación de View proporciona valores predeterminados para estas propiedades. Muchos de estos valores, incluidos el nombre de la clase y la marca de tiempo del evento, se proporcionan automáticamente. Si creas un componente de vista personalizada, debes proporcionar información sobre el contenido y las características de la vista. Esta información puede ser tan simple como una etiqueta de botón e incluir información adicional sobre el estado que desees agregar al evento.

Usa los métodos onPopulateAccessibilityEvent() y onInitializeAccessibilityEvent() para completar o modificar la información de un AccessibilityEvent. Usa el método onPopulateAccessibilityEvent() específicamente para agregar o modificar contenido de texto del evento, que los servicios de accesibilidad como TalkBack convierten en solicitudes sonoras. Usa el método onInitializeAccessibilityEvent() para cargar información adicional sobre el evento, como el estado de selección de la vista.

Además, implementa el método onInitializeAccessibilityNodeInfo(). Los servicios de accesibilidad usan los objetos AccessibilityNodeInfo cargados por este método para investigar la jerarquía de vistas que genera un evento de accesibilidad después de que se recibe y proporcionar comentarios adecuados a los usuarios.

En el siguiente ejemplo de código, se muestra cómo anular estos tres métodos en tu vista:

Kotlin

override fun onPopulateAccessibilityEvent(event: AccessibilityEvent?) {
    super.onPopulateAccessibilityEvent(event)
    // Call the super implementation to populate its text for the
    // event. Then, add text not present in a super class.
    // You typically only need to add the text for the custom view.
    if (text?.isNotEmpty() == true) {
        event?.text?.add(text)
    }
}

override fun onInitializeAccessibilityEvent(event: AccessibilityEvent?) {
    super.onInitializeAccessibilityEvent(event)
    // Call the super implementation to let super classes
    // set appropriate event properties. Then, add the new checked
    // property that is not supported by a super class.
    event?.isChecked = isChecked()
}

override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo?) {
    super.onInitializeAccessibilityNodeInfo(info)
    // Call the super implementation to let super classes set
    // appropriate info properties. Then, add the checkable and checked
    // properties that are not supported by a super class.
    info?.isCheckable = true
    info?.isChecked = isChecked()
    // You typically only need to add the text for the custom view.
    if (text?.isNotEmpty() == true) {
        info?.text = text
    }
}

Java

@Override
public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
    super.onPopulateAccessibilityEvent(event);
    // Call the super implementation to populate its text for the
    // event. Then, add the text not present in a super class.
    // You typically only need to add the text for the custom view.
    CharSequence text = getText();
    if (!TextUtils.isEmpty(text)) {
        event.getText().add(text);
    }
}

@Override
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
    super.onInitializeAccessibilityEvent(event);
    // Call the super implementation to let super classes
    // set appropriate event properties. Then, add the new checked
    // property that is not supported by a super class.
    event.setChecked(isChecked());
}

@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
    super.onInitializeAccessibilityNodeInfo(info);
    // Call the super implementation to let super classes set
    // appropriate info properties. Then, add the checkable and checked
    // properties that are not supported by a super class.
    info.setCheckable(true);
    info.setChecked(isChecked());
    // You typically only need to add the text for the custom view.
    CharSequence text = getText();
    if (!TextUtils.isEmpty(text)) {
        info.setText(text);
    }
}

Puedes implementar estos métodos directamente en tu clase de vistas personalizadas.

Proporciona un contexto de accesibilidad personalizado

Los servicios de accesibilidad pueden inspeccionar la jerarquía de vistas de un componente de la interfaz de usuario que genera un evento de accesibilidad. Esto permite que los servicios de accesibilidad proporcionen información contextual más útil a los usuarios.

Hay casos en los que los servicios de accesibilidad no pueden obtener información adecuada de la jerarquía de vistas. Un ejemplo es un control de interfaz personalizada que tiene dos o más áreas en las que se puede hacer clic por separado, como un control de calendario. En este caso, los servicios no pueden obtener información adecuada, ya que las subsecciones en las que se puede hacer clic no forman parte de la jerarquía de vistas.

Figura 1: Vista de calendario personalizada con elementos de día seleccionables.

En el ejemplo de la figura 1, todo el calendario se implementa como una sola vista, por lo que los servicios de accesibilidad no reciben suficiente información sobre el contenido de la vista y la selección del usuario en ella, a menos que el desarrollador brinde información adicional. Por ejemplo, si un usuario hace clic en el día con la etiqueta 17, el framework de accesibilidad solo recibe la información de descripción de todo el control de calendario. En este caso, el servicio de accesibilidad de TalkBack anuncia "Calendario" o "Calendario de abril", y el usuario no sabe qué día está seleccionado.

Para proporcionar información de contexto adecuada para los servicios de accesibilidad en situaciones como esta, el framework proporciona una manera de especificar una jerarquía de vistas virtual. Una jerarquía de vistas virtual les permite a los desarrolladores de apps proporcionar una jerarquía de vistas complementaria a los servicios de accesibilidad que se ajusta más a la información en la pantalla. Este enfoque permite que los servicios de accesibilidad proporcionen información de contexto más útil a los usuarios.

Otra situación en la que podría ser necesaria una jerarquía de vistas virtual es cuando una interfaz de usuario contiene un conjunto de controles View con funciones estrechamente relacionadas, en el que una acción en un control afecta el contenido de uno o más elementos, como un selector de números con botones hacia arriba y hacia abajo separados. En este caso, los servicios de accesibilidad no pueden obtener información adecuada, ya que la acción en un control cambia el contenido en otro y el servicio podría no identificar la relación de esos controles.

Para manejar esta situación, agrupa los controles relacionados con una vista contenedora y proporciona una jerarquía de vistas virtual desde este contenedor para representar claramente la información y el comportamiento que proporcionan los controles.

Para proporcionar una jerarquía de vistas virtual para una vista, anula el método getAccessibilityNodeProvider() en tu vista personalizada o grupo de vistas y muestra una implementación de AccessibilityNodeProvider. Puedes implementar una jerarquía de vistas virtual mediante la biblioteca de compatibilidad con el método ViewCompat.getAccessibilityNodeProvider() y brindar una implementación con AccessibilityNodeProviderCompat.

Para simplificar la tarea de proporcionar información a los servicios de accesibilidad y administrar el enfoque de accesibilidad, puedes implementar ExploreByTouchHelper. Brinda un AccessibilityNodeProviderCompat y se puede adjuntar como el AccessibilityDelegateCompat de una vista si se llama a setAccessibilityDelegate. Para ver un ejemplo, consulta ExploreByTouchHelperActivity. Los widgets del framework, como CalendarView, también usan ExploreByTouchHelper a través de su vista secundaria SimpleMonthView.

Controla eventos táctiles personalizados

Es posible que los controles de vistas personalizadas requieran un comportamiento no estándar de eventos táctiles, como se muestra en los ejemplos que aparecen más abajo.

Cómo definir acciones basadas en clics

Si tu widget usa la interfaz OnClickListener o OnLongClickListener, el sistema controlará las acciones ACTION_CLICK y ACTION_LONG_CLICK por ti. Si tu app usa un widget más personalizado que se basa en la interfaz OnTouchListener, define controladores personalizados para las acciones de accesibilidad basadas en clics. Para hacerlo, llama al método replaceAccessibilityAction() de cada acción, como se muestra en el siguiente fragmento de código:

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    ...

    // Assumes that the widget is designed to select text when tapped, and selects
    // all text when tapped and held. In its strings.xml file, this app sets
    // "select" to "Select" and "select_all" to "Select all".
    ViewCompat.replaceAccessibilityAction(
        binding.textSelectWidget,
        ACTION_CLICK,
        getString(R.string.select)
    ) { view, commandArguments ->
        selectText()
    }

    ViewCompat.replaceAccessibilityAction(
        binding.textSelectWidget,
        ACTION_LONG_CLICK,
        getString(R.string.select_all)
    ) { view, commandArguments ->
        selectAllText()
    }
}

Java

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...

    // Assumes that the widget is designed to select text when tapped, and select
    // all text when tapped and held. In its strings.xml file, this app sets
    // "select" to "Select" and "select_all" to "Select all".
    ViewCompat.replaceAccessibilityAction(
            binding.textSelectWidget,
            ACTION_CLICK,
            getString(R.string.select),
            (view, commandArguments) -> selectText());

    ViewCompat.replaceAccessibilityAction(
            binding.textSelectWidget,
            ACTION_LONG_CLICK,
            getString(R.string.select_all),
            (view, commandArguments) -> selectAllText());
}

Cómo crear eventos de clics personalizados

Un control personalizado puede usar el método de objeto de escucha de onTouchEvent(MotionEvent) para detectar los eventos de ACTION_DOWN y ACTION_UP, y activar un evento de clic especial. Para mantener la compatibilidad con los servicios de accesibilidad, el código que controla este evento de clic personalizado debe hacer lo siguiente:

  1. Generar un AccessibilityEvent apropiado para la acción de clic interpretada
  2. Habilitar los servicios de accesibilidad para realizar la acción de clic personalizado para los usuarios que no pueden usar una pantalla táctil

Para manejar estos requisitos de manera eficiente, tu código debe anular el método performClick(), que debe llamar a la superimplementación de este método y, luego, ejecutar las acciones que requiera el evento de clic. Cuando se detecta la acción de clic personalizado, el código debe llamar a tu método de performClick(). En el siguiente ejemplo de código, se demuestra este patrón.

Kotlin

class CustomTouchView(context: Context) : View(context) {

    var downTouch = false

    override fun onTouchEvent(event: MotionEvent): Boolean {
        super.onTouchEvent(event)

        // Listening for the down and up touch events.
        return when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                downTouch = true
                true
            }

            MotionEvent.ACTION_UP -> if (downTouch) {
                downTouch = false
                performClick() // Call this method to handle the response and
                // enable accessibility services to
                // perform this action for a user who can't
                // tap the touchscreen.
                true
            } else {
                false
            }

            else -> false  // Return false for other touch events.
        }
    }

    override fun performClick(): Boolean {
        // Calls the super implementation, which generates an AccessibilityEvent
        // and calls the onClick() listener on the view, if any.
        super.performClick()

        // Handle the action for the custom click here.

        return true
    }
}

Java

class CustomTouchView extends View {

    public CustomTouchView(Context context) {
        super(context);
    }

    boolean downTouch = false;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);

        // Listening for the down and up touch events
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downTouch = true;
                return true;

            case MotionEvent.ACTION_UP:
                if (downTouch) {
                    downTouch = false;
                    performClick(); // Call this method to handle the response and
                                    // enable accessibility services to
                                    // perform this action for a user who can't
                                    // tap the touchscreen.
                    return true;
                }
        }
        return false; // Return false for other touch events.
    }

    @Override
    public boolean performClick() {
        // Calls the super implementation, which generates an AccessibilityEvent
        // and calls the onClick() listener on the view, if any.
        super.performClick();

        // Handle the action for the custom click here.

        return true;
    }
}

El patrón anterior ayuda a garantizar que el evento de clic personalizado sea compatible con los servicios de accesibilidad. Para ello, usa el método performClick(), genera un evento de accesibilidad y proporciona un punto de entrada para que los servicios de accesibilidad actúen en nombre de un usuario que realiza el evento de clic personalizado.