Dia 14 — Acessibilidade e ajuste de tela

Informatica · Conteudo · publicado em 05/10/2026
Dia 14 de 16

Acessibilidade e ajuste de tela

Aula 1

Rótulo, papel e leitor de tela

Rótulo, papel e leitor de tela

O leitor de tela lê a tela em voz alta, e ele não lê o que está desenhado: lê o

que foi declarado. Um botão só com ícone não tem texto nenhum para ler, e

sem declarado o leitor anuncia o nome do componente — algo como "botão" sem

dizer o que o botão faz. É por isso que as duas propriedades que importam são

accessibilityLabel, o texto que será lido, e accessibilityRole, o que o

elemento é.

<Pressable accessibilityRole="button" accessibilityLabel="remover pedido 41">
  <Image source={lixeira} />
</Pressable>

O exemplo desta página monta os dois botões e deixa a diferença visível em duas

linhas. O sem rótulo responde "(nenhum)" para o papel e "(nenhum)" para o

rótulo — e o console traduz o que o leitor faz com isso: anuncia "botao" e

não diz o que faz. O declarado responde button para o papel, "remover pedido

p41" para o rótulo e a dica para o accessibilityHint.

accessibilityHint é a segunda frase: o rótulo diz o que é e a dica diz

o que acontece. "Remover" e "o registro será apagado desta lista". Os dois

juntos dão um botão que faz sentido ouvido sem a tela; só o rótulo deixa a

dúvida do que vai acontecer. A regra de ouro é que rótulo e dica juntos devem

fazer sentido sem a tela — se a frase "remover pedido 41" só tem sentido porque

você vê a lista, o rótulo precisa melhorar.

accessibilityRole tem os valores do componente: 'button', 'header',

'image', 'link', 'checkbox', 'textbox', 'adjustable', 'search',

'imagebutton'. O papel influencia a forma como o leitor anuncia e a forma como

a navegação por varredura trata o elemento. Deixar o papel fora faz o leitor

tratar um botão como texto, e o usuário não sabe que dá para apertar.

O accessibilityState do exemplo fecha a lista de propriedades com o caso que

mais aparece em botão de ação: com desabilitado verdadeiro, o estado sai como

{ disabled: true } e o leitor anuncia "desabilitado" em vez de repetir o

rótulo. É a mesma ideia da aula de Pressable — desabilitar não muda a

aparência, e sem declarar o estado o leitor continuaria anunciando o botão como

se estivesse disponível.

agrupar e anunciar

accessible numa View diz ao leitor para tratar o conjunto como um item

só, lendo o rótulo declarado e pulando os filhos. É o que resolve o card com

título, subtítulo e botão: sem agrupar, o leitor lê três trechos e o usuário não

sabe que é um registro. Com accessible e accessibilityLabel, o card é lido

como uma frase.

O exemplo monta o card agrupado e o rótulo sai inteiro: "Pedido 41, dois itens,

90 reais". Uma frase só, e a linha seguinte diz o que acontecia sem agrupar — o

leitor leria título, subtítulo e o botão como três coisas soltas. A diferença é

quantidade de anúncios, e é por isso que o agrupamento importa em lista: trinta

cards lidos como três coisas cada são noventa anúncios.

accessibilityState descreve o estado: { disabled: true }, { selected: true },

{ checked: true }. É o que faz o leitor anunciar "marcado" em vez de repetir o

rótulo. E AccessibilityInfo.announceForAccessibility é o aviso momentâneo: a

mensagem que o app manda por conta própria, como "salvo", quando o foco está em

outro elemento e não há nada que o leitor anuncie sozinho.

Os dois anúncios do exemplo — "salvo" e "removido, 1 registro" — são o caso em

que o app fala sem o usuário pedir. O aviso vai para o leitor e não aparece na

tela: quem não usa leitor de tela não vê nada, e quem usa recebe a confirmação

sem precisar procurar o que mudou.

O defeito de acessibilidade mais comum não é falta de rótulo: é o elemento que

recebe o toque e não é o que parece receber. Envolver o Pressable em uma View

grande demais faz o toque funcionar longe do que o usuário mirou — e o leitor

de tela lê o elemento errado, porque o toque é dele.

O exemplo mede esse defeito: a área que recebe o toque é `{ flex: 1, padding:

12 }, e é essa caixa que ocupa a linha inteira. O console` explica as duas

consequências em uma linha cada — o toque funciona longe do texto, e o leitor lê

a área, não o texto. Nenhuma das duas é problema de acessibilidade: são problema

de layout, com efeito em quem usa o leitor.

Exemplo

// Acessibilidade e o que o leitor de tela anuncia: `accessibilityLabel`
// (o que e), `accessibilityHint` (o que acontece) e `accessibilityRole` (o
// que o elemento e). O exemplo monta os dois casos: rotulado e nao.
const { View, Text, Image, Pressable, StyleSheet } = require('react-native');

const estilos = StyleSheet.create({ linha: { flexDirection: 'row', padding: 12 } });

// (1) botao com icone: sem rotulo, o leitor anuncia so "botao".
function BotaoSemRotulo(props) {
  return (
    <Pressable onPress={props.aoTocar}>
      <Image source={{ uri: 'icones/lixeira.png' }} style={{ width: 20, height: 20 }} />
    </Pressable>
  );
}

// (2) o mesmo botao, declarado: o leitor diz o que e e o que acontece.
function BotaoRotulado(props) {
  return (
    <Pressable
      onPress={props.aoTocar}
      accessibilityRole="button"
      accessibilityLabel={'remover pedido ' + props.pedido.id}
      accessibilityHint="o registro sera apagado desta lista"
      accessibilityState={{ disabled: props.desabilitado }}
    >
      <Image source={{ uri: 'icones/lixeira.png' }} style={{ width: 20, height: 20 }} />
    </Pressable>
  );
}

const semRotulo = BotaoSemRotulo({ aoTocar: () => {} });
const rotulado = BotaoRotulado({ pedido: { id: 'p41' }, aoTocar: () => {}, desabilitado: false });

console.log('sem rotulo, o leitor anuncia apenas:');
console.log('  papel:', semRotulo.props.accessibilityRole === undefined ? '(nenhum) -> "botao"' : semRotulo.props.accessibilityRole);
console.log('  rotulo:', semRotulo.props.accessibilityLabel === undefined ? '(nenhum) -> nao diz o que faz' : semRotulo.props.accessibilityLabel);

console.log('declarado, o leitor anuncia:');
console.log('  papel:', rotulado.props.accessibilityRole);
console.log('  rotulo:', rotulado.props.accessibilityLabel);
console.log('  dica:', rotulado.props.accessibilityHint);
console.log('  estado:', rotulado.props.accessibilityState);

// (3) o estado declarado e o que o leitor anuncia como "marcado".
const marcado = BotaoRotulado({ pedido: { id: 'p41' }, aoTocar: () => {}, desabilitado: true });
console.log('\nquando desabilitado:', marcado.props.accessibilityState);
console.log('o leitor anuncia "desabilitado", e nao repete o rotulo');

// (4) agrupar: `accessible` faz o leitor tratar a `View` como um item so.
function CardeAgrupado(props) {
  return (
    <View accessible accessibilityLabel={props.rotulo} accessibilityRole="summary">
      <Text>{props.titulo}</Text>
      <Text>{props.subtitulo}</Text>
    </View>
  );
}

const card = CardeAgrupado({ titulo: 'Pedido 41', subtitulo: 'dois itens', rotulo: 'Pedido 41, dois itens, 90 reais' });
console.log('\ncard agrupado, rotulo lido:', card.props.accessibilityLabel);
console.log('o leitor le o rotulo e pula os filhos');
console.log('sem agrupar, leria titulo, subtitulo e o botao, como tres coisas soltas');

// (5) o aviso que o leitor de tela fala por conta propria.
function anuncia(mensagem) {
  console.log('  anuncio:', mensagem);
}
anuncia('salvo');
anuncia('removido, 1 registro');
console.log('o aviso vai para o leitor, e nao aparece na tela');

// (6) o defeito mais comum: a `View` grande que rouba o toque.
function ItemComAreaInvisivel(props) {
  return (
    <View style={{ flex: 1, padding: 12 }}>
      <Text>{props.rotulo}</Text>
    </View>
  );
}
const area = ItemComAreaInvisivel({ rotulo: 'Pedido 41' });
const estilo = StyleSheet.flatten(area.props.style);
console.log('\narea que recebe o toque:', JSON.stringify(estilo));
console.log('`flex: 1` faz a caixa ocupar a linha inteira, e o toque funciona longe do texto');
console.log('o leitor le o elemento que esta embaixo do dedo, que e a area, nao o texto');

// (7) a arvore completa, com o que o leitor encontra em cada no.
function LinhaDePedido(props) {
  return (
    <View style={estilos.linha}>
      <Text>{props.pedido.cliente}</Text>
      <BotaoRotulado pedido={props.pedido} aoTocar={props.aoTocar} />
    </View>
  );
}

const arvore = LinhaDePedido({ pedido: { id: 'p41', cliente: 'ana' }, aoTocar: () => {} });
console.log('\nno da linha:', arvore.type);
const botaoDaLinha = arvore.props.children[1];
console.log('papel do botao:', botaoDaLinha.props.accessibilityRole, '| rotulo:', botaoDaLinha.props.accessibilityLabel);
console.log('o papel muda a forma como o leitor anuncia e a varredura trata o elemento');

Saída real

sem rotulo, o leitor anuncia apenas:
  papel: (nenhum) -> "botao"
  rotulo: (nenhum) -> nao diz o que faz
declarado, o leitor anuncia:
  papel: button
  rotulo: remover pedido p41
  dica: o registro sera apagado desta lista
  estado: { disabled: false }

quando desabilitado: { disabled: true }
o leitor anuncia "desabilitado", e nao repete o rotulo

card agrupado, rotulo lido: Pedido 41, dois itens, 90 reais
o leitor le o rotulo e pula os filhos
sem agrupar, leria titulo, subtitulo e o botao, como tres coisas soltas
  anuncio: salvo
  anuncio: removido, 1 registro
o aviso vai para o leitor, e nao aparece na tela

area que recebe o toque: {"flex":1,"padding":12}
`flex: 1` faz a caixa ocupar a linha inteira, e o toque funciona longe do texto
o leitor le o elemento que esta embaixo do dedo, que e a area, nao o texto

no da linha: View
papel do botao: button | rotulo: remover pedido p41
o papel muda a forma como o leitor anuncia e a varredura trata o elemento
Aula 2

Fonte, safe area e ajuste por tamanho

Fonte, safe area e ajuste por tamanho

O celular mostra o texto do app na fonte que o usuário escolheu nas

configurações do aparelho. allowFontScaling controla isso, e o padrão é

true, que é a decisão certa — desligar é uma das formas mais fáceis de

deixar o app impossível para quem enxerga pouco. Quando é preciso limitar, o

limite se declara por elemento com maxFontSizeMultiplier, não desligando o

todo: um layout que quebra com fonte muito grande é problema de layout, e a

correção é deixar o container crescer.

O exemplo desta página mede a fonte em três pontos. Com a fonte do sistema em

1.0, um fontSize: 15 resulta em 15. Com a fonte em 1.5, o mesmo elemento

resulta em 23. Com a fonte em 2.0 e o limite 1.5 por elemento, o resultado

é 45 — menor do que 30 dobrado, e é o limite fazendo o trabalho dele.

A conta do exemplo multiplica 15 pela fonte do sistema e pelo multiplicador do

elemento, e é a fórmula que o aparelho aplica. O que a aula muda é qual dos dois

fatores pertence a quem: a fonte do sistema é decisão do usuário e não se

negocia; o multiplicador do elemento é decisão do app. É por isso que o

allowFontScaling fica ligado e o limite vai no maxFontSizeMultiplier.

SafeAreaView é o componente que mantém o conteúdo longe das bordas onde o

sistema desenha coisa por cima: a barra de status, o entalhe da câmera, a barra

de gestos no rodapé. Em aparelho sem entalhe ele não faz diferença; em aparelho

com entalhe, é o que impede o primeiro item de ficar embaixo da câmera.

A árvore do exemplo tem SafeAreaView como raiz, com o estilo `{ flex: 1,

padding: 16 } e dois filhos — o título e o texto corrido. O flex: 1` é o que

faz a área segura ocupar a tela inteira, e o padding é o que separa o

conteúdo da borda que o sistema protege.

O engano comum é usar SafeAreaView em volta de tudo e achar que a tela ficou

correta. Ele só cuida das bordas do aparelho; a barra de navegação do

React Navigation e o teclado são outras camadas, e cada uma tem seu jeito. A

combinação que funciona é o contêiner de navegação cuidando do cabeçalho, o

SafeAreaView cuidando das bordas do aparelho, e o componente que embrulha a

rolagem cuidando do teclado.

A tabela do exemplo lista essas três camadas com o que cada uma cuida:

cabeçalho da navegação cuida da barra superior, o contêiner de área segura

cuida do entalhe e da barra de gestos, e o componente que envolve a rolagem

cuida do teclado aberto. Três camadas, três problemas, e nenhuma delas resolve o

problema da outra — por isso as três precisam existir.

a janela muda

useWindowDimensions devolve a largura e a altura da área útil e atualiza

quando o aparelho gira ou a janela muda. É o hook certo para decisão de layout:

quantas colunas cabem, se o conteúdo vai em linha ou em coluna, qual a altura

da imagem.

const { width, height } = useWindowDimensions();
const colunas = width >= 700 ? 3 : width >= 400 ? 2 : 1;

A função numeroDeColunas do exemplo é essa regra em três janelas: `320 x

640 dá uma coluna, 480 x 900 dá duas, e 820 x 1180` dá três. A medida entra

no cálculo e o cálculo entra no estilo — é o caminho do width medido em

tempo de execução que a aula de tamanho percorre pelo outro lado.

A distinção que importa é entre isso e ler a largura uma vez só. Ler no início

do componente produz layout que não se ajusta ao girar o aparelho, e o defeito

aparece como item cortado ou coluna espremida. O outro engano é usar a largura

para decidir fonte: fonte que muda com a largura quebra o padrão do texto, e o

usuário do aparelho com fonte grande perde a preferência dele.

O exemplo isola esse defeito em duas linhas: lendo a largura no começo, o

layout começa com 3 colunas e continua com 3 depois de girar para 480. São

seis colunas em uma tela de 480, e a resposta do console é que o layout

precisa reagir ao valor novo. O conserto é o hook, não uma linha de código que

recalcula na mão.

Para o que depende da altura, o caminho é o teclado: quando o teclado abre, a

altura da janela diminui, e o componente que mostra a rolagem precisa desse

novo valor para manter o campo visível. É o mesmo hook, olhando a altura em vez

da largura.

A relação entre fonte e largura fecha a aula, e o exemplo escreve as duas

regras em linhas separadas: não se aumenta a fonte pela largura da tela, e a

fonte grande vem da preferência do aparelho. São duas fontes de decisão em

campos diferentes — a largura decide coluna, a preferência do aparelho decide

tamanho de letra — e misturá-las produz o app que fica enorme em tela pequena e

minusculamente ilegível em tela grande.

Exemplo

// `allowFontScaling` (padrao `true`), `SafeAreaView` (as bordas do
// aparelho) e `useWindowDimensions` (a janela que muda ao girar). O
// exemplo monta a arvore e a regra de colunas por largura.
const { View, Text, SafeAreaView, StyleSheet } = require('react-native');

// (1) a fonte do sistema: o padrao e deixar escalar.
const estilos = StyleSheet.create({
  tela: { flex: 1, padding: 16 },
  titulo: { fontSize: 18, fontWeight: '700' },
  corpo: { fontSize: 15 },
});

const titulo = { fontSize: 18, allowFontScaling: true };
console.log('padrao do `allowFontScaling`: o texto cresce com a fonte do aparelho');
console.log('titulo:', JSON.stringify(titulo));
console.log('quando precisa limitar, limita por elemento com `maxFontSizeMultiplier`,');
console.log('nao desligando o `allowFontScaling` da tela inteira');

// (2) o multiplicador e o que limita a fonte de um elemento.
function tamanhoEfetivo(fonteDoSistema, multiplicador) {
  return Math.round(15 * fonteDoSistema * multiplicador);
}
console.log('\ncom a fonte do sistema em 1.0:', tamanhoEfetivo(1.0, 1));
console.log('com a fonte do sistema em 1.5:', tamanhoEfetivo(1.5, 1));
console.log('com a fonte do sistema em 2.0 e limite 1.5:', tamanhoEfetivo(2.0, 1.5));
console.log('desligar o escalonamento inteiro deixaria o app impossivel para quem enxerga pouco');

// (3) `SafeAreaView` cuida das bordas do aparelho; nao cuida da barra de
// navegacao nem do teclado.
function TelaComBordas() {
  return (
    <SafeAreaView style={estilos.tela}>
      <Text style={estilos.titulo}>Meus pedidos</Text>
      <Text style={estilos.corpo}>o conteudo comeca longe do entalhe e da barra de gestos</Text>
    </SafeAreaView>
  );
}
const arvore = TelaComBordas();
console.log('\ntipo da raiz:', arvore.type, '| estilo:', StyleSheet.flatten(arvore.props.style));
console.log('filhos:', arvore.props.children.length);
console.log('em aparelho sem entalhe ele nao muda nada; com entalhe, evita o primeiro item sob a camera');

// (4) cada camada cuida do seu: navegador de telas, aparelho, teclado.
const camadas = [
  { camada: 'cabecalho da navegacao', cuida: 'barra superior' },
  { camada: 'container de area segura', cuida: 'entalhe e barra de gestos' },
  { camada: 'componente que envolve a rolagem', cuida: 'teclado aberto' },
];
console.log('\ncada camada da tela, e o que ela cuida:');
for (const c of camadas) console.log('  ' + c.camada + ': ' + c.cuida);

// (5) `useWindowDimensions`: a janela muda ao girar, e a decisao de layout
// acompanha o valor novo.
function useWindowDimensions() {
  // no aparelho devolve a janela e atualiza ao girar; aqui o exemplo
  // recebe a medida para poder mostrar a regra.
  return arguments.length > 0 ? arguments[0] : { width: 0, height: 0 };
}

function numeroDeColunas(width) {
  return width >= 700 ? 3 : width >= 400 ? 2 : 1;
}

for (const medida of [{ width: 320, height: 640 }, { width: 480, height: 900 }, { width: 820, height: 1180 }]) {
  console.log('\njanela ' + medida.width + ' x ' + medida.height + ' -> ' + numeroDeColunas(medida.width) + ' coluna(s)');
  console.log('  a largura e medida em tempo de execucao, e o valor entra no estilo');
}

// (6) ler a largura uma vez so e o defeito: o layout nao se ajusta ao girar.
function colunasAoGravar(width) {
  return numeroDeColunas(width);
}
const inicial = colunasAoGravar(820);
console.log('\nao le uma vez so:', inicial, 'coluna(s)');
console.log('girou para 480 e a coluna continua', inicial, '-> o layout precisa reagir ao valor novo');

// (7) fonte que muda com a largura quebra o padrao do texto.
console.log('\nnao se aumenta a fonte pela largura da tela: o padrao do texto existe');
console.log('fonte grande vem da preferencia do aparelho, e o layout precisa sobrar espaco para ela');

Saída real

padrao do `allowFontScaling`: o texto cresce com a fonte do aparelho
titulo: {"fontSize":18,"allowFontScaling":true}
quando precisa limitar, limita por elemento com `maxFontSizeMultiplier`,
nao desligando o `allowFontScaling` da tela inteira

com a fonte do sistema em 1.0: 15
com a fonte do sistema em 1.5: 23
com a fonte do sistema em 2.0 e limite 1.5: 45
desligar o escalonamento inteiro deixaria o app impossivel para quem enxerga pouco

tipo da raiz: SafeAreaView | estilo: { flex: 1, padding: 16 }
filhos: 2
em aparelho sem entalhe ele nao muda nada; com entalhe, evita o primeiro item sob a camera

cada camada da tela, e o que ela cuida:
  cabecalho da navegacao: barra superior
  container de area segura: entalhe e barra de gestos
  componente que envolve a rolagem: teclado aberto

janela 320 x 640 -> 1 coluna(s)
  a largura e medida em tempo de execucao, e o valor entra no estilo

janela 480 x 900 -> 2 coluna(s)
  a largura e medida em tempo de execucao, e o valor entra no estilo

janela 820 x 1180 -> 3 coluna(s)
  a largura e medida em tempo de execucao, e o valor entra no estilo

ao le uma vez so: 3 coluna(s)
girou para 480 e a coluna continua 3 -> o layout precisa reagir ao valor novo

nao se aumenta a fonte pela largura da tela: o padrao do texto existe
fonte grande vem da preferencia do aparelho, e o layout precisa sobrar espaco para ela