Tornar visualizações personalizadas mais acessíveis

Caso seu aplicativo precise de um componente de visualização personalizado, vale a pena se esforçar para tornar a visualização mais acessível. Estas são as principais tarefas para melhorar a acessibilidade da sua visualização personalizada:

Processar cliques do controle direcional

Na maioria dos dispositivos, clicar em uma visualização usando um controle direcional envia um KeyEvent com KEYCODE_DPAD_CENTER para a visualização em foco no momento. Todas as visualizações padrão do Android já processam KEYCODE_DPAD_CENTER corretamente. Ao criar um controle View personalizado, verifique se esse evento tem o mesmo efeito que tocar a visualização na tela touchscreen.

Seu controle personalizado também deve tratar o evento KEYCODE_ENTER da mesma forma que KEYCODE_DPAD_CENTER. Essa abordagem facilita muito a interação a partir de um teclado completo para os usuários.

Implementar métodos da API de acessibilidade

Os eventos de acessibilidade são mensagens sobre a interação do usuário com componentes da interface visual no seu aplicativo. Essas mensagens são processadas pelos Serviços de acessibilidade, que usam as informações nesses eventos para gerar feedback e solicitações complementares. No Android 4.0 (API de nível 14) e versões posteriores, os métodos para gerar eventos de acessibilidade foram ampliados para oferecer informações mais detalhadas do que a interface AccessibilityEventSource introduzida no Android 1.6 (API de nível 4). Os métodos de acessibilidade ampliados fazem parte das classes View e View.AccessibilityDelegate. Os métodos são os seguintes:

sendAccessibilityEvent()
(API de nível 4): esse método é chamado quando um usuário realiza ações em uma visualização. O evento é classificado com um tipo de ação do usuário, como TYPE_VIEW_CLICKED. Normalmente, não é necessário implementar esse método, a menos que você esteja criando uma visualização personalizada.
sendAccessibilityEventUnchecked()
(API de nível 4): esse método é usado quando o código de chamada precisa controlar diretamente a verificação de acessibilidade ativada no dispositivo (AccessibilityManager.isEnabled()). Se você implementar esse método, será necessário realizar a chamada como se a acessibilidade estivesse ativada, independentemente da configuração real do sistema. Normalmente, não é necessário implementar esse método para uma visualização personalizada.
dispatchPopulateAccessibilityEvent()
(API de nível 4): o sistema chama esse método quando sua exibição personalizada gera um evento de acessibilidade. Na API de nível 14, a implementação padrão desse método chama onPopulateAccessibilityEvent() para a visualização e, em seguida, o método dispatchPopulateAccessibilityEvent() para cada filha dessa visualização. Para oferecer compatibilidade com os serviços de acessibilidade nas revisões do Android anteriores à 4.0 (API de nível 14), é necessário modificar esse método e preencher getText() com texto descritivo para sua visualização personalizada, falada por serviços de acessibilidade como o "TalkBack".
onPopulateAccessibilityEvent()
(API de nível 14): esse método define a solicitação de texto falado do AccessibilityEvent para sua visualização. Esse método também será chamado se a visualização for filha de uma outra que gera um evento de acessibilidade.

Observação: a modificação de outros atributos além do texto nesse método pode modificar as propriedades definidas por outros métodos. Embora você possa modificar os atributos do evento de acessibilidade com esse método, limite essas mudanças ao conteúdo de texto e use o método onInitializeAccessibilityEvent() para modificar outras propriedades do evento.

Observação: se a implementação desse evento modificar completamente o texto de saída sem permitir que outras partes do layout modifiquem o próprio conteúdo, não chame a superimplementação desse método no seu código.

onInitializeAccessibilityEvent()
(API de nível 14): o sistema chama esse método para acessar outras informações sobre o estado da visualização, além do conteúdo de texto. Se a visualização personalizada fornecer controles de interação além de um simples TextView ou Button, modifique esse método e defina as outras informações sobre sua visualização no evento usando esse método, como tipo de campo de senha, tipo de caixa de seleção ou estados que oferecem feedback ou interação do usuário. Se você modificar esse método, será necessário chamar a superimplementação e, em seguida, modificar somente as propriedades que não tenham sido definidas pela superclasse.
onInitializeAccessibilityNodeInfo()
(API de nível 14): esse método disponibiliza informações sobre o estado da visualização aos serviços de acessibilidade. A implementação padrão View tem um conjunto modelo de propriedades de visualização, mas se a visualização personalizada oferecer controle de interação além de um simples TextView ou Button, modifique esse método e defina as outras informações sobre sua visualização no objeto AccessibilityNodeInfo processado por esse método.
onRequestSendAccessibilityEvent()
(API de nível 14): o sistema chama esse método quando uma filha da visualização gera um AccessibilityEvent. Essa etapa permite que a visualização mãe mude o evento de acessibilidade com outras informações. Implemente esse método somente se a visualização personalizada puder ter visualizações filhas e se a visualização mãe puder fornecer informações de contexto ao evento que possam ser importantes para os serviços de acessibilidade.

Para oferecer compatibilidade com esses métodos de acessibilidade para uma visualização personalizada, siga uma das seguintes abordagens:

  • Se o aplicativo for segmentado para o Android 4.0 (API de nível 14) ou versões posteriores, modifique e implemente os métodos de acessibilidade listados acima diretamente na classe da sua visualização personalizada.
  • Se você pretende que a visualização personalizada seja compatível com o Android 1.6 (API de nível 4) ou versões posteriores, adicione a Biblioteca de Suporte do Android, revisão 5 ou posterior, ao seu projeto. Em seguida, na classe de visualização personalizada, chame o método ViewCompat.setAccessibilityDelegate() para implementar os métodos de acessibilidade acima. Para ver um exemplo dessa abordagem, consulte a amostra AccessibilityDelegateSupportActivity da Biblioteca de Suporte do Android (revisão 5 ou posterior) em (<sdk>/extras/android/support/v4/samples/Support4Demos/).

Nos dois casos, implemente os seguintes métodos de acessibilidade para a classe da visualização personalizada:

Para saber mais sobre como implementar esses métodos, consulte Preencher eventos de acessibilidade.

Enviar eventos de acessibilidade

Dependendo das especificidades da sua visualização personalizada, pode ser necessário enviar objetos AccessibilityEvent em momentos diferentes ou para eventos não processados pela implementação padrão. A classe View oferece uma implementação padrão para esses tipos de evento:

Observação: os eventos de passagem de cursor são associados ao recurso "Reconhecer por toque", que usa esses eventos como acionadores para emitir avisos sonoros para elementos da interface do usuário.

Em geral, você deverá enviar um AccessibilityEvent sempre que o conteúdo da visualização personalizada mudar. Por exemplo, se você estiver implementando uma barra de controle deslizante personalizada que permite ao usuário selecionar um valor numérico pressionando as setas para a esquerda ou para a direita, sua visualização personalizada deverá emitir um evento do tipo TYPE_VIEW_TEXT_CHANGED sempre que o valor do controle deslizante mudar. A amostra de código a seguir demonstra o uso do método sendAccessibilityEvent() para relatar esse 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;
        }
        ...
    }
    

Preencher eventos de acessibilidade

Cada AccessibilityEvent tem um conjunto de propriedades obrigatórias que descrevem o estado atual da visualização. Essas propriedades incluem itens como o nome da classe da visualização, a descrição do conteúdo e o estado verificado. As propriedades específicas obrigatórias para cada tipo de evento são descritas na documentação de referência AccessibilityEvent. A implementação View oferece valores padrão para essas propriedades. Muitos desses valores, incluindo o nome da classe e o carimbo de data/hora do evento, são disponibilizados automaticamente. Se você estiver criando um componente de visualização personalizado, será preciso fornecer algumas informações sobre o conteúdo e as características da visualização. Essas informações podem ser simples como uma etiqueta de botão, mas também podem incluir outras informações de estado que você queira adicionar ao evento.

O requisito mínimo para fornecer informações para serviços de acessibilidade com uma visualização personalizada é a implementação de dispatchPopulateAccessibilityEvent(). Esse método é chamado pelo sistema para solicitar informações para um AccessibilityEvent e torna sua visualização personalizada compatível com os serviços de acessibilidade no Android 1.6 (API de nível 4) ou versões posteriores. O código de exemplo a seguir mostra uma implementação básica desse 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;
    }
    

Para o Android 4.0 (API de nível 14) ou versões posteriores, use os métodos onPopulateAccessibilityEvent() e onInitializeAccessibilityEvent() para preencher ou modificar as informações em um AccessibilityEvent. Use o método onPopulateAccessibilityEvent() especificamente para adicionar ou modificar o conteúdo de texto do evento, que é transformado em solicitações audíveis por serviços de acessibilidade, como o "TalkBack". Use o método onInitializeAccessibilityEvent() para preencher outras informações sobre o evento, como o estado de seleção da visualização.

Além disso, implemente o método onInitializeAccessibilityNodeInfo(). Os objetos AccessibilityNodeInfo preenchidos por esse método são usados pelos serviços de acessibilidade para investigar a hierarquia de visualizações que gerou um evento de acessibilidade depois de receber esse evento, com o objetivo de ver informações de contexto mais detalhadas e oferecer feedback adequado aos usuários.

O exemplo de código abaixo mostra como modificar esses três métodos usando ViewCompat.setAccessibilityDelegate(). Observe que esta amostra de código requer que a Biblioteca de Suporte do Android para a API de nível 4 (revisão 5 ou posterior) seja adicionada ao projeto.

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);
            }
        }
    }
    

É possível implementar esses métodos diretamente na sua classe de visualização personalizada. Para ver outro exemplo dessa abordagem, consulte a amostra AccessibilityDelegateSupportActivity da Biblioteca de Suporte do Android (revisão 5 ou posterior) em (<sdk>/extras/android/support/v4/samples/Support4Demos/).

Oferecer um contexto de acessibilidade personalizado

No Android 4.0 (API de nível 14), o framework foi aprimorado para permitir que os serviços de acessibilidade inspecionem a hierarquia de visualizações de um componente da interface do usuário que gera um evento de acessibilidade. Essa melhoria permite que os serviços de acessibilidade disponibilizem um conjunto de informações contextuais muito mais detalhado para ajudar os usuários.

Há alguns casos em que os serviços de acessibilidade não recebem informações adequadas da hierarquia da visualizações. Um exemplo disso é um controle de interface personalizado que tem duas ou mais áreas clicáveis separadas, como um controle de agenda. Nesse caso, os serviços não recebem as informações adequadas porque as subseções clicáveis não fazem parte da hierarquia de visualizações.

Figura 1. Visualização de agenda personalizada com elementos de dia selecionáveis.

No exemplo mostrado na Figura 1, toda a agenda é implementada como uma única visualização. Portanto, se você não fizer mais nada, os serviços de acessibilidade não receberão informações suficientes sobre o conteúdo da visualização e a seleção do usuário nela. Por exemplo, se o usuário clicar no dia 17, o framework de acessibilidade receberá apenas as informações de descrição de todo o controle da agenda. Nesse caso, o serviço de acessibilidade do "TalkBack" apenas anunciaria "Agenda" ou, um pouco melhor, "Agenda de abril", mas o usuário ficaria sem saber que dia foi selecionado.

Para oferecer informações de contexto adequadas para serviços de acessibilidade em situações como essa, o framework traz uma maneira de especificar uma hierarquia de visualizações virtual. Uma hierarquia de visualizações virtual é uma maneira de os desenvolvedores de aplicativos fornecerem uma hierarquia de visualizações complementar aos serviços de acessibilidade, que corresponde melhor às informações reais na tela. Essa abordagem permite que os serviços de acessibilidade ofereçam informações de contexto mais úteis aos usuários.

Outra situação em que uma hierarquia de visualizações virtual pode ser necessária é uma interface do usuário que contém um conjunto de controles (visualizações) com funções estreitamente relacionadas, em que uma ação em um controle afeta o conteúdo de um ou mais elementos, como um seletor de número com botões diferentes para cima e para baixo. Nesse caso, os serviços de acessibilidade não recebem as informações adequadas porque a ação em um controle muda o conteúdo de outro, e a relação desses controles pode não estar visível para o serviço. Para lidar com essa situação, agrupe os controles relacionados a uma visualização contida e forneça uma hierarquia de visualizações virtual desse contêiner para representar claramente as informações e o comportamento disponibilizados pelos controles.

Para oferecer uma hierarquia virtual para uma visualização, modifique o método getAccessibilityNodeProvider() na visualização personalizada ou no grupo de visualizações e retorne uma implementação de AccessibilityNodeProvider. Para ver um exemplo de implementação desse recurso de acessibilidade, consulte AccessibilityNodeProviderActivity no projeto da amostra ApiDemos. É possível implementar uma hierarquia de visualizações virtual compatível com o Android 1.6 e versões posteriores usando a Biblioteca de Suporte com o método ViewCompat.getAccessibilityNodeProvider() e fornecendo uma implementação com AccessibilityNodeProviderCompat.

Processar eventos de toque personalizados

Os controles de visualização personalizados podem exigir um comportamento de evento de toque não padrão. Por exemplo, um controle personalizado pode usar o método de listener onTouchEvent(MotionEvent) para detectar os eventos ACTION_DOWN e ACTION_UP e acionar um evento de clique especial. Para manter a compatibilidade com os serviços de acessibilidade, o código que processa esse evento de clique personalizado precisa fazer o seguinte:

  1. Gerar um AccessibilityEvent adequado para a ação de clique interpretada.
  2. Ativar os serviços de acessibilidade para realizar a ação de clique personalizada para usuários que não podem usar uma tela touchscreen.

Para processar esses requisitos de maneira eficiente, seu código deve modificar o método performClick(), que precisa chamar a superimplementação desse método e executar as ações necessárias para o evento de clique. Quando a ação de clique personalizada for detectada, o código chamará o método performClick(). O exemplo de código a seguir demonstra esse padrão.

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;
        }
    }
    

O padrão mostrado acima garante que o evento de clique personalizado seja compatível com os serviços de acessibilidade por meio do método performClick() para gerar um evento de acessibilidade e também fornecer um ponto de entrada de serviços de acessibilidade para agir em nome de um usuário executando esse evento de clique personalizado.

Observação: se a visualização personalizada tiver regiões clicáveis diferentes, como uma visualização de agenda personalizada, será necessário implementar uma hierarquia de visualizações virtual modificando getAccessibilityNodeProvider() na visualização personalizada para que ela seja compatível com os serviços de acessibilidade.