TrackedRange
-
Cmn
class TrackedRange<T : Any?>
A style applied on the text that is tracked by TextFieldBuffer, returned by TextFieldBuffer.addStyle.
TrackedRange acts as a unique handle to a specific style range. Its properties (such as the style object, its range, and expand policy) can be queried and updated using extension properties on TextFieldBuffer:
-
TrackedRange<*>.textRange -
TrackedRange<*>.expandPolicy -
TrackedRange<*>.isValid -
TrackedRange<SpanStyle>.spanStyle -
TrackedRange<ParagraphStyle>.paragraphStyle
All the extension properties reflect the up-to-date state of the style range. e.g. The textRange of this TrackedRange will automatically update when the text is edited. If the style's range collapses to zero length due to text edits, the style will be removed and valid will return false.
This object's lifecycle is bound to the TextFieldBuffer which is returned by TextFieldState.edit, InputTransformation.transformInput and OutputTransformation.transformOutput. Do not keep a reference of it.
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.input.ExpandPolicy import androidx.compose.foundation.text.input.InputTransformation import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.delete import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.Text import androidx.compose.runtime.remember import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight // This sample demonstrates how to use the `TrackedRange` API to track and modify text ranges // dynamically. It implements a basic Markdown-like behavior where text typed inside double // asterisks (e.g., **bold**) is automatically bolded, and the asterisks are removed. val state = rememberTextFieldState("") fun IntRange.toTextRange(): TextRange { // Unlike IntRange, TextRange is exclusive at the end. return TextRange(first, last + 1) } val inputTransformation = remember { InputTransformation { val text = asCharSequence().toString() val matches = "\\*\\*([^*]+)\\*\\*".toRegex().findAll(text).toList() matches .map { match -> val contentRange = match.groups[0]!!.range // Apply bold style to the text inside asterisks (including the // asterisks for now). addStyle( SpanStyle(fontWeight = FontWeight.Bold), contentRange.toTextRange(), ExpandPolicy.InsideOnly, ) } .forEach { trackedRange -> // Remove the asterisks here. // `trackedRange` simplifies this logic: normally, deleting characters at // the start would shift the end index. However, because `trackedRange` // automatically tracks text updates and adjusts its offsets // dynamically, we can safely delete the target range without having to // calculate the offset manually. delete(trackedRange.textRange.start, trackedRange.textRange.start + 2) delete(trackedRange.textRange.end - 2, trackedRange.textRange.end) } } } Column { Text("Type **text** below to automatically bold it.") BasicTextField( state = state, textStyle = LocalTextStyle.current, inputTransformation = inputTransformation, ) }
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.input.ExpandPolicy import androidx.compose.foundation.text.input.TextFieldBuffer import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material3.Button import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.Text import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.remember import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp // This sample demonstrates a realistic rich-text editor scenario using the `TrackedRange` and // `TextFieldTextStyles` APIs. It implements a "Toggle Bold" formatting function on the current // selection. // For simplicity, this sample keeps bold styles non-overlapping and contiguous, assuming they // are // applied exclusively through this method. val state = rememberTextFieldState("Hello World") // This derived state calculates whether the current selection is completely covered by // bold text styles. This ensures the "Bold" toggle button accurately reflects the // state of the selected text. val isSelection100PercentBold by remember { derivedStateOf { val selection = state.selection if (selection.collapsed) { false } else { val spanStyles = state.textStyles.getSpanStyles(selection) var boldCoverage = 0 for (style in spanStyles) { if (style.item.fontWeight == FontWeight.Bold) { val overlapStart = maxOf(style.start, selection.min) val overlapEnd = minOf(style.end, selection.max) if (overlapEnd > overlapStart) { boldCoverage += (overlapEnd - overlapStart) } } } boldCoverage == selection.length } } } fun TextFieldBuffer.unBoldSelection() { // Query existing bold styles in the selection val intersectingStyles = getSpanStyles(selection).filter { it.spanStyle.fontWeight == FontWeight.Bold } // We modify or remove existing styles to exclude the selected range for (style in intersectingStyles) { val range = style.textRange if (range.start >= selection.min && range.end <= selection.max) { // The style is fully inside the selection. Remove it. removeStyle(style) } else if (range.start < selection.min && range.end > selection.max) { // The style completely covers the selection. We need to split it. val oldEnd = range.end // Truncate the start part style.textRange = TextRange(range.start, selection.min) // Add a new style for the end part addStyle( SpanStyle(fontWeight = FontWeight.Bold), TextRange(selection.max, oldEnd), ExpandPolicy.AtEnd, ) } else if (range.start < selection.min) { // The style overlaps with the start of the selection. Truncate it. style.textRange = TextRange(range.start, selection.min) } else { // The style overlaps with the end of the selection. Truncate it. style.textRange = TextRange(selection.max, range.end) } } } fun TextFieldBuffer.boldSelection() { // Query existing bold styles in the selection val intersectingStyles = getSpanStyles(selection).filter { it.spanStyle.fontWeight == FontWeight.Bold } // To keep bold styles non-overlapping, we merge any intersecting bold // styles with the new selection range into a single contiguous bold style. var mergedStart = selection.min var mergedEnd = selection.max for (style in intersectingStyles) { mergedStart = minOf(mergedStart, style.textRange.start) mergedEnd = maxOf(mergedEnd, style.textRange.end) // Remove the fragmented style removeStyle(style) } addStyle( SpanStyle(fontWeight = FontWeight.Bold), TextRange(mergedStart, mergedEnd), ExpandPolicy.AtEnd, ) } Column { Button( onClick = { state.edit { val selection = this.selection if (selection.collapsed) return@edit if (isSelection100PercentBold) { unBoldSelection() } else { boldSelection() } } } ) { Text( "B", fontWeight = if (isSelection100PercentBold) FontWeight.Bold else FontWeight.Normal, ) } BasicTextField(state = state, textStyle = LocalTextStyle.current) }
import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.material3.Text import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp // Wipe the bold style on a given range using the TrackedRange.textRange API val state = TextFieldState("Hello World") state.edit { // Assume we want to "wipe" all bold styles from the first 5 characters. val rangeToWipe = TextRange(0, 5) // Get all span styles that intersect with the wipe range. getSpanStyles(rangeToWipe).forEach { trackedRange -> if (trackedRange.spanStyle.fontWeight == FontWeight.Bold) { val current = trackedRange.textRange if (rangeToWipe.start <= current.start && current.end <= rangeToWipe.end) { // Case 1: The bold style is entirely within the wipe range, remove it. removeStyle(trackedRange) } else if (current.start < rangeToWipe.start && rangeToWipe.end < current.end) { // Case 2: The wipe range is in the middle: split the style into two parts. val oldEnd = current.end // Truncate the original style to end at the start of the wipe range. trackedRange.textRange = TextRange(current.start, rangeToWipe.start) // Add a new bold style starting after the wipe range. addStyle(trackedRange.spanStyle, rangeToWipe.end, oldEnd) } else if (current.start < rangeToWipe.start) { // Case 3: Overlap at the start of wipe: truncate the style's end. trackedRange.textRange = TextRange(current.start, rangeToWipe.start) } else { // Case 4: Overlap at the end of wipe: truncate the style's start. trackedRange.textRange = TextRange(rangeToWipe.end, current.end) } } } }
| See also | |
|---|---|
TextFieldBuffer |