Skip to content

Instantly share code, notes, and snippets.

@stevdza-san
Last active September 6, 2024 09:40
Show Gist options
  • Save stevdza-san/ff9dbec0e072d8090e1e6d16e6b73c91 to your computer and use it in GitHub Desktop.
Save stevdza-san/ff9dbec0e072d8090e1e6d16e6b73c91 to your computer and use it in GitHub Desktop.
Embedd a Hyperlink within a Text using Jetpack Compose.
import androidx.compose.foundation.text.ClickableText
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.TextUnit
@Composable
fun HyperlinkText(
modifier: Modifier = Modifier,
fullText: String,
linkText: List<String>,
linkTextColor: Color = Color.Blue,
linkTextFontWeight: FontWeight = FontWeight.Medium,
linkTextDecoration: TextDecoration = TextDecoration.Underline,
hyperlinks: List<String> = listOf("https://stevdza-san.com"),
fontSize: TextUnit = TextUnit.Unspecified
) {
val annotatedString = buildAnnotatedString {
append(fullText)
linkText.forEachIndexed { index, link ->
val startIndex = fullText.indexOf(link)
val endIndex = startIndex + link.length
addStyle(
style = SpanStyle(
color = linkTextColor,
fontSize = fontSize,
fontWeight = linkTextFontWeight,
textDecoration = linkTextDecoration
),
start = startIndex,
end = endIndex
)
addStringAnnotation(
tag = "URL",
annotation = hyperlinks[index],
start = startIndex,
end = endIndex
)
}
addStyle(
style = SpanStyle(
fontSize = fontSize
),
start = 0,
end = fullText.length
)
}
val uriHandler = LocalUriHandler.current
ClickableText(
modifier = modifier,
text = annotatedString,
onClick = {
annotatedString
.getStringAnnotations("URL", it, it)
.firstOrNull()?.let { stringAnnotation ->
uriHandler.openUri(stringAnnotation.item)
}
}
)
}
@hellosagar
Copy link

Thanks for sharing the code, appreciate that.

I made it a bit more expressive using Maps

Usage

HyperlinkText(
            fullText = "By using our services are agreeing to our\n" + "Terms and Privacy statement",
            hyperLinks = mutableMapOf(
                "Terms" to "https://google.com",
                "Privacy statement" to "https://google.com"
            ),
            textStyle = TextStyle(
                textAlign = TextAlign.Center,
                color = Gray
            ),
            linkTextColor = Purple,
            fontSize = 18.sp
        )

New Implementation

@Composable
fun HyperlinkText(
    modifier: Modifier = Modifier,
    fullText: String,
    hyperLinks: Map<String, String>,
    textStyle: TextStyle = TextStyle.Default,
    linkTextColor: Color = Color.Blue,
    linkTextFontWeight: FontWeight = FontWeight.Normal,
    linkTextDecoration: TextDecoration = TextDecoration.None,
    fontSize: TextUnit = TextUnit.Unspecified
) {
    val annotatedString = buildAnnotatedString {
        append(fullText)

        for((key, value) in hyperLinks){

            val startIndex = fullText.indexOf(key)
            val endIndex = startIndex + key.length
            addStyle(
                style = SpanStyle(
                    color = linkTextColor,
                    fontSize = fontSize,
                    fontWeight = linkTextFontWeight,
                    textDecoration = linkTextDecoration
                ),
                start = startIndex,
                end = endIndex
            )
            addStringAnnotation(
                tag = "URL",
                annotation = value,
                start = startIndex,
                end = endIndex
            )
        }
        addStyle(
            style = SpanStyle(
                fontSize = fontSize
            ),
            start = 0,
            end = fullText.length
        )
    }

    val uriHandler = LocalUriHandler.current

    ClickableText(
        modifier = modifier,
        text = annotatedString,
        style = textStyle,
        onClick = {
            annotatedString
                .getStringAnnotations("URL", it, it)
                .firstOrNull()?.let { stringAnnotation ->
                    uriHandler.openUri(stringAnnotation.item)
                }
        }
    )
}

@lekeCoder
Copy link

lekeCoder commented Feb 19, 2023

useful composable. Thanks.

It's best to wrap uriHandler.openUri(stringAnnotation.item) in a try-catch because it throws exception if no activity is found to handle the intent.

try {
 uriHandler.openUri(stringAnnotation.item)
} catch(e: Exception) {
 // handle it 
}

@IvanovIgorBelarus
Copy link

IvanovIgorBelarus commented Jul 14, 2023

If you have to use @StringRes:

@Composable
fun HyperlinkText(
    modifier: Modifier = Modifier,
    @StringRes fullTextResId: Int,
    linksActions: List<String>,
    hyperLinks: List<String>,
    textStyle: TextStyle = TextStyle.Default,
    linkTextColor: Color = Color.Blue,
    linkTextFontWeight: FontWeight = FontWeight.Normal,
    linkTextDecoration: TextDecoration = TextDecoration.None,
    fontSize: TextUnit = TextUnit.Unspecified
) {
    val fullText = LocalContext.current.getText(fullTextResId).toSpannable()
    val annotations = fullText.getSpans(0, fullText.length, Annotation::class.java)

    val annotatedString = buildAnnotatedString {
        append(fullText)
        linksActions.forEachIndexed { index, actionAnnotation ->
            annotations?.find { it.value == actionAnnotation }?.let {
                addStyle(
                    style = SpanStyle(
                        color = linkTextColor,
                        fontSize = fontSize,
                        fontWeight = linkTextFontWeight,
                        textDecoration = linkTextDecoration
                    ),
                    start = fullText.getSpanStart(it),
                    end = fullText.getSpanEnd(it)
                )
                addStringAnnotation(
                    tag = "URL",
                    annotation = hyperLinks.get(index),
                    start = fullText.getSpanStart(it),
                    end = fullText.getSpanEnd(it)
                )
            }
            addStyle(
                style = SpanStyle(
                    fontSize = fontSize
                ),
                start = 0,
                end = fullText.length
            )
        }
    }

    val uriHandler = LocalUriHandler.current

    ClickableText(
        modifier = modifier,
        text = annotatedString,
        style = textStyle,
        onClick = {
            annotatedString
                .getStringAnnotations("URL", it, it)
                .firstOrNull()?.let { stringAnnotation ->
                    uriHandler.openUri(stringAnnotation.item)
                }
        }
    )
}

Usage:

HyperlinkText(
    modifier = Modifier.fillMaxWidth(),
    fullTextResId = R.string.text_for_link,
    linksActions = listOf("LINK"),
    hyperLinks = listOf("https://google.com")
)

<string name="text_for_link">With text contain a  <annotation type="LINK">link</annotation></string>

if you need a few links:

HyperlinkText(
    modifier = Modifier.fillMaxWidth(),
    fullTextResId = R.string.text_for_link,
    linksActions = listOf("LINK1, LINK2"),
    hyperLinks = listOf("https://google.com", "https://github.com")
)

<string name="text_for_link">With text contain a  <annotation type="LINK1">link1</annotation> and you can paste <annotation type="LINK2">link2</annotation></string>

@lamtech58
Copy link

Why the onClick callback is never called using the top composable exemple:

@composable
fun HyperlinkText(
modifier: Modifier = Modifier,
fullText: String,
linkText: List,
linkTextColor: Color = Color.Blue,
linkTextFontWeight: FontWeight = FontWeight.Medium,
linkTextDecoration: TextDecoration = TextDecoration.Underline,
hyperlinks: List = listOf(),
fontSize: TextUnit = 24.sp
)

@Nike2406
Copy link

Cant find annotation key or value

@iamkingalvarado
Copy link

This implementation as many others online does not work well with Accesibility. TalkBack is not reading each link individually and by that they are not triggered. I'm working on this and if I have a solution for it in Compose only i'll share it here. In the meantime you can maybe can be aware of it too :)

@shivamparihar12
Copy link

Now that ClickableText in Jetpack Compose is deprecated, this is my implementation, of course inspired by @stevdza-san's solution.

import androidx.compose.foundation.text.LinkAnnotation
import androidx.compose.foundation.text.TextLinkStyles
import androidx.compose.foundation.text.withLink
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.TextUnit

@Composable
fun HyperlinkText(
    modifier: Modifier = Modifier,
    text: String,
    linkText: List<String>,
    hyperlinks: List<String>,
    linkTextColor: Color = MaterialTheme.colorScheme.primary,
    linkTextFontWeight: FontWeight = FontWeight.Normal,
    linkTextDecoration: TextDecoration = TextDecoration.Underline,
    fontSize: TextUnit = TextUnit.Unspecified,
    fontFamily: FontFamily = FontFamily.Monospace
) {
    val uriHandler = LocalUriHandler.current

    val annotatedString = buildAnnotatedString {
        var lastIndex = 0
        linkText.forEachIndexed { index, link ->
            val startIndex = text.indexOf(link, lastIndex)
            val endIndex = startIndex + link.length

            if (startIndex > lastIndex) append(text.substring(lastIndex, startIndex))

            val linkUrL = LinkAnnotation.Url(
                hyperlinks[index], TextLinkStyles(
                    SpanStyle(
                        color = linkTextColor,
                        fontSize = fontSize,
                        fontWeight = linkTextFontWeight,
                        textDecoration = linkTextDecoration,
                        fontFamily = fontFamily
                    )
                )
            ) {
                val url = (it as LinkAnnotation.Url).url
                uriHandler.openUri(url)
            }
            withLink(linkUrL) { append(link) }
            append(" ")
            lastIndex = endIndex + 1
        }
        if (lastIndex < text.length) {
            append(text.substring(lastIndex))
        }
        addStyle(
            style = SpanStyle(
                fontSize = fontSize, fontFamily = fontFamily
            ), start = 0, end = text.length
        )
    }
    Text(text = annotatedString, modifier = modifier)
} 

@TDB-star
Copy link

TDB-star commented Sep 6, 2024

Thanks 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment