Jetpack Compose Text hyperlink some section of the text
Asked Answered
P

13

97

How can i add hyperlink to some section of the text of Text component?

With buildAnnotatedString i can set link section blue and underlined as in image below, but how can i also turn that section into link?

enter image description here

   val annotatedLinkString = buildAnnotatedString {
        val str = "Click this link to go to web site"
        val startIndex = str.indexOf("link")
        val endIndex = startIndex + 4
        append(str)
        addStyle(
            style = SpanStyle(
                color = Color(0xff64B5F6),
                textDecoration = TextDecoration.Underline
            ), start = startIndex, end = endIndex
        )
    }

    Text(
        modifier = modifier
            .padding(16.dp)
            .fillMaxWidth(),
        text = annotatedLinkString
    )

I can also get Spanned but is there any way to use it with Text?

val str: Spanned = HtmlCompat.fromHtml(
    "<a href=\"http://www.github.com\">Github</a>", HtmlCompat.FROM_HTML_MODE_LEGACY
)
Prosaic answered 4/1, 2021 at 17:52 Comment(1)
Also consider size of touch target: slack-chats.kotlinlang.org/t/2651976Dervish
S
8

How can i add hyperlink to some section of the text of Text component?

with(AnnotatedString.Builder()) {
    append("link: Jetpack Compose")
    // attach a string annotation that stores a URL to the text "Jetpack Compose".
    addStringAnnotation(
        tag = "URL",
        annotation = "https://developer.android.com/jetpack/compose",
        start = 6,
        end = 21
    )
}

tag:The tag used to distinguish annotations

annotation: The string annotation that is attached

start: The inclusive starting offset of the range

end: The exclusive end offset of the

Source

Sasin answered 4/1, 2021 at 19:15 Comment(5)
What are other tags other than "URL"?Prosaic
Sorry. I misunderstood. I also just learned. It takes 4 parameters. Thank you for your nice question.Garlen
I tried this with annotatedString() and set to Text, added internet permission to manifest but it's not working, i mean nothing is happening when you touch the Text. Would mind checking that out?Prosaic
You will need to use a url handler.Garlen
How to make those start and end numbers not hardcoded?Infinitive
H
100

The marked answer confuses novices, I give a complete example

Please don't forget to end pushStringAnnotation with pop()

val annotatedString = buildAnnotatedString {
    append("By joining, you agree to the ")

    pushStringAnnotation(tag = "policy", annotation = "https://google.com/policy")
    withStyle(style = SpanStyle(color = MaterialTheme.colors.primary)) {
        append("privacy policy")
    }
    pop()

    append(" and ")

    pushStringAnnotation(tag = "terms", annotation = "https://google.com/terms")

    withStyle(style = SpanStyle(color = MaterialTheme.colors.primary)) {
        append("terms of use")
    }

    pop()
}

ClickableText(text = annotatedString, style = MaterialTheme.typography.body1, onClick = { offset ->
    annotatedString.getStringAnnotations(tag = "policy", start = offset, end = offset).firstOrNull()?.let {
        Log.d("policy URL", it.item)
    }

    annotatedString.getStringAnnotations(tag = "terms", start = offset, end = offset).firstOrNull()?.let {
        Log.d("terms URL", it.item)
    }
})

final effect

enter image description here

If you need #tags and @mentions refer to my other answer

enter image description here

Hambrick answered 13/10, 2021 at 4:54 Comment(4)
Nice! You just forgot to call pop() after the first pushStringAnnotation and withStyle method pair.Macro
is there a way to add some selected state/ ripple effect for such a view? right now it looks fully staticSpell
what is a pop()? why do we need this here?Genesa
This is close, but I think hyperlinks usually change the mouse cursor when you hover over them, and ClickableText doesn't seem to do this out of the box. :(Illumine
P
73

For a complete answer you can use ClickableText which returns the position of text, and UriHandler to open URI in a browser.

val annotatedLinkString: AnnotatedString = buildAnnotatedString {

    val str = "Click this link to go to web site"
    val startIndex = str.indexOf("link")
    val endIndex = startIndex + 4
    append(str)
    addStyle(
        style = SpanStyle(
            color = Color(0xff64B5F6),
            fontSize = 18.sp,
            textDecoration = TextDecoration.Underline
        ), start = startIndex, end = endIndex
    )

    // attach a string annotation that stores a URL to the text "link"
    addStringAnnotation(
        tag = "URL",
        annotation = "https://github.com",
        start = startIndex,
        end = endIndex
    )

}

// UriHandler parse and opens URI inside AnnotatedString Item in Browse
val uriHandler = LocalUriHandler.current

// 🔥 Clickable text returns position of text that is clicked in onClick callback
ClickableText(
    modifier = modifier
        .padding(16.dp)
        .fillMaxWidth(),
    text = annotatedLinkString,
    onClick = {
        annotatedLinkString
            .getStringAnnotations("URL", it, it)
            .firstOrNull()?.let { stringAnnotation ->
                uriHandler.openUri(stringAnnotation.item)
            }
    }
)
Prosaic answered 10/1, 2021 at 17:36 Comment(10)
How can I make this work with string resources, seems like a great approach for hardcoded strings.Spinoff
@GuanacoDevs check out my answer below!Shepley
@ChantellOsejo That seems to be a way to go, you may gain a more control. However this answer led to me a more simplified way.Spinoff
Great answer. Just want to point out that if you need to simply run a function (like to move to another fragment) in your app, you can omit the addStringAnnotation and just directly call the function from your onClick method in the ClickableText constructor.Corded
@Prosaic It would be hard to calculate start end index if we have multiple locale in app. Do you know how can i achieve that in that case?Vauntcourier
I understand it's just an example, but you mixed up str.indexOf and hardcoded index - 4, just choose one wayTheatricalize
@ArpitPatel it's a late comment but what kind of problems do you expect? If using constants is a problem getting all strings from strings.xml can solve it I thinkChandelier
So I have two strings 1) English & 2) French I would like to apply URL to certain text which will be different index for both Locale. In that case it's hard to calculate index and apply Hyperlink.Vauntcourier
UriHandler doesn't care about ActivityNotFoundException (an user doesn't have a browser), keep in mind.Pomace
@DaniilPavlenko thanks for this info. It's very importantProsaic
L
23

For anyone looking for a reusable copy-paste solution,

Create a new file LinkText.kt and copy-paste this code,

data class LinkTextData(
    val text: String,
    val tag: String? = null,
    val annotation: String? = null,
    val onClick: ((str: AnnotatedString.Range<String>) -> Unit)? = null,
)

@Composable
fun LinkText(
    linkTextData: List<LinkTextData>,
    modifier: Modifier = Modifier,
) {
    val annotatedString = createAnnotatedString(linkTextData)

    ClickableText(
        text = annotatedString,
        style = MaterialTheme.typography.body1,
        onClick = { offset ->
            linkTextData.forEach { annotatedStringData ->
                if (annotatedStringData.tag != null && annotatedStringData.annotation != null) {
                    annotatedString.getStringAnnotations(
                        tag = annotatedStringData.tag,
                        start = offset,
                        end = offset,
                    ).firstOrNull()?.let {
                        annotatedStringData.onClick?.invoke(it)
                    }
                }
            }
        },
        modifier = modifier,
    )
}

@Composable
private fun createAnnotatedString(data: List<LinkTextData>): AnnotatedString {
    return buildAnnotatedString {
        data.forEach { linkTextData ->
            if (linkTextData.tag != null && linkTextData.annotation != null) {
                pushStringAnnotation(
                    tag = linkTextData.tag,
                    annotation = linkTextData.annotation,
                )
                withStyle(
                    style = SpanStyle(
                        color = MaterialTheme.colors.primary,
                        textDecoration = TextDecoration.Underline,
                    ),
                ) {
                    append(linkTextData.text)
                }
                pop()
            } else {
                append(linkTextData.text)
            }
        }
    }
}

Usage

LinkText(
    linkTextData = listOf(
        LinkTextData(
            text = "Icons made by ",
        ),
        LinkTextData(
            text = "smalllikeart",
            tag = "icon_1_author",
            annotation = "https://www.flaticon.com/authors/smalllikeart",
            onClick = {
                Log.d("Link text", "${it.tag} ${it.item}")
            },
        ),
        LinkTextData(
            text = " from ",
        ),
        LinkTextData(
            text = "Flaticon",
            tag = "icon_1_source",
            annotation = "https://www.flaticon.com/",
            onClick = {
                Log.d("Link text", "${it.tag} ${it.item}")
            },
        )
    ),
    modifier = Modifier
        .padding(
            all = 16.dp,
        ),
)

Screenshot,

Screenshot

Note

  1. I am handling web pages manually using a composable. Use UriHandler or other alternatives if manual control is not required.
  2. Style clickable and other text as required in LinkText.
Lotta answered 19/10, 2021 at 19:41 Comment(4)
What is the usage of tag?Arvizu
@Marat, tag is like id. It is used to identify annotation - Docs: developer.android.com/reference/kotlin/androidx/compose/ui/text/…Lotta
I see. Thank very muchArvizu
@Lotta when click the text which underlined it is not workDronski
H
14

You can use https://github.com/firefinchdev/linkify-text.

It's a single file. You can directly copy it into your project.

It uses Android's Linkify for link detection, which is same as that of TextView's autoLink.

Hickox answered 26/11, 2021 at 16:26 Comment(0)
B
10

The easiest and cleanest solution:

enter image description here

@Composable
fun AnnotatedClickableText() {
  val termsUrl = "https://example.com/terms"
  val privacyUrl = "https://example.com/privacy"
  val annotatedText = buildAnnotatedString {
    append("You agree to our ")
    withStyle(style = SpanStyle(color = Color.Blue, fontWeight = FontWeight.Bold)) {
      appendLink("Terms of Use", termsUrl)
    }
    append(" and ")
    withStyle(style = SpanStyle(color = Color.Blue, fontWeight = FontWeight.Bold)) {
      appendLink("Privacy Policy", privacyUrl)
    }
  }

  ClickableText(
    text = annotatedText,
    onClick = { offset ->
      annotatedText.onLinkClick(offset) { link ->
        println("Clicked URL: $link")
        // Open link in WebView.
      }
    }
  )
}

fun AnnotatedString.Builder.appendLink(linkText: String, linkUrl: String) {
  pushStringAnnotation(tag = linkUrl, annotation = linkUrl)
  append(linkText)
  pop()
}

fun AnnotatedString.onLinkClick(offset: Int, onClick: (String) -> Unit) {
  getStringAnnotations(start = offset, end = offset).firstOrNull()?.let {
    onClick(it.item)
  }
}

Notice the 2 extension functions which make link creation much more simple.

Berneta answered 19/11, 2022 at 18:16 Comment(0)
S
8

How can i add hyperlink to some section of the text of Text component?

with(AnnotatedString.Builder()) {
    append("link: Jetpack Compose")
    // attach a string annotation that stores a URL to the text "Jetpack Compose".
    addStringAnnotation(
        tag = "URL",
        annotation = "https://developer.android.com/jetpack/compose",
        start = 6,
        end = 21
    )
}

tag:The tag used to distinguish annotations

annotation: The string annotation that is attached

start: The inclusive starting offset of the range

end: The exclusive end offset of the

Source

Sasin answered 4/1, 2021 at 19:15 Comment(5)
What are other tags other than "URL"?Prosaic
Sorry. I misunderstood. I also just learned. It takes 4 parameters. Thank you for your nice question.Garlen
I tried this with annotatedString() and set to Text, added internet permission to manifest but it's not working, i mean nothing is happening when you touch the Text. Would mind checking that out?Prosaic
You will need to use a url handler.Garlen
How to make those start and end numbers not hardcoded?Infinitive
C
8

If you want to use @StringRes from strings.xml file you can use the code below

enter image description here

Lets say you have the following string resources:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="disclaimer">By joining you agree to the privacy policy and terms of use.</string>
    <string name="privacy_policy">privacy policy</string>
    <string name="terms_of_use">terms of use</string>
</resources>

You can use it like this:

HighlightedText(
    text = stringResource(id = R.string.disclaimer),
    highlights = listOf(
        Highlight(
            text = stringResource(id = R.string.privacy_policy),
            data = "https://stackoverflow.com/legal/privacy-policy",
            onClick = { link ->
                // do something with link
            }
        ),
        Highlight(
            text = stringResource(id = R.string.terms_of_use),
            data = "https://stackoverflow.com/legal/terms-of-use",
            onClick = { link ->
                // do something with link
            }
        )
    )
)

Here is the source code for the Composable:

data class Highlight(
    val text: String,
    val data: String,
    val onClick: (data: String) -> Unit
)

@Composable
fun HighlightedText(
    text: String,
    highlights: List<Highlight>,
    modifier: Modifier = Modifier
) {
    data class TextData(
        val text: String,
        val tag: String? = null,
        val data: String? = null,
        val onClick: ((data: AnnotatedString.Range<String>) -> Unit)? = null
    )

    val textData = mutableListOf<TextData>()
    if (highlights.isEmpty()) {
        textData.add(
            TextData(
                text = text
            )
        )
    } else {
        var startIndex = 0
        highlights.forEachIndexed { i, link ->
            val endIndex = text.indexOf(link.text)
            if (endIndex == -1) {
                throw Exception("Highlighted text mismatch")
            }
            textData.add(
                TextData(
                    text = text.substring(startIndex, endIndex)
                )
            )
            textData.add(
                TextData(
                    text = link.text,
                    tag = "${link.text}_TAG",
                    data = link.data,
                    onClick = {
                        link.onClick(it.item)
                    }
                )
            )
            startIndex = endIndex + link.text.length
            if (i == highlights.lastIndex && startIndex < text.length) {
                textData.add(
                    TextData(
                        text = text.substring(startIndex, text.length)
                    )
                )
            }
        }
    }

    val annotatedString = buildAnnotatedString {
        textData.forEach { linkTextData ->
            if (linkTextData.tag != null && linkTextData.data != null) {
                pushStringAnnotation(
                    tag = linkTextData.tag,
                    annotation = linkTextData.data,
                )
                withStyle(
                    style = SpanStyle(
                        color = infoLinkTextColor
                    ),
                ) {
                    append(linkTextData.text)
                }
                pop()
            } else {
                append(linkTextData.text)
            }
        }
    }
    ClickableText(
        text = annotatedString,
        style = TextStyle(
            fontSize = 30.sp,
            fontWeight = FontWeight.Normal,
            color = infoTextColor,
            textAlign = TextAlign.Start
        ),
        onClick = { offset ->
            textData.forEach { annotatedStringData ->
                if (annotatedStringData.tag != null && annotatedStringData.data != null) {
                    annotatedString.getStringAnnotations(
                        tag = annotatedStringData.tag,
                        start = offset,
                        end = offset,
                    ).firstOrNull()?.let {
                        annotatedStringData.onClick?.invoke(it)
                    }
                }
            }
        },
        modifier = modifier
    )
}
Concavoconcave answered 2/9, 2022 at 21:3 Comment(0)
S
7

EDIT: There is a bug that prevents accessibility services from properly reading embedded links such as these prior to Jetpack Compose 1.3.0. Even after 1.3.0, there is another bug where the onClick() function is not called by the Accessibility Service (Talkback). See this Google Issue. I would recommend utilizing the AndroidView + old-school TextView option I outline below if your app needs to be accessible, at least until the linked issue is addressed.

--

The answers here are all great if you are using hardcoded strings, but they're not very useful for string resources. Here's some code to give you similar functionality to how old-school TextViews would work with HTML built entirely using Jetpack Compose (no interop APIs). Credit for 99% of this answer goes to the comment on this issue, which I extended to use the Android String resource Annotation tag to support URLs. [Note: BulletSpan is not currently supported in this solution as it is not needed for my use case and I didn't take the time to address its absence in the solution I extended]

const val URL_ANNOTATION_KEY = "url"

/**
 * Much of this class comes from
 * https://issuetracker.google.com/issues/139320238#comment11
 * which seeks to correct the gap in Jetpack Compose wherein HTML style tags in string resources
 * are not respected.
 */
@Composable
@ReadOnlyComposable
private fun resources(): Resources {
    return LocalContext.current.resources
}

fun Spanned.toHtmlWithoutParagraphs(): String {
    return HtmlCompat.toHtml(this, HtmlCompat.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE)
        .substringAfter("<p dir=\"ltr\">").substringBeforeLast("</p>")
}

fun Resources.getText(@StringRes id: Int, vararg args: Any): CharSequence {
    val escapedArgs = args.map {
        if (it is Spanned) it.toHtmlWithoutParagraphs() else it
    }.toTypedArray()
    val resource = SpannedString(getText(id))
    val htmlResource = resource.toHtmlWithoutParagraphs()
    val formattedHtml = String.format(htmlResource, *escapedArgs)
    return HtmlCompat.fromHtml(formattedHtml, HtmlCompat.FROM_HTML_MODE_LEGACY)
}

@Composable
fun annotatedStringResource(@StringRes id: Int, vararg formatArgs: Any): AnnotatedString {
    val resources = resources()
    val density = LocalDensity.current
    return remember(id, formatArgs) {
        val text = resources.getText(id, *formatArgs)
        spannableStringToAnnotatedString(text, density)
    }
}

@Composable
fun annotatedStringResource(@StringRes id: Int): AnnotatedString {
    val resources = resources()
    val density = LocalDensity.current
    return remember(id) {
        val text = resources.getText(id)
        spannableStringToAnnotatedString(text, density)
    }
}

private fun spannableStringToAnnotatedString(
    text: CharSequence,
    density: Density
): AnnotatedString {
    return if (text is Spanned) {
        with(density) {
            buildAnnotatedString {
                append((text.toString()))
                text.getSpans(0, text.length, Any::class.java).forEach {
                    val start = text.getSpanStart(it)
                    val end = text.getSpanEnd(it)
                    when (it) {
                        is StyleSpan -> when (it.style) {
                            Typeface.NORMAL -> addStyle(
                                style = SpanStyle(
                                    fontWeight = FontWeight.Normal,
                                    fontStyle = FontStyle.Normal
                                ),
                                start = start,
                                end = end
                            )
                            Typeface.BOLD -> addStyle(
                                style = SpanStyle(
                                    fontWeight = FontWeight.Bold,
                                    fontStyle = FontStyle.Normal
                                ),
                                start = start,
                                end = end
                            )
                            Typeface.ITALIC -> addStyle(
                                style = SpanStyle(
                                    fontWeight = FontWeight.Normal,
                                    fontStyle = FontStyle.Italic
                                ),
                                start = start,
                                end = end
                            )
                            Typeface.BOLD_ITALIC -> addStyle(
                                style = SpanStyle(
                                    fontWeight = FontWeight.Bold,
                                    fontStyle = FontStyle.Italic
                                ),
                                start = start,
                                end = end
                            )
                        }
                        is TypefaceSpan -> addStyle(
                            style = SpanStyle(
                                fontFamily = when (it.family) {
                                    FontFamily.SansSerif.name -> FontFamily.SansSerif
                                    FontFamily.Serif.name -> FontFamily.Serif
                                    FontFamily.Monospace.name -> FontFamily.Monospace
                                    FontFamily.Cursive.name -> FontFamily.Cursive
                                    else -> FontFamily.Default
                                }
                            ),
                            start = start,
                            end = end
                        )
                        is BulletSpan -> {
                            Log.d("StringResources", "BulletSpan not supported yet")
                            addStyle(style = SpanStyle(), start = start, end = end)
                        }
                        is AbsoluteSizeSpan -> addStyle(
                            style = SpanStyle(fontSize = if (it.dip) it.size.dp.toSp() else it.size.toSp()),
                            start = start,
                            end = end
                        )
                        is RelativeSizeSpan -> addStyle(
                            style = SpanStyle(fontSize = it.sizeChange.em),
                            start = start,
                            end = end
                        )
                        is StrikethroughSpan -> addStyle(
                            style = SpanStyle(textDecoration = TextDecoration.LineThrough),
                            start = start,
                            end = end
                        )
                        is UnderlineSpan -> addStyle(
                            style = SpanStyle(textDecoration = TextDecoration.Underline),
                            start = start,
                            end = end
                        )
                        is SuperscriptSpan -> addStyle(
                            style = SpanStyle(baselineShift = BaselineShift.Superscript),
                            start = start,
                            end = end
                        )
                        is SubscriptSpan -> addStyle(
                            style = SpanStyle(baselineShift = BaselineShift.Subscript),
                            start = start,
                            end = end
                        )
                        is ForegroundColorSpan -> addStyle(
                            style = SpanStyle(color = Color(it.foregroundColor)),
                            start = start,
                            end = end
                        )
                        is Annotation -> {
                            if (it.key == URL_ANNOTATION_KEY) {
                                addStyle(
                                    style = SpanStyle(color = Color.Blue),
                                    start = start,
                                    end = end
                                )
                                addUrlAnnotation(
                                    urlAnnotation = UrlAnnotation(it.value),
                                    start = start,
                                    end = end
                                )
                            }
                        }
                        else -> addStyle(style = SpanStyle(), start = start, end = end)
                    }
                }
            }
        }
    } else {
        AnnotatedString(text = text.toString())
    }
}

@Composable
fun LinkableTextView(
    @StringRes id: Int,
    modifier: Modifier = Modifier,
    style: TextStyle = MaterialTheme.typography.body1
) {
    val uriHandler = LocalUriHandler.current
    
    val annotatedString = annotatedStringResource(id)
    
    ClickableText(
        text = annotatedString,
        style = style,
        onClick = { offset ->
            annotatedString.getStringAnnotations(
                tag = "URL",
                start = offset,
                end = offset
            ).firstOrNull()?.let {
                uriHandler.openUri(it.item)
            }
        },
        modifier = modifier,
    )
}

Usage:

@Composable
fun MyComposableView {
    LinkableTextView(
        id = R.string.my_link_string
    )
}

String resource:

<string name="my_link_string">Click this
    <annotation url="https://www.stackoverflow.com">link</annotation>
    to go to web site
</string>

There is also the "dumb" way of just falling back to using android.widget.TextView which has the behavior you're seeking, and works with accessibility services properly:

@Composable
fun CompatHtmlTextView(@StringRes htmlStringResource: Int) {
    val html = stringResourceWithStyling(htmlStringResource).toString()

    AndroidView(factory = { context ->
        android.widget.TextView(context).apply {
            text = fromHtml(html)
        }
    })
}

@Composable
@ReadOnlyComposable
fun stringResWithStyling(@StringRes id: Int): CharSequence =
    LocalContext.current.resources.getText(id = id) 

/**
 * Compat method that will use the deprecated fromHtml method 
 * prior to Android N and the new one after Android N
 */
@Suppress("DEPRECATION")
fun fromHtml(html: String?): Spanned {
    return when {
        html == null -> {
            // return an empty spannable if the html is null
            SpannableString("")
        } 
        Build.VERSION.SDK_INT >= Build.VERSION_CODES.N -> {
            // FROM_HTML_MODE_LEGACY is the behaviour that was used for versions below android N
            // we are using this flag to give a consistent behaviour
            Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY)
        }
        else -> {
            Html.fromHtml(html)
        }
    }
}

For the Compat option, it's important that you retrieve the string resource as outlined so that the tags are not stripped. You must also format your string resource using CDATA tags, e.g.

<string name="text_with_link"><![CDATA[Visit 
        <a href="https://www.stackoverflow.com/">Stackoverflow</a>
        for the best answers.]]></string>

Failure to use the CDATA tag will not render the string as HTML.

Shepley answered 24/5, 2022 at 21:47 Comment(2)
This is a good solution, until an official one will be provided.Hadst
Note that the Annotation class here is android.text.Annotation. Without specifying that, it's assumed by the compiler to be kotlin.Annotation.Rick
J
3

If your concern is to only open hyperlinks, there's a dynamic approach using a HyperlinkText

Jota answered 4/8, 2022 at 9:4 Comment(0)
E
2

Use this code if you don't have hand on the input text.

 val s = buildAnnotatedString {
    for (link in txt.split(' ')) {
        if (link.matches(".*(#\\w+)|(http(s)?://.+).*".toRegex())) {
            withStyle(SpanStyle(color = Color.Cyan)) {
                append(link + ' ')
            }
        } else {
            append(link + ' ')
        }
    }
}
Text(text = s)

It could be more the # and https:// it's up to you in the regex.

Note: This is not clickable text, If you want one, Check it out the code below (Not recommended for large text).

val uri = LocalUriHandler.current

 FlowRow {
    for (s in txt.split(' ')) {
        if (s.matches(".*(#\\w+)|(http(s)?://.+).*".toRegex())) {
            ClickableText(
                text = AnnotatedString(s + ' '),
                onClick = { runCatching { uri.openUri(s) } },
                style = TextStyle(color = Color.Cyan)
            )
        } else {
            Text(text = s + ' ')
        }
    }
}

And yes you gonna need Flow_Layout accompanist .

enter image description here

Exhaustless answered 10/12, 2022 at 12:52 Comment(0)
A
2

Posting my answer for anyone looking for re-usable code.

Here is the gist for composable function you can use for Jetpack compose and Compose Multiplatform.

In Below code function Expects StringResource which is a Moko-Resource library class you can simply replace it with String if you are not using that Library.

/**
 * Composable function for displaying text with hyperlinks. It uses the ClickableText composable
 * to render hyperlinks and provides a callback for handling hyperlink clicks.
 *
 * @param stringResource A function to retrieve the text with hyperlinks as a string resource.
 *  @example string
 *      "Visit our website [here](https://www.example.com) for more information. " +
 *      "Contact us via [email](mailto:[email protected]) or call us at" +
 *      "[123-456-7890](tel:+1234567890)."
 * @param modifier The modifier for this composable.
 * @param onClick A callback function invoked when a hyperlink is clicked. It provides the clicked
 * token (URL or email) and the type of the link (Hyperlink, Mail, or Tel).

 *
 * @sample
 * ```
 * HyperlinkText(
 *     stringResource = { stringResource(R.string.sample_text_with_links) },
 *     modifier = Modifier.padding(16.dp),
 *     onClick = { token, linkType ->
 *         when (linkType) {
 *             LinkType.HYPERLINK -> openUrl(token)
 *             LinkType.MAIL -> sendEmail(token)
 *             LinkType.TEL -> callNumber(token)
 *         }
 *     }
 * )
 * ```
 */

Gist

https://gist.github.com/khalid64927/c9b87653cab5f28fe275d21d2a1af882

Ailyn answered 16/1 at 2:36 Comment(1)
This is the correct and optimal solution. However, it's worth mentioning that only markdown-style hyperlinks should be used, like this: [Click here] (google.com)Savadove
R
1

If you want to make some part of the text clickable, then the following code will work fine.

@Composable
fun SingUpText() {
    val annotatedText = buildAnnotatedString {
        val grayStyle = SpanStyle(color = Color.Gray)
        pushStyle(grayStyle)
        append("Don't have an account? ")
        pop()

        pushStringAnnotation(
            tag = "SignUp",
            annotation = "SignUp"
        )
        val style = SpanStyle(color = AppColor, fontWeight = FontWeight.Bold)

        pushStyle(style)
        append("Sign Up")
        
        pop()
    }

    ClickableText(text = annotatedText, onClick = {
        annotatedText.getStringAnnotations(
            tag = "SingUp",
            start = it,
            end = it
        ).firstOrNull().let { annotatedText ->
            Log.d("Text_Clicked", "SingUpText:text ")
        }
    })

}
Rolypoly answered 6/4, 2022 at 11:50 Comment(0)
N
1

You can use this code:

@Composable
fun AgreeConditionComponent(
    modifier: Modifier,
    value: Boolean,
    onConditionChecked: (Boolean) -> Unit,
    onConditionTextClicked: () -> Unit,
) {
    val hyperlinkText = "link"
    val annotatedString = buildAnnotatedString {
        append("Click this")
        append(" ")
        val start = length
        val end = length + hyperlinkText.length
        addStringAnnotation(tag = "terms", annotation = "", start = start, end = end)
        withStyle(
            style = SpanStyle(
                textDecoration = TextDecoration.Underline,
                color = Color(0xff64B5F6),
            ),
        ) {
            append(hyperlinkText)
        }
        append(" ")
        append("to go to web site")
    }
    Row(
        modifier = modifier,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Checkbox(
            checked = value,
            onCheckedChange = onConditionChecked,
        )

        Spacer(modifier = Modifier.width(4.dp))

        ClickableText(
            text = annotatedString,
            style = MaterialTheme.typography.subtitle1,
            onClick = { offset ->
                annotatedString.getStringAnnotations(tag = "terms", start = offset, end = offset)
                    .firstOrNull()
                    ?.let { onConditionTextClicked.invoke() }
            }
        )
    }
}

enter image description here

Nadya answered 23/10, 2022 at 16:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.