diff --git a/example/src/main/java/de/charlex/compose/htmltext/example/MainActivity.kt b/example/src/main/java/de/charlex/compose/htmltext/example/MainActivity.kt
index f6a4253..8429929 100644
--- a/example/src/main/java/de/charlex/compose/htmltext/example/MainActivity.kt
+++ b/example/src/main/java/de/charlex/compose/htmltext/example/MainActivity.kt
@@ -4,11 +4,17 @@ import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
+import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
+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.platform.LocalContext
@@ -45,7 +51,24 @@ fun Greeting() {
@Composable
fun StringGreeting() {
- HtmlText(text = "Hello World. This textsentence is formatted in simple html. HtmlText")
+ HtmlText(text = "Hello World. This textsentence is formatted in simple html, . HtmlText")
+}
+
+@Composable
+fun ClickableContentWithLink() {
+ var isExpanded by remember { mutableStateOf(false) }
+
+ Column {
+ HtmlText(
+ modifier = Modifier.clickable {
+ isExpanded = !isExpanded
+ },
+ text = "Hello World. In case parent has a clickable modifier, it will be invoked (unless the annotated string has a clickable link). HtmlText"
+ )
+ AnimatedVisibility(isExpanded) {
+ HtmlText(text = "I am expanded now")
+ }
+ }
}
@Composable
@@ -95,6 +118,7 @@ fun DefaultPreview() {
Column {
Greeting()
StringGreeting()
+ ClickableContentWithLink()
}
}
}
diff --git a/material-html-text/src/main/java/de/charlex/compose/material/HtmlText.kt b/material-html-text/src/main/java/de/charlex/compose/material/HtmlText.kt
index 52f8c9f..5e221d8 100644
--- a/material-html-text/src/main/java/de/charlex/compose/material/HtmlText.kt
+++ b/material-html-text/src/main/java/de/charlex/compose/material/HtmlText.kt
@@ -10,6 +10,8 @@ import android.text.style.StyleSpan
import android.text.style.URLSpan
import android.text.style.UnderlineSpan
import androidx.annotation.StringRes
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.material.LocalTextStyle
@@ -19,7 +21,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.input.pointer.PointerInputScope
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
@@ -41,7 +46,10 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.util.fastAny
+import androidx.compose.ui.util.fastForEach
import androidx.core.text.getSpans
+import kotlinx.coroutines.coroutineScope
/**
* Simple Text composable to show the text with html styling from string resources.
@@ -233,19 +241,24 @@ fun HtmlText(
Text(
modifier = modifier.then(if (clickable) Modifier
.pointerInput(Unit) {
- detectTapGestures(onTap = { pos ->
- layoutResult.value?.let { layoutResult ->
+ interceptTap(onTap = { pos ->
+ val shouldConsumeEvent = layoutResult.value?.let { layoutResult ->
val position = layoutResult.getOffsetForPosition(pos)
- annotatedString
+ return@let annotatedString
.getStringAnnotations(position, position)
.firstOrNull()
?.let { sa ->
if (sa.tag == "url") { // NON-NLS
val url = sa.item
onUriClick?.let { it(url) } ?: uriHandler.openUri(url)
+ true
+ } else {
+ false
}
}
- }
+ } ?: false
+
+ return@interceptTap shouldConsumeEvent
})
}
.semantics {
@@ -358,3 +371,43 @@ fun Spanned.toAnnotatedString(
}
}
}
+
+typealias ShouldConsumePointerEvent = Boolean
+
+suspend fun PointerInputScope.interceptTap(
+ pass: PointerEventPass = PointerEventPass.Initial,
+ onTap: ((Offset) -> ShouldConsumePointerEvent)? = null,
+) = coroutineScope {
+ if (onTap == null) return@coroutineScope
+
+ awaitEachGesture {
+ val down = awaitFirstDown(pass = pass)
+ val downTime = System.currentTimeMillis()
+ val tapTimeout = viewConfiguration.longPressTimeoutMillis
+ val tapPosition = down.position
+
+ do {
+ val event = awaitPointerEvent(pass)
+ val currentTime = System.currentTimeMillis()
+
+ if (event.changes.size != 1) break // More than one event: not a tap
+ if (currentTime - downTime >= tapTimeout) break // Too slow: not a tap
+
+ val change = event.changes[0]
+
+ // Too much movement: not a tap
+ if ((change.position - tapPosition).getDistance() > viewConfiguration.touchSlop) break
+
+ if (change.id == down.id && !change.pressed) {
+ if (onTap(change.position)) {
+ change.consume()
+ down.consume()
+ do {
+ val pointerEvent = awaitPointerEvent()
+ pointerEvent.changes.fastForEach { it.consume() }
+ } while (pointerEvent.changes.fastAny { it.pressed })
+ }
+ }
+ } while (event.changes.any { it.id == down.id && it.pressed })
+ }
+}
\ No newline at end of file
diff --git a/material3-html-text/src/main/java/de/charlex/compose/material3/HtmlText.kt b/material3-html-text/src/main/java/de/charlex/compose/material3/HtmlText.kt
index 8d91fde..9e544f2 100644
--- a/material3-html-text/src/main/java/de/charlex/compose/material3/HtmlText.kt
+++ b/material3-html-text/src/main/java/de/charlex/compose/material3/HtmlText.kt
@@ -10,7 +10,8 @@ import android.text.style.StyleSpan
import android.text.style.URLSpan
import android.text.style.UnderlineSpan
import androidx.annotation.StringRes
-import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
@@ -19,7 +20,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.input.pointer.PointerInputScope
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
@@ -41,7 +45,10 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.util.fastAny
+import androidx.compose.ui.util.fastForEach
import androidx.core.text.getSpans
+import kotlinx.coroutines.coroutineScope
/**
* Simple Text composable to show the text with html styling from string resources.
@@ -233,19 +240,24 @@ fun HtmlText(
Text(
modifier = modifier.then(if (clickable) Modifier
.pointerInput(Unit) {
- detectTapGestures(onTap = { pos ->
- layoutResult.value?.let { layoutResult ->
+ interceptTap(onTap = { pos ->
+ val shouldConsumeEvent = layoutResult.value?.let { layoutResult ->
val position = layoutResult.getOffsetForPosition(pos)
- annotatedString
+ return@let annotatedString
.getStringAnnotations(position, position)
.firstOrNull()
?.let { sa ->
if (sa.tag == "url") { // NON-NLS
val url = sa.item
onUriClick?.let { it(url) } ?: uriHandler.openUri(url)
+ true
+ } else {
+ false
}
}
- }
+ } ?: false
+
+ return@interceptTap shouldConsumeEvent
})
}
.semantics {
@@ -358,3 +370,43 @@ fun Spanned.toAnnotatedString(
}
}
}
+
+typealias ShouldConsumePointerEvent = Boolean
+
+suspend fun PointerInputScope.interceptTap(
+ pass: PointerEventPass = PointerEventPass.Initial,
+ onTap: ((Offset) -> ShouldConsumePointerEvent)? = null,
+) = coroutineScope {
+ if (onTap == null) return@coroutineScope
+
+ awaitEachGesture {
+ val down = awaitFirstDown(pass = pass)
+ val downTime = System.currentTimeMillis()
+ val tapTimeout = viewConfiguration.longPressTimeoutMillis
+ val tapPosition = down.position
+
+ do {
+ val event = awaitPointerEvent(pass)
+ val currentTime = System.currentTimeMillis()
+
+ if (event.changes.size != 1) break // More than one event: not a tap
+ if (currentTime - downTime >= tapTimeout) break // Too slow: not a tap
+
+ val change = event.changes[0]
+
+ // Too much movement: not a tap
+ if ((change.position - tapPosition).getDistance() > viewConfiguration.touchSlop) break
+
+ if (change.id == down.id && !change.pressed) {
+ if (onTap(change.position)) {
+ change.consume()
+ down.consume()
+ do {
+ val pointerEvent = awaitPointerEvent()
+ pointerEvent.changes.fastForEach { it.consume() }
+ } while (pointerEvent.changes.fastAny { it.pressed })
+ }
+ }
+ } while (event.changes.any { it.id == down.id && it.pressed })
+ }
+}
\ No newline at end of file