is there a way to change the background color of a specific word in outlined text field jetpack compose? - android-jetpack-compose

I am looking for a way to change the background color of certain words in outlined text field while typing, but I couldn't find a way to achieve that yet. Does anyone know a way to do this? (Preferably in compose but other solutions are also OK.)

You can use the VisualTransformation property in the TextField to change the text while typing. Use an AnnotatedString to apply different SpanStyle to your text.
Something like:
OutlinedTextField(
value = text,
onValueChange = { text = it },
visualTransformation = ColorsTransformation()
)
class ColorsTransformation() : VisualTransformation {
override fun filter(text: AnnotatedString): TransformedText {
return TransformedText(
buildAnnotatedStringWithColors(text.toString()),
OffsetMapping.Identity)
}
}
In the buildAnnotatedStringWithColors you can move your logic to change the background color of certain words. It is just an example to change the color for each word.
fun buildAnnotatedStringWithColors(text:String): AnnotatedString{
//it is just an example
val words: List<String> = text.split("\\s+".toRegex())// splits by whitespace
val builder = AnnotatedString.Builder()
val colors = listOf(Color.Red,Color.Black,Color.Yellow,Color.Blue)
var count = 0
for (word in words) {
//Use you favorite SpanStyle
builder.withStyle(style = SpanStyle(
background = colors[count % 4],
color = White
)) {
append("$word ")
}
count ++
}
return builder.toAnnotatedString()
}

The simplest solution is using annotated string:
Text(buildAnnotatedString {
append("Hello ")
withStyle(
style = SpanStyle(
fontWeight = FontWeight.Bold,
color = Color.White,
background = Color.Blue,
)
) {
append("World")
}
}, modifier = Modifier.padding(10.dp))
Result
Also take a look at this solution, which will allow you to use a more complex drawing like this:

Related

Why does scaling the text in a row changes the offset of the text view in compose? [duplicate]

I know how to align text in Jetpack Compose to the baseline.
But now I would need to align two differently sized texts that follow each other in a Row by the ascent of the larger of these two fonts. I would like to think of this as aligning two texts "by the top baseline" if that makes sense. Modifier.align(Alignment.Top) does not work as it will not align by the ascent but by the layout's top and then the texts are not aligned correctly at the top.
I have tried to look how to do this, but apparently there's no ready made function or modifier for this? I didn't even find a way to access Text's ascent property etc in Compose. So not sure how this would be possible?
Thanks for any hints! :)
Edit: This is what it looks when Alignment.Top is used. But I would like the two texts to align at the top.
All information about text layout can be retrieved with onTextLayout Text argument. In this case you need a line size, which can be retrieved with getLineBottom, and an actual font size, which can be found in layoutInput.style.fontSize.
I agree that it'd be easier if you could use some simple way to do that, so I've starred your feature request, but for now here's how you can calculate it:
onTextLayout = { textLayoutResult ->
val ascent = textLayoutResult.getLineBottom(0) - textLayoutResult.layoutInput.run {
with(density) {
style.fontSize.toPx()
}
}
},
Full example of aligning two texts:
val ascents = remember { mutableStateMapOf<Int, Float>() }
val texts = remember {
listOf(
"Big text" to 80.sp,
"Small text" to 20.sp,
)
}
Row(
Modifier
.drawBehind {
ascents.maxOfOrNull { it.value }?.let {
drawLine(Color.Red, Offset(0f, it), Offset(size.width, it))
}
}
) {
texts.forEachIndexed { i, info ->
Text(
info.first,
fontSize = info.second,
onTextLayout = { textLayoutResult ->
ascents[i] = textLayoutResult.getLineBottom(0) - textLayoutResult.layoutInput.run {
with(density) {
style.fontSize.toPx()
}
}
},
modifier = Modifier
.alpha(if (ascents.count() == texts.count()) 1f else 0f)
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
val maxAscent = ascents.maxOfOrNull { it.value } ?: 0f
val ascent = ascents[i] ?: 0f
val yOffset = if (maxAscent == ascent) 0 else (maxAscent - ascent).toInt()
layout(placeable.width, placeable.height - yOffset) {
placeable.place(0, yOffset)
}
}
)
}
}
Result:
One workaround would be you can adjust y-axis offset modifier according to your need.
Text(text = "Second", modifier = Modifier.offset(x = 0.dp, y = 5.dp))
you can have negative value for offset as well if you like to up your first text according to your need.
Another option is to use ConstraintLayout. You can simply constrain the tops of the two texts https://developer.android.com/jetpack/compose/layouts/constraintlayout
in addition to one of the previous answers
ascent and descent
Text(
modifier = modifier,
text = text,
onTextLayout = { result ->
val layoutInput = result.layoutInput
val fontSize = with(layoutInput.density) { layoutInput.style.fontSize.toPx() }
val lineHeight = with(layoutInput.density) { layoutInput.style.lineHeight.toPx() }
var baseline = result.firstBaseline
(0 until result.lineCount).forEach { index ->
val top = result.getLineTop(index)
val bottom = result.getLineBottom(index)
val ascent = bottom - fontSize
val descent = bottom - (baseline - fontSize - top)
baseline += lineHeight
}
}
)

Jetpack Compose TabRow flickers when content is accompanist webview

I am experiencing flicker or overlapping when having a compose tabBar implementation with webviews as content. If I change the webviews with another view (ex. Box{Text}) it does not happen.
It seems as if the webview is filling more than it's border for a short while (See .gif below)
Update: I have been looking into if it was a recomposition issue (hence the simple test project) and I cannot identify any reason why it should recompose the tab bar.
When I add height to the tab bar, I can see the text is in the tab bar at all times.
A test project can be fetched here: https://github.com/msuhl/ComposeTabTest and is a very standard implementation
#Composable
private fun MyTabRow(
pagerState: PagerState,
coroutineScope: CoroutineScope,
) {
TabRow(
selectedTabIndex = pagerState.currentPage,
indicator = { tabPositions ->
TabRowDefaults.Indicator(
Modifier.pagerTabIndicatorOffset(pagerState, tabPositions),
color = MaterialTheme.colors.secondary
)
},
) {
tabRowItems.forEachIndexed { index, item ->
Tab(
selected = pagerState.currentPage == index,
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(index) } },
icon = {
Icon(imageVector = item.icon, contentDescription = "")
},
text = {
Text(
text = item.title,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
},
)
}
}
HorizontalPager(
count = tabRowItems.size,
state = pagerState,
) {
ShowWebView("http://google.com")
}
}
It was related to the lazy loading of webview and I was not able to make a direct fix.
Instead I ended up with a working, although kind og hackish, solution
If a LazyColumn is introduced around the webview, the issue does not occur.
In code:
HorizontalPager(
count = tabRowItems.size,
state = pagerState,
) {
LazyColumn {
item {
ShowWebView(url)
}
}
}

Remember color used in parent, but calculated nested child

I am trying to figure out the right way to remember a color between re-compose, where the color is used in the parent, but calculated in a child.
var color by remember { mutableStateOf(Color.Transparent) }
Row(Modifier.height(100.dp)) {
Box(
Modifier
.width(6.dp)
.fillMaxHeight()
.background(color)
)
Column {
HorizontalPager(
count = pages.size,
state = pagerState
) { page ->
val page = pages[page]
color = page.color
}
}
}
However I am running into two issues
When the color changes when I page seem to get into an infinite re-compose.
The color seems to be a blend of the new page color and the previous page color.
Rather than having color as state, which is causing your recomposition loop. You could derive the same thing by hoisting your pagerState object and referencing the current page. Something like the following:
val pagerState = rememberPagerState()
var color = remember(pagerState.currentPage) { pages[pagerState.currentPage].color }
Row(Modifier.height(100.dp)) {
Box(
Modifier
.width(6.dp)
.fillMaxHeight()
.background(color)
)
Column {
HorizontalPager(
count = pages.size,
state = pagerState
) { page ->
}
}
}

Change highlight on clickable Text possible?

I have a #Composable Text("Dropbox") whose Modifier also implements a
clickable {
//..
}
But when I tapping on it
, it gets a grey highlight!?
I do not want this grey hightlight. Can I get rid of this?
Here indication is responsible for showing the highlight.
So making indication = null should do the job
.clickable(
onClick = {
//..
},
indication = null,
interactionSource = remember { MutableInteractionSource() }
)

How to make ClickableText accessible to screen readers

This code creates a ClickableText element in Jetpack Compose Composable:
ClickableText(
text = forgotPasswordAnnotatedString,
onClick = {
context.startActivity(intent)
},
modifier = Modifier
.padding(top = mediumPadding)
)
The annotated string is defined here to make the text look like a link:
val forgotPasswordAnnotatedString = buildAnnotatedString {
append(stringResource(R.string.forgot_password))
addStyle(
style = SpanStyle(
textDecoration = TextDecoration.Underline,
color = Color.White,
fontSize = 16.sp,
fontWeight = FontWeight.Medium
),
start = 0,
end = 21,
)
}
When I encounter this text using the TalkBalk screen reader in Android, the screenreader does not make it clear that this is clickable text that will do something which tapped on. The reader just reads the text.
Is there a way to make it clear to the screen reader that this text is interactive? Otherwise should I just use a button and style it to look like a link?
It looks like your intention is for the whole text to be clickable? In which you best option is probably a TextButton as suggested by
Gabriele Mariotti.
But if you wan't only part of the link to be clickable, or to have multiple clickable sections, the best I've been able to land on is to draw an invisible box overtop of the Text. It means that I can control the touch target of the clickable area to be at least 48.dp and can use the semantics{} modifier to control how a screen reader interprets it.
Would welcome any suggestions.
// remember variables to hold the start and end position of the clickable text
val startX = remember { mutableStateOf(0f) }
val endX = remember { mutableStateOf(0f) }
// convert to Dp and work out width of button
val buttonPaddingX = with(LocalDensity.current) { startX.value.toDp() }
val buttonWidth = with(LocalDensity.current) { (endX.value - startX.value).toDp() }
Text(
text = forgotPasswordAnnotatedString,
onTextLayout = {
startX.value = it.getBoundingBox(0).left // where 0 is the start index of the range you want to be clickable
endX.value = it.getBoundingBox(21 - 1).right // where 21 is the end index of the range you want to be clickable
}
)
Note that buttonPaddingX is relative to the Text position not the Window, so you may have to surround both in a Box{} or use ConstraintLayout.
Then to draw the invisible box
Box(modifier = Modifier
.sizeIn(minWidth = 48.dp, minHeight = 48.dp) // minimum touch target size
.padding(start = buttonPaddingX)
.width(buttonWidth)
// .background(Color.Magenta.copy(alpha = 0.5f)) // uncomment this to debug where the box is drawn
.clickable(onClick = { context.startActivity(intent) })
.semantics {
// tell TalkBack whatever you need to here
role = Role.Button
contentDescription = "Insert button description here"
}
)
In my code I'm using pushStringAnnotation(TAG, annotation) rather than reference string indexes directly. That way I can get the start and end index of the clickable area with annotatedString.getStringAnnotations(TAG,0,annotatedString.length).first(). Useful if there a multiple links within the text.
It's disappointing that ClickableText doesn't have accessibility in mind from the get go, hopefully we'll be able to use it again in a future update.
Adding .semantics.contentDescription to the Modifier changes what is read by the screen reader. I had to word contentDescription to make it clear that this was a link to reset the your password.
The screen reader still doesn't recognize the element a clickable but hopefully the description will be useful to convey to the user that this element is interactive.
ClickableText(
text = forgotPasswordAnnotatedString,
onClick = {
context.startActivity(intent)
},
modifier = Modifier
.padding(top = mediumPadding)
// new code here:
.semantics {
contentDescription = "Forgot your password? link"
}
)

Resources