craft-native 0.0.394 → 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.
- package/dist/android/templates/CraftNativeFlatList.kt.template +13 -0
- package/dist/android/templates/CraftNativeMapGoogle.kt.template +3 -0
- package/dist/android/templates/CraftNativeMapLibre.kt.template +3 -0
- package/dist/android/templates/CraftNativeRefreshableFlatList.kt.template +9 -0
- package/dist/android/templates/MainActivityNative.kt.template +380 -25
- package/dist/cli.js +1 -1
- package/dist/components/index.d.ts +16 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/templates/CraftNativeFlatList.swift +8 -0
- package/dist/ios/templates/CraftNativeMutation.swift +27 -2
- package/dist/ios/templates/CraftNativeScreen.swift +332 -51
- package/dist/macos/templates/CraftNativeMutation.swift +27 -2
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
|
@@ -2389,6 +2651,31 @@ private class CraftNativeKeyboardAvoidingView(context: Context) : FrameLayout(co
|
|
|
2389
2651
|
}
|
|
2390
2652
|
}
|
|
2391
2653
|
|
|
2654
|
+
/**
|
|
2655
|
+
* The `fontFamily` a node passes to its children: its own, which already
|
|
2656
|
+
* carries any family it inherited (see [craftNativeInheritFontFamily]).
|
|
2657
|
+
*/
|
|
2658
|
+
internal fun craftNativeInheritableFontFamily(style: JSONObject): String? =
|
|
2659
|
+
craftNativeOptionalString(style.opt("fontFamily"))?.takeUnless { it == "inherit" }
|
|
2660
|
+
|
|
2661
|
+
/**
|
|
2662
|
+
* CSS-style `fontFamily` inheritance: a node that sets no family (or
|
|
2663
|
+
* `inherit`) takes its nearest ancestor's, so a brand face set on a screen's
|
|
2664
|
+
* root reaches every label, button and input below it. Returns [style]
|
|
2665
|
+
* itself when nothing is inherited or the node sets its own family, else a
|
|
2666
|
+
* shallow copy carrying the inherited one; the protocol document's style is
|
|
2667
|
+
* never written to, so an ancestor's later change still reaches the node.
|
|
2668
|
+
*/
|
|
2669
|
+
internal fun craftNativeInheritFontFamily(style: JSONObject, inherited: String?): JSONObject {
|
|
2670
|
+
if (inherited == null) return style
|
|
2671
|
+
val own = craftNativeOptionalString(style.opt("fontFamily"))
|
|
2672
|
+
if (own != null && own != "inherit") return style
|
|
2673
|
+
return JSONObject().also { copy ->
|
|
2674
|
+
style.keys().forEach { key -> copy.put(key, style.opt(key)) }
|
|
2675
|
+
copy.put("fontFamily", inherited)
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2392
2679
|
/** CSS family fallback order, using bundled app fonts before platform families. */
|
|
2393
2680
|
internal fun craftNativeFontFamily(family: String?, bundledFonts: Map<String, Typeface> = emptyMap()): Typeface =
|
|
2394
2681
|
family?.split(',')?.asSequence()?.map { it.trim().trim('"', '\'') }?.mapNotNull { name ->
|
|
@@ -2401,17 +2688,23 @@ internal fun craftNativeFontFamily(family: String?, bundledFonts: Map<String, Ty
|
|
|
2401
2688
|
}?.firstOrNull() ?: Typeface.DEFAULT
|
|
2402
2689
|
|
|
2403
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
|
+
|
|
2404
2697
|
val content = CraftNativeFlexLayout(context)
|
|
2405
2698
|
// Keep one refresh target across axis changes. Replacing all children of
|
|
2406
2699
|
// SwipeRefreshLayout would also detach its own animated indicator.
|
|
2407
2700
|
private val scrollContainer = FrameLayout(context)
|
|
2408
2701
|
private val vertical = object : ScrollView(context) {
|
|
2409
|
-
override fun onInterceptTouchEvent(event: MotionEvent): Boolean = isEnabled && super.onInterceptTouchEvent(event)
|
|
2410
|
-
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)
|
|
2411
2704
|
}
|
|
2412
2705
|
private val horizontal = object : HorizontalScrollView(context) {
|
|
2413
|
-
override fun onInterceptTouchEvent(event: MotionEvent): Boolean = isEnabled && super.onInterceptTouchEvent(event)
|
|
2414
|
-
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)
|
|
2415
2708
|
}
|
|
2416
2709
|
private var horizontalMode: Boolean? = null
|
|
2417
2710
|
private var pagingEnabled = false
|
|
@@ -2509,7 +2802,7 @@ internal class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(cont
|
|
|
2509
2802
|
}
|
|
2510
2803
|
|
|
2511
2804
|
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
|
2512
|
-
if (refreshHandler == null) return false
|
|
2805
|
+
if (refreshHandler == null || !CraftNativePointers.ownsTouch(this)) return false
|
|
2513
2806
|
return super.onInterceptTouchEvent(event)
|
|
2514
2807
|
}
|
|
2515
2808
|
|
|
@@ -2889,6 +3182,7 @@ internal class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(cont
|
|
|
2889
3182
|
onScroll?.invoke(x, y)
|
|
2890
3183
|
}
|
|
2891
3184
|
scroller.setOnTouchListener { _, event ->
|
|
3185
|
+
if (!CraftNativePointers.ownsTouch(scroller) && event.actionMasked != MotionEvent.ACTION_CANCEL) return@setOnTouchListener false
|
|
2892
3186
|
when (event.actionMasked) {
|
|
2893
3187
|
android.view.MotionEvent.ACTION_DOWN -> {
|
|
2894
3188
|
pageDragging = true
|
|
@@ -2959,6 +3253,9 @@ private class CraftNativeFlexSpacer(context: Context) : View(context)
|
|
|
2959
3253
|
* the other native leaf controls without adding an icon-font dependency.
|
|
2960
3254
|
*/
|
|
2961
3255
|
private class CraftNativeIconView(context: Context) : TextView(context) {
|
|
3256
|
+
override fun dispatchTouchEvent(event: MotionEvent): Boolean =
|
|
3257
|
+
CraftNativePointers.dispatch(this, event) { super.dispatchTouchEvent(it) }
|
|
3258
|
+
|
|
2962
3259
|
/** The app's vector for the icon (CraftNativeIcons), drawn in place of the glyph. */
|
|
2963
3260
|
var vector: Drawable? = null
|
|
2964
3261
|
set(value) {
|
|
@@ -2985,6 +3282,9 @@ private class CraftNativeIconView(context: Context) : TextView(context) {
|
|
|
2985
3282
|
|
|
2986
3283
|
/** A platform EditText with the native end clear affordance and TalkBack action. */
|
|
2987
3284
|
private class CraftNativeTextField(context: Context) : EditText(context) {
|
|
3285
|
+
override fun dispatchTouchEvent(event: MotionEvent): Boolean =
|
|
3286
|
+
CraftNativePointers.dispatch(this, event) { super.dispatchTouchEvent(it) }
|
|
3287
|
+
|
|
2988
3288
|
companion object { val CLEAR_ACTION_ID = View.generateViewId() }
|
|
2989
3289
|
var clearButtonMode = "never"
|
|
2990
3290
|
var onClearRequest: (() -> Unit)? = null
|
|
@@ -3029,8 +3329,14 @@ private class CraftNativeTextField(context: Context) : EditText(context) {
|
|
|
3029
3329
|
}
|
|
3030
3330
|
|
|
3031
3331
|
private class CraftNativeModalView(context: Context) : FrameLayout(context) {
|
|
3332
|
+
override fun dispatchTouchEvent(event: MotionEvent): Boolean =
|
|
3333
|
+
CraftNativePointers.dispatch(this, event) { super.dispatchTouchEvent(it) }
|
|
3334
|
+
|
|
3032
3335
|
val content = CraftNativeFlexLayout(context)
|
|
3033
|
-
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
|
+
}
|
|
3034
3340
|
private var renderedVisible = false
|
|
3035
3341
|
private var animationGeneration = 0
|
|
3036
3342
|
var focusToRestore: View? = null
|
|
@@ -3070,6 +3376,7 @@ private class CraftNativeModalView(context: Context) : FrameLayout(context) {
|
|
|
3070
3376
|
isClickable = true
|
|
3071
3377
|
blocker.isClickable = true
|
|
3072
3378
|
blocker.setOnTouchListener { _, event ->
|
|
3379
|
+
if (!CraftNativePointers.ownsTouch(this)) return@setOnTouchListener false
|
|
3073
3380
|
if (event.action == MotionEvent.ACTION_UP) requestClose(reason = "backdrop")
|
|
3074
3381
|
true
|
|
3075
3382
|
}
|
|
@@ -3150,6 +3457,7 @@ private class CraftNativeModalView(context: Context) : FrameLayout(context) {
|
|
|
3150
3457
|
}
|
|
3151
3458
|
|
|
3152
3459
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
3460
|
+
if (event.actionMasked != MotionEvent.ACTION_CANCEL && !CraftNativePointers.ownsTouch(this)) return false
|
|
3153
3461
|
if (!blocksBackground) {
|
|
3154
3462
|
if (dismissOnOutsideTap && event.action == MotionEvent.ACTION_UP) {
|
|
3155
3463
|
requestClose(reason = "backdrop")
|
|
@@ -3167,7 +3475,7 @@ private class CraftNativeModalView(context: Context) : FrameLayout(context) {
|
|
|
3167
3475
|
val index = group.indexOfChild(this)
|
|
3168
3476
|
return index > 0 && (0 until index).any { siblingIndex ->
|
|
3169
3477
|
val sibling = group.getChildAt(siblingIndex)
|
|
3170
|
-
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
|
|
3171
3479
|
}
|
|
3172
3480
|
}
|
|
3173
3481
|
}
|
|
@@ -3188,6 +3496,9 @@ private data class LayoutBounds(
|
|
|
3188
3496
|
*/
|
|
3189
3497
|
private class CraftNativeActivityIndicator(context: Context) :
|
|
3190
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
|
+
|
|
3191
3502
|
var indicatorScale = 1f
|
|
3192
3503
|
|
|
3193
3504
|
init {
|
|
@@ -3199,6 +3510,9 @@ private class CraftNativeActivityIndicator(context: Context) :
|
|
|
3199
3510
|
|
|
3200
3511
|
/** Android counterpart to iOS's UIDatePicker-backed native DatePicker. */
|
|
3201
3512
|
private class CraftNativeDatePickerView(context: Context) : FrameLayout(context) {
|
|
3513
|
+
override fun dispatchTouchEvent(event: MotionEvent): Boolean =
|
|
3514
|
+
CraftNativePointers.dispatch(this, event) { super.dispatchTouchEvent(it) }
|
|
3515
|
+
|
|
3202
3516
|
private enum class PickerMode { DATE, TIME, DATE_TIME, COUNTDOWN }
|
|
3203
3517
|
private val maxCountdownSeconds = 23 * 3600 + 59 * 60 + 59
|
|
3204
3518
|
|
|
@@ -3445,6 +3759,9 @@ internal class CraftNativeInputEcho {
|
|
|
3445
3759
|
* while this view keeps its place.
|
|
3446
3760
|
*/
|
|
3447
3761
|
private class CraftNativeVideoView(context: Context) : VideoView(context) {
|
|
3762
|
+
override fun dispatchTouchEvent(event: MotionEvent): Boolean =
|
|
3763
|
+
CraftNativePointers.dispatch(this, event) { super.dispatchTouchEvent(it) }
|
|
3764
|
+
|
|
3448
3765
|
private var sourceUri = ""
|
|
3449
3766
|
private var shouldLoop = false
|
|
3450
3767
|
private var shouldPlay = false
|
|
@@ -4210,6 +4527,12 @@ class MainActivity : AppCompatActivity() {
|
|
|
4210
4527
|
// the original tree path without manufacturing a detached view.
|
|
4211
4528
|
val nodeViews = mutableMapOf<String, View>()
|
|
4212
4529
|
val nodePaths = mutableMapOf<String, String>()
|
|
4530
|
+
/**
|
|
4531
|
+
* The `fontFamily` a protocol node inherited from its ancestors when
|
|
4532
|
+
* drawn, so a targeted update of its subtree keeps it. Only nodes that
|
|
4533
|
+
* inherit one are recorded: a screen with no family keeps it empty.
|
|
4534
|
+
*/
|
|
4535
|
+
val inheritedFontFamilies = mutableMapOf<String, String>()
|
|
4213
4536
|
val protocolIds = mutableMapOf<View, String>()
|
|
4214
4537
|
val handlers = mutableMapOf<View, String>()
|
|
4215
4538
|
val linkTargets = mutableMapOf<View, String>()
|
|
@@ -5497,7 +5820,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5497
5820
|
val document = screen.mutations.node(target) ?: return false
|
|
5498
5821
|
val path = screen.nodePaths[target] ?: return false
|
|
5499
5822
|
if (screen.nodeViews[target] == null) return false
|
|
5500
|
-
makeView(screen, document, path, screen.controls)
|
|
5823
|
+
makeView(screen, document, path, screen.controls, inheritedFontFamily = screen.inheritedFontFamilies[target])
|
|
5501
5824
|
val updated = screen.nodeViews[target] ?: return false
|
|
5502
5825
|
screen.lastLayoutBounds.remove(updated)
|
|
5503
5826
|
if (!updateParentLayoutStyle(updated, document.optJSONObject("style") ?: JSONObject())) return false
|
|
@@ -5584,6 +5907,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5584
5907
|
val next = mutableMapOf<String, View>()
|
|
5585
5908
|
screen.nodeViews.clear()
|
|
5586
5909
|
screen.nodePaths.clear()
|
|
5910
|
+
screen.inheritedFontFamilies.clear()
|
|
5587
5911
|
val tree = makeView(screen, document, "root", next)
|
|
5588
5912
|
if (screen.view.getChildAt(0) !== tree) {
|
|
5589
5913
|
(tree.parent as? ViewGroup)?.removeView(tree)
|
|
@@ -5673,6 +5997,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5673
5997
|
screen.controls.clear()
|
|
5674
5998
|
screen.nodeViews.clear()
|
|
5675
5999
|
screen.nodePaths.clear()
|
|
6000
|
+
screen.inheritedFontFamilies.clear()
|
|
5676
6001
|
screen.protocolIds.clear()
|
|
5677
6002
|
screen.handlers.clear()
|
|
5678
6003
|
screen.linkTargets.clear()
|
|
@@ -5681,16 +6006,22 @@ class MainActivity : AppCompatActivity() {
|
|
|
5681
6006
|
screen.flatListOwnerByView.clear()
|
|
5682
6007
|
}
|
|
5683
6008
|
|
|
6009
|
+
/**
|
|
6010
|
+
* [inheritedFontFamily]: the nearest ancestor's `fontFamily`, which the
|
|
6011
|
+
* node takes when it sets none ([craftNativeInheritFontFamily]) and
|
|
6012
|
+
* passes on to its children and list rows.
|
|
6013
|
+
*/
|
|
5684
6014
|
private fun makeView(
|
|
5685
6015
|
screen: Screen,
|
|
5686
6016
|
node: JSONObject,
|
|
5687
6017
|
path: String,
|
|
5688
6018
|
next: MutableMap<String, View>,
|
|
5689
6019
|
previousControls: Map<String, View> = screen.controls,
|
|
6020
|
+
inheritedFontFamily: String? = null,
|
|
5690
6021
|
): View {
|
|
5691
6022
|
val type = canonicalType(node.optString("type", "View"))
|
|
5692
6023
|
val props = node.optJSONObject("props") ?: JSONObject()
|
|
5693
|
-
val style = node.optJSONObject("style") ?: JSONObject()
|
|
6024
|
+
val style = craftNativeInheritFontFamily(node.optJSONObject("style") ?: JSONObject(), inheritedFontFamily)
|
|
5694
6025
|
val events = node.optJSONObject("events") ?: JSONObject()
|
|
5695
6026
|
val children = node.optJSONArray("children") ?: JSONArray()
|
|
5696
6027
|
val protocolId = node.optString("id").takeIf { it.isNotBlank() }
|
|
@@ -5709,7 +6040,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5709
6040
|
val previous = protocolId?.let { previousControls["id:$it"] } ?: keyedPrevious
|
|
5710
6041
|
val reusableInput = previous as? CraftNativeTextField
|
|
5711
6042
|
val control = when (type) {
|
|
5712
|
-
"Text" -> ((previous as? TextView)?.takeUnless { it is Button || it is EditText } ?:
|
|
6043
|
+
"Text" -> ((previous as? TextView)?.takeUnless { it is Button || it is EditText } ?: CraftNativePointerTextView(this)).apply {
|
|
5713
6044
|
includeFontPadding = false
|
|
5714
6045
|
maxLines = craftNativeInt(props.opt("numberOfLines"), 0).takeIf { it > 0 } ?: Int.MAX_VALUE
|
|
5715
6046
|
ellipsize = when (craftNativeOptionalString(props.opt("ellipsizeMode"))) {
|
|
@@ -5732,7 +6063,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5732
6063
|
"RefreshControl" -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
|
|
5733
6064
|
visibility = View.GONE
|
|
5734
6065
|
}
|
|
5735
|
-
"Link" -> ((previous as? TextView)?.takeUnless { it is Button } ?:
|
|
6066
|
+
"Link" -> ((previous as? TextView)?.takeUnless { it is Button } ?: CraftNativePointerTextView(this)).apply {
|
|
5736
6067
|
includeFontPadding = false
|
|
5737
6068
|
configureText(this, (props.opt("title") as? String) ?: childText(children), style)
|
|
5738
6069
|
val linkColor = craftNativeOptionalString(props.opt("color")) ?: craftNativeOptionalString(style.opt("color"))
|
|
@@ -5755,7 +6086,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5755
6086
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
5756
6087
|
}
|
|
5757
6088
|
}
|
|
5758
|
-
"Button" -> (previous as? Button ?:
|
|
6089
|
+
"Button" -> (previous as? Button ?: CraftNativePointerButton(this).apply {
|
|
5759
6090
|
// A plain button, as on iOS (UIButton) and the web: its
|
|
5760
6091
|
// style draws any fill, not Material's grey raised box.
|
|
5761
6092
|
background = null
|
|
@@ -5780,7 +6111,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5780
6111
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
5781
6112
|
}
|
|
5782
6113
|
}
|
|
5783
|
-
"Checkbox" -> (previous as? CheckBox ?:
|
|
6114
|
+
"Checkbox" -> (previous as? CheckBox ?: CraftNativePointerCheckBox(this)).apply {
|
|
5784
6115
|
text = (props.opt("label") as? String) ?: childText(children)
|
|
5785
6116
|
configureText(this, text.toString(), style)
|
|
5786
6117
|
val checkboxChecked = props.opt("checked")
|
|
@@ -5965,7 +6296,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5965
6296
|
addTextChangedListener(watcher)
|
|
5966
6297
|
}
|
|
5967
6298
|
}
|
|
5968
|
-
"SegmentedControl" -> (previous as? android.widget.RadioGroup ?:
|
|
6299
|
+
"SegmentedControl" -> (previous as? android.widget.RadioGroup ?: CraftNativePointerRadioGroup(this)).apply {
|
|
5969
6300
|
orientation = android.widget.LinearLayout.HORIZONTAL
|
|
5970
6301
|
// Reconciliation is silent. Only the platform's user selection
|
|
5971
6302
|
// listener below emits a value, with the stable option value.
|
|
@@ -6016,7 +6347,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
6016
6347
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject().put("value", value).put("index", index)))
|
|
6017
6348
|
}
|
|
6018
6349
|
}
|
|
6019
|
-
"Picker" -> (previous as? Spinner ?:
|
|
6350
|
+
"Picker" -> (previous as? Spinner ?: CraftNativePointerSpinner(this)).apply {
|
|
6020
6351
|
val itemOptions = props.optJSONArray("items")?.let { items ->
|
|
6021
6352
|
(0 until items.length()).mapNotNull { index ->
|
|
6022
6353
|
val item = items.optJSONObject(index) ?: return@mapNotNull null
|
|
@@ -6160,7 +6491,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
6160
6491
|
screen.pressBaseBackgrounds[this] = this.background
|
|
6161
6492
|
reconcileChildren(screen, this, children, identity, next, style, previousControls)
|
|
6162
6493
|
}
|
|
6163
|
-
"Switch" -> (previous as? Switch ?:
|
|
6494
|
+
"Switch" -> (previous as? Switch ?: CraftNativePointerSwitch(this)).apply {
|
|
6164
6495
|
setOnCheckedChangeListener(null)
|
|
6165
6496
|
val requestedValue = props.opt("value")
|
|
6166
6497
|
val requestedChecked = props.opt("checked")
|
|
@@ -6199,7 +6530,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
6199
6530
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject().put("value", checked)))
|
|
6200
6531
|
}
|
|
6201
6532
|
}
|
|
6202
|
-
"Slider" -> (previous as? SeekBar ?:
|
|
6533
|
+
"Slider" -> (previous as? SeekBar ?: CraftNativePointerSeekBar(this)).apply {
|
|
6203
6534
|
val minimum = craftNativeFiniteNumber(props, "minimumValue", 0.0)
|
|
6204
6535
|
val maximumCandidate = craftNativeFiniteNumber(props, "maximumValue", 1.0)
|
|
6205
6536
|
val maximum = if (maximumCandidate > minimum) maximumCandidate else minimum + Double.MIN_VALUE
|
|
@@ -6280,13 +6611,14 @@ class MainActivity : AppCompatActivity() {
|
|
|
6280
6611
|
"Icon" -> (previous as? CraftNativeIconView ?: CraftNativeIconView(this)).apply {
|
|
6281
6612
|
configureIcon(this, props, style)
|
|
6282
6613
|
}
|
|
6283
|
-
"Image" -> (previous as?
|
|
6614
|
+
"Image" -> (previous as? CraftNativePointerImageView ?: CraftNativePointerImageView(this)).apply {
|
|
6284
6615
|
scaleType = when (craftNativeResizeMode(style, props)) {
|
|
6285
6616
|
"cover" -> ImageView.ScaleType.CENTER_CROP
|
|
6286
6617
|
"stretch" -> ImageView.ScaleType.FIT_XY
|
|
6287
6618
|
"center" -> ImageView.ScaleType.CENTER
|
|
6288
6619
|
else -> ImageView.ScaleType.CENTER_INSIDE
|
|
6289
6620
|
}
|
|
6621
|
+
configureObjectGeometry(props, style)
|
|
6290
6622
|
adjustViewBounds = false
|
|
6291
6623
|
val onLoadStart = craftNativeEventName(events, "onLoadStart")
|
|
6292
6624
|
if (onLoadStart.isNotBlank()) screen.imageLoadStartHandlers[this] = onLoadStart else screen.imageLoadStartHandlers.remove(this)
|
|
@@ -6305,6 +6637,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
6305
6637
|
"center" -> ImageView.ScaleType.CENTER
|
|
6306
6638
|
else -> ImageView.ScaleType.CENTER_INSIDE
|
|
6307
6639
|
}
|
|
6640
|
+
image.configureObjectGeometry(props, style)
|
|
6308
6641
|
val tint = craftNativeOptionalString(style.opt("tintColor"))
|
|
6309
6642
|
?.let { runCatching { craftNativeColor(it) }.getOrNull() }
|
|
6310
6643
|
if (tint == null) image.clearColorFilter() else image.setColorFilter(tint)
|
|
@@ -6477,7 +6810,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
6477
6810
|
(getSystemService(INPUT_METHOD_SERVICE) as? android.view.inputmethod.InputMethodManager)
|
|
6478
6811
|
?.hideSoftInputFromWindow(windowToken, 0)
|
|
6479
6812
|
}
|
|
6480
|
-
reconcileFlatList(screen, nativeList, children, identity, protocolId ?: identity, props, events)
|
|
6813
|
+
reconcileFlatList(screen, nativeList, children, identity, protocolId ?: identity, props, events, craftNativeInheritableFontFamily(style))
|
|
6481
6814
|
}
|
|
6482
6815
|
"View", "SafeAreaView" -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
|
|
6483
6816
|
configureContainer(this, style)
|
|
@@ -6529,6 +6862,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
6529
6862
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
6530
6863
|
}
|
|
6531
6864
|
control.setOnTouchListener { view, event ->
|
|
6865
|
+
if (!CraftNativePointers.ownsTouch(view) && event.actionMasked != MotionEvent.ACTION_CANCEL) return@setOnTouchListener false
|
|
6532
6866
|
when (event.actionMasked) {
|
|
6533
6867
|
android.view.MotionEvent.ACTION_DOWN -> {
|
|
6534
6868
|
val active = screen.pressActiveOpacities[view] ?: 0.5f
|
|
@@ -6597,6 +6931,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
6597
6931
|
control.setOnLongClickListener(null)
|
|
6598
6932
|
control.isLongClickable = false
|
|
6599
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)
|
|
6600
6936
|
applyAccessibility(control, props, type, explicitKey ?: protocolId ?: path)
|
|
6601
6937
|
// One element across an animated change (craft.animation.next).
|
|
6602
6938
|
control.transitionName = craftNativeOptionalString(props.opt("transitionID"))?.takeIf { it.isNotBlank() }
|
|
@@ -6622,6 +6958,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
6622
6958
|
}
|
|
6623
6959
|
}
|
|
6624
6960
|
protocolId?.let {
|
|
6961
|
+
if (inheritedFontFamily != null) {
|
|
6962
|
+
screen.inheritedFontFamilies[it] = inheritedFontFamily
|
|
6963
|
+
} else if (screen.inheritedFontFamilies.isNotEmpty()) {
|
|
6964
|
+
screen.inheritedFontFamilies.remove(it)
|
|
6965
|
+
}
|
|
6625
6966
|
screen.nodeViews[it] = control
|
|
6626
6967
|
screen.nodePaths[it] = path
|
|
6627
6968
|
}
|
|
@@ -6742,10 +7083,12 @@ class MainActivity : AppCompatActivity() {
|
|
|
6742
7083
|
}
|
|
6743
7084
|
val desired = mutableListOf<View>()
|
|
6744
7085
|
val desiredOrder = mutableMapOf<View, Int>()
|
|
7086
|
+
// `style` is the parent's effective style, its inherited family included.
|
|
7087
|
+
val fontFamily = craftNativeInheritableFontFamily(style)
|
|
6745
7088
|
for (index in 0 until children.length()) {
|
|
6746
7089
|
val child = children.opt(index)
|
|
6747
7090
|
val childView = when (child) {
|
|
6748
|
-
is JSONObject -> makeView(screen, child, "$path.$index", next, previousControls)
|
|
7091
|
+
is JSONObject -> makeView(screen, child, "$path.$index", next, previousControls, fontFamily)
|
|
6749
7092
|
is String -> TextView(this).apply {
|
|
6750
7093
|
includeFontPadding = false
|
|
6751
7094
|
configureText(this, child, craftNativeTypography(style))
|
|
@@ -6790,6 +7133,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
6790
7133
|
owner: String,
|
|
6791
7134
|
props: JSONObject,
|
|
6792
7135
|
events: JSONObject,
|
|
7136
|
+
fontFamily: String?,
|
|
6793
7137
|
) {
|
|
6794
7138
|
screen.flatListOwners.entries.removeAll { it.value == owner }
|
|
6795
7139
|
screen.flatListOwnerByView[list] = owner
|
|
@@ -6819,6 +7163,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
6819
7163
|
val threshold = craftNativeNonNegativeDouble(props, "threshold", 0.1)
|
|
6820
7164
|
val authoredThreshold = craftNativeNonNegativeDouble(props, "onEndReachedThreshold", threshold)
|
|
6821
7165
|
val horizontal = craftNativeBoolean(props.opt("horizontal"))
|
|
7166
|
+
// A row redraws when its own node changes; an ancestor's new family
|
|
7167
|
+
// leaves every node as it was, so rows drawn before are redrawn below.
|
|
7168
|
+
val familyChanged = list.inheritedFontFamily != fontFamily
|
|
7169
|
+
val rowsDrawnBefore = familyChanged && screen.flatListRows[list]?.isNotEmpty() == true
|
|
6822
7170
|
list.apply(
|
|
6823
7171
|
nodes = nodes,
|
|
6824
7172
|
horizontal = horizontal,
|
|
@@ -6829,7 +7177,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
6829
7177
|
val rows = screen.flatListRows.getOrPut(list) { mutableMapOf() }
|
|
6830
7178
|
val previous = rows[itemIdentity].orEmpty()
|
|
6831
7179
|
val next = mutableMapOf<String, View>()
|
|
6832
|
-
|
|
7180
|
+
// Rows are drawn later, as they scroll in: each takes the
|
|
7181
|
+
// list's family from this render's closure.
|
|
7182
|
+
val view = makeView(screen, node, "$path.$itemIdentity", next, previous, fontFamily)
|
|
6833
7183
|
releaseViewState(screen, previous.values.filter { it !in next.values })
|
|
6834
7184
|
rows[itemIdentity] = next
|
|
6835
7185
|
view
|
|
@@ -6858,6 +7208,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
6858
7208
|
craftNativeBoolean(props.opt("alwaysBounceVertical"), !horizontal),
|
|
6859
7209
|
craftNativeBoolean(props.opt("alwaysBounceHorizontal"), horizontal),
|
|
6860
7210
|
)
|
|
7211
|
+
if (familyChanged) {
|
|
7212
|
+
list.inheritedFontFamily = fontFamily
|
|
7213
|
+
if (rowsDrawnBefore) list.refreshThemeDefaults()
|
|
7214
|
+
}
|
|
6861
7215
|
}
|
|
6862
7216
|
|
|
6863
7217
|
private fun registerFlatListOwnership(screen: Screen, node: JSONObject, owner: String) {
|
|
@@ -8358,6 +8712,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
8358
8712
|
screen.controls.clear()
|
|
8359
8713
|
screen.nodeViews.clear()
|
|
8360
8714
|
screen.nodePaths.clear()
|
|
8715
|
+
screen.inheritedFontFamilies.clear()
|
|
8361
8716
|
screen.protocolIds.clear()
|
|
8362
8717
|
screen.handlers.clear()
|
|
8363
8718
|
screen.pressInHandlers.clear()
|