Dia 1 — Layout com Flexbox
flexDirection: linha e coluna
flexDirection: linha e coluna
flexDirection decide o eixo principal do View. É a propriedade que faz os
filhos ficarem lado a lado ou empilhados, e o valor padrão é column — por isso
a primeira coisa que aparece numa tela é uma pilha de Text um embaixo do outro,
sem que ninguém tenha pedido coluna. Trocar o valor para row gira o eixo
principal para o horizontal, e é essa troca que transforma um View qualquer
numa barra: os filhos passam a ocupar a largura da esquerda para a direita.
| Valor | Eixo principal | Ordem dos filhos |
|---|---|---|
column | vertical | de cima para baixo |
row | horizontal | da esquerda para a direita |
column-reverse | vertical | de baixo para cima |
row-reverse | horizontal | da direita para a esquerda |
O exemplo desta página monta os dois casos com os mesmos dois filhos e imprime o
eixo que cada árvore carrega: a que usa o estilo coluna responde column, e a
que usa o estilo linha responde row. A contagem de filhos é 2 nos dois
casos, e os rótulos saem na mesma ordem — primeiro e segundo. É a prova de
que flexDirection não mexe no conteúdo: ele muda o caminho que os filhos
percorrem, não quem eles são.
O que trava não é o nome da propriedade: é achar que alignItems também mexe
no eixo principal. Ele mexe no eixo cruzado, sempre. Se o flexDirection é
column, o eixo cruzado é a horizontal e alignItems decide se o filho gruda
na esquerda, na direita ou no centro. Se o flexDirection é row, o eixo
cruzado é a vertical e o mesmo alignItems decide se o filho sobe, desce ou
centraliza. A propriedade não muda de nome; o que muda é a direção em que ela
age.
const estilos = StyleSheet.create({ linha: { flexDirection: 'row' }, coluna: { flexDirection: 'column' }, }); // ler o eixo de uma arvore ja montada function eixoDe(arvore) { return StyleSheet.flatten(arvore.props.style).flexDirection || 'column'; }
StyleSheet.flatten é o que torna essa leitura possível. O estilo de um
elemento pode ser um objeto, um array de objetos ou nada; flatten resolve
qualquer um dos três em um objeto só, que é a forma de consultar o valor efetivo
de uma propriedade. E o || 'column' da função é o valor padrão escrito como
código: a função devolve column para uma árvore que não declara eixo nenhum, e
o exemplo mostra isso passando uma árvore sem style.
O eixo principal decide o resto
Regra que resolve quase tudo: **justifyContent age no eixo principal e
alignItems no cruzado**. Guardar a frase resolve o caso em que o aluno troca
flexDirection, esquece de mexer no alinhamento e não entende por que o texto
ficou colado na borda. Nada de errado aconteceu: os dois alinhamentos continuam
válidos, estão medindo em eixos diferentes.
O eixo transversal também tem nome quando o eixo principal é row: a altura da
barra. E em column a altura deixa de ser o eixo principal e vira a soma dos
filhos, o que significa que um View de coluna sem altura definida não tem
altura própria — os filhos que a determinam.
Vale medir o eixo pelo valor efetivo e não pelo que está no StyleSheet, porque
os dois divergem com frequência. Uma barra pode declarar flexDirection em um
estilo compartilhado e receber flexDirection de outro lugar por um segundo
argumento do StyleSheet.create. O exemplo imprime a linha inteira como { flexDirection: 'row' }, e é esse objeto — não o nome do estilo — que diz qual eixo está valendo naquela árvore.
Quando o eixo é column-reverse, o cruzão continua sendo a horizontal, e o
começo da linha passa a ser embaixo. É por isso que a inversão do eixo não
inverte também o alinhamento: justifyContent: 'flex-start' num eixo invertido
gruda os filhos na borda que passou a ser a última.
Alinhar dentro de um filho é outra coisa
alignSelf sobrescreve o alignItems do pai em um único filho, e é o recurso
certo quando um View de linha tem três filhos e só um deles deve ir para o
fim. justifyContent: 'flex-end' empurraria os três; alignSelf: 'flex-end'
empurra um. A diferença entre os dois é a mesma diferença entre alinhar o grupo
e alinhar o membro.
// o pai alinha os filhos; o filho se posiciona por conta <View style={{ flexDirection: 'row', alignItems: 'center' }}> <Text>aviso</Text> <Text style={{ alignSelf: 'flex-end' }}>agora</Text> </View>
O mesmo alignSelf funciona nos dois eixos, e ele sempre age no cruzado. Numa
coluna ele empurra o filho para a esquerda ou para a direita; numa linha, para
cima ou para baixo. Escrever alignSelf: 'flex-end' esperando que o texto
desça em uma coluna não funciona, porque o eixo principal da coluna é o
vertical, e no eixo principal quem distribui é o pai, por justifyContent.
alignSelf aceita auto, que é o padrão: o filho herda o alignItems do pai.
Escrever auto explicitamente é a forma de cancelar um alignSelf que veio de
um estilo compartilhado, e é mais claro do que trocar o estilo inteiro.
quando reverto o eixo
row-reverse e column-reverse existem para o padrão de escrita de fora para
dentro: idioma que se lê da direita para a esquerda, e barra de navegação
empurrada para o fim da linha. Também aparecem quando se usa `position:
absolute com right em vez de left`, o que o React Native aceita mas evita
por ser menos previsível.
O exemplo trata o reverso como mais um valor da mesma propriedade, e é o que ele
é: StyleSheet.flatten de { flexDirection: 'row-reverse' } devolve um objeto
com uma chave só, com o mesmo formato do row. Não existe propriedade
diferente para inverter, não existe flag e não existe efeito colateral: o eixo
principal muda de direção e o cruzado continua ortogonal a ele.
Na dúvida, o conserto quase nunca é reverter o eixo: é reordenar a lista que
alimenta o map. Inverter o eixo muda a direção em que os filhos aparecem;
reordenar os dados muda a ordem em que eles aparecem — e só a segunda abordagem
resolve o caso em que a ordem importa para outra coisa, como a ordem de leitura
do conteúdo ou a ordem de envio de um formulário.
Exemplo
// `flexDirection` escolhe o eixo principal do `View`. O exemplo monta os // dois casos, linha e coluna, e le o estilo de cada arvore para mostrar o // que muda de verdade quando o valor muda. const { View, Text, StyleSheet } = require('react-native'); const estilos = StyleSheet.create({ coluna: { flexDirection: 'column' }, linha: { flexDirection: 'row' }, item: { padding: 8 }, }); function Item(props) { return <Text style={estilos.item}>{props.rotulo}</Text>; } function EmColuna() { return ( <View style={estilos.coluna}> <Item rotulo="primeiro" /> <Item rotulo="segundo" /> </View> ); } function EmLinha() { return ( <View style={estilos.linha}> <Item rotulo="primeiro" /> <Item rotulo="segundo" /> </View> ); } // `flatten` resolve o array ou o objeto de estilo em um objeto so, que e a // forma de ler o valor efetivo de uma propriedade. function eixoDe(arvore) { return StyleSheet.flatten(arvore.props.style).flexDirection || 'column'; } console.log('eixo da coluna:', eixoDe(EmColuna())); console.log('eixo da linha:', eixoDe(EmLinha())); // os filhos continuam os mesmos; o que muda e o caminho que eles percorrem const linha = EmLinha(); console.log('filhos na linha:', linha.props.children.length); console.log('rotulos:', linha.props.children.map((f) => f.props.children)); // o eixo transversal: com `row`, o cruzado e a altura const estiloLinha = StyleSheet.flatten(linha.props.style); console.log('linha inteira:', estiloLinha); console.log('padrao quando nada e declarado:', eixoDe({ props: {} })); console.log('o reverso usa o mesmo par de eixos:', StyleSheet.flatten({ flexDirection: 'row-reverse' }));
Saída real
eixo da coluna: column
eixo da linha: row
filhos na linha: 2
rotulos: [ 'primeiro', 'segundo' ]
linha inteira: { flexDirection: 'row' }
padrao quando nada e declarado: column
o reverso usa o mesmo par de eixos: { flexDirection: 'row-reverse' }
justifyContent, alignItems e flexWrap
justifyContent, alignItems e flexWrap
justifyContent distribui os filhos ao longo do eixo principal e alignItems
ao longo do eixo cruzado. Com flexDirection: 'row', o primeiro decide o que
acontece na horizontal e o segundo na vertical; com flexDirection: 'column' os
papéis invertem. É por isso que justifyContent: 'center' centraliza a pilha
inteira de uma tela, mas centraliza lado a lado numa linha — o mesmo valor, dois
resultados, porque o eixo que ele mede mudou.
justifyContent | E os filhos ficam |
|---|---|
flex-start | colados no começo do eixo, que é o padrão |
center | centralizados no eixo, com o mesmo espaço antes e depois |
flex-end | colados no fim do eixo |
space-between | colados nas pontas, com o espaço todo entre eles |
space-around | espaço igual entre eles, metade desse espaço nas pontas |
space-evenly | espaço igual em todas as frestas, inclusive nas pontas |
alignItems aceita os mesmos seis valores mais baseline, que alinha os filhos
pela linha de base do texto — o que importa quando a linha tem um Text
grande ao lado de um pequeno e os dois precisam parecer da mesma altura de linha,
e não estar centralizados.
O exemplo desta página monta um cabeçalho de linha com space-between e
alignItems: 'center', e imprime o estilo efetivo da raiz já achatado em três
chaves: flexDirection: 'row', justifyContent: 'space-between',
alignItems: 'center'. Depois passa pelos seis valores de justifyContent um a
um, e cada um sobrevive no objeto como foi escrito. É o que separa "o valor não
fez nada" de "o valor fez, e o eixo estava errado": no objeto o valor está lá de
qualquer forma, e o que muda o resultado é o eixo em que ele age.
distribuir é o mesmo que centralizar
space-around com dois filhos e space-evenly com três dá a mesma sensação de
ar entre eles; a diferença está nas pontas. space-around põe metade do espaço
na borda, então o grupo fica visualmente deslocado para o meio. space-evenly
divide em partes iguais e o grupo fica opticamente centrado, que é o motivo de
ele ser o valor certo para a linha de um rodapé com três textos. Quando o
espaço é pouco, space-between e space-evenly produzem a mesma tela — a
diferença só aparece quando sobra espaço.
Os três valores com space medem o mesmo intervalo: o que sobra entre o último
filho e a borda. O que muda é quem fica com esse sobra. Em space-between o
sobra vai todo para dentro, e as pontas ficam limpas. Em space-around cada
filho ganha a mesma margem dos dois lados, e a borda ganha metade. Em
space-evenly a borda ganha a mesma margem que o espaço entre os filhos, e o
resultado é o único dos três que fica balanceado sem ajuste.
Como a diferença é de proporção, ela só aparece com espaço sobrando. Três itens
curtos que quase preenchem a largura renderizam igual nos três valores — e o
escolhido errado só importa quando a largura mudar. Como a largura muda quando o
aparelho gira, o valor que "dava para escolher qualquer um" deixa de servir.
O detalhe que costuma passar: center não é o mesmo que space-evenly com um
filho — com um só, os dois põem o espaço igualmente dos dois lados. Com dois
filhos, center os junta no meio e deixa o vazio todo fora; space-evenly os
separa e distribui o vazio entre eles e as bordas. Depende do que a linha faz:
agrupar é center, espalhar é space-*.
flexWrap é o que decide se quebra
flexWrap: 'nowrap' é o padrão: os filhos nunca quebram linha, e se a soma
delas passa da largura do pai, o View transborda ou os filhos encolhem, porque
o padrão do flex é poder encolher. flexWrap: 'wrap' permite a quebra: o filho
que não cabe passa para a linha de baixo inteiro. É a propriedade de Tags,
filtros e atalhos de teclado, onde a quantidade de itens varia e ninguém quer
cortar texto ao meio.
O exemplo testa a conta do wrap sem depender de largura de tela real. Ele soma
as larguras dos itens — 70, 90 e 60, o que dá 220 — compara com a largura
da linha de 150 e imprime que não cabe. Em nowrap e sem flex, sobram 70
pixels para dividir entre os três filhos, e é essa divisão que faz cada um
encolher e o Text quebrar por dentro. Em wrap, o exemplo conta quantos itens
cabem antes de estourar e imprime 1: o primeiro item sozinho já passa dos
150, e os outros dois vão para a linha de baixo.
// a linha de filtro quebra quando os itens somam mais que a largura <View style={{ flexDirection: 'row', flexWrap: 'wrap' }}> {filtros.map((f) => ( <Text key={f.id} style={{ padding: 6 }}>{f.rotulo}</Text> ))} </View>
Um Text não quebra no meio de uma palavra por padrão: ele quebra em espaços.
Com wrap, a quebra acontece entre os itens inteiros; sem wrap, o que quebra é
o texto de cada item. A diferença é visível na hora: uma linha de filtro que
corta "Todos" no meio está com nowrap e itens encolhendo.
Quando a linha quebra, a altura do pai cresce — a altura é a soma das alturas
das linhas. É por isso que uma linha de filtro com wrap e altura fixa corta a
segunda linha em vez de mostrar. Quem decide é a altura, e o conserto é deixar a
altura acompanhar o conteúdo ou colocar a linha dentro de um ScrollView.
O valor que produz o exemplo é o mesmo que o flexWrap decide: o View de
filtro com wrap e altura livre mostra todas as linhas, e o mesmo View com
height fixa mostra a primeira e corta a segunda. A propriedade decide se a
quebra existe; a altura decide se ela aparece inteira.
o sintoma que denuncia alinhamento trocado
Se um View de linha não obedece a justifyContent: 'center', quase sempre a
falta é width. View de linha ocupa a largura disponível por padrão, então
centralizar filhos dentro dele não muda nada: já estão no centro do espaço que ele
ocupa. Para o justifyContent fazer efeito visível, o View precisa medir
menos que a tela — width, maxWidth, ou nenhum filho ocupando a linha inteira
com flex: 1. Alinhar é só operação com o espaço que sobrou.
O contra-sintoma do mesmo problema aparece do outro lado: quando a linha ocupa
toda a largura e ninguém força nada, o primeiro filho gruda na borda. Isso não é
justifyContent falhando — é o padrão flex-start funcionando, e o defeito
está em não ter declarado nada. A confusão entre os dois casos é o que faz o
aluno culpar a propriedade errada.
Há um terceiro sintoma, mais difícil: o alinhamento funciona no eixo errado. A
caixa centraliza na vertical quando o Wanted era a horizontal, e mexer em
justifyContent não muda nada. A verificação é uma frase — *qual é o
flexDirection deste View?* — e a resposta diz qual dos dois alinhamentos age
onde. O exemplo resolve a pergunta antes de qualquer valor: ele imprime o estilo
da raiz com flexDirection: 'row' ao lado dos dois alinhamentos, e a leitura é
que aquele cabeçalho é justificado na horizontal e alinhado na vertical.
alignItems: 'center' num View sem height é o mesmo caso por outro nome.
Sem altura declarada, a altura do pai é a soma dos filhos, o eixo cruzado não
tem sobra e centralizar não tem para onde mover. Centralizar exige sobra em um
dos eixos, e sobra é sempre consequência de alguma medida declarada em algum
lugar.
Exemplo
// `justifyContent` age no eixo principal, `alignItems` no cruzado, e // `flexWrap` decide se os filhos quebram linha. O exemplo monta um // cabecalho em linha e imprime o efeito de cada valor de alinhamento. const { View, Text, StyleSheet } = require('react-native'); const estilos = StyleSheet.create({ cabecalho: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }, titulo: { fontSize: 18 }, contador: { fontSize: 12, paddingHorizontal: 8 }, }); function Cabecalho(props) { return ( <View style={estilos.cabecalho}> <Text style={estilos.titulo}>{props.titulo}</Text> <Text style={estilos.contador}>{props.total + ' itens'}</Text> </View> ); } const arvore = Cabecalho({ titulo: 'Cardapio', total: 12 }); console.log('tipo da raiz:', arvore.type); console.log('estilo da raiz:', StyleSheet.flatten(arvore.props.style)); console.log('titulo:', arvore.props.children[0].props.children); console.log('contador:', arvore.props.children[1].props.children); // cada valor de `justifyContent` produz uma distribuicao diferente const alinhamentos = ['flex-start', 'center', 'flex-end', 'space-between', 'space-around', 'space-evenly']; for (const valor of alinhamentos) { const estilo = StyleSheet.flatten({ flexDirection: 'row', justifyContent: valor }); console.log(valor + ' ->', estilo.justifyContent, '| eixo principal: horizontal'); } // `alignItems` no eixo cruzado, que aqui e a altura console.log('no cruzado:', StyleSheet.flatten({ alignItems: 'baseline' }).alignItems); // `flexWrap`: os itens quebram linha, ou encolhem function somaLarguras(itens) { return itens.reduce((total, item) => total + item.largura, 0); } const itens = [{ largura: 70 }, { largura: 90 }, { largura: 60 }]; const larguraDaLinha = 150; console.log('soma das larguras:', somaLarguras(itens)); console.log('cabe em ' + larguraDaLinha + '?', somaLarguras(itens) <= larguraDaLinha); console.log('com nowrap e sem flex, sobram', somaLarguras(itens) - larguraDaLinha, 'px para dividir entre 3 filhos'); console.log('com wrap, cada linha tem no maximo', itens.filter((i) => somaLarguras(itens.slice(0, itens.indexOf(i) + 1)) <= larguraDaLinha).length, 'item(ns)');
Saída real
tipo da raiz: View
estilo da raiz: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center'
}
titulo: Cardapio
contador: 12 itens
flex-start -> flex-start | eixo principal: horizontal
center -> center | eixo principal: horizontal
flex-end -> flex-end | eixo principal: horizontal
space-between -> space-between | eixo principal: horizontal
space-around -> space-around | eixo principal: horizontal
space-evenly -> space-evenly | eixo principal: horizontal
no cruzado: baseline
soma das larguras: 220
cabe em 150? false
com nowrap e sem flex, sobram 70 px para dividir entre 3 filhos
com wrap, cada linha tem no maximo 1 item(ns)