Sunstone Apps
← Voltar para o blog

React Native Android

Desligando sons de toque no Android em React Native sem brigar com Switch

Um toggle de som parece uma configuração só. No Android, ele pode ser dois sistemas sonoros e dois alvos de toque fingindo ser um único controle.

Publicado: 7 min de leitura
Imagem de prévia OpenGraph deste artigo. Desligando sons de toque no Android em React Native sem brigar com Switch

O bug não estava no arquivo de áudio.

Estávamos adicionando uma configuração de som ao Daily Sudoku: Offline Puzzle. A configuração deveria silenciar efeitos curtos de gameplay: acertos, erros, unidades completas, auto-complete e vitória.

Essa parte foi a correção fácil. O app já tocava sons de jogo por um helper único, então a primeira versão só precisou de um soundEnabled persistido e uma guarda antes de criar players de áudio.

Então o Android revelou o requisito real de produto.

Feedback nativo de toque também é som

Em aparelho real, desligar “Sons do jogo” ainda deixava um tap audível quando o jogador tocava no tabuleiro ou em alguns botões. Esse som não vinha dos efeitos em expo-audio. Era feedback nativo de toque do Android vindo de Pressable e TouchableOpacity.

No código, são sistemas diferentes:

  • efeitos sonoros de gameplay vêm de players de áudio do app;
  • feedback de toque Android vem dos controles nativos;
  • o usuário vê uma configuração só e espera que os dois parem.

Então a configuração não podia significar honestamente só SFX de gameplay. Ela precisava significar som do app.

Centralize a decisão

A correção foi parar de passar flags de som pelo tabuleiro e pelos controles. Adicionamos um gerenciador de som perto da raiz do app, abaixo de settings, e fizemos wrappers de UI lerem dessa fonte:

<SettingsProvider initial={bootstrap.settings}>
  <SoundProvider>
    <ThemedAppContent />
  </SoundProvider>
</SettingsProvider>

Os wrappers concentram as props específicas do Android:

<Pressable android_disableSound={!isAppSoundEnabled} {...props} />
<TouchableOpacity touchSoundDisabled={!isAppSoundEnabled} {...props} />

Com isso, células do tabuleiro, teclado numérico, modais, configurações, cards da loja e ações de pós-vitória passaram a obedecer a mesma decisão.

A armadilha do Switch

O bug que sobrou era mais sutil. A linha de settings tinha um Pressable para a label e um Switch nativo como controle visual. Tocar na label funcionava. Tocar exatamente no switch alternava duas vezes: uma pela linha e outra pelo switch nativo.

Tentar deixar o Switch passivo com props de pointer não foi confiável o suficiente. A correção chata foi melhor:

  • tornar a linha inteira o único elemento interativo;
  • dar à linha accessibilityRole="switch" e accessibilityState;
  • trocar o Switch nativo por um indicador visual passivo com aparência de switch;
  • deixar o wrapper sensível a som cuidar do feedback nativo de toque.

A UI continua parecendo um switch, mas existe só um alvo de toque e uma única transição de estado.

A regra que ficou

Quando uma configuração React Native controla todo feedback sonoro do app no Android, não separe áudio de gameplay e feedback nativo de toque na implementação enquanto mostra um único controle ao usuário. Centralize o estado de som, passe touchables por wrappers e evite switches interativos aninhados que possam disparar uma segunda mudança.

A versão sem glamour é a confiável: uma fonte de verdade, um alvo de toque, uma mudança de estado.

Como fica o contrato final do componente

O contrato final é pequeno de propósito. O provider de som expõe se o som do app está ativo e uma função para efeitos de gameplay. Componentes de UI não sabem como settings são persistidos, se a config global de áudio está ligada ou qual prop de plataforma desliga som nativo. Eles só renderizam AppPressable ou AppTouchableOpacity.

Isso mantém a decisão de produto em um lugar. Se o texto mudar de “Sons do jogo” para “Sons do app”, muda a copy. Se o Android mudar o formato da prop em uma versão futura do React Native, muda o wrapper. Tabuleiro, loja, modais e pós-vitória não precisam reaprender a política.

export const AppPressable = React.forwardRef<React.ComponentRef<typeof Pressable>, PressableProps>(
  function AppPressable({ android_disableSound, ...props }, ref) {
    const { isAppSoundEnabled } = useSoundManager();

    return (
      <Pressable
        ref={ref}
        android_disableSound={android_disableSound ?? !isAppSoundEnabled}
        {...props}
      />
    );
  },
);

A mesma ideia vale para TouchableOpacity com touchSoundDisabled. O wrapper não é abstração de estilo. Ele existe porque um comportamento de plataforma virou parte de uma configuração de produto.

Por que não manter o Switch nativo?

O Switch nativo é um bom controle quando ele é dono da interação. Ele fica arriscado quando está aninhado dentro de outra linha pressionável que também alterna o mesmo estado. Dá para tentar coordenar handlers, mas o código começa a depender de comportamento de evento que varia por plataforma e método de input.

Para essa configuração, a linha inteira já era o melhor alvo acessível porque incluía label e texto explicativo. Então a linha virou o controle. O elemento com aparência de switch virou apresentação. Acessibilidade continua anunciando um switch porque a linha tem accessibilityRole="switch" e accessibilityState={{ checked: value }}.

Essa distinção importava: não removemos a semântica de switch. Removemos o segundo controle nativo interativo.

Checklist para toggles de som em React Native Android

Ao implementar um toggle parecido, confira estes pontos antes de chamar de pronto:

  • A configuração desliga players de áudio do app?
  • Ela desliga som nativo Android em Pressable?
  • Ela desliga som nativo Android em TouchableOpacity?
  • Existem controles interativos aninhados que podem alternar duas vezes?
  • O elemento acessível ainda expõe semântica de switch?
  • O comportamento foi testado em aparelho Android real, não só em simulador ou web?

O último ponto importa porque esse bug aparecia onde qualidade de produto é julgada: no telefone, com feedback real do sistema, usando toque normal.

Testando a configuração manualmente

O teste manual é simples, mas precisa acontecer em hardware Android. Ligue o som, toque uma célula do tabuleiro, toque um número, abra settings e toque alguns botões de modal. Depois desligue o som e repita os mesmos toques. O resultado esperado não é só sumirem os SFX customizados; o feedback de tap do sistema também deve sumir.

Vale testar com dedo e com mouse quando estiver usando emulador. Input de mouse pode expor bugs de duplo toggle porque cai exatamente no controle visual. Input com dedo pode expor problema de tamanho de alvo porque é menos preciso. Uma linha de setting deve sobreviver aos dois.

O que não abstrair

Não crie um sistema universal de botões só porque wrappers de som existem. O wrapper deve resolver a política de som da plataforma e sair do caminho. Estilo, layout e semântica de produto devem continuar locais, a menos que o app já tenha um primitivo compartilhado de design.

Essa contenção deixa a correção fácil de revisar. O diff deve dizer: estes alvos de toque existentes agora respeitam a configuração de som. Ele não deve redesenhar secretamente todos os botões do app.

Onde esse padrão deve morar

Os wrappers sensíveis a som devem ficar perto da UI compartilhada do app, não dentro do tabuleiro. O tabuleiro foi apenas onde o bug apareceu primeiro. Quando a decisão de produto virou “silenciar todo feedback sonoro do app”, todo alvo de toque passou a fazer parte da política.

Esse posicionamento evita uma falha comum: corrigir a tela onde o bug foi relatado e deixar o mesmo tap nativo em modais, settings, cards da loja ou ações de pós-vitória. Uma configuração global não deve depender de cada feature owner lembrando uma prop do Android.

Nomes importam

O nome interno deve acompanhar o comportamento de produto. Chamar o estado de isGameplaySoundEnabled fazia sentido quando ele só controlava SFX. Depois que a decisão mudou, isAppSoundEnabled ficou mais honesto. Essa renomeação pequena reduz erros futuros porque o tipo avisa o próximo dev que isso é mais amplo que áudio de jogo.

A copy da UI pode continuar amigável, mas o código deve preservar a invariante: uma configuração controla efeitos sonoros customizados e feedback nativo de toque.

A copy de produto também precisa ser testada

O rótulo original dizia “Sons do jogo”, mas o comportamento ficou mais amplo que efeitos curtos de gameplay. Isso cria uma decisão de copy. Se usuários interpretam a configuração como todo feedback audível, a implementação deve acompanhar. Se o app adicionar música ou voz um dia, talvez faça sentido separar controles.

Por enquanto, uma configuração é mais simples porque o app só tem efeitos curtos e feedback nativo de toque. O ponto importante é que texto de ajuda e comportamento não se contradigam. Uma configuração de mudo que ainda faz clique parece quebrada, mesmo quando o código consegue explicar o motivo.

O emulador não basta

Esse bug era fácil de perder testando apenas onde feedback nativo não era óbvio. Devices Android reais, configurações de som do sistema e input preciso de mouse no emulador revelaram partes diferentes do problema. É um bom lembrete para settings de UI: teste o comportamento na plataforma que é dona do feedback.

Conclusão

Um toggle de som é um contrato de produto, não só uma flag de áudio. Em React Native Android, cumprir essa promessa significa controlar efeitos do app, feedback nativo de toque e semântica de switch a partir de uma fonte de verdade clara.