skip to Main Content

Until recently, I had always used XML’s to create my screens, and I knew how to make certain TextViews clickable with those XML’s.

Now, I’m transitioning to exclusively use Jetpack Compose, and no longer depend on XML.

What I’m currently trying to do is have one part of a text have a keyword that (if clicked) will let you go to another screen on the app.

For example, if my text says "Click here to go to the second screen", then when you click on "here" it should take you to the other screen. I looked at resources online for Compose, which have suggested using ClickableText, but this makes the whole sentence clickable while just making "here" a different color, which is not what I want.

For compose, what would be the best way for me to have a Text which can have the sentence above but only have one word ("here") clickable and do the click acion?

2

Answers


  1. You can achieve above thing by buildAnnotatedString. Define your text here and pass in clickable text

        @Composable
        fun SpannableTextScreen() {
        
            val text = buildAnnotatedString {
                append("Click ")
                pushStringAnnotation(tag = "click", annotation = "click")
                withStyle(
                    SpanStyle(
                        textDecoration = TextDecoration.Underline,
                        color = Color.Red
                    )
                ) {
                    append("here ")
                }
                pop()
                append("to go to the second screen")
            }
            val context = LocalContext.current
        
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(horizontal = 20.dp),
                contentAlignment = Alignment.Center
            ) {
                ClickableText(text = text, onClick = { offset ->
                    text.getStringAnnotations(tag = "click", start = offset, end = offset).firstOrNull()
                        ?.let {
                            // on click operation here
                            Toast.makeText(context, "hey", Toast.LENGTH_SHORT).show()
                        }
                })
            }
        
        }
    

    only here text is clickable , Now when you click on here a toast will show , you can modify according to your requirement

    enter image description here

    Login or Signup to reply.
  2. The correct answer is already set, i’ll just try to expand it and make it more reusable, you can do something like this and create your own composable:

    First, a text that can be highlighted:

    @Composable
    fun HighlightedText(
        text: String,
        highlightedSentences: List<String>,
        normalTextSpanStyle: SpanStyle = LocalTextStyle.current.toSpanStyle()
            .copy(color = LocalContentColor.current),
        highlightedSentencesTextSpanStyle: SpanStyle = normalTextSpanStyle
            .copy(
                color = LocalTextSelectionColors.current.handleColor,
                background = LocalTextSelectionColors.current.backgroundColor
            ),
        ignoreCase: Boolean = true,
        content: (@Composable (AnnotatedString) -> Unit)
    ) {
    
        val highlightedSentencesFiltered =
            highlightedSentences.filter { it.trim().isNotBlank() }.distinct()
    
        var annotatedString = buildAnnotatedString {
            withStyle(style = normalTextSpanStyle) {
                append(text)
            }
        }
    
        highlightedSentencesFiltered.forEach { highlightString ->
    
            annotatedString = buildAnnotatedString {
    
                var currentRange = (0..0)
                var lastAnnotationSizeAdded = 0
    
                annotatedString.windowed(
                    highlightString.length,
                    step = 1,
                    partialWindows = true
                ) { windowChars ->
    
                    currentRange = (currentRange.last..currentRange.last + 1)
    
                    if (lastAnnotationSizeAdded > 0) {
                        lastAnnotationSizeAdded--
                        return@windowed
                    }
    
                    if (windowChars.first().toString().isBlank()) {
                        withStyle(style = normalTextSpanStyle) {
                            append(windowChars.first())
                        }
                        return@windowed
                    }
    
                    val existingAnnotationsInRange =
                        annotatedString.getStringAnnotations(currentRange.first, currentRange.last)
                    if (existingAnnotationsInRange.isNotEmpty()) {
                        existingAnnotationsInRange.forEach { existingAnnotation ->
                            withStyle(style = highlightedSentencesTextSpanStyle) {
                                pushStringAnnotation(
                                    tag = existingAnnotation.tag,
                                    annotation = existingAnnotation.item
                                )
                                append(existingAnnotation.item)
                                lastAnnotationSizeAdded += existingAnnotation.item.length
                            }
                        }
                        lastAnnotationSizeAdded -= 1
                        return@windowed
                    }
    
                    if ((ignoreCase && windowChars.toString()
                            .uppercase() == highlightString.uppercase())
                        || (!ignoreCase && windowChars.toString() == highlightString)
                    ) {
                        withStyle(style = highlightedSentencesTextSpanStyle) {
                            pushStringAnnotation(
                                tag = windowChars.toString(),
                                annotation = windowChars.toString()
                            )
                            append(windowChars.toString())
                            lastAnnotationSizeAdded += windowChars.length - 1
                        }
                        return@windowed
                    }
    
                    withStyle(style = normalTextSpanStyle) {
                        append(windowChars.first())
                    }
    
                }
            }
    
        }
        content(annotatedString)
    }
    

    Then a Highlighted text that can be clicked:

    @Composable
    fun MyClickableText(
        modifier: Modifier = Modifier,
        text: String,
        clickableParts: Map<String, (String) -> Unit>,
        normalTextSpanStyle: SpanStyle = LocalTextStyle.current.toSpanStyle()
            .copy(color = LocalContentColor.current),
        clickableTextSpanStyle: SpanStyle = normalTextSpanStyle.copy(color = Color.Blue)
    ) {
        HighlightedText(
            text = text,
            highlightedSentences = clickableParts.keys.toList(),
            normalTextSpanStyle = normalTextSpanStyle,
            highlightedSentencesTextSpanStyle = clickableTextSpanStyle
        ) {
            ClickableText(
                modifier = modifier,
                text = it,
                style = MaterialTheme.typography.bodyMedium,
                onClick = { offset ->
                    it.getStringAnnotations(offset, offset)
                        .firstOrNull()?.let { span ->
                            clickableParts[span.tag]?.invoke(span.tag)
                        }
                })
        }
    }
    

    With that you will be able to tell wich words/sentences you want to be clickable and the actions it should call when clicked as well as highlight sentences, in case you need for filtering uses for instance.

    Using as a clickable text:

    enter image description here

    Using as a highlight tool:

    enter image description here

    Hope it helps.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search