Dia 2 — Tamanho, espaçamento e borda
width, height, flex e o limite
width, height, flex e o limite
width e height são número de pixel: width: 120 ocupa 120 pixels, sempre,
em qualquer aparelho. No celular isso quase nunca é o que se quer, porque a
largura útil muda de um modelo para outro. A resposta do sistema de layout é
flex, e flex: 1 significa "cresça para ocupar o que sobrar". Um View com
flex: 1 dentro de uma coluna de altura definida cresce até encostar no
próximo irmão; dois irmãos com flex: 1 dividem o espaço ao meio; com
flex: 2 e flex: 1, o primeiro fica com o dobro.
O exemplo desta página começa pelo atalho: flex: 1 expande para `{ flexGrow: 1,
flexShrink: 1, flexBasis: '0%' } e flex: 2 para { flexGrow: 2, flexShrink: 1,
flexBasis: '0%' }. Depois faz a conta da divisão em 200 pixels: com flex: 1` e
flex: 2 o resultado é 67 e 133; com dois flex: 1 é 100 e 100. Com
dois filhos sem flex nenhum, a mesma função devolve NaN e NaN — não há
proporção para dividir, e o erro é o sinal de que flex não foi declarado.
Porcentagem funciona nos dois: width: '100%' é a largura do pai, e
width: '50%' é metade dela — e é isso que o exemplo imprime no último console,
50%. A diferença entre porcentagem e flex é quem decide o tamanho: na
porcentagem o filho decide; no flex o pai distribui o espaço que sobrou. Isso
importa em lista com item de largura variável — onde a porcentagem é o certo,
porque cada item quer uma fração do pai.
flex é um atalho de três propriedades
flex: 1 escreve, expandido, flexGrow: 1, flexShrink: 1, flexBasis: 0%. O
flexBasis é o tamanho inicial: 0% significa que o cálculo começa do zero e
o espaço é distribuído por proporção. É por isso que dois filhos com flex: 1
ficam exatamente iguais mesmo com conteúdos de tamanhos muito diferentes — o
conteúdo não entra na conta. Quando se escreve flex: 1 e o resultado é
estranho, quase sempre o culpado é flexBasis assumindo o conteúdo, e a
solução é flexBasis: 0 explícito ou width fixa em um dos lados.
O flexGrow é o número que entra na divisão, e é ele que o exemplo usa no
cálculo: 200 pixels com flex: 1 e flex: 2 dão 67 e 133, que é a mesma
regra na proporção 1:2. Quando a função do exemplo não encontra nenhum
flexGrow — os dois filhos com estilo vazio — o total dá zero, e a divisão por
zero aparece como NaN. Esse NaN é o mesmo defeito que aparece na tela quando
se espera que o filho ocupe o espaço e ele fica com o tamanho do conteúdo.
O flexShrink é o que impede o transbordo horizontal: com nowrap e filhos
largos demais, o padrão do flex é encolher. Colocar flexShrink: 0 em um
item de largura intrínseca tira essa proteção e é a forma mais rápida de fazer
uma linha transbordar.
A função expandeFlex do exemplo mostra o outro ramo: quando o valor não é um
número, ela devolve { flexGrow: 0, flexShrink: 1, flexBasis: 'auto' }. É o
flex que não foi declarado, e flexBasis: 'auto' é a diferença entre ocupar o
espaço sobrando e medir pelo conteúdo.
min e max são limites, não tamanhos
minWidth, maxWidth, minHeight e maxHeight não definem o tamanho: eles
limitam. É a diferença entre um Text que pode encolher até 60 pixels de altura
e o mesmo Text com maxHeight: 60, que corta o excedente sem avisar. Para
texto com número controlado de linhas existe propriedade própria, tratada na
aula de tipografia; maxHeight em texto é a versão cega.
O limite convive com o flex sem brigar por quem manda, e o exemplo junta os
dois no mesmo objeto: { flex: 1, maxWidth: 420, minHeight: 40 }. A leitura é que
o filho cresce até o que sobrar, para no maxWidth, e nunca fica com menos que o
minHeight. Se o limite inferior e o superior se cruzam — minHeight: 100 e
maxHeight: 60 — o resultado é o menor deles, porque o piso vem depois do teto.
// a caixa ocupa o que sobrou, mas nunca passa de 420 <View style={{ flex: 1, maxWidth: 420, padding: 16 }}> <Text style={{ flexShrink: 1 }}>conteudo que precisa caber</Text> </View>
flexShrink: 1 no Text é o que permite o conteúdo caber dentro do limite: sem
ele, um texto largo empurra a caixa para fora do maxWidth em vez de encolher
dentro dela. Os dois lados do exemplo — flex: 1 no pai, flexShrink: 1 no
filho — são o par que resolve largura variável.
O limite também é a ferramenta de responsividade mais barata que existe: um
maxWidth não pergunta o tamanho da tela, ele só impede o crescimento. Em vez
de decidir por aparelho, deixa o conteúdo ocupar o que tem e trava em um teto.
a medida que vem de fora
Número fixo serve para o que é sempre igual: a barra de rodapé, um botão de
48 de altura, a margem padrão da tela. Tudo que depende do aparelho — largura da
coluna, quantos itens cabem numa linha, altura disponível depois do teclado — é
medido em tempo de execução, e o valor medido entra no estilo como `width:
medida` em vez de um número escrito à mão. É o mesmo caminho que a aula de
ajuste por tamanho de tela percorre, com o hook que entrega a janela.
O exemplo fecha nessa distinção com as duas medidas lado a lado: o pai de altura
fixa imprime 200, um número escrito no StyleSheet, e o filho responde 50%,
uma proporção que só significa alguma coisa com a medida do pai. As duas formas
convivem no mesmo arquivo sem se misturar — o que decide é se o número pertence
à caixa ou ao aparelho.
Exemplo
// `flex` e a forma de ocupar o espaco que sobrou, e ele e um atalho de // tres propriedades. O exemplo expande o atalho e calcula como o espaco // e dividido entre os filhos. const { View, Text, StyleSheet } = require('react-native'); const estilos = StyleSheet.create({ linha: { flexDirection: 'row', height: 200 }, coluna: { flex: 1 }, }); function expandeFlex(valor) { const numero = typeof valor === 'number' ? valor : parseFloat(String(valor)); if (!numero) { return { flexGrow: 0, flexShrink: 1, flexBasis: 'auto' }; } return { flexGrow: numero, flexShrink: 1, flexBasis: '0%' }; } console.log('flex: 1 vira:', expandeFlex(1)); console.log('flex: 2 vira:', expandeFlex(2)); // O espaco que sobra e dividido na proporcao dos `flexGrow`. function divide(espaco, filhos) { const total = filhos.reduce((soma, f) => soma + f.flex, 0); if (total === 0) return filhos.map(() => espaco / filhos.length); return filhos.map((f) => Math.round((espaco * f.flex) / total)); } console.log('200 px para flex 1 e flex 2:', divide(200, [{ flex: 1 }, { flex: 2 }])); console.log('200 px para dois flex 1:', divide(200, [{ flex: 1 }, { flex: 1 }])); console.log('200 px sem flex nenhum:', divide(200, [{}, {}])); // A arvore montada mostra que `flex` viaja dentro do estilo do filho. function Linha() { return ( <View style={estilos.linha}> <View style={estilos.coluna}> <Text>lateral</Text> </View> <View style={estilos.coluna}> <Text>conteudo</Text> </View> </View> ); } const arvore = Linha(); console.log('altura fixa do pai:', StyleSheet.flatten(arvore.props.style).height); console.log('estilo do primeiro filho:', StyleSheet.flatten(arvore.props.children[0].props.style)); console.log('os dois filhos tem o mesmo estilo:', arvore.props.children[0].props.style === arvore.props.children[1].props.style); // Limites: `maxWidth` limita, nao define. const comLimite = StyleSheet.flatten({ flex: 1, maxWidth: 420, minHeight: 40 }); console.log('com limite:', comLimite); console.log('largura percentual e medida do pai:', StyleSheet.flatten({ width: '50%' }).width);
Saída real
flex: 1 vira: { flexGrow: 1, flexShrink: 1, flexBasis: '0%' }
flex: 2 vira: { flexGrow: 2, flexShrink: 1, flexBasis: '0%' }
200 px para flex 1 e flex 2: [ 67, 133 ]
200 px para dois flex 1: [ 100, 100 ]
200 px sem flex nenhum: [ NaN, NaN ]
altura fixa do pai: 200
estilo do primeiro filho: { flex: 1 }
os dois filhos tem o mesmo estilo: true
com limite: { flex: 1, maxWidth: 420, minHeight: 40 }
largura percentual e medida do pai: 50%
padding, margin e borda
padding, margin e borda
padding é o espaço dentro da caixa, entre a borda e o conteúdo, e ele
empurra o conteúdo para longe da borda sem aumentar a caixa quando a caixa tem
altura fixa. margin é o espaço de fora, entre a caixa e os irmãos, e é o
que empurra as outras caixas. A diferença aparece em dois lugares: padding
some com o fundo da caixa, margin não — o fundo atravessa a área do margin e
o que se vê é o fundo do pai. E padding aumenta o tamanho da caixa quando o
tamanho é automático; margin nunca aumenta o tamanho de ninguém.
O exemplo desta página monta um cartão com as três coisas juntas — padding: 12,
marginHorizontal: 16, marginBottom: 8, mais borda e raio — e imprime o
estilo achatado. É a prova de que padding e margin convivem no mesmo objeto
sem se misturar: são chaves diferentes, com efeitos diferentes, e nenhuma
sobrescreve a outra.
Os atalhos existem para os quatro lados: paddingVertical e
paddingHorizontal são paddingTop mais paddingBottom, e
paddingHorizontal é paddingLeft mais paddingRight. Escrever o valor curto
cabe em uma linha e evita o erro de esquecer um dos lados — que é o defeito
clássico de um card com texto colado na lateral só de um lado.
A função expandeAtalho do exemplo faz essa conta na mão e mostra o resultado:
paddingVertical: 8 e paddingHorizontal: 16 viram um objeto com as quatro
chaves por extenso, paddingTop: 8, paddingBottom: 8, paddingLeft: 16 e
paddingRight: 16. Os atalhos originais continuam no objeto ao lado das quatro
chaves — o que mostra que atalho e valor por extenso são a mesma medida, e que
escrever os dois é redundância, não soma.
// atalho lateral: mesma medida nos dois lados <Text style={{ paddingHorizontal: 16 }}>linha do formulario</Text> // equivalente, escrito por extenso <Text style={{ paddingLeft: 16, paddingRight: 16 }}>linha do formulario</Text>
margin tem os mesmos atalhos, mais marginTop isolado — que é a forma de
empurrar um bloco para baixo sem mexer nos lados. Padding em marginTop não
existe: para espaçar título de conteúdo, é marginBottom no título.
a borda entra na conta do tamanho
borderWidth soma no tamanho da caixa. Uma caixa com width: 100 e
borderWidth: 2 tem 104 de largura: os 2 pixels de cada lado são borda, não
conteúdo — e é exatamente isso que a função tamanhoExterno do exemplo devolve,
{ largura: 104, altura: 4 }. A altura de 4 vem da mesma conta em cima de uma
altura que não foi declarada: 0 de conteúdo mais 2 de borda de cada lado. É
a forma mais curta de entender por que width: '100%' com borda transborda a
caixa em 4 pixels.
borderRadius só faz sentido junto com borderWidth ou com
backgroundColor — raio sem nada para arredondar arredonda nada, e é o motivo
de borderRadius: 12 sozinho não mudar nada na tela. O exemplo mostra os dois
casos lado a lado: so raio imprime { borderRadius: 12 }, e raio com borda
imprime { borderRadius: 12, borderWidth: 2 }. A chave não muda; o que muda é se
existe pixel para curvar.
O raio é medido nos cantos e o valor não passa de metade do lado menor: um
borderRadius: 40 num quadrado de 60 vira uma forma de 40, não um círculo. O
exemplo faz essa conta com Math.min e imprime 12 como raio efetivo quando o
pedido foi 40 numa caixa de 60. Pedir mais que o permitido não dá erro: o valor
simplesmente satura, e o raio vira metade do lado menor.
borderColor sem borderWidth também não aparece, pela mesma razão: o estilo
precisa da espessura para existir. E borderTopWidth: 0 com os demais lados
presentes é o separador de uma linha: é o jeito mais leve de desenhar um
divisor entre dois itens de lista.
espaçamento que não depende do estilo repetido
Repetir marginBottom: 12 em quinze itens é o caminho curto para um arquivo que
ninguém consegue revisar. A alternativa é uma constante no módulo do estilo e um
map que aplica: o número deixa de estar espalhado e passa a ter um nome. Se
depois o espaçamento mudar, muda em um lugar só. É a mesma jogada do
StyleSheet.create — trocar número escrito por valor com nome.
O exemplo usa marginBottom: 8 num lugar só — o estilo do título do cartão — e
padding: 12 no cartão inteiro. O espaçamento vertical entre o título e o texto
não vem de um número repetido em cada Text, e sim de uma chave no StyleSheet
que o map reaproveita. É por isso que o estilo da raiz impresso pelo exemplo
continua legível: cada número tem um nome que diz o que ele controla.
| Precisa de | Use |
|---|---|
| afastar do próprio conteúdo | padding |
| afastar de outro elemento | margin |
| desenhar um separador | borderBottomWidth sem borderBottomColor transparente |
| arredondar só um canto | borderTopLeftRadius |
O separador do exemplo é a linha real dessa tabela: `{ borderBottomWidth: 1,
borderBottomColor: '#e6e8eb' } num View` sem altura nem filhos. Um elemento
vazio só com borda desenha uma linha de 1 pixel na base, e é mais leve que um
View de altura 1 com fundo — porque não cria caixa, não ocupa espaço e não
entra na conta do flex do pai.
Exemplo
// `padding` e espaco dentro da caixa, `margin` e espaco fora dela. O // exemplo calcula o tamanho final de uma caixa e mostra por que a borda // soma no tamanho e por que o raio sozinho nao arredonda nada. const { View, Text, StyleSheet } = require('react-native'); const estilos = StyleSheet.create({ cartao: { padding: 12, marginHorizontal: 16, marginBottom: 8, borderWidth: 2, borderColor: '#d0d7de', borderRadius: 10, }, titulo: { marginBottom: 6, fontSize: 16 }, separador: { borderBottomWidth: 1, borderBottomColor: '#e6e8eb' }, }); // atalho e a mesma medida nos dois lados function expandeAtalho(estilo) { const saida = Object.assign({}, estilo); if (estilo.paddingVertical !== undefined) { saida.paddingTop = estilo.paddingVertical; saida.paddingBottom = estilo.paddingVertical; } if (estilo.paddingHorizontal !== undefined) { saida.paddingLeft = estilo.paddingHorizontal; saida.paddingRight = estilo.paddingHorizontal; } return saida; } const cartao = StyleSheet.flatten(estilos.cartao); console.log('cartao:', cartao); console.log('atalhos expandidos:', expandeAtalho({ paddingVertical: 8, paddingHorizontal: 16 })); // A borda SOMA no tamanho. A caixa de 100 com borda de 2 tem 104. function tamanhoExterno(estilo) { const largura = (estilo.width || 0) + 2 * (estilo.borderWidth || 0); const altura = (estilo.height || 0) + 2 * (estilo.borderWidth || 0); return { largura, altura }; } console.log('width 100 com borderWidth 2:', tamanhoExterno({ width: 100, borderWidth: 2 })); // Padding aumenta a caixa quando o tamanho e automatico; margin nunca. function alturaDe(conteudo, estilo) { return conteudo + 2 * (estilo.padding || 0); } console.log('altura com padding 12:', alturaDe(40, { padding: 12 }), '| sem padding:', alturaDe(40, {})); // Raio sem borda e raio sem fundo: nada aparece para arredondar. console.log('so raio:', StyleSheet.flatten({ borderRadius: 12 })); console.log('raio com borda:', StyleSheet.flatten({ borderRadius: 12, borderWidth: 2 })); // O raio nunca passa de metade do lado menor. const raioReal = Math.min(12, Math.min(60, 60) / 2); console.log('raio pedido 40 numa caixa 60, o raio efetivo:', raioReal); // A arvore montada leva o estilo inteiro no objeto, e o `flatten` resolve. function Cartao(props) { return ( <View style={estilos.cartao}> <Text style={estilos.titulo}>{props.titulo}</Text> <View style={estilos.separador} /> <Text>{props.corpo}</Text> </View> ); } const arvore = Cartao({ titulo: 'Pedido 41', corpo: 'dois itens' }); console.log('tipo da raiz:', arvore.type); console.log('estilo da raiz:', StyleSheet.flatten(arvore.props.style)); console.log('separador:', StyleSheet.flatten(arvore.props.children[1].props.style));
Saída real
cartao: {
padding: 12,
marginHorizontal: 16,
marginBottom: 8,
borderWidth: 2,
borderColor: '#d0d7de',
borderRadius: 10
}
atalhos expandidos: {
paddingVertical: 8,
paddingHorizontal: 16,
paddingTop: 8,
paddingBottom: 8,
paddingLeft: 16,
paddingRight: 16
}
width 100 com borderWidth 2: { largura: 104, altura: 4 }
altura com padding 12: 64 | sem padding: 40
so raio: { borderRadius: 12 }
raio com borda: { borderRadius: 12, borderWidth: 2 }
raio pedido 40 numa caixa 60, o raio efetivo: 12
tipo da raiz: View
estilo da raiz: {
padding: 12,
marginHorizontal: 16,
marginBottom: 8,
borderWidth: 2,
borderColor: '#d0d7de',
borderRadius: 10
}
separador: { borderBottomWidth: 1, borderBottomColor: '#e6e8eb' }