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")
}
)
Related
I have a string that contains html, how can I display this in a Jetpack compose Text?
In a TextView I would use a Spanned and do something like:
TextView.setText(Html.fromHtml("<p>something", HtmlCompat.FROM_HTML_MODE_LEGACY)
How can I do this with Text from Jetpack compose?
Same answer as Yhondri, but using HtmlCompat if you are targeting api >24:
#Composable
fun Html(text: String) {
AndroidView(factory = { context ->
TextView(context).apply {
setText(HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY))
}
})
}
I have done it this way instead of using TextView in AndroidView and it seems to work quite well for me. The below composable also wraps up the text and expands when you click on it.
#Composable
fun ExpandingText(
description: String,
modifier: Modifier = Modifier,
textStyle: TextStyle = MaterialTheme.typography.body2,
expandable: Boolean = true,
collapsedMaxLines: Int = 3,
expandedMaxLines: Int = Int.MAX_VALUE,
) {
val text = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Html.fromHtml(description, Html.FROM_HTML_MODE_LEGACY)
} else {
HtmlCompat.fromHtml(description, HtmlCompat.FROM_HTML_MODE_LEGACY)
}
var canTextExpand by remember(text) { mutableStateOf(true) }
var expanded by remember { mutableStateOf(false) }
val interactionSource = remember { MutableInteractionSource() }
Text(
text = text.toString(),
style = textStyle,
overflow = TextOverflow.Ellipsis,
maxLines = if (expanded) expandedMaxLines else collapsedMaxLines,
modifier = Modifier
.clickable(
enabled = expandable && canTextExpand,
onClick = { expanded = !expanded },
indication = rememberRipple(bounded = true),
interactionSource = interactionSource,
)
.animateContentSize(animationSpec = spring())
.then(modifier),
onTextLayout = {
if (!expanded) {
canTextExpand = it.hasVisualOverflow
}
}
)
}
Unfortunately, Jetpack compose does NOT support HTML yet...
So, what you could do is:
Option 1: Create your own HTML parser
Jetpack compose supports basic styling such as Bold, color, font etc.. So what you can do is loop through the original HTML text and apply text style manually.
Option 2: Integrate the old TextView into your Jetpack compose.
Please read: Adopting Compose in your app
Thanks.
You can integrate the old TextView into your Jetpack compose like follows:
AndroidView(factory = { context ->
TextView(context).apply {
text = Html.fromHtml(your_html)
}
})
More info: https://foso.github.io/Jetpack-Compose-Playground/viewinterop/androidview/
you can use the code below:
#Composable
private fun TextHtml() {
Text(text = buildAnnotatedString {
withStyle(style = SpanStyle(color = Gray600)) {
append("normal text")
}
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold,color = Gray700)) {
append("bold text ")
}
})
}
use withStyle to apply the html tags and use append() inside it to add the string
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,
)
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.
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"
)
I want to use material icons as argument passing it to the textField.
#Composable
fun NormalTextField(
icon: () -> Unit, // how to pass material icon to textField
label: String
) {
val (text, setText) = mutableStateOf("")
TextField(
leadingIcon = icon,
value = text,
onValueChange = setText,
label = label
)
}
The leadingIcon argument of texfield is a composable function (the label too), so one way to do it is:
#Composable
fun Example() {
NormalTextField(label = "Email") {
Icon(
imageVector = Icons.Outlined.Email,
contentDescription = null
)
}
}
#Composable
fun NormalTextField(
label: String,
Icon: #Composable (() -> Unit)
) {
val (text, setText) = mutableStateOf("")
TextField(
leadingIcon = Icon,
value = text,
onValueChange = setText,
label = { Text(text = label) }
)
}
This can be done using InlineTextContent. Here is an example how to insert the icon at the start of the text. You can wrap this into another composable if you just want to pass the icon as a parameter.
Text(text = buildAnnotatedString {
appendInlineContent("photoIcon", "photoIcon")
append("very long breaking text very long breaking text very long breaking text very long breaking text very long breaking text")
}, inlineContent = mapOf(
Pair("photoIcon", InlineTextContent(
Placeholder(width = 1.7.em, height = 23.sp, placeholderVerticalAlign = PlaceholderVerticalAlign.TextTop)
) {
Image(
painterResource(R.drawable.ic_cameraicon),"play",
modifier = Modifier.fillMaxWidth().padding(end = 10.dp),
alignment = Alignment.Center,
contentScale = ContentScale.FillWidth)
}
)), lineHeight = 23.sp, color = Color.White, fontFamily = HelveticaNeue, fontSize = 18.sp, fontWeight = FontWeight.Medium)
The result would look like this: