A entrada de rich text permite que os usuários formatem o que digitam, incluindo colocar uma palavra em negrito,
adicionar um título, alinhar um parágrafo ou inserir um @mention destacado.
No Compose, você cria isso com base em campos de texto baseados em estado
(BasicTextField e TextField). Os estilos ficam em
TextFieldState junto com o texto, então eles resistem a edições, mudanças
de seleção e recomposições, além de permanecerem sincronizados conforme o usuário digita, exclui e
cola.
Nesta página, mostramos como aplicar, atualizar, consultar, extrair e remover estilos em um
TextFieldState usando TextFieldBuffer e como controlar uma barra de ferramentas de formatação
do TextFieldState.textStyles.
Aplicar estilos inline e de parágrafo
É possível aplicar formatação no nível de caracteres e de parágrafos em qualquer
escopo de receptor TextFieldBuffer, incluindo TextFieldState.edit,
InputTransformation.transformInput e
OutputTransformation.transformOutput.
Controlar a expansão de limites com ExpandPolicy
Ao chamar addStyle, você pode transmitir um ExpandPolicy para definir como o
intervalo estilizado se comporta quando o usuário insere novos caracteres diretamente no
limite start ou end:
- Inserção dentro de
(start, end): sempre expande o intervalo estilizado. - Inserção antes de
start: sempre desloca os deslocamentosstarteendpara a direita. - Inserção direta em
startouend: controlada peloExpandPolicyespecificado.
A tabela a seguir descreve as opções de ExpandPolicy disponíveis e os casos de uso típicos:
| Política | Comportamento de limite | Casos de uso recomendados | Exemplo |
|---|---|---|---|
ExpandPolicy.InsideOnly |
O texto digitado em start ou end fica fora do intervalo estilizado. |
Tokens discretos, como @mentions, #hashtags ou pílulas de plano de fundo, em que a digitação ao lado da tag não deve herdar a cor de fundo da tag. |
![]() |
ExpandPolicy.AtStart |
O texto digitado em start herda o estilo, e o texto digitado em end permanece sem estilo. |
O texto digitado diretamente na frente do texto estilizado também deve adotar a mesma formatação. | ![]() |
ExpandPolicy.AtEnd |
O texto digitado em end herda o estilo, e o texto digitado em start permanece sem estilo. |
Digitação padrão do editor de texto rich text, em que a formatação é mantida ao continuar digitando após uma palavra em negrito ou itálico. | ![]() |
ExpandPolicy.AtBoth |
O texto digitado em start ou end expande o intervalo e herda o estilo. |
Destaques ativos ou blocos formatados contínuos que devem crescer de qualquer borda. | ![]() |
Aplicar formatação de caracteres com SpanStyle
Use addStyle com um SpanStyle para aplicar formatação no nível do caractere,
como cor do texto, destaque do plano de fundo, FontWeight, FontStyle
ou sublinhado a um TextRange específico no buffer.
Principais comportamentos para intervalos de SpanStyle
- Ordem de camadas: os estilos são aplicados na ordem em que são adicionados ao buffer. Se
vários intervalos
SpanStylesobrepostos definirem a mesma propriedade com valores diferentes, o estilo adicionado por último será renderizado por cima. - Requisito de intervalo não vazio: é necessário transmitir um intervalo não recolhido em que
start<end. ChamaraddStyleem uma seleção recolhida de tamanho0não tem efeito, e transmitir índices invertidos ou fora dos limites gera umaIllegalArgumentException.
TextField( state = rememberTextFieldState(), inputTransformation = InputTransformation { for (i in 0 until changes.changeCount) { val range = changes.getRange(i) if (!range.collapsed) { addStyle( spanStyle = SpanStyle( fontWeight = FontWeight.ExtraBold, fontStyle = FontStyle.Italic, color = Color.Red, ), range = range, expandPolicy = ExpandPolicy.AtEnd ) } } } )
Aplicar formatação de bloco com ParagraphStyle
Use addStyle com um ParagraphStyle para aplicar formatação estrutural
no nível do bloco, como textAlign, lineHeight,
textDirection ou textIndent, a um intervalo de parágrafos.
fun setParagraphAlignment(state: TextFieldState, alignment: TextAlign) { val targetRange = if (state.selection.collapsed) { TextRange(0, state.text.length) } else { state.selection } if (targetRange.collapsed) return state.edit { // 1. Remove existing paragraph styles in this range getParagraphStyles(targetRange).forEach { removeStyle(it) } // 2. Apply the new paragraph alignment addStyle( paragraphStyle = ParagraphStyle(textAlign = alignment), range = targetRange, expandPolicy = ExpandPolicy.InsideOnly ) } }
ParagraphStyle.Armadilhas que devem ser evitadas com ParagraphStyle
Como ParagraphStyle se aplica a blocos inteiros de texto, evite usá-lo nos
seguintes cenários:
- Expansão para parágrafos adjacentes: defina
ExpandPolicycom cuidado para que os intervalosParagraphStyleadjacentes não se sobreponham após as edições do usuário. Por exemplo, se[0, 5)usarExpandPolicy.AtEnde[5, 10)usarExpandPolicy.AtStart, a inserção de texto no índice5vai expandir os dois estilos em uma sobreposição e gerar umIllegalArgumentException. - Aninhamento de intervalos
ParagraphStyle: embora intervalos completamente aninhados (como[0, 10)e[2, 8)) sejam válidos emAnnotatedString, evite aninhar intervalosParagraphStyleem um campo de texto. O layout divide os intervalos aninhados em parágrafos separados, e as edições subsequentes podem transformar acidentalmente os intervalos aninhados em uma sobreposição parcial.
Atualizar um estilo no lugar com TrackedRange
Chamar addStyle com um TextRange e ExpandPolicy retorna um identificador TrackedRange, que pode ser TrackedRange<SpanStyle> ou TrackedRange<ParagraphStyle>. Um TrackedRange é uma referência dinâmica a um
intervalo de estilo no TextFieldBuffer que atualiza automaticamente os deslocamentos de caracteres start
e end à medida que o texto é inserido, excluído ou substituído ao redor dele.
Mutabilidade no local
Não é necessário chamar removeStyle e addStyle para atualizar um estilo atual
ou ajustar os limites dele. Em um escopo TextFieldBuffer, TrackedRange
fornece propriedades de extensão que atualizam o intervalo no local, preservando
a ordem de camadas original em relação a outros estilos:
TrackedRange<SpanStyle>.spanStyle: lê ou substitui o payloadSpanStyleem umTrackedRange<SpanStyle>.TrackedRange<ParagraphStyle>.paragraphStyle: lê ou substitui o payloadParagraphStyleem umTrackedRange<ParagraphStyle>.TrackedRange<*>.textRange: lê ou atualiza os limites atuais deTextRangeno local. Definir um intervalo recolhido, invertido ou fora dos limites gera umIllegalArgumentException.TrackedRange<*>.expandPolicy: lê ou atualiza oExpandPolicyno local.TrackedRange<*>.isValid: retornatruese o intervalo de estilo ainda estiver ativo no buffer oufalsese as edições tiverem causado o colapso do comprimento para0ou se ele tiver sido removido.
// Trim an existing bold span in place when the selection overlaps one edge val range = style.textRange if (range.start < selection.min && range.end <= selection.max) { // Selection covers the end of the span - shrink end boundary in place style.textRange = TextRange(range.start, selection.min) } else if (range.start >= selection.min && range.end > selection.max) { // Selection covers the start of the span - shrink start boundary in place style.textRange = TextRange(selection.max, range.end) }
Confinamento de escopo e regras de ciclo de vida
Lembre-se das seguintes regras de escopo e ciclo de vida ao trabalhar com
instâncias de TrackedRange:
- Restritas a
TextFieldBuffer: as propriedades de extensãoTrackedRangesó existem no escopoTextFieldBufferem que o identificador foi criado ou consultado. - Remoção automática ao recolher: se as edições do usuário excluírem todos os caracteres cobertos
por um
TrackedRange, o comprimento dele será reduzido para0,isValidretornaráfalsee o estilo será removido do buffer.
Consultar e remover estilos
O Compose oferece duas maneiras de inspecionar estilos ativos, dependendo se você está modificando o buffer ou lendo o estado da IU:
- Dentro de um bloco
TextFieldBuffer: chamegetSpanStylesegetParagraphStylesdiretamente no receptorTextFieldBufferpara receber identificadoresTrackedRangemutáveis. - Fora de um bloco de edição: leia
TextFieldState.textStyles, que retorna listasAnnotatedString.Rangeimutáveis que refletem o estado confirmado.
Consultar estilos de interseção em TextFieldBuffer
TextFieldBuffer.getSpanStyles e
TextFieldBuffer.getParagraphStyles retornam todas as instâncias de TrackedRange
que se cruzam com o TextRange fornecido, ordenadas por quando foram adicionadas ao
buffer.
Por exemplo, é possível consultar getSpanStyles(selection) dentro de state.edit para
inspecionar ou atualizar estilos que abrangem a seleção atual do usuário:
state.edit { if (selection.collapsed) return@edit // Query all bold spans that overlap with the current selection val boldStyles: List<TrackedRange<SpanStyle>> = getSpanStyles(selection).filter { it.spanStyle.fontWeight == FontWeight.Bold } // ... }
Os intervalos de estilo usam um intervalo matemático semiaberto [start, end). O retorno de um estilo de toque depende se você consulta uma seleção ou um único ponto de cursor:
- Para uma seleção não vazia
[start, end):- Retornado: qualquer estilo que se sobreponha, contenha ou esteja dentro do
intervalo de seleção. Por exemplo, a consulta
[5, 15)corresponde a[0, 20),[8, 12)ou[0, 10). - Não retornados: estilos que tocam apenas as bordas externas sem sobrepor caracteres. Por exemplo, a consulta
[5, 15)exclui[0, 5)e[15, 25).
- Retornado: qualquer estilo que se sobreponha, contenha ou esteja dentro do
intervalo de seleção. Por exemplo, a consulta
- Para um cursor recolhido
[index, index]:- Retornado: qualquer estilo que contenha o cursor ou que comece na posição
dele. Por exemplo, um cursor em
10corresponde a[0, 20)e[10, 20). - Não retornado (regra de fim de toque): qualquer estilo que termine na posição do cursor. Por exemplo, um cursor em
10exclui[0, 10).
- Retornado: qualquer estilo que contenha o cursor ou que comece na posição
dele. Por exemplo, um cursor em
Remover ou alternar estilos com removeStyle
Transmita um identificador TrackedRange para removeStyle para excluir esse intervalo de estilo
específico do TextFieldBuffer.
removeStyle retorna true se o TrackedRange foi encontrado e removido ou
false se ele já foi removido ou pertencia a um buffer diferente. Ele remove
apenas o identificador TrackedRange segmentado, deixando outros estilos sobrepostos
intactos.
fun toggleBold(state: TextFieldState) { state.edit { if (selection.collapsed) return@edit // Query all bold spans that overlap with the current selection val boldStyles: List<TrackedRange<SpanStyle>> = getSpanStyles(selection).filter { it.spanStyle.fontWeight == FontWeight.Bold } if (boldStyles.isNotEmpty()) { // Remove or trim existing bold spans covering the selection for (style in boldStyles) { val range = style.textRange if (range.start >= selection.min && range.end <= selection.max) { removeStyle(style) } else if (range.start < selection.min && range.end > selection.max) { val oldEnd = range.end style.textRange = TextRange(range.start, selection.min) addStyle( spanStyle = SpanStyle(fontWeight = FontWeight.Bold), range = TextRange(selection.max, oldEnd), expandPolicy = ExpandPolicy.AtEnd ) } else if (range.start < selection.min) { style.textRange = TextRange(range.start, selection.min) } else { style.textRange = TextRange(selection.max, range.end) } } } else { // No bold spans intersect the selection - apply bold addStyle( spanStyle = SpanStyle(fontWeight = FontWeight.Bold), range = selection, expandPolicy = ExpandPolicy.AtEnd ) } } }
Extrair ou serializar rich text
Para mostrar a entrada formatada em um elemento de composição Text somente leitura ou serializá-la em
um serviço de back-end, leia TextFieldState.textStyles fora de um bloco de edição.
state.textStyles.getSpanStyles e
state.textStyles.getParagraphStyles retornam
List<AnnotatedString.Range<SpanStyle>> e
List<AnnotatedString.Range<ParagraphStyle>>, que podem ser transmitidos diretamente para
o construtor AnnotatedString:
fun TextFieldState.toAnnotatedString(): AnnotatedString { val fullRange = TextRange(0, text.length) return AnnotatedString( text = text.toString(), spanStyles = textStyles.getSpanStyles(fullRange), paragraphStyles = textStyles.getParagraphStyles(fullRange) ) }
Exemplo completo no Jetchat
Para conferir uma implementação completa que combina estilos de caracteres inline (negrito, itálico, sublinhado, tachado e cor), consulte o exemplo Jetchat no GitHub:
RichTextState.kt: encapsula consultas de estiloTextFieldState, alternância de span, corte ou divisão entre limites de seleção e muito mais.UserInput.kt: conectaRichTextStateà barra de ferramentas de formatação de rich text e ao campo de entrada de mensagens.



