Criar visualizações personalizadas mais acessíveis

Caso seu aplicativo precise de um componente de visualização personalizado, será necessário tornar a visualização mais acessível. As etapas abaixo podem melhorar a acessibilidade da sua visualização personalizada, conforme descrito nesta página:

  • Processar cliques do controle direcional.
  • Implementar métodos da API de acessibilidade.
  • Enviar objetos AccessibilityEvent específicos para sua visualização personalizada.
  • Preencher AccessibilityEvent e AccessibilityNodeInfo na visualização.

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 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 precisa tratar o evento KEYCODE_ENTER da mesma forma que KEYCODE_DPAD_CENTER. Isso facilita as interações do usuário com um teclado completo.

Implementar métodos da API de acessibilidade

Eventos de acessibilidade são mensagens sobre interações dos usuários com os componentes da interface visual do app. Essas mensagens são processadas pelos serviços de acessibilidade, que usam as informações nesses eventos para gerar feedback e solicitações complementares. Os métodos de acessibilidade fazem parte das classes View e View.AccessibilityDelegate. Os métodos são estes:

dispatchPopulateAccessibilityEvent()
O sistema chama esse método quando sua visualização personalizada gera um evento de acessibilidade. A implementação padrão desse método chama onPopulateAccessibilityEvent() para a visualização. Em seguida, chama o método dispatchPopulateAccessibilityEvent() para cada filha dessa visualização.
onInitializeAccessibilityEvent()
O sistema chama esse método para extrair mais informações sobre o estado da visualização, além do conteúdo de texto. Se a visualização personalizada oferecer controle interativo além de um simples TextView ou Button, substitua esse método e defina as outras informações sobre sua visualização, como o tipo de campo de senha, o tipo de caixa de seleção ou estados que oferecem interação ou feedback do usuário no evento. Se você substituir esse método, chame a superimplementação e modifique somente as propriedades que não são definidas pela superclasse.
onInitializeAccessibilityNodeInfo()
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 uma TextView ou um Button simples, substitua esse método e defina as outras informações sobre sua visualização no objeto AccessibilityNodeInfo processado por esse método.
onPopulateAccessibilityEvent()
Este método define a solicitação de texto falado do AccessibilityEvent para sua visualização. Ele também é chamado se a visualização for filha de uma outra que gera um evento de acessibilidade.
onRequestSendAccessibilityEvent()
O sistema chama esse método quando uma filha da visualização gera um AccessibilityEvent. Essa etapa permite que a visualização mãe adicione mais informações ao evento de acessibilidade. Implemente esse método somente se a visualização personalizada puder ter visualizações filhas e se a mãe puder fornecer informações de contexto ao evento que sejam úteis para os serviços de acessibilidade.
sendAccessibilityEvent()
O sistema chama esse método quando um usuário realiza uma ação em uma visualização. O evento é classificado com um tipo de ação do usuário, como TYPE_VIEW_CLICKED. Em geral, será necessário enviar um AccessibilityEvent sempre que o conteúdo da visualização personalizada mudar.
sendAccessibilityEventUnchecked()
Esse método é usado quando o código de chamada precisa controlar diretamente a verificação de acessibilidade sendo ativada no dispositivo (AccessibilityManager.isEnabled()). Se você implementar esse método, faça a chamada como se a acessibilidade estivesse ativada, independente da configuração do sistema. Normalmente, não é necessário implementar esse método para uma visualização personalizada.

Para oferecer suporte à acessibilidade, substitua e implemente os métodos anteriores diretamente na classe de visualização personalizada.

No mínimo, implemente os métodos de acessibilidade abaixo para a classe da visualização personalizada:

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

Para mais informações sobre como implementar esses métodos, consulte a seção sobre como 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 estes tipos de evento:

Em geral, será necessário 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 precisará emitir um evento do tipo TYPE_VIEW_TEXT_CHANGED sempre que o valor do controle deslizante mudar. O exemplo de código abaixo 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 do AccessibilityEvent.

A implementação View oferece valores padrão para essas propriedades obrigatórias. 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 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 e podem incluir outras informações de estado que você queira adicionar ao evento.

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 serviços de acessibilidade usam os objetos AccessibilityNodeInfo preenchidos por esse método para investigar a hierarquia de visualização que gera um evento de acessibilidade após o recebimento e fornecer feedback adequado aos usuários.

O exemplo de código abaixo mostra como substituir esses três métodos na visualização:

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

É possível implementar esses métodos diretamente na sua classe de visualização personalizada.

Oferecer um contexto de acessibilidade personalizado

Os serviços de acessibilidade podem inspecionar a hierarquia de visualização que contém um componente da interface do usuário que gera um evento de acessibilidade. Isso permite que os serviços de acessibilidade forneçam informações contextuais mais avançadas para ajudar os usuários.

Há casos em que os serviços de acessibilidade não recebem informações adequadas da hierarquia de visualização. 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ção.

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

No exemplo da Figura 1, toda a agenda é implementada como uma única visualização. Dessa forma, os serviços de acessibilidade não recebem informações suficientes sobre a seleção do usuário e o conteúdo na visualização, a menos que o desenvolvedor forneça outros informações. Por exemplo, se o usuário clicar no dia marcado como 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 anuncia "Agenda" ou "Agenda de abril", e o usuário não sabe qual dia está 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ção virtual. Uma hierarquia de visualização virtual é uma maneira de os desenvolvedores de apps fornecerem uma hierarquia de visualização complementar aos serviços de acessibilidade, que corresponde melhor às informações 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ção virtual pode ser necessária é uma interface do usuário que contém um conjunto de controles View 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 informações adequadas, porque uma ação em um controle muda o conteúdo em 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 em um contêiner de visualização e forneça uma hierarquia de visualização 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, substitua o método getAccessibilityNodeProvider() na visualização personalizada ou no grupo e retorne uma implementação de AccessibilityNodeProvider. É possível implementar uma hierarquia de visualização virtual usando a Biblioteca de Suporte com o método ViewCompat.getAccessibilityNodeProvider() e fornecer uma implementação com AccessibilityNodeProviderCompat.

Para gerenciar o foco em acessibilidade e fornecer informações a esses serviços de forma mais simples, você pode implementar o ExploreByTouchHelper. Ele fornece um AccessibilityNodeProviderCompat e pode ser anexado como o AccessibilityDelegateCompat de uma visualização chamando setAccessibilityDelegate. Para conferir um exemplo, consulte ExploreByTouchHelperActivity. ExploreByTouchHelper também é usado por widgets de framework, como CalendarView, pela visualização filha SimpleMonthView.

Processar eventos de toque personalizados

Os controles de visualização personalizados podem exigir um comportamento de evento de toque não padrão, conforme demonstrado nos exemplos abaixo.

Definir ações com base em cliques

Se o widget usar a interface OnClickListener ou OnLongClickListener, o sistema vai gerenciar as ações ACTION_CLICK e ACTION_LONG_CLICK para você. Se o app usar um widget mais personalizado que depende da interface OnTouchListener, você precisará definir gerenciadores personalizados para as ações de acessibilidade com base em cliques. Para fazer isso, chame o método replaceAccessibilityAction() para cada ação, conforme mostrado no snippet de código abaixo:

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

Criar eventos de clique personalizados

Um controle personalizado pode usar o método 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 forma eficiente, seu código precisa substituir o método performClick() e depois 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 vai chamar o método performClick(). O exemplo de código abaixo 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
                // 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;
    }
}

O padrão anterior ajuda a garantir que o evento de clique personalizado seja compatível com os serviços de acessibilidade usando o método performClick() para gerar um evento de acessibilidade. Além disso, ele fornece um ponto de entrada para que os serviços de acessibilidade atuem em nome de um usuário realizando o evento de clique personalizado.