Dia 3 — Texto e tipografia
fontSize, fontWeight e cor
fontSize, fontWeight e cor
fontSize é o tamanho em pixels e é o número que mais aparece em style do
Text. A escala da interface do sistema não usa 12 e 18: usa um degrau, e o
primeiro sintoma de uma tela amadora é o Text com três tamanhos que não
pertencem a nenhum degrau. fontWeight aceita os nomes 'normal', 'bold',
'100' a '900', e '500' a '700' é a faixa que o Android e o iOS respeitam
igual — acima de '700' o iOS costuma engrossar tudo, o que produz um título
mais pesado no celular da Apple do que no Android, sem que nada tenha mudado no
código.
color no Text é cor de texto; a cor de fundo é backgroundColor e pertence
ao View. Confundir os dois é o erro mais comum da aula: backgroundColor
num Text pinta um retângulo em volta das palavras, e color num View não
faz nada porque o View não desenha texto. Quando o Text é filho do View, a
cor do texto não é herdada — cada Text declara a sua.
O exemplo desta página monta um cartão com quatro níveis de texto e imprime, para
cada um, o fontSize e o fontWeight alinhados em coluna: 22 px com 700
para "Pedido 41 confirmado", 15 px com 400, 12 px com 500 e 11 px com
600 para a etiqueta "EM ROTA". São quatro degraus, não três, e cada um com um
papel: título, corpo, nota e etiqueta. O color de cada um está no mesmo objeto
do tamanho, e é o que faz o View do cartão precisar de padding e não de cor.
O quarto nível é o que prova a separação entre texto e fundo: a etiqueta usa
color: '#ffffff' e backgroundColor: '#1f6feb', e as duas propriedades
moram no mesmo Text. Uma tela inteira com a cor só no Text tem o
backgroundColor no View que o contém — e aí o retângulo azul pinta o cartão
inteiro, não a palavra.
const estilos = StyleSheet.create({ titulo: { fontSize: 22, fontWeight: '700', color: '#101418' }, corpo: { fontSize: 15, fontWeight: '400', color: '#57606a' }, nota: { fontSize: 12, fontWeight: '500', color: '#8b949e' }, });
Os quatro níveis do exemplo são o argumento contra a escala ad hoc: o
fontSize de cada um se explica pelo papel que a peça cumpre na tela, e não
por gosto. 22 é o título porque é o texto que o olho pega primeiro; 15 é o
corpo porque é o texto que se lê em bloco; 12 é a nota porque é informação
secundária; 11 é a etiqueta porque é texto dentro de uma forma.
fontFamily e o nome que o aparelho não tem
fontFamily recebe o nome de uma fonte instalada no aparelho, e não um
caminho de arquivo. No Android, uma fonte que não existe no sistema é
silenciosamente ignorada: o Text sai na fonte padrão e ninguém recebe erro. No
iOS o nome da fonte é o nome interno do arquivo, que quase nunca é o nome que
está escrito na pasta. A fonte que acompanha o app entra como arquivo no
projeto, é registrada no arquivo de configuração de cada plataforma e só depois
passa a valer pelo fontFamily.
A consequência prática é que fontFamily é a única destas propriedades que
pode não ter efeito nenhum sem erro nenhum. fontSize errado produz texto
maior; fontWeight desconhecido produz o peso mais próximo; fontFamily
errado produz exatamente a fonte padrão, indistinguível de não ter escrito
nada. Por isso o teste que economiza tempo é o mesmo: comentar a propriedade,
comparar a tela, e só depois investigar o nome.
O detalhe que economiza tempo: antes de depurar fontFamily, testar sem a
propriedade. Se o resultado muda, o nome estava errado; se não muda, o problema
está em outro lugar.
O mesmo vale para letterSpacing, que o exemplo já aplica na nota: 0.4 é um
número pequeno, e o efeito é abrir as letras o suficiente para a etiqueta estreita
caber sem parecer outro texto. Ele não muda o tamanho nem o peso — só o espaço
entre os glifos.
lineHeight é o espaço entre as linhas
lineHeight não é o tamanho da fonte: é a altura de cada linha. Aumentar
lineHeight deixa o parágrafo arejado sem aumentar a letra, e é a diferença
entre um texto longo que dá vontade de ler e um bloco compacto que cansa. Em
português, com cedilha e acento, a linha precisa de ar para a haste da letra
não encostar na seguinte: fontSize: 15 com lineHeight: 22 é o degrau
confortável, e lineHeight menor que fontSize corta os acentos de cima.
O exemplo fecha a conta com a função linhasOcupadas, que multiplica linhas por
lineHeight e devolve 66 para três linhas de 22. É o mesmo cálculo que o
Text faz de verdade quando o conteúdo quebra, e é por isso que lineHeight
determina a altura do parágrafo de forma previsível: o número de linhas depende
da largura, a altura de cada linha não.
E o caso do erro está no último console: fontSize: 15 com lineHeight: 13
imprime os dois números juntos, e a leitura é que 13 é menor que 15. Cada linha
tem 13 pixels para uma letra que precisa de 15, e o desenho corta o topo — o
acento, o til, a barra do ç. Não é defeito do aparelho: é a conta fechando
errada por dois pixels.
| Propriedade | Controla |
|---|---|
fontSize | o tamanho da letra |
fontWeight | a espessura do traço |
lineHeight | a distância entre uma linha e a seguinte |
letterSpacing | o espaço entre as letras, útil em rótulo de botão |
color | a cor do texto |
Exemplo
// O trio que define o texto: `fontSize`, `fontWeight` e `color`. O // exemplo monta uma hierarquia de tres niveis e le o estilo que cada // `Text` levou ate a arvore. const { View, Text, StyleSheet } = require('react-native'); const estilos = StyleSheet.create({ cartao: { padding: 16 }, titulo: { fontSize: 22, fontWeight: '700', color: '#101418', lineHeight: 28 }, corpo: { fontSize: 15, fontWeight: '400', color: '#57606a', lineHeight: 22 }, nota: { fontSize: 12, fontWeight: '500', color: '#8b949e', letterSpacing: 0.4 }, cota: { fontSize: 11, fontWeight: '600', color: '#ffffff', backgroundColor: '#1f6feb' }, }); function Cartao() { return ( <View style={estilos.cartao}> <Text style={estilos.titulo}>Pedido 41 confirmado</Text> <Text style={estilos.corpo}>O desenho saiu para producao.</Text> <Text style={estilos.nota}>atualizado agora</Text> <Text style={estilos.cota}>EM ROTA</Text> </View> ); } const arvore = Cartao(); console.log('tipo da raiz:', arvore.type); console.log('quantos filhos de texto:', arvore.props.children.length); const titulo = arvore.props.children[0]; console.log('titulo:', titulo.type, StyleSheet.flatten(titulo.props.style)); // O texto de cada nivel e o que o aluno le no `props.children`. for (const filho of arvore.props.children) { const estilo = StyleSheet.flatten(filho.props.style); console.log( String(estilo.fontSize).padStart(4), 'px', String(estilo.fontWeight).padStart(4), '|', filho.props.children ); } // `lineHeight` e o espaco entre linhas: menor que `fontSize` corta acento. function linhasOcupadas(linhas, estilo) { return linhas * estilo.lineHeight; } console.log('tres linhas com lineHeight 22:', linhasOcupadas(3, { lineHeight: 22 })); console.log('lineHeight 13 com fontSize 15 corta o acento:', StyleSheet.flatten({ fontSize: 15, lineHeight: 13 }));
Saída real
tipo da raiz: View
quantos filhos de texto: 4
titulo: Text { fontSize: 22, fontWeight: '700', color: '#101418', lineHeight: 28 }
22 px 700 | Pedido 41 confirmado
15 px 400 | O desenho saiu para producao.
12 px 500 | atualizado agora
11 px 600 | EM ROTA
tres linhas com lineHeight 22: 66
lineHeight 13 com fontSize 15 corta o acento: { fontSize: 15, lineHeight: 13 }
textAlign, numberOfLines e o texto longo
textAlign, numberOfLines e o texto longo
textAlign alinha o texto dentro do Text, e vale 'left', 'right',
'center' e 'justify' — este último só tem efeito quando o texto quebra em
várias linhas. Ele não move o Text na tela: para isso é alignSelf no pai, ou
o Text dentro de um View com alignItems: 'center'. Confundir os dois é o
erro mais comum da aula, e a diferença aparece na hora de centralizar um
parágrafo inteiro, que precisa dos dois: o View centraliza a caixa do texto e
o textAlign centraliza as linhas dentro da caixa.
numberOfLines limita o número de linhas e corta o resto com reticências. É a
propriedade que mantém uma lista com altura previsível, e o valor exato depende
do desenho: numberOfLines={2} para o título de um item e numberOfLines={4}
para o texto corrido. ellipsizeMode diz onde cortar: 'tail' no fim, que
é o padrão, 'head' no começo, útil quando o fim do texto é o que importa, e
'middle' no meio.
O exemplo desta página monta um item de lista com foto, título e resumo, e as
propriedades de corte chegam intactas na árvore: o título declara
numberOfLines igual a 1 e o resumo igual a 2, e os dois com
ellipsizeMode 'tail'. As duas propriedades são props do Text, não chaves
do StyleSheet — e é por isso que elas não aparecem no objeto de estilo: o que
está no estilo é o textAlign, que o exemplo imprime como left para o título.
O contraste do mesmo item é o que dá sentido aos números: o título de 44
caracteres corta em uma linha e o resumo de 90 corta em duas. Os dois textos são
do mesmo tamanho na tela, e mesmo assim o número de linhas vale um para um e dois
para o outro. O que decide é o papel da peça na tela, não o comprimento do dado.
<Text numberOfLines={2} ellipsizeMode="tail"> {descricao} </Text>
A propriedade ellipsizeMode só aparece quando o corte acontece de fato. Com
numberOfLines={2} e um texto de uma linha só, sobra linha e o tail não é
desenhado — o valor está declarado e não produz nada. É a mesma relação que
existe entre borderColor e borderWidth: um declara a intenção, o outro
declara que há pixel para ela.
a altura que o texto ocupa de verdade
O Text sem numberOfLines cresce conforme o conteúdo, e o View que o
contém herda esse crescimento. Do mesmo texto em três larguras diferentes saem
três alturas, e é por isso que um card que "pula" ao carregar mais item quase
sempre tem Text sem limite de linhas. Com numberOfLines fixo, a altura fica
determinada por fontSize vezes lineHeight vezes o número de linhas, e o
card não se move.
A função altura do exemplo faz essa multiplicação em três casos, e os números
mostram por que o limite importa: com lineHeight: 20 e uma linha o título tem
20 de altura, e com três linhas tem 60. O resumo, com lineHeight: 18 e duas
linhas, dá 36. Nenhum desses números depende do conteúdo — dependem de
lineHeight e do limite declarado, que é o que torna a altura previsível.
A demonstração do outro lado usa a função linhasAte, que quebra um texto de 51
caracteres em pedaços de tamanho fixo e conta quantos pedaços saem: 3 linhas
com 20 caracteres por linha, 5 linhas com 12. O mesmo texto produz alturas
diferentes conforme a largura, e é exatamente isso que o numberOfLines trava:
com o limite em 2, o título nunca passa de 40 de altura, aconteça o que
acontecer com a largura da tela.
O detalhe do corte: numberOfLines conta linhas renderizadas, e o React
Native mede a largura disponível antes de quebrar. Se o Text estiver dentro de
um View sem largura definida, o cálculo parte da largura da tela inteira e o
resultado muda conforme o conteúdo dos vizinhos.
O exemplo trava a altura pelo lado do pai, e é o que o conserto exige: a coluna
do texto declara flex: 1, que é o que garante que ela meça a largura que sobra
depois da foto de 80. Sem essa declaração, a coluna mediria a largura do
conteúdo e o numberOfLines contaria linhas de um tamanho que não existe na
tela.
quando o texto precisa ser todo lido
Truncar é a decisão certa para item de lista e a decisão errada para o corpo de
uma mensagem. Três saídas para o texto que não pode ser cortado: remover o
numberOfLines e deixar o View crescer; pôr o texto dentro de um ScrollView
para a rolagem assumir; ou declarar flexShrink: 1 no Text dentro de uma
linha, que é o que impede o texto de passar da borda quando ele divide espaço
com uma imagem.
flexShrink merece atenção porque resolve um defeito que parece de estilo e é
de tamanho: um Text ao lado de um Image com width: 80 e flex: 1 divide
o espaço com a imagem, mas um Text com conteúdo longo não encolhe por padrão
e transborda. flexShrink: 1 devolve a ele a capacidade de encolher e quebrar
linha dentro da fatia que sobrou.
A ordem das duas propriedades é o detalhe que separa o item de lista do corpo de
mensagem. O flexShrink: 1 não impede o corte — ele impede o transbordo. Com os
dois, o texto encolhe até a fatia que sobrou da foto e aí quebra, e o
numberOfLines decide até onde vai. Sem o numberOfLines, o mesmo flexShrink
deixa o texto ocupar todas as linhas que precisar dentro daquela fatia, que é o
comportamento certo para uma descrição longa.
Exemplo
// `numberOfLines` corta o texto depois de N linhas e `textAlign` alinha // as linhas DENTRO do `Text`. O exemplo calcula a altura que o texto // ocupa e mostra o corte da lista de tamanho fixo. const { View, Text, StyleSheet } = require('react-native'); const estilos = StyleSheet.create({ linha: { flexDirection: 'row', alignItems: 'center', padding: 12 }, foto: { width: 80, height: 80, borderRadius: 8 }, coluna: { flex: 1, marginLeft: 12 }, titulo: { fontSize: 16, fontWeight: '600', textAlign: 'left', lineHeight: 20 }, resumo: { fontSize: 13, color: '#57606a', lineHeight: 18 }, }); function Item(props) { return ( <View style={estilos.linha}> <View style={estilos.foto} /> <View style={estilos.coluna}> <Text style={estilos.titulo} numberOfLines={1} ellipsizeMode="tail"> {props.titulo} </Text> <Text style={estilos.resumo} numberOfLines={2} ellipsizeMode="tail"> {props.resumo} </Text> </View> </View> ); } const arvore = Item({ titulo: 'Bolo de cenoura com cobertura de chocolate', resumo: 'Massa fofinha de cenoura com cenoura ralada na massa e cobertura de chocolate meio amarga servida gelada.', }); console.log('tipo da raiz:', arvore.type); // as props de corte vao no `Text` e chegam intactas na arvore const coluna = arvore.props.children[1]; const titulo = coluna.props.children[0]; const resumo = coluna.props.children[1]; console.log('titulo corta em', titulo.props.numberOfLines, 'linha(s), no', titulo.props.ellipsizeMode); console.log('resumo corta em', resumo.props.numberOfLines, 'linha(s), no', resumo.props.ellipsizeMode); console.log('align do titulo:', StyleSheet.flatten(titulo.props.style).textAlign); // A altura com `numberOfLines` e previsivel: linhas vezes `lineHeight`. function altura(estilo, numeroDeLinhas) { return numeroDeLinhas * estilo.lineHeight; } console.log('titulo com lineHeight 20 e 1 linha:', altura({ lineHeight: 20 }, 1)); console.log('titulo com lineHeight 20 e 3 linhas:', altura({ lineHeight: 20 }, 3)); console.log('resumo com lineHeight 18 e 2 linhas:', altura({ lineHeight: 18 }, 2)); // Sem `numberOfLines`, a altura depende do conteudo: e o card que "pula". function linhasAte(texto, caracteresPorLinha) { const linhas = []; for (let i = 0; i < texto.length; i += caracteresPorLinha) { linhas.push(texto.slice(i, i + caracteresPorLinha)); } return linhas.length; } const longo = 'Massa fofinha de cenoura com cobertura de chocolate.'; console.log('linhas com 20 caracteres por linha:', linhasAte(longo, 20)); console.log('linhas com 12 caracteres por linha:', linhasAte(longo, 12)); console.log('a largura muda o numero de linhas, e o `numberOfLines` trava isso');
Saída real
tipo da raiz: View titulo corta em 1 linha(s), no tail resumo corta em 2 linha(s), no tail align do titulo: left titulo com lineHeight 20 e 1 linha: 20 titulo com lineHeight 20 e 3 linhas: 60 resumo com lineHeight 18 e 2 linhas: 36 linhas com 20 caracteres por linha: 3 linhas com 12 caracteres por linha: 5 a largura muda o numero de linhas, e o `numberOfLines` trava isso