craft-native 0.0.395 → 0.0.396

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  package {{PACKAGE_NAME}}
2
2
 
3
3
  import android.content.Context
4
+ import android.view.MotionEvent
4
5
  import android.view.View
5
6
  import android.view.ViewGroup
6
7
  import android.widget.FrameLayout
@@ -18,6 +19,9 @@ internal typealias CraftNativeRenderItem = (node: JSONObject, identity: String,
18
19
 
19
20
  /** A stable-key RecyclerView host for stx native FlatList nodes. */
20
21
  internal class CraftNativeFlatList(context: Context) : RecyclerView(context) {
22
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean =
23
+ CraftNativePointers.dispatch(this, event) { super.dispatchTouchEvent(it) }
24
+
21
25
  private data class Item(
22
26
  val identity: String,
23
27
  val node: JSONObject,
@@ -227,11 +231,13 @@ internal class CraftNativeFlatList(context: Context) : RecyclerView(context) {
227
231
  .map { it.second }
228
232
 
229
233
  override fun onInterceptTouchEvent(event: android.view.MotionEvent): Boolean {
234
+ if (event.actionMasked != MotionEvent.ACTION_CANCEL && !CraftNativePointers.ownsTouch(this)) return false
230
235
  if (!scrollEnabled) return false
231
236
  return super.onInterceptTouchEvent(event)
232
237
  }
233
238
 
234
239
  override fun onTouchEvent(event: android.view.MotionEvent): Boolean {
240
+ if (event.actionMasked != MotionEvent.ACTION_CANCEL && !CraftNativePointers.ownsTouch(this)) return false
235
241
  if (!scrollEnabled) return false
236
242
  return super.onTouchEvent(event)
237
243
  }
@@ -304,6 +310,7 @@ internal class CraftNativeFlatList(context: Context) : RecyclerView(context) {
304
310
  }
305
311
  var dismissed = false
306
312
  setOnTouchListener { _, event ->
313
+ if (!CraftNativePointers.ownsTouch(this) && event.actionMasked != MotionEvent.ACTION_CANCEL) return@setOnTouchListener false
307
314
  when (event.actionMasked) {
308
315
  android.view.MotionEvent.ACTION_DOWN -> dismissed = false
309
316
  android.view.MotionEvent.ACTION_MOVE -> if (!dismissed) {
@@ -1,5 +1,8 @@
1
1
  /** The Android host for the small, host-neutral native trail map contract. */
2
2
  private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMapReadyCallback {
3
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean =
4
+ CraftNativePointers.dispatch(this, event) { super.dispatchTouchEvent(it) }
5
+
3
6
  private data class PolygonValue(val points: List<LatLng>, val fillColor: Int, val strokeColor: Int, val strokeWidth: Float)
4
7
  private val mapView = MapView(context)
5
8
  private val scaleView = LinearLayout(context).apply {
@@ -1,5 +1,8 @@
1
1
  /** A key-free native Android map. STX geometry never enters a WebView. */
2
2
  internal class CraftNativeMapView(context: Context) : FrameLayout(context), OnMapReadyCallback {
3
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean =
4
+ CraftNativePointers.dispatch(this, event) { super.dispatchTouchEvent(it) }
5
+
3
6
  private val mapView: MapView
4
7
  private var map: MapLibreMap? = null
5
8
  private var destroyed = false
@@ -1,11 +1,20 @@
1
1
  package {{PACKAGE_NAME}}
2
2
 
3
3
  import android.content.Context
4
+ import android.view.MotionEvent
4
5
  import org.json.JSONObject
5
6
  import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
6
7
 
7
8
  /** Swipe-to-refresh host for a native FlatList. */
8
9
  internal class CraftNativeRefreshableFlatList(context: Context) : SwipeRefreshLayout(context) {
10
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean =
11
+ CraftNativePointers.dispatch(this, event) { super.dispatchTouchEvent(it) }
12
+
13
+ override fun onInterceptTouchEvent(event: MotionEvent): Boolean =
14
+ CraftNativePointers.ownsTouch(this) && super.onInterceptTouchEvent(event)
15
+ override fun onTouchEvent(event: MotionEvent): Boolean =
16
+ (event.actionMasked == MotionEvent.ACTION_CANCEL || CraftNativePointers.ownsTouch(this)) && super.onTouchEvent(event)
17
+
9
18
  val list = CraftNativeFlatList(context)
10
19
  private var refreshHandler: (() -> Unit)? = null
11
20
 
@@ -366,6 +366,9 @@ private object CraftNativeSkewTransforms {
366
366
 
367
367
  /** A DOM-free SVG surface for the geometry emitted by the shared STX native protocol. */
368
368
  private class CraftNativeSvgView(context: Context) : View(context) {
369
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean =
370
+ CraftNativePointers.dispatch(this, event) { super.dispatchTouchEvent(it) }
371
+
369
372
  var drawing: JSONObject = JSONObject()
370
373
  set(value) {
371
374
  field = value
@@ -972,8 +975,184 @@ internal class CraftNativeCelebration(private val view: View) {
972
975
  }
973
976
  }
974
977
 
978
+ /** Authored nodes are boundaries; native control implementation views are not. */
979
+ internal object CraftNativePointers {
980
+ private data class State(var mode: String? = null, var container: Boolean = false,
981
+ var active: Boolean = false, var cancelled: Boolean = false, var route: List<String> = emptyList())
982
+ private val states = java.util.WeakHashMap<View, State>()
983
+
984
+ fun configure(view: View, props: JSONObject, style: JSONObject, container: Boolean) {
985
+ val raw = if (props.has("pointerEvents") && !props.isNull("pointerEvents")) props.opt("pointerEvents") else style.opt("pointerEvents")
986
+ val state = states.getOrPut(view) { State() }
987
+ state.mode = (raw as? String)?.takeIf { it in setOf("auto", "none", "box-none", "box-only") } ?: "auto"
988
+ state.container = container
989
+ }
990
+
991
+ private fun route(view: View): List<String> {
992
+ val result = mutableListOf<String>()
993
+ var candidate: View? = view
994
+ while (candidate != null) {
995
+ states[candidate]?.mode?.let { result.add(it) }
996
+ candidate = candidate.parent as? View
997
+ }
998
+ return result
999
+ }
1000
+
1001
+ fun ownsTouch(view: View): Boolean = route(view).firstOrNull() !in setOf("none", "box-none")
1002
+
1003
+ fun dispatch(view: View, event: MotionEvent, native: (MotionEvent) -> Boolean): Boolean {
1004
+ val state = states.getOrPut(view) { State() }
1005
+ val route = route(view)
1006
+ if (event.actionMasked == MotionEvent.ACTION_CANCEL) {
1007
+ state.active = false
1008
+ return native(event)
1009
+ }
1010
+ if (event.actionMasked == MotionEvent.ACTION_DOWN) {
1011
+ state.active = false
1012
+ state.cancelled = false
1013
+ state.route = route
1014
+ } else if (state.active && state.route != route) {
1015
+ val cancel = MotionEvent.obtain(event)
1016
+ cancel.action = MotionEvent.ACTION_CANCEL
1017
+ try { native(cancel) } finally { cancel.recycle() }
1018
+ state.active = false
1019
+ state.cancelled = true
1020
+ }
1021
+ if (state.cancelled) return false
1022
+ if (state.mode == "box-only" && event.actionMasked == MotionEvent.ACTION_DOWN &&
1023
+ (event.x < 0 || event.x >= view.width || event.y < 0 || event.y >= view.height)) return false
1024
+ val ancestors = route.drop(1)
1025
+ if ("none" in route || "box-only" in ancestors || (state.mode == "box-none" && !state.container)) return false
1026
+ val handled = native(event) || (state.mode == "box-only" && state.container && view.isEnabled)
1027
+ if (event.actionMasked == MotionEvent.ACTION_DOWN) state.active = handled
1028
+ if (event.actionMasked == MotionEvent.ACTION_UP) state.active = false
1029
+ return handled
1030
+ }
1031
+ }
1032
+
1033
+ internal class CraftNativePointerTextView(context: Context) : TextView(context) {
1034
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean =
1035
+ CraftNativePointers.dispatch(this, event) { super.dispatchTouchEvent(it) }
1036
+
1037
+ }
1038
+
1039
+ internal class CraftNativePointerButton(context: Context) : Button(context) {
1040
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean =
1041
+ CraftNativePointers.dispatch(this, event) { super.dispatchTouchEvent(it) }
1042
+
1043
+ }
1044
+
1045
+ internal class CraftNativePointerCheckBox(context: Context) : CheckBox(context) {
1046
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean =
1047
+ CraftNativePointers.dispatch(this, event) { super.dispatchTouchEvent(it) }
1048
+
1049
+ }
1050
+
1051
+ internal class CraftNativePointerRadioGroup(context: Context) : android.widget.RadioGroup(context) {
1052
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean =
1053
+ CraftNativePointers.dispatch(this, event) { super.dispatchTouchEvent(it) }
1054
+ }
1055
+
1056
+ internal class CraftNativePointerSpinner(context: Context) : Spinner(context) {
1057
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean =
1058
+ CraftNativePointers.dispatch(this, event) { super.dispatchTouchEvent(it) }
1059
+
1060
+ }
1061
+
1062
+ internal class CraftNativePointerSwitch(context: Context) : Switch(context) {
1063
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean =
1064
+ CraftNativePointers.dispatch(this, event) { super.dispatchTouchEvent(it) }
1065
+
1066
+ }
1067
+
1068
+ internal class CraftNativePointerSeekBar(context: Context) : SeekBar(context) {
1069
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean =
1070
+ CraftNativePointers.dispatch(this, event) { super.dispatchTouchEvent(it) }
1071
+
1072
+ }
1073
+
1074
+ internal class CraftNativePointerImageView(context: Context) : ImageView(context) {
1075
+ private var objectFit: String? = null
1076
+ private var objectX = 0.5f
1077
+ private var objectY = 0.5f
1078
+
1079
+ fun configureObjectGeometry(props: JSONObject, style: JSONObject) {
1080
+ fun value(name: String): Any? = props.opt(name)?.takeUnless { it == JSONObject.NULL }
1081
+ ?: style.opt(name)?.takeUnless { it == JSONObject.NULL }
1082
+ val fit = value("objectFit")
1083
+ val position = value("objectPosition")
1084
+ objectFit = if (fit != null || position != null) (fit as? String)
1085
+ ?.takeIf { it in setOf("fill", "contain", "cover", "none", "scale-down") } ?: "fill" else null
1086
+ val point = parsePosition(position)
1087
+ objectX = point.first; objectY = point.second
1088
+ cropToPadding = objectFit != null
1089
+ updateObjectGeometry()
1090
+ }
1091
+
1092
+ private fun parsePosition(raw: Any?): Pair<Float, Float> {
1093
+ val center = 0.5f to 0.5f
1094
+ val text = (raw as? String)?.trim()?.lowercase() ?: return center
1095
+ var parts = text.split(Regex("\\s+"))
1096
+ if (parts.size !in 1..2) return center
1097
+ if (parts.size == 1) parts = if (parts[0] in setOf("top", "bottom")) listOf("center", parts[0]) else listOf(parts[0], "center")
1098
+ else if (parts[0] in setOf("top", "bottom") || parts[1] in setOf("left", "right")) parts = parts.reversed()
1099
+ fun fraction(part: String, low: String, high: String): Float? = when (part) {
1100
+ low -> 0f; high -> 1f; "center" -> 0.5f
1101
+ else -> if (part.endsWith("%")) part.dropLast(1).toFloatOrNull()?.takeIf { it.isFinite() }?.div(100f) else null
1102
+ }
1103
+ return (fraction(parts[0], "left", "right") ?: return center) to (fraction(parts[1], "top", "bottom") ?: return center)
1104
+ }
1105
+
1106
+ override fun setImageDrawable(drawable: android.graphics.drawable.Drawable?) {
1107
+ super.setImageDrawable(drawable)
1108
+ updateObjectGeometry()
1109
+ }
1110
+
1111
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
1112
+ super.onSizeChanged(w, h, oldw, oldh)
1113
+ updateObjectGeometry()
1114
+ }
1115
+
1116
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
1117
+ super.onLayout(changed, left, top, right, bottom)
1118
+ updateObjectGeometry()
1119
+ }
1120
+
1121
+ private fun updateObjectGeometry() {
1122
+ val fit = objectFit ?: return
1123
+ val source = drawable ?: return
1124
+ val sw = source.intrinsicWidth.toFloat(); val sh = source.intrinsicHeight.toFloat()
1125
+ if (sw <= 0 || sh <= 0) return
1126
+ val w = (width - paddingLeft - paddingRight).coerceAtLeast(0).toFloat()
1127
+ val h = (height - paddingTop - paddingBottom).coerceAtLeast(0).toFloat()
1128
+ val contain = minOf(w / sw, h / sh)
1129
+ val scale = when (fit) {
1130
+ "cover" -> maxOf(w / sw, h / sh)
1131
+ "none" -> 1f
1132
+ "scale-down" -> minOf(1f, contain)
1133
+ else -> contain
1134
+ }
1135
+ val sx = if (fit == "fill") w / sw else scale
1136
+ val sy = if (fit == "fill") h / sh else scale
1137
+ scaleType = ScaleType.MATRIX
1138
+ imageMatrix = Matrix().apply {
1139
+ setScale(sx, sy)
1140
+ postTranslate((w - sw * sx) * objectX, (h - sh * sy) * objectY)
1141
+ }
1142
+ }
1143
+
1144
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean =
1145
+ CraftNativePointers.dispatch(this, event) { super.dispatchTouchEvent(it) }
1146
+
1147
+ }
1148
+
975
1149
  /** A deterministic flex-shaped container shared by normal views and scroll content. */
976
1150
  internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
1151
+ override fun onInterceptTouchEvent(event: MotionEvent): Boolean =
1152
+ CraftNativePointers.ownsTouch(this) && super.onInterceptTouchEvent(event)
1153
+ override fun onTouchEvent(event: MotionEvent): Boolean =
1154
+ (event.actionMasked == MotionEvent.ACTION_CANCEL || CraftNativePointers.ownsTouch(this)) && super.onTouchEvent(event)
1155
+
977
1156
  private var celebration: CraftNativeCelebration? = null
978
1157
  fun configureCelebration(raw: JSONObject?, radius: Float) {
979
1158
  if (raw != null && celebration == null) celebration = CraftNativeCelebration(this)
@@ -1012,10 +1191,76 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
1012
1191
  private var safeAreaPadding = intArrayOf(0, 0, 0, 0)
1013
1192
  private var applyingPadding = false
1014
1193
  private var safeAreaEdges = setOf("top", "right", "bottom", "left")
1194
+ private var overflowTouchTarget: View? = null
1195
+
1196
+ private fun childEvent(child: View, event: MotionEvent): MotionEvent = MotionEvent.obtain(event).also {
1197
+ it.offsetLocation((scrollX - child.left).toFloat(), (scrollY - child.top).toFloat())
1198
+ if (!child.matrix.isIdentity) {
1199
+ val inverse = android.graphics.Matrix()
1200
+ if (child.matrix.invert(inverse)) it.transform(inverse)
1201
+ }
1202
+ }
1203
+
1204
+ private fun overflowContains(group: ViewGroup, x: Float, y: Float): Boolean {
1205
+ if (group.clipChildren) return false
1206
+ for (index in group.childCount - 1 downTo 0) {
1207
+ val child = group.getChildAt(index)
1208
+ if (child.visibility != View.VISIBLE || child.alpha <= 0.01f) continue
1209
+ val point = floatArrayOf(x + group.scrollX - child.left, y + group.scrollY - child.top)
1210
+ val inverse = android.graphics.Matrix()
1211
+ if (!child.matrix.isIdentity && child.matrix.invert(inverse)) inverse.mapPoints(point)
1212
+ if (point[0] >= 0 && point[0] < child.width && point[1] >= 0 && point[1] < child.height) return true
1213
+ if (child is ViewGroup && overflowContains(child, point[0], point[1])) return true
1214
+ }
1215
+ return false
1216
+ }
1217
+
1218
+ // Android normally rejects a zero-height child's entire subtree before
1219
+ // dispatch, even when it draws outside that box. Only that overflow path
1220
+ // needs routing here; ordinary gestures stay in ViewGroup's dispatcher.
1221
+ private fun dispatchOverflow(event: MotionEvent): Boolean? {
1222
+ if (event.actionMasked == MotionEvent.ACTION_DOWN) {
1223
+ overflowTouchTarget = null
1224
+ val children = (0 until childCount).map { getChildAt(it) }
1225
+ .filter { it.visibility == View.VISIBLE && it.alpha > 0.01f }
1226
+ .sortedBy { it.z }.reversed()
1227
+ val reachesOverflow = children.any { child ->
1228
+ val local = childEvent(child, event)
1229
+ try {
1230
+ child is ViewGroup && (local.x < 0 || local.x >= child.width || local.y < 0 || local.y >= child.height) &&
1231
+ overflowContains(child, local.x, local.y)
1232
+ } finally { local.recycle() }
1233
+ }
1234
+ if (!reachesOverflow) return null
1235
+ for (child in children) {
1236
+ val local = childEvent(child, event)
1237
+ try {
1238
+ val inside = local.x >= 0 && local.x < child.width && local.y >= 0 && local.y < child.height
1239
+ if ((inside || (child is ViewGroup && overflowContains(child, local.x, local.y))) && child.dispatchTouchEvent(local)) {
1240
+ overflowTouchTarget = child
1241
+ return true
1242
+ }
1243
+ } finally { local.recycle() }
1244
+ }
1245
+ return null
1246
+ }
1247
+ val target = overflowTouchTarget ?: return null
1248
+ val local = childEvent(target, event)
1249
+ try {
1250
+ if (target.parent !== this) local.action = MotionEvent.ACTION_CANCEL
1251
+ target.dispatchTouchEvent(local)
1252
+ if (local.actionMasked == MotionEvent.ACTION_UP || local.actionMasked == MotionEvent.ACTION_CANCEL) overflowTouchTarget = null
1253
+ return true
1254
+ } finally { local.recycle() }
1255
+ }
1256
+
1015
1257
  private var shieldedModalTouch = false
1016
1258
  private var dismissibleModalTouch: CraftNativeModalView? = null
1017
1259
 
1018
- override fun dispatchTouchEvent(event: MotionEvent): Boolean {
1260
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean =
1261
+ CraftNativePointers.dispatch(this, event) { dispatchNativeTouchEvent(it) }
1262
+
1263
+ private fun dispatchNativeTouchEvent(event: MotionEvent): Boolean {
1019
1264
  if (event.actionMasked == MotionEvent.ACTION_DOWN) {
1020
1265
  val x = event.x.roundToInt()
1021
1266
  val y = event.y.roundToInt()
@@ -1023,12 +1268,12 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
1023
1268
  .map { getChildAt(it) }
1024
1269
  .filterIsInstance<CraftNativeModalView>()
1025
1270
  .firstOrNull { child ->
1026
- child.dismissOnOutsideTap && child.visibility == View.VISIBLE && child.alpha > 0.01f
1271
+ CraftNativePointers.ownsTouch(child) && child.dismissOnOutsideTap && child.visibility == View.VISIBLE && child.alpha > 0.01f
1027
1272
  && (x < child.left || x >= child.right || y < child.top || y >= child.bottom)
1028
1273
  }
1029
1274
  shieldedModalTouch = (childCount - 1 downTo 0).any { index ->
1030
1275
  val child = getChildAt(index)
1031
- child is CraftNativeModalView && !child.blocksBackground && child.visibility == View.VISIBLE
1276
+ child is CraftNativeModalView && CraftNativePointers.ownsTouch(child) && !child.blocksBackground && child.visibility == View.VISIBLE
1032
1277
  && (x < child.left || x >= child.right || y < child.top || y >= child.bottom)
1033
1278
  && child.hasBlockingModalBelow()
1034
1279
  }
@@ -1046,6 +1291,7 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
1046
1291
  if (event.actionMasked == MotionEvent.ACTION_UP || event.actionMasked == MotionEvent.ACTION_CANCEL) shieldedModalTouch = false
1047
1292
  return true
1048
1293
  }
1294
+ dispatchOverflow(event)?.let { return it }
1049
1295
  return super.dispatchTouchEvent(event)
1050
1296
  }
1051
1297
 
@@ -2328,7 +2574,15 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
2328
2574
 
2329
2575
  /** An image-backed flex container with ordinary native children over the image. */
2330
2576
  private class CraftNativeImageBackgroundView(context: Context) : FrameLayout(context) {
2331
- val image = ImageView(context)
2577
+ override fun onInterceptTouchEvent(event: MotionEvent): Boolean =
2578
+ CraftNativePointers.ownsTouch(this) && super.onInterceptTouchEvent(event)
2579
+ override fun onTouchEvent(event: MotionEvent): Boolean =
2580
+ (event.actionMasked == MotionEvent.ACTION_CANCEL || CraftNativePointers.ownsTouch(this)) && super.onTouchEvent(event)
2581
+
2582
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean =
2583
+ CraftNativePointers.dispatch(this, event) { super.dispatchTouchEvent(it) }
2584
+
2585
+ val image = CraftNativePointerImageView(context)
2332
2586
  val content = CraftNativeFlexLayout(context)
2333
2587
 
2334
2588
  init {
@@ -2342,6 +2596,14 @@ private class CraftNativeImageBackgroundView(context: Context) : FrameLayout(con
2342
2596
 
2343
2597
  /** A flex container that responds to the activity's IME inset. */
2344
2598
  private class CraftNativeKeyboardAvoidingView(context: Context) : FrameLayout(context) {
2599
+ override fun onInterceptTouchEvent(event: MotionEvent): Boolean =
2600
+ CraftNativePointers.ownsTouch(this) && super.onInterceptTouchEvent(event)
2601
+ override fun onTouchEvent(event: MotionEvent): Boolean =
2602
+ (event.actionMasked == MotionEvent.ACTION_CANCEL || CraftNativePointers.ownsTouch(this)) && super.onTouchEvent(event)
2603
+
2604
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean =
2605
+ CraftNativePointers.dispatch(this, event) { super.dispatchTouchEvent(it) }
2606
+
2345
2607
  val content = CraftNativeFlexLayout(context)
2346
2608
  private var behavior = "padding"
2347
2609
  private var keyboardVerticalOffset = 0
@@ -2426,17 +2688,23 @@ internal fun craftNativeFontFamily(family: String?, bundledFonts: Map<String, Ty
2426
2688
  }?.firstOrNull() ?: Typeface.DEFAULT
2427
2689
 
2428
2690
  internal class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(context) {
2691
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean =
2692
+ CraftNativePointers.dispatch(this, event) { super.dispatchTouchEvent(it) }
2693
+
2694
+ override fun onTouchEvent(event: MotionEvent): Boolean =
2695
+ (event.actionMasked == MotionEvent.ACTION_CANCEL || CraftNativePointers.ownsTouch(this)) && super.onTouchEvent(event)
2696
+
2429
2697
  val content = CraftNativeFlexLayout(context)
2430
2698
  // Keep one refresh target across axis changes. Replacing all children of
2431
2699
  // SwipeRefreshLayout would also detach its own animated indicator.
2432
2700
  private val scrollContainer = FrameLayout(context)
2433
2701
  private val vertical = object : ScrollView(context) {
2434
- override fun onInterceptTouchEvent(event: MotionEvent): Boolean = isEnabled && super.onInterceptTouchEvent(event)
2435
- override fun onTouchEvent(event: MotionEvent): Boolean = isEnabled && super.onTouchEvent(event)
2702
+ override fun onInterceptTouchEvent(event: MotionEvent): Boolean = isEnabled && CraftNativePointers.ownsTouch(this) && super.onInterceptTouchEvent(event)
2703
+ override fun onTouchEvent(event: MotionEvent): Boolean = isEnabled && (event.actionMasked == MotionEvent.ACTION_CANCEL || CraftNativePointers.ownsTouch(this)) && super.onTouchEvent(event)
2436
2704
  }
2437
2705
  private val horizontal = object : HorizontalScrollView(context) {
2438
- override fun onInterceptTouchEvent(event: MotionEvent): Boolean = isEnabled && super.onInterceptTouchEvent(event)
2439
- override fun onTouchEvent(event: MotionEvent): Boolean = isEnabled && super.onTouchEvent(event)
2706
+ override fun onInterceptTouchEvent(event: MotionEvent): Boolean = isEnabled && CraftNativePointers.ownsTouch(this) && super.onInterceptTouchEvent(event)
2707
+ override fun onTouchEvent(event: MotionEvent): Boolean = isEnabled && (event.actionMasked == MotionEvent.ACTION_CANCEL || CraftNativePointers.ownsTouch(this)) && super.onTouchEvent(event)
2440
2708
  }
2441
2709
  private var horizontalMode: Boolean? = null
2442
2710
  private var pagingEnabled = false
@@ -2534,7 +2802,7 @@ internal class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(cont
2534
2802
  }
2535
2803
 
2536
2804
  override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
2537
- if (refreshHandler == null) return false
2805
+ if (refreshHandler == null || !CraftNativePointers.ownsTouch(this)) return false
2538
2806
  return super.onInterceptTouchEvent(event)
2539
2807
  }
2540
2808
 
@@ -2914,6 +3182,7 @@ internal class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(cont
2914
3182
  onScroll?.invoke(x, y)
2915
3183
  }
2916
3184
  scroller.setOnTouchListener { _, event ->
3185
+ if (!CraftNativePointers.ownsTouch(scroller) && event.actionMasked != MotionEvent.ACTION_CANCEL) return@setOnTouchListener false
2917
3186
  when (event.actionMasked) {
2918
3187
  android.view.MotionEvent.ACTION_DOWN -> {
2919
3188
  pageDragging = true
@@ -2984,6 +3253,9 @@ private class CraftNativeFlexSpacer(context: Context) : View(context)
2984
3253
  * the other native leaf controls without adding an icon-font dependency.
2985
3254
  */
2986
3255
  private class CraftNativeIconView(context: Context) : TextView(context) {
3256
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean =
3257
+ CraftNativePointers.dispatch(this, event) { super.dispatchTouchEvent(it) }
3258
+
2987
3259
  /** The app's vector for the icon (CraftNativeIcons), drawn in place of the glyph. */
2988
3260
  var vector: Drawable? = null
2989
3261
  set(value) {
@@ -3010,6 +3282,9 @@ private class CraftNativeIconView(context: Context) : TextView(context) {
3010
3282
 
3011
3283
  /** A platform EditText with the native end clear affordance and TalkBack action. */
3012
3284
  private class CraftNativeTextField(context: Context) : EditText(context) {
3285
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean =
3286
+ CraftNativePointers.dispatch(this, event) { super.dispatchTouchEvent(it) }
3287
+
3013
3288
  companion object { val CLEAR_ACTION_ID = View.generateViewId() }
3014
3289
  var clearButtonMode = "never"
3015
3290
  var onClearRequest: (() -> Unit)? = null
@@ -3054,8 +3329,14 @@ private class CraftNativeTextField(context: Context) : EditText(context) {
3054
3329
  }
3055
3330
 
3056
3331
  private class CraftNativeModalView(context: Context) : FrameLayout(context) {
3332
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean =
3333
+ CraftNativePointers.dispatch(this, event) { super.dispatchTouchEvent(it) }
3334
+
3057
3335
  val content = CraftNativeFlexLayout(context)
3058
- private val blocker = View(context)
3336
+ private val blocker = object : View(context) {
3337
+ override fun onTouchEvent(event: MotionEvent): Boolean =
3338
+ (event.actionMasked == MotionEvent.ACTION_CANCEL || CraftNativePointers.ownsTouch(this)) && super.onTouchEvent(event)
3339
+ }
3059
3340
  private var renderedVisible = false
3060
3341
  private var animationGeneration = 0
3061
3342
  var focusToRestore: View? = null
@@ -3095,6 +3376,7 @@ private class CraftNativeModalView(context: Context) : FrameLayout(context) {
3095
3376
  isClickable = true
3096
3377
  blocker.isClickable = true
3097
3378
  blocker.setOnTouchListener { _, event ->
3379
+ if (!CraftNativePointers.ownsTouch(this)) return@setOnTouchListener false
3098
3380
  if (event.action == MotionEvent.ACTION_UP) requestClose(reason = "backdrop")
3099
3381
  true
3100
3382
  }
@@ -3175,6 +3457,7 @@ private class CraftNativeModalView(context: Context) : FrameLayout(context) {
3175
3457
  }
3176
3458
 
3177
3459
  override fun onTouchEvent(event: MotionEvent): Boolean {
3460
+ if (event.actionMasked != MotionEvent.ACTION_CANCEL && !CraftNativePointers.ownsTouch(this)) return false
3178
3461
  if (!blocksBackground) {
3179
3462
  if (dismissOnOutsideTap && event.action == MotionEvent.ACTION_UP) {
3180
3463
  requestClose(reason = "backdrop")
@@ -3192,7 +3475,7 @@ private class CraftNativeModalView(context: Context) : FrameLayout(context) {
3192
3475
  val index = group.indexOfChild(this)
3193
3476
  return index > 0 && (0 until index).any { siblingIndex ->
3194
3477
  val sibling = group.getChildAt(siblingIndex)
3195
- sibling is CraftNativeModalView && sibling.blocksBackground && sibling.visibility == View.VISIBLE && sibling.alpha > 0.01f
3478
+ sibling is CraftNativeModalView && CraftNativePointers.ownsTouch(sibling) && sibling.blocksBackground && sibling.visibility == View.VISIBLE && sibling.alpha > 0.01f
3196
3479
  }
3197
3480
  }
3198
3481
  }
@@ -3213,6 +3496,9 @@ private data class LayoutBounds(
3213
3496
  */
3214
3497
  private class CraftNativeActivityIndicator(context: Context) :
3215
3498
  android.widget.ProgressBar(context, null, android.R.attr.progressBarStyleHorizontal) {
3499
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean =
3500
+ CraftNativePointers.dispatch(this, event) { super.dispatchTouchEvent(it) }
3501
+
3216
3502
  var indicatorScale = 1f
3217
3503
 
3218
3504
  init {
@@ -3224,6 +3510,9 @@ private class CraftNativeActivityIndicator(context: Context) :
3224
3510
 
3225
3511
  /** Android counterpart to iOS's UIDatePicker-backed native DatePicker. */
3226
3512
  private class CraftNativeDatePickerView(context: Context) : FrameLayout(context) {
3513
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean =
3514
+ CraftNativePointers.dispatch(this, event) { super.dispatchTouchEvent(it) }
3515
+
3227
3516
  private enum class PickerMode { DATE, TIME, DATE_TIME, COUNTDOWN }
3228
3517
  private val maxCountdownSeconds = 23 * 3600 + 59 * 60 + 59
3229
3518
 
@@ -3470,6 +3759,9 @@ internal class CraftNativeInputEcho {
3470
3759
  * while this view keeps its place.
3471
3760
  */
3472
3761
  private class CraftNativeVideoView(context: Context) : VideoView(context) {
3762
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean =
3763
+ CraftNativePointers.dispatch(this, event) { super.dispatchTouchEvent(it) }
3764
+
3473
3765
  private var sourceUri = ""
3474
3766
  private var shouldLoop = false
3475
3767
  private var shouldPlay = false
@@ -5748,7 +6040,7 @@ class MainActivity : AppCompatActivity() {
5748
6040
  val previous = protocolId?.let { previousControls["id:$it"] } ?: keyedPrevious
5749
6041
  val reusableInput = previous as? CraftNativeTextField
5750
6042
  val control = when (type) {
5751
- "Text" -> ((previous as? TextView)?.takeUnless { it is Button || it is EditText } ?: TextView(this)).apply {
6043
+ "Text" -> ((previous as? TextView)?.takeUnless { it is Button || it is EditText } ?: CraftNativePointerTextView(this)).apply {
5752
6044
  includeFontPadding = false
5753
6045
  maxLines = craftNativeInt(props.opt("numberOfLines"), 0).takeIf { it > 0 } ?: Int.MAX_VALUE
5754
6046
  ellipsize = when (craftNativeOptionalString(props.opt("ellipsizeMode"))) {
@@ -5771,7 +6063,7 @@ class MainActivity : AppCompatActivity() {
5771
6063
  "RefreshControl" -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
5772
6064
  visibility = View.GONE
5773
6065
  }
5774
- "Link" -> ((previous as? TextView)?.takeUnless { it is Button } ?: TextView(this)).apply {
6066
+ "Link" -> ((previous as? TextView)?.takeUnless { it is Button } ?: CraftNativePointerTextView(this)).apply {
5775
6067
  includeFontPadding = false
5776
6068
  configureText(this, (props.opt("title") as? String) ?: childText(children), style)
5777
6069
  val linkColor = craftNativeOptionalString(props.opt("color")) ?: craftNativeOptionalString(style.opt("color"))
@@ -5794,7 +6086,7 @@ class MainActivity : AppCompatActivity() {
5794
6086
  send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
5795
6087
  }
5796
6088
  }
5797
- "Button" -> (previous as? Button ?: Button(this).apply {
6089
+ "Button" -> (previous as? Button ?: CraftNativePointerButton(this).apply {
5798
6090
  // A plain button, as on iOS (UIButton) and the web: its
5799
6091
  // style draws any fill, not Material's grey raised box.
5800
6092
  background = null
@@ -5819,7 +6111,7 @@ class MainActivity : AppCompatActivity() {
5819
6111
  send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
5820
6112
  }
5821
6113
  }
5822
- "Checkbox" -> (previous as? CheckBox ?: CheckBox(this)).apply {
6114
+ "Checkbox" -> (previous as? CheckBox ?: CraftNativePointerCheckBox(this)).apply {
5823
6115
  text = (props.opt("label") as? String) ?: childText(children)
5824
6116
  configureText(this, text.toString(), style)
5825
6117
  val checkboxChecked = props.opt("checked")
@@ -6004,7 +6296,7 @@ class MainActivity : AppCompatActivity() {
6004
6296
  addTextChangedListener(watcher)
6005
6297
  }
6006
6298
  }
6007
- "SegmentedControl" -> (previous as? android.widget.RadioGroup ?: android.widget.RadioGroup(this)).apply {
6299
+ "SegmentedControl" -> (previous as? android.widget.RadioGroup ?: CraftNativePointerRadioGroup(this)).apply {
6008
6300
  orientation = android.widget.LinearLayout.HORIZONTAL
6009
6301
  // Reconciliation is silent. Only the platform's user selection
6010
6302
  // listener below emits a value, with the stable option value.
@@ -6055,7 +6347,7 @@ class MainActivity : AppCompatActivity() {
6055
6347
  send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject().put("value", value).put("index", index)))
6056
6348
  }
6057
6349
  }
6058
- "Picker" -> (previous as? Spinner ?: Spinner(this)).apply {
6350
+ "Picker" -> (previous as? Spinner ?: CraftNativePointerSpinner(this)).apply {
6059
6351
  val itemOptions = props.optJSONArray("items")?.let { items ->
6060
6352
  (0 until items.length()).mapNotNull { index ->
6061
6353
  val item = items.optJSONObject(index) ?: return@mapNotNull null
@@ -6199,7 +6491,7 @@ class MainActivity : AppCompatActivity() {
6199
6491
  screen.pressBaseBackgrounds[this] = this.background
6200
6492
  reconcileChildren(screen, this, children, identity, next, style, previousControls)
6201
6493
  }
6202
- "Switch" -> (previous as? Switch ?: Switch(this)).apply {
6494
+ "Switch" -> (previous as? Switch ?: CraftNativePointerSwitch(this)).apply {
6203
6495
  setOnCheckedChangeListener(null)
6204
6496
  val requestedValue = props.opt("value")
6205
6497
  val requestedChecked = props.opt("checked")
@@ -6238,7 +6530,7 @@ class MainActivity : AppCompatActivity() {
6238
6530
  send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject().put("value", checked)))
6239
6531
  }
6240
6532
  }
6241
- "Slider" -> (previous as? SeekBar ?: SeekBar(this)).apply {
6533
+ "Slider" -> (previous as? SeekBar ?: CraftNativePointerSeekBar(this)).apply {
6242
6534
  val minimum = craftNativeFiniteNumber(props, "minimumValue", 0.0)
6243
6535
  val maximumCandidate = craftNativeFiniteNumber(props, "maximumValue", 1.0)
6244
6536
  val maximum = if (maximumCandidate > minimum) maximumCandidate else minimum + Double.MIN_VALUE
@@ -6319,13 +6611,14 @@ class MainActivity : AppCompatActivity() {
6319
6611
  "Icon" -> (previous as? CraftNativeIconView ?: CraftNativeIconView(this)).apply {
6320
6612
  configureIcon(this, props, style)
6321
6613
  }
6322
- "Image" -> (previous as? ImageView ?: ImageView(this)).apply {
6614
+ "Image" -> (previous as? CraftNativePointerImageView ?: CraftNativePointerImageView(this)).apply {
6323
6615
  scaleType = when (craftNativeResizeMode(style, props)) {
6324
6616
  "cover" -> ImageView.ScaleType.CENTER_CROP
6325
6617
  "stretch" -> ImageView.ScaleType.FIT_XY
6326
6618
  "center" -> ImageView.ScaleType.CENTER
6327
6619
  else -> ImageView.ScaleType.CENTER_INSIDE
6328
6620
  }
6621
+ configureObjectGeometry(props, style)
6329
6622
  adjustViewBounds = false
6330
6623
  val onLoadStart = craftNativeEventName(events, "onLoadStart")
6331
6624
  if (onLoadStart.isNotBlank()) screen.imageLoadStartHandlers[this] = onLoadStart else screen.imageLoadStartHandlers.remove(this)
@@ -6344,6 +6637,7 @@ class MainActivity : AppCompatActivity() {
6344
6637
  "center" -> ImageView.ScaleType.CENTER
6345
6638
  else -> ImageView.ScaleType.CENTER_INSIDE
6346
6639
  }
6640
+ image.configureObjectGeometry(props, style)
6347
6641
  val tint = craftNativeOptionalString(style.opt("tintColor"))
6348
6642
  ?.let { runCatching { craftNativeColor(it) }.getOrNull() }
6349
6643
  if (tint == null) image.clearColorFilter() else image.setColorFilter(tint)
@@ -6568,6 +6862,7 @@ class MainActivity : AppCompatActivity() {
6568
6862
  send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
6569
6863
  }
6570
6864
  control.setOnTouchListener { view, event ->
6865
+ if (!CraftNativePointers.ownsTouch(view) && event.actionMasked != MotionEvent.ACTION_CANCEL) return@setOnTouchListener false
6571
6866
  when (event.actionMasked) {
6572
6867
  android.view.MotionEvent.ACTION_DOWN -> {
6573
6868
  val active = screen.pressActiveOpacities[view] ?: 0.5f
@@ -6636,6 +6931,8 @@ class MainActivity : AppCompatActivity() {
6636
6931
  control.setOnLongClickListener(null)
6637
6932
  control.isLongClickable = false
6638
6933
  }
6934
+ CraftNativePointers.configure(control, props, style, control is CraftNativeFlexLayout || control is CraftNativeImageBackgroundView ||
6935
+ control is CraftNativeKeyboardAvoidingView || control is CraftNativeScrollView || control is CraftNativeRefreshableFlatList || control is CraftNativeModalView)
6639
6936
  applyAccessibility(control, props, type, explicitKey ?: protocolId ?: path)
6640
6937
  // One element across an animated change (craft.animation.next).
6641
6938
  control.transitionName = craftNativeOptionalString(props.opt("transitionID"))?.takeIf { it.isNotBlank() }
package/dist/cli.js CHANGED
@@ -3047,7 +3047,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
3047
3047
  `);
3048
3048
  }
3049
3049
  // package.json
3050
- var version = "0.0.395";
3050
+ var version = "0.0.396";
3051
3051
 
3052
3052
  // bin/cli.ts
3053
3053
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];