Skip to content

Instantly share code, notes, and snippets.

@stevdza-san
Last active May 6, 2024 11:14
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 :)

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