How to highlight specific word of the text in jetpack compose? - android

I wanted to know how to highlight the specific part of the text in jetpack compose. I tried Html.fromHtml() like this
Text(text = Html.fromHtml(" <font color='red'> Hello </font> World").toString())
But it didn't work. Is there any way I can do this in compose?

You can use the AnnotatedString to display the text with multiple styles.
Something like:
Text(buildAnnotatedString {
withStyle(style = SpanStyle(color = Color.Red)) {
append("Hello")
}
append(" World ")
})

Check this function below. Here paragraph is your string source and searchQuery is the specific text you want to highlight.
This provides you a dynamic state for text and search highlights.
#Composable
fun getData(): StateFlow<AnnotatedString?> {
val span = SpanStyle(
color = MaterialTheme.colorScheme.onPrimaryContainer,
fontWeight = FontWeight.SemiBold,
background = MaterialTheme.colorScheme.primaryContainer
)
return combine(paragraph, searchQuery) { text, query ->
buildAnnotatedString {
var start = 0
while (text.indexOf(query, start, ignoreCase = true) != -1 && query.isNotBlank()) {
val firstIndex = text.indexOf(query, start, true)
val end = firstIndex + query.length
append(text.substring(start, firstIndex))
withStyle(style = span) {
append(text.substring(firstIndex, end))
}
start = end
}
append(text.substring(start, text.length))
toAnnotatedString()
}
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
}

You can use AnnotatedString to append each word/section with it's own style or to add different style at any index which is great if you're using a string resource.
For the hello world example you could construct something like this:
val annotatedString = buildAnnotatedString {
val str = "Hello World" // or stringResource(id = R.string.hello_world)
val boldStr = "Hello" // or stringResource(id = R.string.hello)
val startIndex = str.indexOf(boldStr)
val endIndex = startIndex + boldStr.length
append(str)
addStyle(style = SpanStyle(color = Color.Red), start = startIndex, end = endIndex)
}
Text(
text = annotatedString,
)
Using addStyle in this way allows us to do some fun things like adding multiple styles to the same text
val annotatedString = buildAnnotatedString {
val str = "Hello Wonderful World" // or stringResource(id = R.string.hello_world)
val boldStr = "Wonderful World" // or stringResource(id = R.string.world)
val startIndex = str.indexOf(boldStr)
val endIndex = startIndex + boldStr.length
append(str)
addStyle(style = SpanStyle(color = Color.Red), start = startIndex, end = endIndex)
val italicsStr = "Wonderful"
val italicsStartIndex = str.indexOf(italicsStr)
val italicsEndIndex = startIndex + italicsStr.length
addStyle(style = SpanStyle(fontStyle = FontStyle.Italic), start = italicsStartIndex, end = italicsEndIndex)
}
Text(
text = annotatedString,
style = TextStyle(fontWeight = FontWeight.Bold),
color = Color.Blue,
)

Related

Normal text with clickable text (not hyper link)

I feel that there is a better way to do this thing, I know that this code works, but I just want to know if there is another (best) way to do this.
Row() {
Text(text = "By signing up, you agree with the ")
Text(
text = "Terms of Service",
modifier = Modifier.clickable { },
color = Color.Blue
)
}
Row(
modifier = Modifier.offset(y = (-20).dp)
){
Text(text = "and ")
Text(
text = "Privacy Policy",
modifier = Modifier
.clickable { },
color = Color.Blue
)
}
You can use an AnnotatedString and the UriHandler to open the url.
Something like:
val annotatedString = buildAnnotatedString {
append("By signing up, you agree with the ")
pushStringAnnotation(tag = "terms", annotation = "https://....")
withStyle(style = SpanStyle(color = Blue)) {
append("Terms of Service")
}
pop()
append(" and ")
pushStringAnnotation(tag = "policy", annotation = "https://....")
withStyle(style = SpanStyle(color = Blue)) {
append("Privacy Policy")
}
pop()
}
val uriHandler = LocalUriHandler.current
ClickableText(
text = annotatedString,
onClick = { offset ->
annotatedString.getStringAnnotations(tag = "terms", start = offset, end = offset).firstOrNull()?.let {
uriHandler.openUri(it.item)
}
annotatedString.getStringAnnotations(tag = "policy", start = offset, end = offset).firstOrNull()?.let {
uriHandler.openUri(it.item)
}
})

Jetpack compose bold only string placeholder

I have a string resource like this
<string name="my_string">Fancy string with an %1$s placeholder</string>
and I would like to have this as output: "Fancy string with an amazing placeholder". Which is the string with the content of the placeholder in bold.
How can I get the desired output?
Finally I got the desired result with
val placeholder = "Amazing"
val globalText = stringResource(id = R.string.my_string, placeholder)
val start = globalText.indexOf(placeholder)
val spanStyles = listOf(
AnnotatedString.Range(SpanStyle(fontWeight = FontWeight.Bold),
start = start,
end = start + placeholder.length
)
)
Text(text = AnnotatedString(text = globalText, spanStyles = spanStyles))
Assuming that you are displaying this in a Text composable, do this. Make sure to include a backslash before the $ character:
Row(modifier = Modifier.wrapContentWidth()) {
val s = LocalContext.current.getString(R.string.my_string)
val p = s.indexOf("%1\$s")
Text(s.substring(0, p))
Text("amazing", fontWeight = FontWeight.Bold)
Text(s.substring(p + 4))
}
Previous comments are too complicated.
It's enough to use html tags in your string resource:
<string name="my_string">Fancy string with an <b>amazing</b> <i>placeholder</i></string>
If you are use Composable - you can use buildAnnotatedString for some cases. Documentation here
Text(
buildAnnotatedString {
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold, color = Color.Red)) {
append("W")
}
append("orld")
}
)

How to make middle ellipsis in Text with Jetpack Compose

I need to make Middle Ellipsis in Jetpack Compose Text. As far as I see there is only Clip, Ellipsis and Visible options for TextOverflow.
Something like this: 4gh45g43h...bh4bh6b64
It is not officially supported yet, keep an eye on this issue.
For now, you can use the following method. I use SubcomposeLayout to get onTextLayout result without actually drawing the initial text.
It takes so much code and calculations to:
Make sure the ellipsis is necessary, given all the modifiers applied to the text.
Make the size of the left and right parts as close to each other as possible, based on the size of the characters, not just their number.
#Composable
fun MiddleEllipsisText(
text: String,
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
fontSize: TextUnit = TextUnit.Unspecified,
fontStyle: FontStyle? = null,
fontWeight: FontWeight? = null,
fontFamily: FontFamily? = null,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
textAlign: TextAlign? = null,
lineHeight: TextUnit = TextUnit.Unspecified,
softWrap: Boolean = true,
onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current,
) {
// some letters, like "r", will have less width when placed right before "."
// adding a space to prevent such case
val layoutText = remember(text) { "$text $ellipsisText" }
val textLayoutResultState = remember(layoutText) {
mutableStateOf<TextLayoutResult?>(null)
}
SubcomposeLayout(modifier) { constraints ->
// result is ignored - we only need to fill our textLayoutResult
subcompose("measure") {
Text(
text = layoutText,
color = color,
fontSize = fontSize,
fontStyle = fontStyle,
fontWeight = fontWeight,
fontFamily = fontFamily,
letterSpacing = letterSpacing,
textDecoration = textDecoration,
textAlign = textAlign,
lineHeight = lineHeight,
softWrap = softWrap,
maxLines = 1,
onTextLayout = { textLayoutResultState.value = it },
style = style,
)
}.first().measure(Constraints())
// to allow smart cast
val textLayoutResult = textLayoutResultState.value
?: // shouldn't happen - onTextLayout is called before subcompose finishes
return#SubcomposeLayout layout(0, 0) {}
val placeable = subcompose("visible") {
val finalText = remember(text, textLayoutResult, constraints.maxWidth) {
if (text.isEmpty() || textLayoutResult.getBoundingBox(text.indices.last).right <= constraints.maxWidth) {
// text not including ellipsis fits on the first line.
return#remember text
}
val ellipsisWidth = layoutText.indices.toList()
.takeLast(ellipsisCharactersCount)
.let widthLet#{ indices ->
// fix this bug: https://issuetracker.google.com/issues/197146630
// in this case width is invalid
for (i in indices) {
val width = textLayoutResult.getBoundingBox(i).width
if (width > 0) {
return#widthLet width * ellipsisCharactersCount
}
}
// this should not happen, because
// this error occurs only for the last character in the string
throw IllegalStateException("all ellipsis chars have invalid width")
}
val availableWidth = constraints.maxWidth - ellipsisWidth
val startCounter = BoundCounter(text, textLayoutResult) { it }
val endCounter = BoundCounter(text, textLayoutResult) { text.indices.last - it }
while (availableWidth - startCounter.width - endCounter.width > 0) {
val possibleEndWidth = endCounter.widthWithNextChar()
if (
startCounter.width >= possibleEndWidth
&& availableWidth - startCounter.width - possibleEndWidth >= 0
) {
endCounter.addNextChar()
} else if (availableWidth - startCounter.widthWithNextChar() - endCounter.width >= 0) {
startCounter.addNextChar()
} else {
break
}
}
startCounter.string.trimEnd() + ellipsisText + endCounter.string.reversed().trimStart()
}
Text(
text = finalText,
color = color,
fontSize = fontSize,
fontStyle = fontStyle,
fontWeight = fontWeight,
fontFamily = fontFamily,
letterSpacing = letterSpacing,
textDecoration = textDecoration,
textAlign = textAlign,
lineHeight = lineHeight,
softWrap = softWrap,
onTextLayout = onTextLayout,
style = style,
)
}[0].measure(constraints)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
}
private const val ellipsisCharactersCount = 3
private const val ellipsisCharacter = '.'
private val ellipsisText = List(ellipsisCharactersCount) { ellipsisCharacter }.joinToString(separator = "")
private class BoundCounter(
private val text: String,
private val textLayoutResult: TextLayoutResult,
private val charPosition: (Int) -> Int,
) {
var string = ""
private set
var width = 0f
private set
private var _nextCharWidth: Float? = null
private var invalidCharsCount = 0
fun widthWithNextChar(): Float =
width + nextCharWidth()
private fun nextCharWidth(): Float =
_nextCharWidth ?: run {
var boundingBox: Rect
// invalidCharsCount fixes this bug: https://issuetracker.google.com/issues/197146630
invalidCharsCount--
do {
boundingBox = textLayoutResult
.getBoundingBox(charPosition(string.count() + ++invalidCharsCount))
} while (boundingBox.right == 0f)
_nextCharWidth = boundingBox.width
boundingBox.width
}
fun addNextChar() {
string += text[charPosition(string.count())]
width += nextCharWidth()
_nextCharWidth = null
}
}
My testing code:
val text = remember { LoremIpsum(100).values.first().replace("\n", " ") }
var length by remember { mutableStateOf(77) }
var width by remember { mutableStateOf(0.5f) }
Column {
MiddleEllipsisText(
text.take(length),
fontSize = 30.sp,
modifier = Modifier
.background(Color.LightGray)
.padding(10.dp)
.fillMaxWidth(width)
)
Slider(
value = length.toFloat(),
onValueChange = { length = it.roundToInt() },
valueRange = 2f..text.length.toFloat()
)
Slider(
value = width,
onValueChange = { width = it },
)
}
Result:
There is currently no specific function in Compose yet.
A possible approach is to process the string yourself before using it, with the kotlin functions.
val word = "4gh45g43hbh4bh6b64" //put your string here
val chunks = word.chunked((word.count().toDouble()/2).roundToInt())
val midEllipsis = "${chunks[0]}…${chunks[1]}"
println(midEllipsis)
I use the chunked function to divide the string into an array of strings, which will always be two because as a parameter I give it the size of the string divided by 2 and rounded up.
Result : 4gh45g43h…bh4bh6b64
To use the .roundToInt() function you need the following import
import kotlin.math.roundToInt
Test this yourself in the playground
Since TextView already supports ellipsize in the middle you can just wrap it in compose using AndroidView
AndroidView(
factory = { context ->
TextView(context).apply {
maxLines = 1
ellipsize = MIDDLE
}
},
update = { it.text = "A looooooooooong text" }
)

Linkify with Compose Text

I can't find how to linkify my Text() using Jetpack Compose.
Before compose all I had to do was:
Linkify.addLinks(myTextView, Linkify.EMAIL_ADDRESSES or Linkify.WEB_URLS)
And all the links contained in my TextView were becoming clickable links, obviously.
Important: The content of the Text is coming from an API and the links do not have a fixed position and content may contain multiple links.
I want to keep this behavior with using Jetpack Compose but I can't find any information about doing that.
Does anyone know?
In case someone is looking for a solution, the following will make any links clickable and styled in your text:
#Composable
fun LinkifyText(text: String, modifier: Modifier = Modifier) {
val uriHandler = LocalUriHandler.current
val layoutResult = remember {
mutableStateOf<TextLayoutResult?>(null)
}
val linksList = extractUrls(text)
val annotatedString = buildAnnotatedString {
append(text)
linksList.forEach {
addStyle(
style = SpanStyle(
color = Color.Companion.Blue,
textDecoration = TextDecoration.Underline
),
start = it.start,
end = it.end
)
addStringAnnotation(
tag = "URL",
annotation = it.url,
start = it.start,
end = it.end
)
}
}
Text(text = annotatedString, style = MaterialTheme.typography.body1, modifier = modifier.pointerInput(Unit) {
detectTapGestures { offsetPosition ->
layoutResult.value?.let {
val position = it.getOffsetForPosition(offsetPosition)
annotatedString.getStringAnnotations(position, position).firstOrNull()
?.let { result ->
if (result.tag == "URL") {
uriHandler.openUri(result.item)
}
}
}
}
},
onTextLayout = { layoutResult.value = it }
)
}
private val urlPattern: Pattern = Pattern.compile(
"(?:^|[\\W])((ht|f)tp(s?):\\/\\/|www\\.)"
+ "(([\\w\\-]+\\.){1,}?([\\w\\-.~]+\\/?)*"
+ "[\\p{Alnum}.,%_=?&#\\-+()\\[\\]\\*$~#!:/{};']*)",
Pattern.CASE_INSENSITIVE or Pattern.MULTILINE or Pattern.DOTALL
)
fun extractUrls(text: String): List<LinkInfos> {
val matcher = urlPattern.matcher(text)
var matchStart: Int
var matchEnd: Int
val links = arrayListOf<LinkInfos>()
while (matcher.find()) {
matchStart = matcher.start(1)
matchEnd = matcher.end()
var url = text.substring(matchStart, matchEnd)
if (!url.startsWith("http://") && !url.startsWith("https://"))
url = "https://$url"
links.add(LinkInfos(url, matchStart, matchEnd))
}
return links
}
data class LinkInfos(
val url: String,
val start: Int,
val end: Int
)
I think the better solution for now is create your own component with textview like that:
#Composable
fun DefaultLinkifyText(modifier: Modifier = Modifier, text: String?) {
val context = LocalContext.current
val customLinkifyTextView = remember {
TextView(context)
}
AndroidView(modifier = modifier, factory = { customLinkifyTextView }) { textView ->
textView.text = text ?: ""
LinkifyCompat.addLinks(textView, Linkify.ALL)
Linkify.addLinks(textView, Patterns.PHONE,"tel:",
Linkify.sPhoneNumberMatchFilter, Linkify.sPhoneNumberTransformFilter)
textView.movementMethod = LinkMovementMethod.getInstance()
}
}
You can still use Linkify.addLinks but convert the result into AnnotatedString like this:
fun String.linkify(
linkStyle: SpanStyle,
) = buildAnnotatedString {
append(this#linkify)
val spannable = SpannableString(this#linkify)
Linkify.addLinks(spannable, Linkify.WEB_URLS)
val spans = spannable.getSpans(0, spannable.length, URLSpan::class.java)
for (span in spans) {
val start = spannable.getSpanStart(span)
val end = spannable.getSpanEnd(span)
addStyle(
start = start,
end = end,
style = linkStyle,
)
addStringAnnotation(
tag = "URL",
annotation = span.url,
start = start,
end = end
)
}
}
fun AnnotatedString.urlAt(position: Int, onFound: (String) -> Unit) =
getStringAnnotations("URL", position, position).firstOrNull()?.item?.let {
onFound(it)
}
Use it in your composable like this:
val linkStyle = SpanStyle(
color = MaterialTheme.colors.primary,
textDecoration = TextDecoration.Underline,
)
ClickableText(
text = remember(text) { text.linkify(linkStyle) },
onClick = { position -> text.urlAt(position, onClickLink) },
)
You can use AnnotatedString to achieve this behavior.
docs: https://developer.android.com/reference/kotlin/androidx/compose/ui/text/AnnotatedString
Also, this one may help you:
AutoLink for Android Compose Text
Based on above answers,
You can use https://github.com/firefinchdev/linkify-text
Its a single file, you can directly copy it to your project.
Also, it uses Android's Linkify for link detection, which is same as that of TextView's autoLink.
A similar, but simpler solution that I went with to get the proper Material Design look and feel:
#Composable
fun BodyWithLinks(body: String, modifier: Modifier = Modifier) {
AndroidView(
modifier = modifier,
factory = { context ->
(MaterialTextView(context) as AppCompatTextView).apply {
val spannableString = SpannableString(body)
Linkify.addLinks(spannableString, Linkify.ALL)
text = spannableString
setTextAppearance(R.style.Theme_Body_1)
}
},
)
}
This is an example if you have multiple clickable words in one sentence and you want to navigate inside the application:
#Composable
fun InformativeSignUpText() {
val informativeText = stringResource(R.string.sign_up_already_have_an_account)
val logInSubstring = stringResource(R.string.general_log_in)
val supportSubstring = stringResource(R.string.general_support)
val logInIndex = informativeText.indexOf(logInSubstring)
val supportIndex = informativeText.indexOf(supportSubstring)
val informativeAnnotatedText = buildAnnotatedString {
append(informativeText)
addStyle(
style = SpanStyle(
color = MaterialTheme.colors.primary
),
start = logInIndex,
end = logInIndex + logInSubstring.length
)
addStringAnnotation(
tag = logInSubstring,
annotation = logInSubstring,
start = logInIndex,
end = logInIndex + logInSubstring.length
)
addStyle(
style = SpanStyle(
color = MaterialTheme.colors.primary
),
start = supportIndex,
end = supportIndex + supportSubstring.length
)
addStringAnnotation(
tag = supportSubstring,
annotation = supportSubstring,
start = supportIndex,
end = supportIndex + supportSubstring.length
)
}
ClickableText(
modifier = Modifier.padding(
top = 16.dp
),
style = MaterialTheme.typography.subtitle1.copy(
color = Nevada
),
text = informativeAnnotatedText,
onClick = { offset ->
informativeAnnotatedText.getStringAnnotations(
tag = logInSubstring,
start = offset,
end = offset
).firstOrNull()?.let {
Log.d("mlogs", it.item)
}
informativeAnnotatedText.getStringAnnotations(
tag = supportSubstring,
start = offset,
end = offset
).firstOrNull()?.let {
Log.d("mlogs", it.item)
}
}
)
}
Suppose you already have a Spanned that potentially contains clickable spans (i.e. you've already done the linkify part), then you can use this:
#Composable
fun StyledText(text: CharSequence, modifier: Modifier = Modifier) {
val clickable = rememberSaveable {
text is Spanned && text.getSpans(0, text.length, ClickableSpan::class.java).isNotEmpty()
}
AndroidView(
modifier = modifier,
factory = { context ->
TextView(context).apply {
if (clickable) {
movementMethod = LinkMovementMethod.getInstance()
}
}
},
update = {
it.text = text
}
)
}
This will also render any other span types that may be there.

AutoLink for Android Compose Text

Is there any way to use android:autoLink feature on JetPack Compose Text?
I know, that it is maybe not "declarative way" for using this feature in one simple tag/modifier, but maybe there is some easy way for this?
For styling text I can use this way
val apiString = AnnotatedString.Builder("API provided by")
apiString.pushStyle(
style = SpanStyle(
color = Color.Companion.Blue,
textDecoration = TextDecoration.Underline
)
)
apiString.append("https://example.com")
Text(text = apiString.toAnnotatedString())
But, how can I manage clicks here? And would be great, if I programatically say what behaviour I expect from the system (email, phone, web, etc). Like it. works with TextView.
Thank you
We can achieve Linkify kind of TextView in Android Compose like this example below,
#Composable
fun LinkifySample() {
val uriHandler = UriHandlerAmbient.current
val layoutResult = remember {
mutableStateOf<TextLayoutResult?>(null)
}
val text = "API provided by"
val annotatedString = annotatedString {
pushStyle(
style = SpanStyle(
color = Color.Companion.Blue,
textDecoration = TextDecoration.Underline
)
)
append(text)
addStringAnnotation(
tag = "URL",
annotation = "https://example.com",
start = 0,
end = text.length
)
}
Text(
fontSize = 16.sp,
text = annotatedString, modifier = Modifier.tapGestureFilter { offsetPosition ->
layoutResult.value?.let {
val position = it.getOffsetForPosition(offsetPosition)
annotatedString.getStringAnnotations(position, position).firstOrNull()
?.let { result ->
if (result.tag == "URL") {
uriHandler.openUri(result.item)
}
}
}
},
onTextLayout = { layoutResult.value = it }
)
}
In the above example, we can see we give the text and also we use addStringAnnotation to set the tag. And using tapGestureFilter, we can get the clicked annotation.
Finally using UriHandlerAmbient.current we can open the link like email, phone, or web.
Reference : https://www.hellsoft.se/rendering-markdown-with-jetpack-compose/
The most important part of jetpack compose is the compatibility with native android components.
Create a component that use TextView and use it:
#Composable
fun DefaultLinkifyText(modifier: Modifier = Modifier, text: String?) {
val context = LocalContext.current
val customLinkifyTextView = remember {
TextView(context)
}
AndroidView(modifier = modifier, factory = { customLinkifyTextView }) { textView ->
textView.text = text ?: ""
LinkifyCompat.addLinks(textView, Linkify.ALL)
Linkify.addLinks(textView, Patterns.PHONE,"tel:",
Linkify.sPhoneNumberMatchFilter, Linkify.sPhoneNumberTransformFilter)
textView.movementMethod = LinkMovementMethod.getInstance()
}
}
How to use:
DefaultLinkifyText(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
text = "6999999 and https://stackoverflow.com/ works fine"
)

Categories

Resources