SUBSCRIBE NOW
avatar
I always learn something just by skimming it that makes me want to bookmark the issue now and dig deeper later
SUBSCRIBE NOW
avatar
Keep up the good work with the newsletter 💪 I really enjoy it
SUBSCRIBE NOW
avatar
Dispatch is a must read for Android devs today and my go-to for keeping up with all things Jetpack Compose
SUBSCRIBE NOW
avatar
Dispatch has been my go-to resource as it's packed with useful information while being fun at the same time
SUBSCRIBE NOW
avatar
The content is light, fun, and still useful. I especially appreciate the small tips that are in each issue
SUBSCRIBE NOW
avatar
I truly love this newsletter ❤️‍🔥 Spot on content and I know there's a lot of effort that goes behind it
SUBSCRIBE NOW
avatar
Thanks for taking the time and energy to do it so well
JetpackCompose.app's Newsletter
avatar
I always learn something just by skimming it that makes me want to bookmark the issue now and dig deeper later
JetpackCompose.app's Newsletter
avatar
Keep up the good work with the newsletter 💪 I really enjoy it
JetpackCompose.app's Newsletter
avatar
Dispatch is a must read for Android devs today and my go-to for keeping up with all things Jetpack Compose
JetpackCompose.app's Newsletter
avatar
Dispatch has been my go-to resource as it's packed with useful information while being fun at the same time
JetpackCompose.app's Newsletter
avatar
The content is light, fun, and still useful. I especially appreciate the small tips that are in each issue
JetpackCompose.app's Newsletter
avatar
I truly love this newsletter ❤️‍🔥 Spot on content and I know there's a lot of effort that goes behind it
JetpackCompose.app's Newsletter
avatar
Thanks for taking the time and energy to do it so well

Linked Text

Author: Tad Fisher

Jetpack Compose Component that styles and handles clickable links which are part of the text that's passed to it. Here's how you use it

val text = buildAnnotatedString {
    withLink("https://www.google.com") {
        append("Google")
    }
}

LinkedText(
    text = text,
    onClick = { url -> onBrowseUrl(url) }
)

import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.material.ContentAlpha
import androidx.compose.material.LocalContentAlpha
import androidx.compose.material.LocalContentColor
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.ripple.LocalRippleTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ExperimentalComposeApi
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.semantics.AccessibilityAction
import androidx.compose.ui.semantics.SemanticsPropertyKey
import androidx.compose.ui.semantics.SemanticsPropertyReceiver
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withAnnotation
private const val LinkTag = "Link"
val ClickLink = SemanticsPropertyKey<AccessibilityAction<(index: Int) -> Boolean>>(
name = "ClickLink",
mergePolicy = { parentValue, childValue ->
AccessibilityAction(
parentValue?.label ?: childValue.label,
parentValue?.action ?: childValue.action
)
}
)
fun SemanticsPropertyReceiver.onClickLink(
label: String? = null,
action: ((Int) -> Boolean)?
) {
this[ClickLink] = AccessibilityAction(label, action)
}
@Composable
fun LinkedText(
text: AnnotatedString,
modifier: Modifier = Modifier,
enabled: Boolean = true,
color: Color = LocalContentColor.current.copy(LocalContentAlpha.current),
style: TextStyle = LocalTextStyle.current,
softWrap: Boolean = true,
overflow: TextOverflow = TextOverflow.Clip,
maxLines: Int = Int.MAX_VALUE,
onTextLayout: (TextLayoutResult) -> Unit = {},
linkColors: LinkColors = LinkedTextDefaults.linkColors(),
onClick: (String) -> Unit
) {
var layoutResult: TextLayoutResult? by remember { mutableStateOf(null) }
var pressedIndex: Int by remember { mutableStateOf(-1) }
val annotations = text.getLinkAnnotations(0, text.length)
val styledText = AnnotatedString.Builder(text).apply {
for ((i, ann) in annotations.withIndex()) {
val textColor = linkColors.textColor(enabled, i == pressedIndex).value
val backgroundColor = linkColors.backgroundColor(enabled, i == pressedIndex).value
addStyle(
SpanStyle(color = textColor, background = backgroundColor),
start = ann.start,
end = ann.end
)
}
}.toAnnotatedString()
val pressIndicator = Modifier.pointerInput(onClick) {
detectTapGestures(
onPress = { pos ->
layoutResult?.getOffsetForPosition(pos)?.let { offset ->
val index = annotations.indexOfFirst {
it.start <= offset && it.end >= offset
}
pressedIndex = index
if (index >= 0) {
tryAwaitRelease()
pressedIndex = -1
}
}
},
onTap = { pos ->
layoutResult?.getOffsetForPosition(pos)?.let { offset ->
annotations.firstOrNull { it.start <= offset && it.end >= offset }
?.item
?.let { url -> onClick(url) }
}
}
)
}
val actionSemantics = Modifier.semantics {
onClickLink { index ->
annotations.getOrNull(index)?.let {
onClick(it.item)
}
true
}
}
Text(
text = styledText,
modifier = modifier
.then(pressIndicator)
.then(actionSemantics),
color = color,
style = style,
softWrap = softWrap,
overflow = overflow,
maxLines = maxLines,
onTextLayout = { result ->
layoutResult = result
onTextLayout(result)
}
)
}
fun AnnotatedString.getLinkAnnotations(start: Int, end: Int): List<AnnotatedString.Range<String>> =
getStringAnnotations(LinkTag, start, end)
fun AnnotatedString.Builder.addLink(
url: String,
start: Int,
end: Int
) = addStringAnnotation(LinkTag, url, start, end)
@OptIn(ExperimentalComposeApi::class, ExperimentalTextApi::class)
fun <R : Any> AnnotatedString.Builder.withLink(
url: String,
block: AnnotatedString.Builder.() -> R
): R = withAnnotation(LinkTag, url, block)
object LinkedTextDefaults {
@Composable
fun linkColors(
textColor: Color = MaterialTheme.colors.primary,
disabledTextColor: Color = textColor.copy(ContentAlpha.disabled),
pressedTextColor: Color = textColor,
backgroundColor: Color = Color.Unspecified,
disabledBackgroundColor: Color = backgroundColor,
pressedBackgroundColor: Color = textColor.copy(
alpha = LocalRippleTheme.current.rippleAlpha().pressedAlpha
)
): LinkColors = DefaultLinkColors(
textColor = textColor,
disabledTextColor = disabledTextColor,
pressedTextColor = pressedTextColor,
backgroundColor = backgroundColor,
disabledBackgroundColor = disabledBackgroundColor,
pressedBackgroundColor = pressedBackgroundColor
)
}
interface LinkColors {
@Composable
fun textColor(enabled: Boolean, isPressed: Boolean): State<Color>
@Composable
fun backgroundColor(enabled: Boolean, isPressed: Boolean): State<Color>
}
@Immutable
private data class DefaultLinkColors(
private val textColor: Color,
private val disabledTextColor: Color,
private val pressedTextColor: Color,
private val backgroundColor: Color,
private val disabledBackgroundColor: Color,
private val pressedBackgroundColor: Color
) : LinkColors {
@Composable
override fun textColor(enabled: Boolean, isPressed: Boolean): State<Color> =
animateColorAsState(
when {
!enabled -> disabledTextColor
isPressed -> pressedTextColor
else -> textColor
}
)
@Composable
override fun backgroundColor(enabled: Boolean, isPressed: Boolean): State<Color> =
animateColorAsState(
when {
!enabled -> disabledBackgroundColor
isPressed -> pressedBackgroundColor
else -> backgroundColor
}
)
}
view raw LinkedText.kt hosted with ❤ by GitHub

Have a project you'd like to submit? Fill this form, will ya!

If you like this snippet, you might also like:

Maker OS is an all-in-one productivity system for developers

I built Maker OS to track, manage & organize my life. Now you can do it too!