La entrada de texto enriquecido permite que los usuarios den formato a lo que escriben, como poner una palabra en negrita,
agregar un encabezado, alinear un párrafo o insertar un @mention destacado.
En Compose, compilas esto sobre los campos de texto basados en el estado (BasicTextField y TextField). Los estilos se encuentran en TextFieldState junto con el texto, por lo que sobreviven a las ediciones, los cambios de selección y las recomposiciones, y se mantienen sincronizados a medida que el usuario escribe, borra y pega.
En esta página, se muestra cómo aplicar, actualizar, consultar, extraer y quitar estilos en un
TextFieldState con TextFieldBuffer, y cómo controlar una barra de herramientas de formato
desde TextFieldState.textStyles.
Cómo aplicar estilos intercalados y de párrafo
Puedes aplicar formato a nivel de carácter y de párrafo dentro de cualquier
alcance del receptor de TextFieldBuffer, incluidos TextFieldState.edit,
InputTransformation.transformInput y
OutputTransformation.transformOutput.
Controla la expansión de límites con ExpandPolicy
Cuando llamas a addStyle, puedes pasar un ExpandPolicy para definir cómo se comporta el rango con diseño cuando el usuario inserta caracteres nuevos directamente en su límite start o end:
- Inserción dentro de
(start, end): Siempre expande el rango con diseño. - Inserción antes de
start: Siempre desplaza los desplazamientosstartyendhacia la derecha. - Inserción directamente en
startoend: Se controla con elExpandPolicyque especifiques.
En la siguiente tabla, se describen las opciones de ExpandPolicy disponibles y sus
casos de uso típicos:
| Política | Comportamiento de límite | Casos de uso recomendados | Ejemplo |
|---|---|---|---|
ExpandPolicy.InsideOnly |
El texto que se escribe en start o end permanece fuera del rango con diseño. |
Tokens discretos, como @mentions, #hashtags o pastillas de fondo, en los que escribir junto a la etiqueta no debería heredar el color de fondo de la etiqueta. |
![]() |
ExpandPolicy.AtStart |
El texto que se escribe en start hereda el estilo, y el texto que se escribe en end permanece sin estilo. |
El texto que se escriba directamente delante del texto con estilo también debe adoptar el mismo formato. | ![]() |
ExpandPolicy.AtEnd |
El texto que se escribe en end hereda el estilo, y el texto que se escribe en start permanece sin estilo. |
Escritura estándar en el editor de texto enriquecido, en la que, si se sigue escribiendo después de una palabra en negrita o cursiva, se mantiene ese formato. | ![]() |
ExpandPolicy.AtBoth |
El texto que se escribe en start o end expande el rango y hereda el estilo. |
Son bloques con formato continuo o destacados activos que deben crecer desde cualquiera de los bordes. | ![]() |
Aplica formato de caracteres con SpanStyle
Usa addStyle con un SpanStyle para aplicar formato a nivel de carácter, como el color del texto, el resaltado del fondo, FontWeight, FontStyle o el subrayado a un TextRange específico en el búfer.
Comportamientos clave para los rangos de SpanStyle
- Orden de las capas: Los estilos se aplican en el orden en que los agregas al búfer. Si
varios rangos de
SpanStylesuperpuestos establecen la misma propiedad con diferentes valores, el estilo que se agrega más tarde se renderiza en la parte superior. - Requisito de rango no vacío: Debes pasar un rango no contraído en el que
start<end. Llamar aaddStyleen una selección contraída de longitud0no tiene efecto, y pasar índices invertidos o fuera de los límites arroja unaIllegalArgumentException.
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 ) } } } )
Aplica el formato de bloque con ParagraphStyle
Usa addStyle con un ParagraphStyle para aplicar un formato estructural a nivel de bloque, como textAlign, lineHeight, textDirection o textIndent, a un rango de párrafos.
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 ) } }
ParagraphStyleErrores que se deben evitar con ParagraphStyle
Debido a que ParagraphStyle se aplica a bloques de texto completos, evita usarlo en las siguientes situaciones:
- Expansión a párrafos adyacentes: Configura
ExpandPolicycon cuidado para que los rangos deParagraphStyleadyacentes no se superpongan después de las ediciones del usuario. Por ejemplo, si[0, 5)usaExpandPolicy.AtEndy[5, 10)usaExpandPolicy.AtStart, insertar texto en el índice5expande ambos estilos en una superposición y arroja unaIllegalArgumentException. - Anidamiento de rangos
ParagraphStyle: Aunque los rangos completamente anidados (como[0, 10)y[2, 8)) son válidos enAnnotatedString, evita anidar rangosParagraphStyleen un campo de texto. El diseño divide los rangos anidados en párrafos separados, y las ediciones posteriores pueden convertir, de forma involuntaria, los rangos anidados en una superposición parcial.
Actualiza un diseño in situ con TrackedRange
Si llamas a addStyle con un TextRange y un ExpandPolicy, se muestra un controlador TrackedRange, que puede ser TrackedRange<SpanStyle> o TrackedRange<ParagraphStyle>. Un TrackedRange es una referencia dinámica a un intervalo de estilo en el TextFieldBuffer que actualiza automáticamente sus desplazamientos de caracteres start y end a medida que se inserta, borra o reemplaza texto a su alrededor.
Mutabilidad local
No es necesario que llames a removeStyle ni a addStyle para actualizar un estilo existente o ajustar sus límites. Dentro de un permiso TextFieldBuffer, TrackedRange proporciona propiedades de extensión que actualizan el intervalo en su lugar y, al mismo tiempo, conservan su orden de capas original en relación con otros estilos:
TrackedRange<SpanStyle>.spanStyle: Lee o reemplaza la carga útil deSpanStyleen unTrackedRange<SpanStyle>.TrackedRange<ParagraphStyle>.paragraphStyle: Lee o reemplaza la carga útil deParagraphStyleen unTrackedRange<ParagraphStyle>.TrackedRange<*>.textRange: Lee o actualiza los límites actuales deTextRangeen su lugar. Si se establece un rango contraído, invertido o fuera de los límites, se arroja unaIllegalArgumentException.TrackedRange<*>.expandPolicy: Lee o actualiza elExpandPolicyin situ.TrackedRange<*>.isValid: Devuelvetruesi el intervalo de estilo aún está activo en el búfer ofalsesi las ediciones hicieron que su longitud se contrajera a0o si se quitó.
// 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) }
Confinamiento del alcance y reglas del ciclo de vida
Ten en cuenta las siguientes reglas de alcance y ciclo de vida cuando trabajes con instancias de TrackedRange:
- Limitadas a
TextFieldBuffer: Las propiedades de extensión deTrackedRangesolo existen dentro del alcance deTextFieldBufferen el que se creó o consultó el controlador. - Reducción automática en el colapso: Si las ediciones del usuario borran todos los caracteres cubiertos por un
TrackedRange, su longitud se reduce a0,isValiddevuelvefalsey el estilo se quita del búfer.
Cómo consultar y quitar estilos
Compose proporciona dos formas de inspeccionar los estilos activos, según si estás mutando el búfer o leyendo el estado de tu IU:
- Dentro de un bloque
TextFieldBuffer: Llama agetSpanStylesygetParagraphStylesdirectamente en el receptorTextFieldBufferpara obtener controladoresTrackedRangemutables. - Fuera de un bloque de edición: Lee
TextFieldState.textStyles, que devuelve listasAnnotatedString.Rangeinmutables que reflejan el estado confirmado.
Consulta estilos que se intersecan en TextFieldBuffer
TextFieldBuffer.getSpanStyles y TextFieldBuffer.getParagraphStyles devuelven todas las instancias de TrackedRange que se cruzan con el TextRange determinado, ordenadas según el momento en que se agregaron al búfer.
Por ejemplo, puedes consultar getSpanStyles(selection) dentro de state.edit para inspeccionar o actualizar los estilos que abarcan la selección actual del usuario:
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 } // ... }
Los intervalos de estilo usan un rango matemático semiabierto [start, end). La devolución de un estilo de contacto depende de si consultas una selección o un solo punto del cursor:
- Para una selección no vacía
[start, end):- Se muestra: Cualquier estilo que se superponga, contenga o se encuentre dentro del rango de selección. Por ejemplo, la consulta
[5, 15)coincide con[0, 20),[8, 12)o[0, 10). - No se devuelven: Estilos que solo tocan los bordes exteriores sin caracteres superpuestos. Por ejemplo, la consulta
[5, 15)excluye[0, 5)y[15, 25).
- Se muestra: Cualquier estilo que se superponga, contenga o se encuentre dentro del rango de selección. Por ejemplo, la consulta
- Para un cursor contraído
[index, index]:- Devuelto: Cualquier estilo que contenga el cursor o que comience en la posición del cursor. Por ejemplo, un cursor en
10coincide con[0, 20)y[10, 20). - No se devuelve (regla de finalización de contacto): Cualquier estilo que termine en la posición del cursor. Por ejemplo, un cursor en
10excluye[0, 10).
- Devuelto: Cualquier estilo que contenga el cursor o que comience en la posición del cursor. Por ejemplo, un cursor en
Cómo quitar o activar o desactivar estilos con removeStyle
Pasa un controlador TrackedRange a removeStyle para borrar ese intervalo de estilo específico de TextFieldBuffer.
removeStyle devuelve true si se encontró y quitó el TrackedRange, o
false si ya se quitó o pertenecía a un búfer diferente. Solo quita el controlador TrackedRange de destino y deja intactos otros estilos superpuestos.
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 ) } } }
Extrae o serializa texto enriquecido
Para mostrar la entrada con formato en un elemento Text de solo lectura que admite composición o serializarla en un servicio de backend, lee TextFieldState.textStyles fuera de un bloque de edición.
state.textStyles.getSpanStyles y state.textStyles.getParagraphStyles devuelven List<AnnotatedString.Range<SpanStyle>> y List<AnnotatedString.Range<ParagraphStyle>>, que puedes pasar directamente al constructor AnnotatedString:
fun TextFieldState.toAnnotatedString(): AnnotatedString { val fullRange = TextRange(0, text.length) return AnnotatedString( text = text.toString(), spanStyles = textStyles.getSpanStyles(fullRange), paragraphStyles = textStyles.getParagraphStyles(fullRange) ) }
Ejemplo completo en Jetchat
Para ver una implementación completa que combina el diseño de caracteres intercalados (negrita, cursiva, subrayado, tachado y color), consulta la muestra de Jetchat en GitHub:
RichTextState.kt: Encapsula las consultas de estiloTextFieldState, el cambio de intervalo, el recorte o la división entre los límites de selección, y mucho más.UserInput.kt: ConectaRichTextStatea la barra de herramientas de formato de texto enriquecido y al campo de entrada de mensajes.



