Search code examples
androidandroid-jetpack-composeandroid-compose-textfieldjetbrains-compose

How to mix selectable text and clickable text at once?


I'm trying to build a panel that contains a lot of information. Those informations must be selectable, in order to able the user to copy it. But, beyond this, this information panel contains some parts that, when clicked, must to perform something like a button click (such as navigation, alert, etc).

My problem is that, even properly building an AnnotatedString that annotates the clickable parts and setting it to a ClickableText, if I put it inside a SelectionContainer I can select the text but I can not to click the desired clickable parts (most probably due to multiple clicks handling under the hood).

To illustrate, following have an example of text that should be presented:

Date: 99/99/9999
Count: 99999
    
    max: xxxx
    min: xxxx
    mean: xxxx
...

The "xxxx" are an example of items that should be clickable.

Then, putting it to a ClickableText and, by its time, inside a SelectionContainer the text is selectable, but not clickable in the "xxxxx" parts. The AnnotatedString was built as described in here.

Note that this can be achieved by forgeting the ClickableText and building all this information panel using components separately. However I'm wondering how to do this using the existing resources of the API, such as AnnotatedString, ClickableText and SelectableContainer.

Also, if there are another better solution to do that, feel free to show us.


Solution

    1. Build your own tap gesture and consume touch on demand:

      suspend fun PointerInputScope.detectTapGestureIfMatch(
          onTap: (Offset) -> Boolean,
      ) {
          awaitEachGesture {
              awaitFirstDown()
      
              val up = waitForUpOrCancellation()
              if (up != null && onTap(up.position)) {
                  up.consume()
              }
          }
      }
      
    2. You can check out this answer on how to add tagged annotations to your text and process them when clicked. Then, if you find an annotation, you can do your action and return true so it'll be consumed, otherwise return false - and SelectableText will get that touch.

      modifier = Modifier
          .pointerInput(Unit) {
              detectTapGestureIfMatch { position ->
                  val annotation = layoutResult
                      ?.getOffsetForPosition(position)
                      ?.let { offset ->
                          annotatedString.getStringAnnotations(
                              start = offset,
                              end = offset,
                              tag = "clickableTag",
                          ).firstOrNull()
                      }
                  if (annotation != null) {
                      uriHandler.openUri(annotation.item)
                      true
                  } else {
                      false
                  }
              }
          }