Status Update
Comments
no...@google.com <no...@google.com> #2
Over to Ralston to take a look. I imagine with the new relocation logic it should be fixed?
si...@google.com <si...@google.com> #3
What is happening here is that the TextField does not know that it is in a scrollable container, and since the keyboard is going to hide the currently focused text, the text field calls View.requestRectangleOnScreen which causes the entire app to pan up, and that clips the top bar.
The Relocation APIs are experimental right now. It is not used in TextField as we are past the alpha stage and can only use stable APIs in TextField. So this bug can only be fixed post 1.0
wa...@gmail.com <wa...@gmail.com> #4
This should be fixed by
I verified that this sample code now works when soft input mode is AdjustResize.
ja...@gmail.com <ja...@gmail.com> #5
I wasn't able to find this issue before, so mine was marked as a duplicate of it. In case anyone's interested: in it I demonstrate how multiple styles can be set on a text field but how and why the styles are lost upon editing. See
Anyway, about this issue: with TextView
one can manually alter the Spannables
and create a WYSIWYG editor. With Compose's TextField
, this is unfortunately not possible because of this bug. A real shame, because now the only way to make a WYSIWYG editor is by using an "old" TextView
or by going the "WebView
with a bunch of JavaScript" route many apps unfortunately choose when it comes to text editing.
wa...@gmail.com <wa...@gmail.com> #6
I reported too , They have recognized the issue , I think they're just taking too much time and I am impatient because I also wanted to create a wysiwig editor
st...@google.com <st...@google.com> #7
Btw this is reported as dup too:
da...@gmail.com <da...@gmail.com> #8
Is there any news on this issue? This seems like it would have a lot of use cases and no real workaround (that I could find in compose) but a relatively easy fix
ja...@gmail.com <ja...@gmail.com> #9
@ post #8: latest news is from the October 24 (2022) roadmap update, that shows "multistyle text editing" being on the backlog:
no...@gmail.com <no...@gmail.com> #10
While the TextField API supports AnnotatedStrings, the implementation is not actually there. Maybe the API was created thinking the lapse without a proper implementation would be short, but it's been 4 years since this issue was opened, so it would be great to add at least a big warning note somewhere (e.g. note in the docs and a @Deprecated
annotation in the code) to let developers know that AnnotatedString is actually not supported by TextField.
no...@gmail.com <no...@gmail.com> #11
BTW, regarding posts #8 and #9, the update from March 22nd to the roadmap still shows "Multistyle text editing" being on the backlog:
wa...@gmail.com <wa...@gmail.com> #12
Workaround
To retain paragraph and span styles in annotated string, You can use this function, It works very well. I haven't found any bug, But if you do find any please write down here
private fun <T> List<AnnotatedString.Range<T>>.mapMoving(pointer: Int, offset: Int): List<AnnotatedString.Range<T>> {
return mapNotNull {
if (it.end >= pointer || it.start >= pointer) {
AnnotatedString.Range(
item = it.item,
start = if (it.start >= pointer) (it.start + offset) else it.start,
end = if (it.end >= pointer) (it.end + offset) else it.end
).let { range ->
if (range.start == range.end) null else range
}
} else it
}
}
fun differ(value: TextFieldValue, newValue: TextFieldValue): TextFieldValue {
val pointer = if (value.selection.reversed) value.selection.end else value.selection.start
val lengthDifference = newValue.text.length - value.text.length
return TextFieldValue(
annotatedString = AnnotatedString(
text = newValue.text,
spanStyles = value.annotatedString.spanStyles.mapMoving(pointer, lengthDifference),
paragraphStyles = value.annotatedString.paragraphStyles.mapMoving(pointer, lengthDifference),
),
selection = newValue.selection
)
}
Usage
var textFieldValue by remember { mutableStateOf(TextFieldValue("")) }
OutlinedTextField(
value = textFieldValue,
onValueChange = {
textFieldValue = differ(textFieldValue, it)
}
)
[Deleted User] <[Deleted User]> #13
Are there any updates on fixing it in the framework? The workaround works at first glance but it has some bugs. It doesn’t retain annotations annotatedString.getStringAnnotations()
as well and it causes an infinite loop between onValueChange()
and differ()
functions.
kl...@google.com <kl...@google.com> #14
I think the loop issue you're talking about is
[Deleted User] <[Deleted User]> #15
Yes, it looks like it’s a same loop issue but caused by a different scenario. In my case I’m getting into the loop only when I use the Workaround and differ()
function. By debugging it further and looking at your questions from value.annotatedString
is called when copying the styles spanStyles = value.annotatedString.spanStyles.mapMoving()
and this seem to be the reason for the infinite loop. This however is a merit of this workaround and removing it makes this workaround not usable.
sl...@gmail.com <sl...@gmail.com> #16
se...@google.com <se...@google.com> #17
This is (high) on the backlog.
Not currently planned for 1.7.
jo...@gmail.com <jo...@gmail.com> #18
As per:
If this is not planned for 1.7, then does that mean that BasicTextField2 will not go stable in 1.7??
ha...@google.com <ha...@google.com> #19
BasicTextField2 (it's renamed to BasicTextField
now in 1.7
) is going stable this release. However multi-styled text editing support is planned for a future release. It's still one of the top items in our roadmap for the new BasicTextField.
am...@gmail.com <am...@gmail.com> #20
is there any workaround to make it work for now?
i am using combination of the old BasicTextField and VisualTransformation to show multistyled text, but when user writes fast or on slow devices, selection probably is updated faster than my manual updating for ranges that causes a lot of trouble for ranges of styled text portions
val myVisualTransformation = remember(boldRanges) {
MyVisualTransformation(boldRanges)
}
BasicTextField(
value = textFieldValue,
onValueChange = { newValue ->
boldRanges = updateTextRanges(
oldFieldValue = textFieldValue,
newFieldValue = newValue,
oldStyles = boldRanges,
)
textFieldValue = newValue
},
modifier = Modifier.padding(8.dp),
visualTransformation = myVisualTransformation,
)
fun updateTextRanges(
oldFieldValue: TextFieldValue,
newFieldValue: TextFieldValue,
oldStyles: List<TextRange>
): List<TextRange> {
val delta = newFieldValue.text.length - oldFieldValue.text.length
if (delta == 0) return oldStyles
val newStyles = oldStyles
.mapNotNull { styleRange ->
when {
// If the change is before the range, shift the range by the delta
oldFieldValue.selection.max <= styleRange.min -> {
TextRange(styleRange.start + delta, styleRange.end + delta)
}
// If the change is after the range, do not affect the range
oldFieldValue.selection.min > styleRange.max -> {
styleRange
}
// if the change is on the end of the range and it is adding a new char
oldFieldValue.selection.min == styleRange.max && delta > 0 -> {
styleRange
}
// If the change is within the range, adjust the end of the range
oldFieldValue.selection in styleRange -> {
TextRange(styleRange.start, styleRange.end + delta)
}
// If the change overlaps the start of the range, adjust the start
oldFieldValue.selection.min < styleRange.min && oldFieldValue.selection.max < styleRange.max -> {
TextRange(newFieldValue.selection.max, styleRange.end + delta)
}
// If the change overlaps the end of the range, adjust the end
oldFieldValue.selection.min > styleRange.min && oldFieldValue.selection.min < styleRange.max -> {
TextRange(styleRange.start, oldFieldValue.selection.min)
}
// If the change completely overlaps the range, remove the range
oldFieldValue.selection.min < styleRange.min && oldFieldValue.selection.max >= styleRange.max -> {
null
}
else -> {
styleRange
}
}
}
.map { range ->
range.coerceIn(0, newFieldValue.text.length)
}
return newStyles
}
class MyVisualTransformation(
private val boldRanges: List<TextRange>
) : VisualTransformation {
override fun filter(text: AnnotatedString): TransformedText {
if (text.spanStyles.isNotEmpty()) return TransformedText(text, OffsetMapping.Identity)
val builder = buildAnnotatedString {
append(text)
boldRanges.forEach { range ->
addStyle(SpanStyle(fontWeight = FontWeight.Bold), range.min, range.max)
}
}
return TransformedText(builder, OffsetMapping.Identity)
}
}
jo...@tmediatech.com <jo...@tmediatech.com> #21
Right now I'm using BasicTextField1 on top of an invisible BasicTextField2 and binding their string values together so I can get the VisualTransformation AND the recieveContent
..... Would love to just use one text field instead of two.
Any timeline updates on this? 🫠
pa...@gmail.com <pa...@gmail.com> #22
pinging! this should be high priority, im using old basictextfield and i need the ability of large texts using new basictextfield with annotated strings
ya...@side.co <ya...@side.co> #23
Any news currently on this developments in order to support AnnotatedString with OutputTransformation related builder ?
Thanks
ro...@devrev.ai <ro...@devrev.ai> #24
ai...@gmail.com <ai...@gmail.com> #25
al...@rockettrade.com <al...@rockettrade.com> #26
This is my proof-of-concept workaround composable for anyone that needs it. The way it works is it expects a string like "Sample string with %1$s and %2$s and maybe a $3%s inside."
Then make sure to set up the annotated item list matching the order... I know it is not the best, but a start for anyone who still needs one desperately.
@Composable
fun AnnotatedText(
modifier: Modifier = Modifier,
baseText: String,
baseTextStyle: TextStyle = MaterialTheme.typography.titleMedium,
annotatedItems: List<AnnotatedItem> = listOf(),
globalSpanStyle: SpanStyle? = null,
onLinkClicked: () -> Unit = {},
) {
val annotatedText = buildAnnotatedString {
var currentIndex = 0
var remainingText = baseText
val baseSpanStyle = baseTextStyle.asSpanStyle()
annotatedItems.forEachIndexed { index, annotatedItem ->
val placeholder = "%${index + 1}\$s"
var start = remainingText.indexOf(placeholder)
while (start != -1) {
append(remainingText.substring(currentIndex, start))
val spanStyle = if (annotatedItem.useBaseStyle) {
baseSpanStyle
} else {
annotatedItem.style ?: globalSpanStyle ?: baseSpanStyle
}
if (annotatedItem.isLink) {
val linkAnnotation = LinkAnnotation.Url(
url = annotatedItem.text,
styles = TextLinkStyles(
style = SpanStyle(
color = spanStyle.color,
fontSize = spanStyle.fontSize,
fontWeight = spanStyle.fontWeight,
fontFamily = spanStyle.fontFamily,
letterSpacing = spanStyle.letterSpacing,
textDecoration = TextDecoration.Underline,
),
),
linkInteractionListener = { onLinkClicked() },
)
withLink(linkAnnotation) {
append(annotatedItem.text)
}
} else {
withStyle(spanStyle) {
append(annotatedItem.text)
}
}
currentIndex = start + placeholder.length
remainingText = remainingText.substring(currentIndex)
start = remainingText.indexOf(placeholder)
currentIndex = 0
}
}
withStyle(baseTextStyle.toSpanStyle()) {
append(remainingText)
}
}
Text(modifier = modifier, text = annotatedText, style = baseTextStyle)
}
data class AnnotatedItem(
val text: String = "",
val style: SpanStyle? = null,
val useBaseStyle: Boolean = false,
val isLink: Boolean = false,
)
Simple usage...
val annotatedStrings = listOf(
AnnotatedItem(someText),
AnnotatedItem(additionalText.orEmpty()),
)
AnnotatedText(
baseText = stringResource(id = R.string.annotated_text),
baseTextStyle = MaterialTheme.typography.titleMedium,
globalSpanStyle = MaterialTheme.typography.titleMedium.copy(
fontWeight = FontWeight.Bold,
).asSpanStyle(),
annotatedItems = annotatedStrings,
)
dh...@gmail.com <dh...@gmail.com> #27
Is there anything I can do to help this happen ?
Description