Seu confete em React Native renderiza atrás dos cards: ordem de irmãos, zIndex e elevation
Um overlay que renderiza atrás do conteúdo que deveria comemorar quase nunca é bug de z-index. É bug de ordem de pintura, e o Android ainda adiciona uma segunda pegadinha por cima disso.
Quando você termina um puzzle no Daily Sudoku: Offline Puzzle, a tela de vitória solta uma rajada curta de confete sobre as estrelas, o tempo e o card de pontuação. É um efeito pequeno, barato de renderizar de propósito: algumas peças coloridas caem do topo da tela uma vez e somem. Não precisa parecer festa de estádio. Precisa só marcar aquele segundo em que o jogo diz “mandou bem” antes da próxima partida.
Por um tempo esse momento falhava em duas frentes. O confete caía atrás dos cards de resultado, então metade dele desaparecia assim que entrava na parte densa da tela. E o que dava para ver caía rápido demais, mais parecido com papel derrubado do que com uma comemoração. Os dois bugs moravam no mesmo componente pequeno, e vale registrar os dois porque a causa raiz aparece em muita interface React Native.
O sintoma: uma comemoração que você quase não vê
O overlay é uma view StyleSheet.absoluteFill com um conjunto de peças animadas dentro. Na tela de vitória a árvore era, em essência, assim:
<SafeAreaScreen className="flex-1 bg-background px-5">
<WinCelebration enabled={isAnimated} colors={colors} />
<View className="flex-1 justify-between py-4">
{/* card de estrelas, card de tempo/pontuação, botões de ação */}
</View>
</SafeAreaScreen>
Isso lê de forma natural, de cima para baixo: prepara a comemoração e depois renderiza o conteúdo. Também está exatamente ao contrário do comportamento desejado. O confete era pintado primeiro, os cards vinham depois, e os cards venciam. As peças estavam ali o tempo todo, animando e descendo pela tela. Só estavam embaixo de tudo.
Se o seu instinto é “coloca um zIndex”, segura. Esse instinto é o que manda a pessoa para uma hora frustrante colocando zIndex: 999 em views aleatórias e vendo nada mudar.
Por que renderiza atrás: ordem de pintura entre irmãos, não z-index
No navegador, z-index é o modelo mental que a maioria dos devs busca primeiro. No React Native existe uma regra mais simples por baixo que você precisa internalizar: entre irmãos, os filhos que vêm depois pintam por cima dos que vêm antes. Não há contexto de empilhamento implícito atrelado ao posicionamento como há na web. Se dois irmãos se sobrepõem e nenhum define zIndex, quem aparece depois no JSX vence.
Então a correção não é um número mágico. É ordem no documento. Mova o overlay para ser o último filho da tela:
<SafeAreaScreen className="flex-1 bg-background px-5">
<View className="flex-1 justify-between py-4">
{/* card de estrelas, card de tempo/pontuação, botões de ação */}
</View>
<WinCelebration enabled={isAnimated} colors={colors} />
</SafeAreaScreen>
Esse único movimento resolve iOS e web de imediato. O overlay é posicionado de forma absoluta, então não afeta o layout do conteúdo, e por ser agora o último irmão ele pinta por cima. Essa é a versão da correção em que a maioria dos tutoriais para, e se você só publicasse para iOS acharia que terminou.
Você não terminou, porque o Android tem opinião própria.
A pegadinha do Android: elevation vence zIndex
No Android, o React Native mapeia para o sistema de views nativo, e esse sistema tem uma propriedade chamada elevation. A elevation controla a sombra do Material, mas também participa do empilhamento: uma view com elevation maior pode renderizar acima de um irmão mesmo quando a ordem do JSX ou o zIndex sugeririam o contrário. Cards, surfaces e qualquer coisa estilizada para parecer “elevada” costumam carregar elevation, às vezes indiretamente por um estilo de surface compartilhado.
Essa é a parte que queima as pessoas. Você reordena o JSX, fica perfeito no simulador iOS, publica, e um testador Android reporta que o confete ainda some atrás dos cards. A reordenação estava certa; ela só não é suficiente quando um irmão tem elevation e o seu overlay não tem nenhuma.
A correção robusta é ser explícito nos dois eixos. Defina zIndex como dica de empilhamento cross-platform e elevation especificamente para o Android:
<View pointerEvents="none" style={[StyleSheet.absoluteFill, { zIndex: 20, elevation: 20 }]}>
{pieces.map(({ id, ...piece }) => (
<ConfettiPiece key={id} {...piece} />
))}
</View>
Dois detalhes importam aqui além dos números. Primeiro, pointerEvents="none" não é opcional. Um overlay de tela cheia por cima dos seus botões vai engolir alegremente cada toque se você deixar, e o usuário vai achar que a tela de vitória travou. Com pointerEvents="none", os toques passam direto para os botões “Novo jogo” e “Home” que estão embaixo. Segundo, os valores não precisam ser grandes. zIndex: 20 não é mais poderoso que zIndex: 5; ele só precisa ganhar do que os cards usam. Números redondos com uma folga deixam a intenção legível.
Combinar reordenação e o par explícito de zIndex/elevation é uma redundância intencional. A reordenação expressa a intenção de forma limpa; os valores explícitos de empilhamento defendem contra um irmão que carrega elevation em silêncio agora ou no futuro.
O segundo bug: caindo rápido demais
Com o overlay finalmente visível, o outro problema ficou óbvio: as peças caíam em cerca de um segundo e sumiam. Confete nessa velocidade não lê como comemoração; lê como acidente.
Cada peça anima com Reanimated, levando um único shared value de 0 a 1 e mapeando isso em uma translação para baixo com um leve desvio horizontal e rotação. A velocidade mora inteira na duração da animação:
// antes: ~1,1s a ~1,9s, dependendo da peça
durationMs: 1100 + (index % 4) * 260,
// depois: ~2,3s a ~3,6s
durationMs: 2300 + (index % 4) * 420,
Praticamente dobrar a duração é a mudança inteira. É tentador “consertar” um efeito que parece ralo adicionando mais peças, mas essa é a alavanca errada por dois motivos. Mais peças custam mais por frame, o que importa em hardware mais fraco. E densidade não era o que faltava; tempo em tela era. Peças mais lentas permanecem, se sobrepõem de forma mais natural conforme escalonam, e dão ao olho algo para acompanhar. O espalhamento por módulo (index % 4) evita que as peças caiam em sincronia, então elas continuam parecendo orgânicas em vez de uma folha única descendo.
Dois controles moldam a velocidade percebida aqui: o durationMs por peça e o pequeno delayMs que escalona quando cada uma começa. Mexemos nos dois. O escalonamento é o que transforma dezesseis quedas idênticas em algo que parece um espalhamento.
Mantendo o efeito honesto em celulares baratos
Nada disso vale muito se engasgar nos aparelhos que a maioria dos nossos jogadores realmente usa. A comemoração inteira é feita para ser leve: dezesseis peças, uma animação cada, rodando na UI thread via Reanimated para a thread de JavaScript ficar livre para a navegação e para a requisição de anúncio que vem depois da vitória. O overlay nunca intercepta toques, então não adiciona trabalho de gesto. E o efeito fica atrás da configuração de animação do app, então quem escolhe movimento reduzido ou desligado não paga por ele.
Esse último ponto é fácil de esquecer. Um overlay de comemoração é exatamente o tipo de movimento decorativo que uma opção “reduzir animações” pensada em acessibilidade deveria desligar. Ligar o efeito a essa configuração desde o começo é mais barato do que fazer o retrofit depois, e significa que o orçamento de performance em celulares fracos tem uma válvula de escape que também é o comportamento correto de acessibilidade.
Como identificar essa classe de bug
O bug de overlay-atrás-do-conteúdo é comum o suficiente para merecer um pequeno checklist. Se uma camada decorativa ou de comemoração não aparece onde você espera por cima do conteúdo:
- Cheque a ordem dos irmãos primeiro, não o z-index. Pergunte qual elemento aparece depois no JSX. No React Native, irmãos posteriores pintam por cima por padrão, e reordenar costuma ser a correção mais limpa.
- Confirme especificamente no Android. Se está certo no iOS e errado no Android, suspeite de
elevation. Um irmão elevado pode ganhar do seu overlay mesmo com a ordem correta. Defina umaelevationexplícita no overlay para igualar ou superar. - Verifique se os toques ainda passam. Se os botões embaixo de um overlay de tela cheia param de responder, está faltando
pointerEvents="none". Isso é fácil de passar batido porque o overlay é invisível entre as animações. - Separe posição de empilhamento. Posicionamento absoluto tira o overlay do fluxo de layout, mas não decide sozinho o que pinta por cima.
- Ajuste o movimento com duração e delay, não com contagem. Se um efeito parece ralo, mais tempo em tela costuma ler melhor do que mais elementos, e é mais gentil com o orçamento de frames.
O que fica
A correção aqui foram três edições pequenas: mover o overlay para o fim da árvore, adicionar um par explícito de zIndex/elevation com pointerEvents="none", e praticamente dobrar a duração da queda. O que vale guardar não é o diff. É o modelo mental.
Empilhamento no React Native é ordem de pintura entre irmãos primeiro, zIndex depois, e no Android, elevation como critério de desempate nativo que pode sobrepor os dois se você não for explícito. Uma vez que esse modelo está na sua cabeça, “meu overlay está atrás do conteúdo” deixa de ser um mistério que você resolve aumentando um número e vira uma reordenação de uma linha mais uma guarda deliberada de Android. E uma vez que o confete está de fato por cima, a única coisa que sobra para acertar é deixá-lo permanecer tempo suficiente para parecer uma vitória.