Skip to content

Commit b179786

Browse files
committed
Recognize drag and long press gestures
1 parent c495222 commit b179786

1 file changed

Lines changed: 65 additions & 2 deletions

File tree

  • Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui

Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui/Render.kt

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ package com.pureswift.swiftui
33
import androidx.compose.foundation.background
44
import androidx.compose.foundation.border
55
import androidx.compose.foundation.clickable
6+
import androidx.compose.foundation.ExperimentalFoundationApi
7+
import androidx.compose.foundation.combinedClickable
8+
import androidx.compose.foundation.gestures.detectDragGestures
9+
import androidx.compose.ui.geometry.Offset
10+
import androidx.compose.ui.input.pointer.pointerInput
611
import androidx.compose.foundation.rememberScrollState
712
import androidx.compose.foundation.horizontalScroll
813
import androidx.compose.foundation.verticalScroll
@@ -1062,6 +1067,7 @@ private fun fontWeightFor(name: String): FontWeight = when (name) {
10621067
/// animation (explicit `withAnimation` tree, or this node's `.animation`
10631068
/// modifier) applies, and `snap` otherwise.
10641069
@Composable
1070+
@OptIn(ExperimentalFoundationApi::class)
10651071
internal fun ViewNode.composeModifiers(): Modifier {
10661072
val implicit = modifiers.firstOrNull { it.kind == "animation" && it.args["curve"] != null }
10671073
?.let { AnimSpec(it.args.string("curve") ?: "easeInOut", (it.args.double("durationMs") ?: 350.0).toInt()) }
@@ -1124,9 +1130,66 @@ internal fun ViewNode.composeModifiers(): Modifier {
11241130
modifier.clip(shape)
11251131
}
11261132

1133+
// Tap and long press share one detector: two competing pointer
1134+
// handlers on the same view would fight over the gesture.
11271135
"onTapGesture" -> {
11281136
val id = entry.args.long("action")
1129-
if (id != null) modifier.clickable { SwiftBridge.sink.invokeVoid(id) } else modifier
1137+
val longID = modifiers.firstOrNull { it.kind == "longPress" }?.args?.long("action")
1138+
when {
1139+
id == null -> modifier
1140+
longID == null -> modifier.clickable { SwiftBridge.sink.invokeVoid(id) }
1141+
else -> modifier.combinedClickable(
1142+
onClick = { SwiftBridge.sink.invokeVoid(id) },
1143+
onLongClick = { SwiftBridge.sink.invokeVoid(longID) },
1144+
)
1145+
}
1146+
}
1147+
1148+
"longPress" -> {
1149+
val id = entry.args.long("action")
1150+
// already wired above when this view also has a tap handler
1151+
val hasTap = modifiers.any { it.kind == "onTapGesture" }
1152+
if (id == null || hasTap) modifier
1153+
else modifier.combinedClickable(onClick = {}, onLongClick = { SwiftBridge.sink.invokeVoid(id) })
1154+
}
1155+
1156+
// Continuous drag: positions are reported in points, converted from
1157+
// the pointer's pixels, as "<phase>;<startX>,<startY>;<x>,<y>".
1158+
"drag" -> {
1159+
val id = entry.args.long("action")
1160+
if (id == null) modifier else {
1161+
// The detector is a long-running coroutine, so it must be keyed
1162+
// by the node's STABLE id: keying by the callback id would tear
1163+
// the gesture down mid-drag, since every onChanged re-evaluates
1164+
// the tree and mints a fresh id. The lambda then reads the id
1165+
// from a holder so it never dispatches to a reclaimed callback.
1166+
val latest = remember(this@composeModifiers.id) { longArrayOf(id) }
1167+
latest[0] = id
1168+
modifier.pointerInput(this@composeModifiers.id) {
1169+
var start = Offset.Zero
1170+
// Accumulate per-event deltas rather than reading absolute
1171+
// positions: a view that offsets itself by the translation
1172+
// moves under the finger, and absolute readings would
1173+
// compensate for that motion and collapse toward zero.
1174+
var total = Offset.Zero
1175+
fun payload(phase: String): String {
1176+
val end = start + total
1177+
return "$phase;${start.x.toDp().value},${start.y.toDp().value};" +
1178+
"${end.x.toDp().value},${end.y.toDp().value}"
1179+
}
1180+
detectDragGestures(
1181+
onDragStart = { start = it; total = Offset.Zero },
1182+
onDrag = { change, dragAmount ->
1183+
change.consume()
1184+
total += dragAmount
1185+
SwiftBridge.sink.invokeString(latest[0], payload("changed"))
1186+
},
1187+
// ends carrying the final translation, as SwiftUI does
1188+
onDragEnd = { SwiftBridge.sink.invokeString(latest[0], payload("ended")) },
1189+
onDragCancel = { SwiftBridge.sink.invokeString(latest[0], payload("ended")) },
1190+
)
1191+
}
1192+
}
11301193
}
11311194

11321195
// Dim disabled content; controls also drop interactivity via their
@@ -1155,7 +1218,7 @@ private val KNOWN_MODIFIER_KINDS = setOf(
11551218
"scale", "opacity", "border", "shadow", "clipShape", "onTapGesture", "disabled",
11561219
"font", "fontWeight", "italic", "foregroundColor", "lineLimit", "multilineTextAlignment",
11571220
"tint", "onAppear", "onDisappear", "task", "onChange", "animation", "tag", "tabItem",
1158-
"transition", "focused",
1221+
"transition", "focused", "longPress", "drag",
11591222
)
11601223

11611224
// Folds a frame entry: fixed size, fill (maxWidth/Height .infinity), bounded

0 commit comments

Comments
 (0)