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.
Related
How can I change the typography of a text with animation in jetpack compose?
try this code. i think it works
val rem = remember {mutableStateOf(true)}
Button(onClick = {rem.value = !rem.value})
{(text = "animate text!")}
val animRem = animateFloatAsState(targetValue = if (rem.value) 1f else .1f)
val decoratedLabel: #Composable (() -> Unit) =
#Composable {
val labelAnimatedStyle = lerp(
MaterialTheme.typography.subtitle1,
MaterialTheme.typography.caption,
animRem.value
)
Decoration2(
contentColor = TextFieldDefaults.textFieldColors()
.labelColor(
true,
// if label is used as a placeholder (aka not as a small header
// at the top), we don't use an error color
rem.value,
remember { MutableInteractionSource() }
).value,
typography = labelAnimatedStyle,
content = { Text(text = "label") }
)
}
decoratedLabel()
you should duplicate this function. because it is an internal function and you can't use it in your code
#Composable fun Decoration(
contentColor: Color,
typography: TextStyle? = null,
contentAlpha: Float? = null,
content: #Composable () -> Unit) {
val colorAndEmphasis: #Composable () -> Unit = #Composable {
CompositionLocalProvider(LocalContentColor provides contentColor) {
if (contentAlpha != null) {
CompositionLocalProvider(
LocalContentAlpha provides contentAlpha,
content = content
)
} else {
CompositionLocalProvider(
LocalContentAlpha provides contentColor.alpha,
content = content
)
}
}
}
if (typography != null) ProvideTextStyle(typography, colorAndEmphasis) else colorAndEmphasis()}
There is requirements to create ChipGroup with maximum lines/rows allowed, for all items that are not shown there should be last chip with text like "+3 more items".
I have tried FlowRow from accompanist, which works fine, but number of rows cannot be limited there (or I don't know how to :))
Another option I have tried is to implement custom layout. I have managed to limit number of rows (implementation similar to FlowRow with additional conditions), but I'm not sure how to append last item from layout(...placementBlock: Placeable.PlacementScope.() -> Unit)
Any help is appreciated
In such cases SubcomposeLayout should be used: it allows you to insert a composable depending on already measured views.
Applying this to FlowRow would take more time, because of many other arguments, so I've took my own simplified variant as the base.
#Composable
fun ChipVerticalGrid(
modifier: Modifier = Modifier,
spacing: Dp,
moreItemsView: #Composable (Int) -> Unit,
content: #Composable () -> Unit,
) {
SubcomposeLayout(
modifier = modifier
) { constraints ->
val contentConstraints = constraints.copy(minWidth = 0, minHeight = 0)
var currentRow = 0
var currentOrigin = IntOffset.Zero
val spacingValue = spacing.toPx().toInt()
val mainMeasurables = subcompose("content", content)
val placeables = mutableListOf<Pair<Placeable, IntOffset>>()
for (i in mainMeasurables.indices) {
val measurable = mainMeasurables[i]
val placeable = measurable.measure(contentConstraints)
fun Placeable.didOverflowWidth() =
currentOrigin.x > 0f && currentOrigin.x + width > contentConstraints.maxWidth
if (placeable.didOverflowWidth()) {
currentRow += 1
val nextRowOffset = currentOrigin.y + placeable.height + spacingValue
if (nextRowOffset + placeable.height > contentConstraints.maxHeight) {
var morePlaceable: Placeable
do {
val itemsLeft = mainMeasurables.count() - placeables.count()
morePlaceable = subcompose(itemsLeft) {
moreItemsView(itemsLeft)
}[0].measure(contentConstraints)
val didOverflowWidth = morePlaceable.didOverflowWidth()
if (didOverflowWidth) {
val removed = placeables.removeLast()
currentOrigin = removed.second
}
} while (didOverflowWidth)
placeables.add(morePlaceable to currentOrigin)
break
}
currentOrigin = currentOrigin.copy(x = 0, y = nextRowOffset)
}
placeables.add(placeable to currentOrigin)
currentOrigin = currentOrigin.copy(x = currentOrigin.x + placeable.width + spacingValue)
}
layout(
width = maxOf(constraints.minWidth, placeables.maxOfOrNull { it.first.width + it.second.x } ?: 0),
height = maxOf(constraints.minHeight, placeables.lastOrNull()?.run { first.height + second.y } ?: 0),
) {
placeables.forEach {
val (placeable, origin) = it
placeable.place(origin.x, origin.y)
}
}
}
}
Usage:
val words = LoremIpsum().values.first().split(" ").map { it.filter { it.isLetter()} }
val itemView = #Composable { text: String ->
Text(
text,
modifier = Modifier
.background(color = Color.Gray, shape = CircleShape)
.padding(vertical = 3.dp, horizontal = 5.dp)
)
}
ChipVerticalGrid(
spacing = 7.dp,
moreItemsView = {
itemView("$it more items")
},
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.padding(7.dp)
) {
words.forEach { word ->
itemView(word)
}
}
Result:
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,
)
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"
)