Dia 4 — Imagem, toque e evento
Image, require e a pasta de imagens
Image, require e a pasta de imagens
Image é o componente de imagem, e a propriedade source recebe a origem em
duas formas que não se misturam. A local é um require de um caminho, e o
require é uma função do JavaScript que o ambiente de build entende: ele
transforma o caminho em um número de recurso dentro do aplicativo, e é por isso
que o caminho tem de ser literal — require('./logo.png') funciona e
require(caminhoVariavel) não, porque o build precisa ler o arquivo antes de o
código rodar e não consegue adivinhar o valor de uma variável.
const logo = require('./assets/logo.png'); <Image source={logo} style={{ width: 120, height: 40 }} />
O motivo do caminho literal fica claro pelo lado do require: ele não é um
import comum, é uma chamada que o build instrumenta. No exemplo, a função
carregarImagem registra o caminho num array e devolve um objeto com o
recurso, e o array final sai como [ './assets/logo.png' ]. Um único caminho
entrou. Se o caminho viesse de uma variável, essa linha do array seria undefined
— e o Image receberia um recurso que não existe.
A remota é uma uri, uma string com o endereço completo começando por
https://. source={{ uri: endereco }} é o objeto, e a string sozinha não
serve. A consequência prática é que uma imagem remota ocupa memória e tempo de
rede: o Image mostra o retângulo vazio até o download terminar, e por isso a
placeholder e a imagem de carregamento são parte do componente, não um detalhe
de estilo.
As duas formas chegam ao componente com a mesma chave source e formatos
diferentes: o exemplo imprime { uri: 'https://exemplo.test/capa/41.jpg' } para
a remota e { recursoDoApp: './assets/logo.png' } para a local. O
uri é o endereço que o aparelho busca em tempo de execução; o recurso do app
já está no pacote instalado. A mesma chave, dois lugares onde a imagem está.
resizeMode decide o que sobra
contain mostra a imagem inteira e deixa espaço vazio ao redor; cover preenche
o retângulo inteiro e corta o que sobra; stretch distorce até caber, e é a
única das três que muda a proporção — usar stretch em foto é o defeito mais
visível de uma tela mal acabada. O padrão é cover.
A caixa e a foto são coisas separadas, e é essa separação que costuma
confundir: width e height no Image definem o retângulo que a imagem
ocupa na tela, e resizeMode decide o que a foto faz dentro desse retângulo.
cover com width: 100, height: 40 corta bastante foto vertical; contain com
as mesmas medidas deixa a foto pequena e centralizada. Escolher resizeMode
antes de escolher a medida evita a metade dos retrabalho.
O exemplo separa as duas coisas em duas funções. A capa declara
resizeMode: 'cover' e mede { largura: '100%', altura: 180 }; a marca declara
resizeMode: 'contain' e mede { largura: 120, altura: 40 }. As duas caixas
têm formatos opostos — uma ocupa tudo e corta, a outra é pequena e cabe — e é a
mesma propriedade decidindo os dois casos. O resizeMode não diz o tamanho da
foto: diz o que acontece com o que não cabe na caixa.
// retrato: a foto preenche e o que sobra e cortado <Image source={{ uri }} style={{ width: '100%', height: 180 }} resizeMode="cover" /> // logo: a imagem inteira aparece, com o espaco sobrando <Image source={logo} style={{ width: 120, height: 40 }} resizeMode="contain" />
O último console do exemplo é o teste que separa as três coisas: um Image
sem resizeMode declarado responde undefined na propriedade, e o valor que
aparece na tela é o padrão da plataforma. undefined no objeto e cover na
tela são o mesmo comportamento — é só que a segunda não está escrita no código
da aula. É por isso que declarar resizeMode explicitamente é documentação, e
não redundância.
A proporção também é a medida. stretch é o único dos três que muda a
proporção, e por isso é o único que produz uma foto que parece estragada; os
outros dois preservam a proporção e decidem o que fazer com a sobra — cortar
(cover) ou sobrar (contain).
ImageBackground e o resto
ImageBackground é um Image que aceita filhos: o conteúdo vai por cima da
foto, e é o componente de tela de abertura. Ele não tem resizeMode próprio —
usa style, com flex: 1 para cobrir tudo. Um aviso prático: a imagem de
fundo é carregada junto com o componente e, em celular, imagem de fundo pesada
é a primeira causa de tela branca na abertura.
A diferença em relação ao Image é estrutural, e está em quem recebe os filhos:
o Image não aceita filho nenhum, o ImageBackground aceita e os desenha por
cima. Por isso o flex: 1 é obrigatório no ImageBackground — sem ele a
imagem não tem altura para cobrir, e o conteúdo aparece sobre um retângulo do
tamanho da foto.
O require tem uma segunda utilidade fora da imagem: qualquer arquivo de
módulo importado com ele vira recurso do aplicativo — um JSON de configuração
ou um .txt de termos. O caminho é relativo ao arquivo que chamou, então o
mesmo nome usado de duas pastas diferentes aponta para arquivos diferentes.
Exemplo
// `source` aceita um `require` (imagem local, resolvido no build) ou um // objeto com `uri` (imagem remota). O exemplo mostra as duas formas e o // que o `Image` recebeu em cada caso. const { View, Image, StyleSheet } = require('react-native'); // No app, `require` de um arquivo de imagem e resolvido pelo Metro e vira // um numero de recurso. Aqui nao ha build, entao o exemplo registra a // chamada para mostrar o que o componente receberia. const requisicoes = []; function carregarImagem(caminho) { requisicoes.push(caminho); return { recursoDoApp: caminho }; } const logo = carregarImagem('./assets/logo.png'); const foto = { uri: 'https://exemplo.test/capa/41.jpg' }; const estilos = StyleSheet.create({ capa: { width: '100%', height: 180 }, marca: { width: 120, height: 40 }, cartao: { width: 200, height: 100 }, }); function Capa() { return <Image source={foto} style={estilos.capa} resizeMode="cover" />; } function Marca() { return <Image source={logo} style={estilos.marca} resizeMode="contain" />; } const capa = Capa(); const marca = Marca(); console.log('caminhos pedidos no build:', requisicoes); console.log('tipo do componente:', capa.type, marca.type); console.log('source remoto:', capa.props.source); console.log('source local:', marca.props.source); console.log('resizeMode da capa:', capa.props.resizeMode); console.log('resizeMode da marca:', marca.props.resizeMode); // A caixa e a foto sao coisas separadas: medida e retangulo, nao a foto. function retangulo(estilo) { return { largura: estilo.width, altura: estilo.height }; } console.log('a caixa da capa e:', retangulo(StyleSheet.flatten(capa.props.style))); console.log('a caixa da marca e:', retangulo(StyleSheet.flatten(marca.props.style))); console.log('`cover` preenche a caixa e corta a foto'); console.log('`contain` mostra a foto inteira e sobra espaco'); console.log('`stretch` distorce ate caber: e o unico que muda a proporcao'); // O padrao de `resizeMode` e `cover`; declarado aqui so para o exemplo. console.log('sem resizeMode declarado:', Image({ source: foto, style: {} }).props.resizeMode);
Saída real
caminhos pedidos no build: [ './assets/logo.png' ]
tipo do componente: Image Image
source remoto: { uri: 'https://exemplo.test/capa/41.jpg' }
source local: { recursoDoApp: './assets/logo.png' }
resizeMode da capa: cover
resizeMode da marca: contain
a caixa da capa e: { largura: '100%', altura: 180 }
a caixa da marca e: { largura: 120, altura: 40 }
`cover` preenche a caixa e corta a foto
`contain` mostra a foto inteira e sobra espaco
`stretch` distorce ate caber: e o unico que muda a proporcao
sem resizeMode declarado: undefined
Pressable, TouchableOpacity e onPress
Pressable, TouchableOpacity e onPress
Pressable é o botão do React Native atual e o que resolve o problema que os
componentes antigos tinham: ele não embrulha o conteúdo em outro
componente, o que significa que onPress, onLongPress, onPressIn e
onPressOut chegam direto ao elemento que você declarou. O conteúdo é filho
dele, e pode ser um Text, um Image ou um View inteiro.
TouchableOpacity e TouchableHighlight são a geração anterior e continuam
existindo. A diferença é visual e automática: o TouchableOpacity reduz a
opacidade do filho ao toque, e o TouchableHighlight acende o fundo. O
Pressable não faz nada sozinho — o feedback depende do style, que pode ser
uma função que recebe { pressed } e devolve o estilo conforme o estado:
<Pressable style={({ pressed }) => [ estilos.botao, pressed && estilos.apertado, ]} onPress={salvar} > <Text style={estilos.rotulo}>Salvar</Text> </Pressable>
O exemplo desta página mostra por que o Pressable não embrulha: ele imprime o
tipo do filho direto e a resposta é Text. No TouchableOpacity a mesma
informação viria de um nível abaixo, dentro de um componente intermediário que o
aluno não declarou. É a razão de Pressable ter substituído os outros: menos
uma camada entre o que se escreve e o que se vê.
O estilo como função é o segundoitem do exemplo. Chamar a função com pressed
igual a false devolve o botão em estado normal, com backgroundColor,
paddingVertical, paddingHorizontal e borderRadius. Chamar com pressed
igual a true devolve exatamente o mesmo objeto mais opacity: 0.7 — e é essa
a forma do feedback: um estilo que se comporta como função do estado do toque, e
não um efeito visual que o componente aplica sozinho.
os quatro eventos de toque
onPress dispara quando o toque termina dentro do elemento. onPressIn dispara
na entrada, onPressOut na saída, e onLongPress quando o dedo fica parado por
mais tempo que o sistema considera pressionado. A combinação útil é
onPressIn mudando o estado para mostrar o botão apertado e onPressOut
voltando: é a forma de ter um botão com a cor certa no dedo e uma cor de
desabilitado quando não há nada a fazer.
A diferença entre os eventos aparece no exemplo de um jeito seco: onPress está
declarado e responde function, e onLongPress responde undefined porque não
foi declarado. Não existe evento "ligado por padrão" esperando ser usado — o que
não está no objeto não dispara. Disparar onPress duas vezes no objeto
registrou 2 toques, que é a prova de que a propriedade guardada é a mesma
função que o dedo chamaria.
A ordem importa nos que dependem do estado: onPressIn antes de onPress na
entrada, onPressOut depois de onPress na saída. Usar só onPress e deixar o
feedback para o style é a forma mais simples e cobre quase tudo; usar os quatro
é para o botão que precisa reagir ao dedo antes de soltar — botão que
cresce, que mostra uma prévia, ou que desmarca ao arrastar para fora.
onPress é assíncrono quando a função é async, e o Pressable não espera:
uma requisição disparada em onPress que demora três segundos produz uma tela
que parece travada. O botão precisa de um estado de carregamento próprio, e é
por isso que a aula de API traz a parte de desabilitar e mostrar o
carregando.
disabled e o que ele não faz
A propriedade se chama disabled, com um d: disable é a que erra todo
mundo. Ela desabilita o toque e a semitransparência do TouchableOpacity,
mas em Pressable só desabilita o toque — a aparência continua a mesma. Por
isso o padrão é combinar a propriedade com o estilo:
<Pressable disabled={carregando} style={({ pressed }) => [estilos.botao, pressed && estilos.apertado, carregando && estilos.desabilitado]}>
O exemplo separa o que a propriedade trava do que ela não trava, e a segunda
linha é a que surpreende o aluno: com carregando verdadeiro, disabled
responde true e o onPress continua sendo function. O evento existe no
objeto; o que o componente faz é não chamá-lo. O aviso é sobre aparência e não
sobre funcionamento — desabilitar o toque não deixa o botão cinza.
A aparência vem do terceiro item da função de estilo, e o exemplo resolve isso
no console: o estado de carregamento sai com backgroundColor trocado para
'#a8b3bf', com todo o resto igual. É o estado que o componente não produziu —
foi a função de estilo que devolveu, porque carregando entrou no array.
O detalhe do flexDirection volta aqui e tem nome: um Pressable com mais de
um filho precisa de flexDirection: 'row' no próprio estilo. O exemplo imprime
row para o Pressable que recebeu o estilo de linha — e a razão é a mesma do
View: o Pressable se comporta como View, e column é o padrão dele.
O que disabled não faz é esconder o elemento, impedir que ele receba o foco
do teclado ou impedir a animação — só o toque. E a regra de acessibilidade vale
aqui: um botão que só tem ícone precisa de accessibilityLabel, porque o leitor
de tela anuncia o rótulo, e sem rótulo ele anuncia o nome do componente. O
exemplo declara os dois no mesmo Pressable — accessibilityRole como
'button' e accessibilityLabel como 'salvar pedido' — e imprime os dois
valores ao lado do tipo Pressable.
Exemplo
// `Pressable` recebe `onPress` direto e o estilo pode ser uma funcao que // recebe `{ pressed }`. O exemplo chama o evento, le o estilo nos dois // estados e mostra o que `disabled` trava. const { View, Text, Pressable, TouchableOpacity, StyleSheet } = require('react-native'); const estilos = StyleSheet.create({ botao: { backgroundColor: '#1f6feb', paddingVertical: 12, paddingHorizontal: 20, borderRadius: 8 }, apertado: { opacity: 0.7 }, desabilitado: { backgroundColor: '#a8b3bf' }, linha: { flexDirection: 'row', alignItems: 'center' }, }); let toques = 0; function salvar() { toques += 1; return 'salvou'; } function Botao(props) { return ( <Pressable style={({ pressed }) => [ estilos.botao, pressed && estilos.apertado, props.carregando && estilos.desabilitado, ]} onPress={salvar} disabled={props.carregando} accessibilityRole="button" accessibilityLabel="salvar pedido" > <Text>{props.rotulo}</Text> </Pressable> ); } const arvore = Botao({ rotulo: 'Salvar', carregando: false }); console.log('tipo:', arvore.type); console.log('papel declarado:', arvore.props.accessibilityRole); console.log('rotulo para o leitor de tela:', arvore.props.accessibilityLabel); console.log('tipo de onPress:', typeof arvore.props.onPress); console.log('tipo de onLongPress:', typeof arvore.props.onLongPress); // O estilo e uma funcao: o exemplo chama com `pressed` nos dois estados. const funcaoDeEstilo = arvore.props.style; console.log('estado normal:', StyleSheet.flatten(funcaoDeEstilo({ pressed: false }))); console.log('estado apertado:', StyleSheet.flatten(funcaoDeEstilo({ pressed: true }))); console.log('estado carregando:', StyleSheet.flatten( funcaoDeEstilo({ pressed: false }).map((e) => e).concat([estilos.desabilitado]) )); // Disparar o evento como o runtime faria. arvore.props.onPress(); arvore.props.onPress(); console.log('toques registrados:', toques); console.log('o retorno de onPress:', salvar()); // Com mais de um filho, o `Pressable` precisa da linha declarada. const comLinha = Pressable({ style: estilos.linha, children: 'um' }); console.log('Pressable com um filho:', StyleSheet.flatten(comLinha.props.style).flexDirection); // `disabled` trava o toque; a aparencia e do estilo. const travado = Botao({ rotulo: 'Salvar', carregando: true }); console.log('disabled quando carregando:', travado.props.disabled); console.log('o evento ainda existe no objeto:', typeof travado.props.onPress === 'function'); // `TouchableOpacity` embrulha o filho e cuida da opacidade sozinho. const antigo = TouchableOpacity({ onPress: salvar, activeOpacity: 0.6, children: 'ok' }); console.log('TouchableOpacity:', antigo.type, '| activeOpacity:', antigo.props.activeOpacity); console.log('Pressable nao embrulha:', arvore.props.children.type);
Saída real
tipo: Pressable
papel declarado: button
rotulo para o leitor de tela: salvar pedido
tipo de onPress: function
tipo de onLongPress: undefined
estado normal: {
backgroundColor: '#1f6feb',
paddingVertical: 12,
paddingHorizontal: 20,
borderRadius: 8
}
estado apertado: {
backgroundColor: '#1f6feb',
paddingVertical: 12,
paddingHorizontal: 20,
borderRadius: 8,
opacity: 0.7
}
estado carregando: {
backgroundColor: '#a8b3bf',
paddingVertical: 12,
paddingHorizontal: 20,
borderRadius: 8
}
toques registrados: 2
o retorno de onPress: salvou
Pressable com um filho: row
disabled quando carregando: true
o evento ainda existe no objeto: true
TouchableOpacity: TouchableOpacity | activeOpacity: 0.6
Pressable nao embrulha: Text