Mejora la accesibilidad de las vistas personalizadas

Si tu aplicación requiere un componente de vistas personalizadas, debes realizar algunas acciones adicionales para facilitar el acceso a las vistas. Las siguientes son las tareas principales para mejorar la accesibilidad de tu vista personalizada:

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á enfocada actualmente. Todas las vistas estándar de Android ya manejan KEYCODE_DPAD_CENTER de manera correcta. Si compilas un control de View personalizado, asegúrate de que este evento tenga el mismo efecto que tocar la vista en la pantalla táctil.

El control personalizado también debería tratar el evento de KEYCODE_ENTER del mismo modo que KEYCODE_DPAD_CENTER. Este enfoque permite que la interacción desde un teclado completo sea mucho más fácil para los usuarios.

Implementa métodos de API de accesibilidad

Los eventos de accesibilidad son mensajes sobre la interacción de los usuarios con componentes visuales de la interfaz en tu aplicación. Estos mensajes se manejan mediante los servicios de accesibilidad, que usan la información en estos eventos para generar comentarios y solicitudes adicionales. En Android 4.0 (API nivel 14) y las versiones posteriores, los métodos para generar eventos de accesibilidad se expandieron a fin de proporcionar información más detallada que la interfaz de AccessibilityEventSource que se había introducido en Android 1.6 (API nivel 4). Los métodos de accesibilidad expandida forman parte de la clase de View y la clase de View.AccessibilityDelegate. Los métodos son los siguientes:

sendAccessibilityEvent()
(API nivel 4). Se 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. Por lo general, no es necesario implementar este método, a menos que estés creando una vista personalizada.
sendAccessibilityEventUnchecked()
(API nivel 4). Este método se usa cuando el código de llamada necesita controlar de forma directa que la marca de accesibilidad esté habilitada en el dispositivo (AccessibilityManager.isEnabled()). Si implementas este método, debes realizar la llamada como si la accesibilidad estuviese habilitada, independientemente de la configuración real del sistema. Por lo general, no es necesario que implementes este método para una vista personalizada.
dispatchPopulateAccessibilityEvent()
(API nivel 4). El sistema llama a este método cuando tu vista personalizada genera un evento de accesibilidad. A partir de la API nivel 14, la implementación predeterminada de este método llama a onPopulateAccessibilityEvent() para esta vista y, luego, al método de dispatchPopulateAccessibilityEvent() para cada elemento secundario de esta vista. A fin de brindar compatibilidad con los servicios de accesibilidad en las versiones de Android anteriores a 4.0 (API nivel 14), debes anular este método y completar getText() con texto descriptivo para tu vista personalizada, que los servicios de accesibilidad como TalkBack anunciarán en voz alta.
onPopulateAccessibilityEvent()
(API nivel 14). 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.

Nota: Si en este método modificas atributos adicionales más allá del texto, es posible que se reemplacen las propiedades establecidas por otros métodos. Si bien puedes modificar atributos del evento de accesibilidad con este método, debes limitar estos cambios al contenido de texto y usar el método de onInitializeAccessibilityEvent() para modificar otras propiedades del evento.

Nota: Si tu implementación de este evento anula por completo el texto de salida sin permitir que otras partes de tu diseño modifiquen su contenido, no llames a la superimplementación de este método en tu código.

onInitializeAccessibilityEvent()
(API nivel 14). El sistema llama a este método a fin de 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, debes anular este método y definir la información adicional sobre tu vista en el evento mediante este método, 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. Si anulas este método, debes llamar a su superimplementación y solo modificar las propiedades que la superclase no haya definido.
onInitializeAccessibilityNodeInfo()
(API nivel 14). 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, debes anular este método y establecer la información adicional sobre tu vista en el objeto AccessibilityNodeInfo manejado por este método.
onRequestSendAccessibilityEvent()
(API nivel 14). El sistema llama a este método después de que un elemento secundario de tu vista genera un AccessibilityEvent. Este paso permite que la vista superior agregue información adicional al evento de accesibilidad. Solo debes implementar este método si tu vista personalizada puede tener vistas secundarias y si la vista superior puede proporcionar información de contexto al evento de accesibilidad que sería útil para los servicios de accesibilidad.

A fin de admitir estos métodos de accesibilidad para una vista personalizada, debes seguir uno de los enfoques a continuación:

  • Si tu aplicación se orienta a Android 4.0 (API nivel 14) y versiones posteriores, anula e implementa los métodos de accesibilidad anteriores directamente en tu clase de vistas personalizadas.
  • Si tu vista personalizada es compatible con Android 1.6 (API nivel 4) y versiones posteriores, agrega la biblioteca de compatibilidad de Android, revisión 5 o versiones posteriores, a tu proyecto. Luego, en tu clase de vistas personalizadas, llama al método ViewCompat.setAccessibilityDelegate() para implementar los métodos de accesibilidad anteriores. Para ver un ejemplo de este enfoque, consulta la muestra de la biblioteca de compatibilidad de Android (revisión 5 o versiones posteriores) AccessibilityDelegateSupportActivity en (<sdk>/extras/android/support/v4/samples/Support4Demos/)

En cualquier caso, debes implementar los siguientes métodos de accesibilidad para tu clase de vistas personalizadas:

Para obtener más información sobre cómo implementar estos métodos, consulta Cómo propagar 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:

Nota: Los eventos de colocar el cursor sobre un elemento están asociados con el evento de Exploración táctil, que los usa como activadores a fin de proporcionar solicitudes sonoras para elementos de la interfaz de usuario.

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 permite que un usuario seleccione un valor numérico cuando presiona las flechas izquierda o derecha, tu vista personalizada debe emitir un evento del tipo TYPE_VIEW_TEXT_CHANGED cada vez que cambie el valor del control deslizante. En el siguiente código de ejemplo, se demuestra 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 requeridas que describe el estado actual de la vista. 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 requeridas 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, aunque también puede incluir información adicional sobre el estado que quieras agregar al evento.

El requisito mínimo para proporcionar información sobre los servicios de accesibilidad con una vista personalizada es implementar dispatchPopulateAccessibilityEvent(). El sistema llama a este método a fin de solicitar información para un AccessibilityEvent y hace que tu vista personalizada sea compatible con los servicios de accesibilidad en Android 1.6 (API nivel 4) y las versiones posteriores. En el siguiente código de ejemplo, se demuestra una implementación básica de este método.

Kotlin

    override fun dispatchPopulateAccessibilityEvent(event: AccessibilityEvent): Boolean {
        // Call the super implementation to populate its text to the event, which
        // calls onPopulateAccessibilityEvent() on API Level 14 and up.
        return super.dispatchPopulateAccessibilityEvent(event).let { completed ->

            // In case this is running on a API revision earlier that 14, check
            // the text content of the event and add an appropriate text
            // description for this custom view:
            if (text?.isNotEmpty() == true) {
                event.text.add(text)
                true
            } else {
                completed
            }
        }
    }
    

Java

    @Override
    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
        // Call the super implementation to populate its text to the event, which
        // calls onPopulateAccessibilityEvent() on API Level 14 and up.
        boolean completed = super.dispatchPopulateAccessibilityEvent(event);

        // In case this is running on a API revision earlier that 14, check
        // the text content of the event and add an appropriate text
        // description for this custom view:
        CharSequence text = getText();
        if (!TextUtils.isEmpty(text)) {
            event.getText().add(text);
            return true;
        }
        return completed;
    }
    

En el caso de Android 4.0 (API nivel 14) y las versiones posteriores, usa los métodos onPopulateAccessibilityEvent() y onInitializeAccessibilityEvent() para propagar o modificar la información en 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 audibles. Usa el método onInitializeAccessibilityEvent() para propagar 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 propagados por este método para investigar la jerarquía de vistas que generó un evento de accesibilidad después de recibir ese evento, para obtener información de contexto más detallada y para brindar comentarios apropiados a los usuarios.

En el siguiente código de ejemplo, se muestra cómo anular estos tres métodos mediante ViewCompat.setAccessibilityDelegate(). Ten en cuenta que este código de ejemplo requiere que agregues la biblioteca de compatibilidad de Android para la API nivel 4 (revisión 5 o versiones posteriores) a tu proyecto.

Kotlin

    ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {

        override fun onPopulateAccessibilityEvent(host: View, event: AccessibilityEvent) {
            super.onPopulateAccessibilityEvent(host, event)
            // We call the super implementation to populate its text for the
            // event. Then we add our text not present in a super class.
            // Very often you only need to add the text for the custom view.
            if (text?.isNotEmpty() == true) {
                event.text.add(text)
            }
        }

        override fun onInitializeAccessibilityEvent(host: View, event: AccessibilityEvent) {
            super.onInitializeAccessibilityEvent(host, event);
            // We call the super implementation to let super classes
            // set appropriate event properties. Then we add the new property
            // (checked) which is not supported by a super class.
            event.isChecked = isChecked()
        }

        override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
            super.onInitializeAccessibilityNodeInfo(host, info)
            // We call the super implementation to let super classes set
            // appropriate info properties. Then we add our properties
            // (checkable and checked) which are not supported by a super class.
            info.isCheckable = true
            info.isChecked = isChecked()
            // Quite often you only need to add the text for the custom view.
            if (text?.isNotEmpty() == true) {
                info.text = text
            }
        }
    })
    

Java

    ViewCompat.setAccessibilityDelegate(new AccessibilityDelegateCompat() {
        @Override
        public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
            super.onPopulateAccessibilityEvent(host, event);
            // We call the super implementation to populate its text for the
            // event. Then we add our text not present in a super class.
            // Very often you 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(View host, AccessibilityEvent event) {
            super.onInitializeAccessibilityEvent(host, event);
            // We call the super implementation to let super classes
            // set appropriate event properties. Then we add the new property
            // (checked) which is not supported by a super class.
            event.setChecked(isChecked());
        }
        @Override
        public void onInitializeAccessibilityNodeInfo(View host,
                AccessibilityNodeInfoCompat info) {
            super.onInitializeAccessibilityNodeInfo(host, info);
            // We call the super implementation to let super classes set
            // appropriate info properties. Then we add our properties
            // (checkable and checked) which are not supported by a super class.
            info.setCheckable(true);
            info.setChecked(isChecked());
            // Quite often you 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. Si deseas ver otro ejemplo de este enfoque, consulta el AccessibilityDelegateSupportActivity de muestra de la biblioteca de compatibilidad de Android (revisión 5 o versiones posteriores) en <sdk>/extras/android/support/v4/samples/Support4Demos/.

Proporciona un contexto de accesibilidad personalizado

En Android 4.0 (API nivel 14), se mejoró el marco de trabajo a fin de permitir que los servicios de accesibilidad inspeccionen la jerarquía de vistas de un componente de la interfaz de usuario que genera un evento de accesibilidad. Esta mejora permite que los servicios de accesibilidad proporcionen un conjunto mucho más completo de información contextual para ayudar a los usuarios.

En algunos casos, 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 obtienen información adecuada porque las subsecciones en las que se puede hacer clic no forman parte de la jerarquía de vistas.

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

En el ejemplo que se muestra en la figura 1, todo el calendario se implementa como una sola vista, de manera que, si no realizas ninguna otra acción, los servicios de accesibilidad no reciben información suficiente sobre el contenido de la vista y la selección del usuario en la vista. Por ejemplo, si un usuario hace clic en el día que contiene 17, el marco de trabajo 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 solo anunciaría "Calendario" o la variante "Calendario de abril", solo ligeramente mejor, y el usuario no sabría qué día se seleccionó.

A fin de proporcionar información de contexto adecuada para los servicios de accesibilidad en situaciones como esta, el marco de trabajo proporciona una manera de especificar una jerarquía de vistas virtual. Una jerarquía de vistas virtual les permite a los desarrolladores de aplicaciones proporcionar una jerarquía de vistas complementaria a los servicios de accesibilidad que se ajusta más a la información real 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 (vistas) 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 por separado. 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. A fin de 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.

A fin de 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. Si deseas ver una implementación de ejemplo de esta función de accesibilidad, consulta AccessibilityNodeProviderActivity en el proyecto de muestra de ApiDemos. Puedes implementar una jerarquía de vistas virtual compatible con Android 1.6 y versiones posteriores si usas la biblioteca de compatibilidad con el método ViewCompat.getAccessibilityNodeProvider() y proporcionas una implementación con AccessibilityNodeProviderCompat.

Maneja eventos táctiles personalizados

Es posible que los controles de vistas personalizadas requieran un comportamiento no estándar de eventos táctiles. Por ejemplo, un control personalizado podría 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. A fin de mantener la compatibilidad con los servicios de accesibilidad, el código que maneja 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 a fin de realizar la acción de clic personalizado para los usuarios que no pueden usar una pantalla táctil

A fin de 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
                    // thereby enable accessibility services to
                    // perform this action for a user who cannot
                    // click 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
                                        // thereby enable accessibility services to
                                        // perform this action for a user who cannot
                                        // click 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 que se mostró anteriormente garantiza que el evento de clic personalizado sea compatible con los servicios de accesibilidad. Para ello, usa el método de performClick() a fin de generar un evento de accesibilidad y proporcionar un punto de entrada para que los servicios de accesibilidad actúen en nombre de un usuario a fin de realizar este evento de clic personalizado.

Nota: Si tu vista personalizada tiene distintas regiones en las que se puede hacer clic, como una vista de calendario personalizado, debes implementar una jerarquía de vistas virtual. Para ello, anula getAccessibilityNodeProvider() en tu vista personalizada a fin de que sea compatible con los servicios de accesibilidad.