@craft-native/android 0.0.175 → 0.0.177
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/README.md +12 -2
- package/dist/index.d.ts +19 -0
- package/dist/index.js +20 -3
- package/dist/index.js.map +3 -3
- package/package.json +1 -1
- package/templates/AndroidManifest.xml.template +1 -0
- package/templates/CraftBridge.kt.template +51 -0
- package/templates/CraftCrashReporter.kt.template +544 -0
- package/templates/CraftNative.kt.template +17 -0
- package/templates/CraftNativeFlatList.kt.template +20 -9
- package/templates/CraftNativeMutation.kt.template +4 -1
- package/templates/CraftNativeServices.kt.template +31 -1
- package/templates/MainActivityNative.kt.template +986 -304
- package/templates/build.gradle.kts.app.template +4 -2
|
@@ -12,7 +12,9 @@ import android.database.ContentObserver
|
|
|
12
12
|
import android.content.Intent
|
|
13
13
|
import android.media.AudioAttributes
|
|
14
14
|
import android.media.AudioFormat
|
|
15
|
+
import android.media.MediaPlayer
|
|
15
16
|
import android.media.AudioTrack
|
|
17
|
+
import android.graphics.Bitmap
|
|
16
18
|
import android.graphics.BitmapFactory
|
|
17
19
|
import android.graphics.Canvas
|
|
18
20
|
import android.graphics.Color
|
|
@@ -54,6 +56,7 @@ import android.text.Layout
|
|
|
54
56
|
import android.view.View
|
|
55
57
|
import android.view.ViewGroup
|
|
56
58
|
import android.view.ViewOutlineProvider
|
|
59
|
+
import android.view.ViewParent
|
|
57
60
|
import android.view.ViewTreeObserver
|
|
58
61
|
import android.widget.AdapterView
|
|
59
62
|
import android.widget.ArrayAdapter
|
|
@@ -80,6 +83,7 @@ import androidx.core.content.ContextCompat
|
|
|
80
83
|
import androidx.core.view.AccessibilityDelegateCompat
|
|
81
84
|
import androidx.core.view.WindowInsetsCompat
|
|
82
85
|
import androidx.core.view.ViewCompat
|
|
86
|
+
import androidx.core.view.WindowCompat
|
|
83
87
|
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
|
|
84
88
|
import androidx.javascriptengine.JavaScriptIsolate
|
|
85
89
|
import androidx.javascriptengine.JavaScriptConsoleCallback
|
|
@@ -113,26 +117,63 @@ import kotlin.math.max
|
|
|
113
117
|
import kotlin.math.min
|
|
114
118
|
import kotlin.math.PI
|
|
115
119
|
import kotlin.math.ceil
|
|
120
|
+
import kotlin.math.abs
|
|
121
|
+
import kotlin.math.exp
|
|
116
122
|
import kotlin.math.roundToInt
|
|
117
123
|
import kotlin.math.roundToLong
|
|
118
124
|
import kotlin.math.sin
|
|
125
|
+
import kotlin.math.pow
|
|
119
126
|
|
|
120
127
|
private const val CRAFT_NATIVE_CAPABILITY_TIMEOUT_MS = 30_000L
|
|
121
128
|
|
|
129
|
+
private fun craftNativeOptionalString(value: Any?): String? = value
|
|
130
|
+
?.takeUnless { it == JSONObject.NULL }
|
|
131
|
+
?.toString()
|
|
132
|
+
?.takeUnless { it == "null" || it == "undefined" }
|
|
133
|
+
?.takeIf { it.isNotBlank() }
|
|
134
|
+
|
|
135
|
+
private fun craftNativeEventName(events: JSONObject, vararg names: String): String =
|
|
136
|
+
names.firstNotNullOfOrNull { name -> craftNativeOptionalString(events.opt(name)) }.orEmpty()
|
|
137
|
+
|
|
138
|
+
internal fun craftNativePadding(style: JSONObject?, name: String, fallback: Int, density: Float): Int {
|
|
139
|
+
val value = style?.optDouble(name, Double.NaN) ?: Double.NaN
|
|
140
|
+
val pixels = value * density
|
|
141
|
+
return if (value.isFinite() && value >= 0 && pixels.isFinite()) pixels.toInt() else fallback
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
internal fun craftNativeFiniteFloat(style: JSONObject?, name: String, fallback: Float): Float {
|
|
145
|
+
val value = style?.optDouble(name, Double.NaN) ?: Double.NaN
|
|
146
|
+
val narrowed = value.toFloat()
|
|
147
|
+
return if (value.isFinite() && narrowed.isFinite()) narrowed else fallback
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
internal fun craftNativeFiniteDouble(style: JSONObject?, name: String, fallback: Double): Double {
|
|
151
|
+
val value = style?.optDouble(name, Double.NaN) ?: Double.NaN
|
|
152
|
+
return value.takeIf { it.isFinite() } ?: fallback
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
internal fun craftNativeNonNegativeDouble(style: JSONObject?, name: String, fallback: Double): Double {
|
|
156
|
+
val value = craftNativeFiniteDouble(style, name, Double.NaN)
|
|
157
|
+
return if (value.isFinite() && value >= 0) value else fallback
|
|
158
|
+
}
|
|
159
|
+
|
|
122
160
|
private data class CraftNativeLength(
|
|
123
161
|
val points: Int? = null,
|
|
124
162
|
val fraction: Double? = null,
|
|
125
163
|
) {
|
|
126
|
-
fun resolve(base: Int?): Int? = points ?: fraction?.let { value -> base?.let { max(0, (it * value).roundToInt()) } }
|
|
164
|
+
fun resolve(base: Int?): Int? = points?.coerceAtLeast(0) ?: fraction?.let { value -> base?.let { max(0, (it * value).roundToInt()) } }
|
|
127
165
|
|
|
128
166
|
companion object {
|
|
129
167
|
fun from(value: Any?, density: Float): CraftNativeLength? {
|
|
130
168
|
if (value == null || value == JSONObject.NULL) return null
|
|
131
|
-
if (value is Number)
|
|
169
|
+
if (value is Number) {
|
|
170
|
+
val points = value.toDouble() * density
|
|
171
|
+
return points.takeIf { it.isFinite() }?.let { CraftNativeLength(points = it.roundToInt()) }
|
|
172
|
+
}
|
|
132
173
|
val text = value.toString().trim()
|
|
133
|
-
if (text.endsWith('%')) return text.dropLast(1).toDoubleOrNull()?.let { CraftNativeLength(fraction = it / 100) }
|
|
134
|
-
if (text.endsWith("px")) return text.dropLast(2).toDoubleOrNull()?.let { CraftNativeLength(points = (it * density).roundToInt()) }
|
|
135
|
-
return text.toDoubleOrNull()?.let { CraftNativeLength(points = (it * density).roundToInt()) }
|
|
174
|
+
if (text.endsWith('%')) return text.dropLast(1).toDoubleOrNull()?.takeIf { it.isFinite() }?.let { CraftNativeLength(fraction = it / 100) }
|
|
175
|
+
if (text.endsWith("px")) return text.dropLast(2).toDoubleOrNull()?.takeIf { it.isFinite() }?.let { CraftNativeLength(points = (it * density).roundToInt()) }
|
|
176
|
+
return text.toDoubleOrNull()?.takeIf { it.isFinite() }?.let { CraftNativeLength(points = (it * density).roundToInt()) }
|
|
136
177
|
}
|
|
137
178
|
}
|
|
138
179
|
}
|
|
@@ -160,11 +201,13 @@ private data class CraftNativeLayoutStyle(
|
|
|
160
201
|
fun length(name: String): CraftNativeLength? = CraftNativeLength.from(raw.opt(name), density)
|
|
161
202
|
fun number(name: String): Int? {
|
|
162
203
|
if (!raw.has(name) || raw.isNull(name)) return null
|
|
163
|
-
return raw.optDouble(name, Double.NaN).
|
|
204
|
+
return raw.optDouble(name, Double.NaN).takeIf { it.isFinite() }?.let { points ->
|
|
205
|
+
(points * density).takeIf { it.isFinite() }?.roundToInt()
|
|
206
|
+
}
|
|
164
207
|
}
|
|
165
208
|
fun decimal(name: String): Double? {
|
|
166
209
|
if (!raw.has(name) || raw.isNull(name)) return null
|
|
167
|
-
return raw.optDouble(name, Double.NaN).
|
|
210
|
+
return raw.optDouble(name, Double.NaN).takeIf { it.isFinite() }
|
|
168
211
|
}
|
|
169
212
|
val flex = decimal("flex")
|
|
170
213
|
return CraftNativeLayoutStyle(
|
|
@@ -277,6 +320,10 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
277
320
|
val color: String = "#000000",
|
|
278
321
|
val opacity: Float = 1f,
|
|
279
322
|
val typeface: Typeface = Typeface.DEFAULT,
|
|
323
|
+
val strokeLinecap: String? = null,
|
|
324
|
+
val strokeLinejoin: String? = null,
|
|
325
|
+
val strokeDasharray: List<Float> = emptyList(),
|
|
326
|
+
val vectorEffect: String? = null,
|
|
280
327
|
) {
|
|
281
328
|
fun merge(node: JSONObject): SvgPaint {
|
|
282
329
|
val props = node.optJSONObject("props") ?: JSONObject()
|
|
@@ -286,17 +333,32 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
286
333
|
props.has(name) && !props.isNull(name) -> props.optString(name)
|
|
287
334
|
else -> null
|
|
288
335
|
}
|
|
336
|
+
fun raw(name: String): Any? = when {
|
|
337
|
+
style.has(name) && !style.isNull(name) -> style.opt(name)
|
|
338
|
+
props.has(name) && !props.isNull(name) -> props.opt(name)
|
|
339
|
+
else -> null
|
|
340
|
+
}
|
|
341
|
+
fun dashes(rawValue: Any?): List<Float>? {
|
|
342
|
+
val values = rawValue as? JSONArray ?: return null
|
|
343
|
+
return (0 until values.length()).mapNotNull {
|
|
344
|
+
values.optDouble(it, Double.NaN).toFloat().takeIf { value -> value.isFinite() && value >= 0 }
|
|
345
|
+
}
|
|
346
|
+
}
|
|
289
347
|
// Keep the cascade explicit: computed style wins over a raw prop
|
|
290
348
|
// when both provide the same SVG paint value.
|
|
291
349
|
val weight = value("fontWeight")
|
|
292
|
-
val ownOpacity = value("opacity")?.toFloatOrNull() ?: 1f
|
|
350
|
+
val ownOpacity = value("opacity")?.toFloatOrNull()?.takeIf { it.isFinite() } ?: 1f
|
|
293
351
|
return copy(
|
|
294
352
|
fill = value("fill") ?: fill,
|
|
295
353
|
stroke = value("stroke") ?: stroke,
|
|
296
|
-
strokeWidth = value("strokeWidth")?.toFloatOrNull() ?: strokeWidth,
|
|
354
|
+
strokeWidth = value("strokeWidth")?.toFloatOrNull()?.takeIf { it.isFinite() } ?: strokeWidth,
|
|
297
355
|
color = value("color") ?: color,
|
|
298
356
|
opacity = ownOpacity * opacity,
|
|
299
357
|
typeface = if (weight in setOf("bold", "700")) Typeface.DEFAULT_BOLD else typeface,
|
|
358
|
+
strokeLinecap = value("strokeLinecap") ?: strokeLinecap,
|
|
359
|
+
strokeLinejoin = value("strokeLinejoin") ?: strokeLinejoin,
|
|
360
|
+
strokeDasharray = dashes(raw("strokeDasharray")) ?: strokeDasharray,
|
|
361
|
+
vectorEffect = value("vectorEffect") ?: vectorEffect,
|
|
300
362
|
)
|
|
301
363
|
}
|
|
302
364
|
}
|
|
@@ -354,7 +416,7 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
354
416
|
val y = values.optDouble(1, Double.NaN).toFloat()
|
|
355
417
|
val width = values.optDouble(2, Double.NaN).toFloat()
|
|
356
418
|
val height = values.optDouble(3, Double.NaN).toFloat()
|
|
357
|
-
if (!x.isFinite() || !y.isFinite() || !width.isFinite() || !height.isFinite()) return null
|
|
419
|
+
if (!x.isFinite() || !y.isFinite() || !width.isFinite() || !height.isFinite() || width <= 0f || height <= 0f) return null
|
|
358
420
|
return Quadruple(width, height, x, y)
|
|
359
421
|
}
|
|
360
422
|
|
|
@@ -375,6 +437,7 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
375
437
|
private fun drawPath(canvas: Canvas, node: JSONObject, style: SvgPaint) {
|
|
376
438
|
val props = node.optJSONObject("props") ?: JSONObject()
|
|
377
439
|
val geometry = props.optJSONArray("geometry") ?: return
|
|
440
|
+
if ((0 until geometry.length()).any { !geometry.optDouble(it, Double.NaN).toFloat().isFinite() }) return
|
|
378
441
|
val path = Path()
|
|
379
442
|
var index = 0
|
|
380
443
|
while (index < geometry.length()) {
|
|
@@ -401,34 +464,29 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
401
464
|
alpha = (style.opacity * 255f).roundToInt().coerceIn(0, 255)
|
|
402
465
|
this.style = Paint.Style.STROKE
|
|
403
466
|
strokeWidth = style.strokeWidth
|
|
404
|
-
strokeCap = when (
|
|
467
|
+
strokeCap = when (style.strokeLinecap) {
|
|
405
468
|
"round" -> Paint.Cap.ROUND
|
|
406
469
|
"square" -> Paint.Cap.SQUARE
|
|
407
470
|
else -> Paint.Cap.BUTT
|
|
408
471
|
}
|
|
409
|
-
strokeJoin = when (
|
|
472
|
+
strokeJoin = when (style.strokeLinejoin) {
|
|
410
473
|
"round" -> Paint.Join.ROUND
|
|
411
474
|
"bevel" -> Paint.Join.BEVEL
|
|
412
475
|
else -> Paint.Join.MITER
|
|
413
476
|
}
|
|
414
|
-
|
|
415
|
-
val
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
val pattern: FloatArray = if (dashPattern.size % 2 == 1) {
|
|
421
|
-
FloatArray(dashPattern.size * 2) { index -> dashPattern[index % dashPattern.size] }
|
|
422
|
-
} else {
|
|
423
|
-
dashPattern
|
|
424
|
-
}
|
|
425
|
-
pathEffect = DashPathEffect(pattern, 0f)
|
|
477
|
+
if (style.strokeDasharray.isNotEmpty()) {
|
|
478
|
+
val dashPattern = style.strokeDasharray.toFloatArray()
|
|
479
|
+
val pattern: FloatArray = if (dashPattern.size % 2 == 1) {
|
|
480
|
+
FloatArray(dashPattern.size * 2) { index -> dashPattern[index % dashPattern.size] }
|
|
481
|
+
} else {
|
|
482
|
+
dashPattern
|
|
426
483
|
}
|
|
484
|
+
pathEffect = DashPathEffect(pattern, 0f)
|
|
427
485
|
}
|
|
428
486
|
}
|
|
429
487
|
// `vector-effect="non-scaling-stroke"`: the stroke is in the view's
|
|
430
488
|
// pixels, so a stretched chart's line keeps its width.
|
|
431
|
-
if (
|
|
489
|
+
if (style.vectorEffect == "non-scaling-stroke") {
|
|
432
490
|
val line = Path(path).apply { transform(viewTransform) }
|
|
433
491
|
canvas.save()
|
|
434
492
|
canvas.concat(inverseTransform)
|
|
@@ -449,15 +507,17 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
449
507
|
}
|
|
450
508
|
if (text.isEmpty()) return
|
|
451
509
|
val size = (if (props.has("fontSize")) props.optDouble("fontSize", 12.0) else nodeStyle.optDouble("fontSize", 12.0))
|
|
452
|
-
.
|
|
510
|
+
.takeIf { it.isFinite() }
|
|
511
|
+
?.toFloat()
|
|
512
|
+
?: 12f
|
|
453
513
|
val paint = android.graphics.Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
454
514
|
color = resolveColor(style.fill, style.color, Color.BLACK)
|
|
455
515
|
alpha = (style.opacity * 255f).roundToInt().coerceIn(0, 255)
|
|
456
516
|
textSize = size
|
|
457
517
|
typeface = style.typeface
|
|
458
518
|
}
|
|
459
|
-
var x = props.optDouble("x", 0.0).toFloat()
|
|
460
|
-
val y = props.optDouble("y", 0.0).toFloat()
|
|
519
|
+
var x = props.optDouble("x", 0.0).takeIf { it.isFinite() }?.toFloat() ?: 0f
|
|
520
|
+
val y = props.optDouble("y", 0.0).takeIf { it.isFinite() }?.toFloat() ?: 0f
|
|
461
521
|
when (props.optString("textAnchor")) {
|
|
462
522
|
"middle" -> x -= paint.measureText(text) / 2f
|
|
463
523
|
"end" -> x -= paint.measureText(text)
|
|
@@ -482,6 +542,8 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
482
542
|
private val polylines = mutableMapOf<String, Polyline>()
|
|
483
543
|
private val markerHandlers = mutableMapOf<String, String>()
|
|
484
544
|
private var onMarkerTap: ((String) -> Unit)? = null
|
|
545
|
+
private var lifecycleStarted = false
|
|
546
|
+
private var destroyed = false
|
|
485
547
|
|
|
486
548
|
init {
|
|
487
549
|
addView(mapView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
|
|
@@ -490,11 +552,15 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
490
552
|
}
|
|
491
553
|
|
|
492
554
|
override fun onMapReady(map: GoogleMap) {
|
|
555
|
+
if (destroyed) return
|
|
493
556
|
googleMap = map
|
|
494
557
|
map.setOnMarkerClickListener { marker ->
|
|
495
558
|
val id = markers.entries.firstOrNull { it.value == marker }?.key ?: return@setOnMarkerClickListener false
|
|
496
559
|
onMarkerTap?.invoke(id)
|
|
497
|
-
|
|
560
|
+
// The native contract dispatches the marker event itself. Consume
|
|
561
|
+
// the click so Google Maps does not also show an unsolicited info
|
|
562
|
+
// window for the marker title.
|
|
563
|
+
true
|
|
498
564
|
}
|
|
499
565
|
applyPending()
|
|
500
566
|
}
|
|
@@ -513,7 +579,7 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
513
579
|
map.uiSettings.isScrollGesturesEnabled = pendingProps.optBoolean("scrollEnabled", true)
|
|
514
580
|
map.uiSettings.isZoomGesturesEnabled = pendingProps.optBoolean("zoomEnabled", true)
|
|
515
581
|
map.uiSettings.isRotateGesturesEnabled = pendingProps.optBoolean("rotateEnabled", false)
|
|
516
|
-
map.mapType = when (pendingProps.
|
|
582
|
+
map.mapType = when (craftNativeOptionalString(pendingProps.opt("mapType")).orEmpty().lowercase()) {
|
|
517
583
|
"satellite" -> GoogleMap.MAP_TYPE_SATELLITE
|
|
518
584
|
"hybrid" -> GoogleMap.MAP_TYPE_HYBRID
|
|
519
585
|
"terrain" -> GoogleMap.MAP_TYPE_TERRAIN
|
|
@@ -529,20 +595,32 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
529
595
|
val type = child.optString("type").lowercase()
|
|
530
596
|
val props = child.optJSONObject("props") ?: JSONObject()
|
|
531
597
|
val events = child.optJSONObject("events") ?: JSONObject()
|
|
532
|
-
val id = props.opt("id")
|
|
533
|
-
?: child.
|
|
598
|
+
val id = craftNativeOptionalString(props.opt("id"))
|
|
599
|
+
?: craftNativeOptionalString(child.opt("id"))
|
|
534
600
|
?: "overlay-$index"
|
|
535
601
|
val points = coordinates(props.optJSONArray("geometry"))
|
|
536
602
|
if (type == "mapmarker" || type == "marker") {
|
|
537
603
|
points.firstOrNull()?.let { point ->
|
|
538
|
-
nextMarkers[id] = point to props.
|
|
539
|
-
events.
|
|
540
|
-
.
|
|
604
|
+
nextMarkers[id] = point to craftNativeOptionalString(props.opt("title"))
|
|
605
|
+
listOf(events.opt("onPress"), events.opt("onMarkerPress"))
|
|
606
|
+
.firstNotNullOfOrNull(::craftNativeOptionalString)
|
|
607
|
+
?.let { nextHandlers[id] = it }
|
|
541
608
|
}
|
|
542
609
|
} else if (type == "mappolyline" || type == "polyline") {
|
|
543
610
|
if (points.size > 1) {
|
|
544
|
-
|
|
545
|
-
|
|
611
|
+
// Keep malformed authored widths inert, matching the iOS
|
|
612
|
+
// host. An omitted or null width still gets the shared
|
|
613
|
+
// default; an attacker-controlled NaN, infinity, zero,
|
|
614
|
+
// or negative value must not become a valid overlay.
|
|
615
|
+
val rawWidth = props.opt("strokeWidth")
|
|
616
|
+
val authoredWidth = when (rawWidth) {
|
|
617
|
+
null, JSONObject.NULL -> 3.0
|
|
618
|
+
is Number -> rawWidth.toDouble()
|
|
619
|
+
else -> 3.0
|
|
620
|
+
}
|
|
621
|
+
val width = authoredWidth.toFloat()
|
|
622
|
+
if (!authoredWidth.isFinite() || !width.isFinite() || width <= 0f) continue
|
|
623
|
+
nextPolylines[id] = Triple(points, color(craftNativeOptionalString(props.opt("strokeColor")).orEmpty()), width)
|
|
546
624
|
}
|
|
547
625
|
}
|
|
548
626
|
}
|
|
@@ -558,10 +636,18 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
558
636
|
}
|
|
559
637
|
markerHandlers.clear()
|
|
560
638
|
markerHandlers.putAll(nextHandlers)
|
|
561
|
-
polylines.
|
|
562
|
-
|
|
639
|
+
polylines.keys.filter { nextPolylines[it] == null }.forEach { id ->
|
|
640
|
+
polylines.remove(id)?.remove()
|
|
641
|
+
}
|
|
563
642
|
for ((id, value) in nextPolylines) {
|
|
564
|
-
polylines[id]
|
|
643
|
+
val polyline = polylines[id]
|
|
644
|
+
if (polyline != null) {
|
|
645
|
+
polyline.points = value.first
|
|
646
|
+
polyline.color = value.second
|
|
647
|
+
polyline.width = value.third
|
|
648
|
+
} else {
|
|
649
|
+
map.addPolyline(PolylineOptions().addAll(value.first).color(value.second).width(value.third))?.let { polylines[id] = it }
|
|
650
|
+
}
|
|
565
651
|
}
|
|
566
652
|
if (pendingProps.optBoolean("fitToBounds", false)) fitToBounds()
|
|
567
653
|
region(pendingProps.optJSONObject("region") ?: pendingProps.optJSONObject("initialRegion"))?.let { bounds ->
|
|
@@ -585,16 +671,43 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
585
671
|
|
|
586
672
|
override fun onAttachedToWindow() {
|
|
587
673
|
super.onAttachedToWindow()
|
|
674
|
+
if (destroyed) return
|
|
588
675
|
mapView.onStart()
|
|
589
676
|
mapView.onResume()
|
|
677
|
+
lifecycleStarted = true
|
|
590
678
|
}
|
|
591
679
|
|
|
592
680
|
override fun onDetachedFromWindow() {
|
|
593
|
-
|
|
594
|
-
|
|
681
|
+
if (lifecycleStarted) {
|
|
682
|
+
mapView.onPause()
|
|
683
|
+
mapView.onStop()
|
|
684
|
+
lifecycleStarted = false
|
|
685
|
+
}
|
|
595
686
|
super.onDetachedFromWindow()
|
|
596
687
|
}
|
|
597
688
|
|
|
689
|
+
fun destroy() {
|
|
690
|
+
if (destroyed) return
|
|
691
|
+
destroyed = true
|
|
692
|
+
if (lifecycleStarted) {
|
|
693
|
+
mapView.onPause()
|
|
694
|
+
mapView.onStop()
|
|
695
|
+
lifecycleStarted = false
|
|
696
|
+
}
|
|
697
|
+
onMarkerTap = null
|
|
698
|
+
markerHandlers.clear()
|
|
699
|
+
markers.values.forEach { it.remove() }
|
|
700
|
+
markers.clear()
|
|
701
|
+
polylines.values.forEach { it.remove() }
|
|
702
|
+
polylines.clear()
|
|
703
|
+
mapView.onDestroy()
|
|
704
|
+
googleMap = null
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
fun lowMemory() {
|
|
708
|
+
if (!destroyed) mapView.onLowMemory()
|
|
709
|
+
}
|
|
710
|
+
|
|
598
711
|
private fun hasLocationPermission(): Boolean =
|
|
599
712
|
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
|
|
600
713
|
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
|
|
@@ -604,7 +717,10 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
604
717
|
return (0 until geometry.length() - 1 step 2).mapNotNull { index ->
|
|
605
718
|
val latitude = geometry.optDouble(index, Double.NaN)
|
|
606
719
|
val longitude = geometry.optDouble(index + 1, Double.NaN)
|
|
607
|
-
if (latitude.isFinite() && longitude.isFinite()
|
|
720
|
+
if (latitude.isFinite() && longitude.isFinite() &&
|
|
721
|
+
latitude >= -90.0 && latitude <= 90.0 &&
|
|
722
|
+
longitude >= -180.0 && longitude <= 180.0
|
|
723
|
+
) LatLng(latitude, longitude) else null
|
|
608
724
|
}
|
|
609
725
|
}
|
|
610
726
|
|
|
@@ -615,7 +731,12 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
615
731
|
val longitude = center.optDouble("longitude", Double.NaN)
|
|
616
732
|
val latitudeDelta = span.optDouble("latitudeDelta", span.optDouble("latitude_delta", Double.NaN))
|
|
617
733
|
val longitudeDelta = span.optDouble("longitudeDelta", span.optDouble("longitude_delta", Double.NaN))
|
|
618
|
-
if (!latitude.isFinite() || !longitude.isFinite() || !latitudeDelta.isFinite() || !longitudeDelta.isFinite()
|
|
734
|
+
if (!latitude.isFinite() || !longitude.isFinite() || !latitudeDelta.isFinite() || !longitudeDelta.isFinite() ||
|
|
735
|
+
latitude < -90.0 || latitude > 90.0 || longitude < -180.0 || longitude > 180.0 ||
|
|
736
|
+
latitudeDelta <= 0.0 || latitudeDelta > 180.0 ||
|
|
737
|
+
longitudeDelta <= 0.0 || longitudeDelta > 360.0 ||
|
|
738
|
+
latitude - latitudeDelta / 2.0 < -90.0 || latitude + latitudeDelta / 2.0 > 90.0
|
|
739
|
+
) return null
|
|
619
740
|
return LatLngBounds(LatLng(latitude - latitudeDelta / 2, longitude - longitudeDelta / 2), LatLng(latitude + latitudeDelta / 2, longitude + longitudeDelta / 2))
|
|
620
741
|
}
|
|
621
742
|
|
|
@@ -661,6 +782,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
661
782
|
(0 until raw.length()).mapNotNull { raw.optString(it).takeIf { edge -> edge in setOf("top", "right", "bottom", "left") } }.toSet()
|
|
662
783
|
}
|
|
663
784
|
applySafeAreaPadding()
|
|
785
|
+
invalidateMeasurements()
|
|
664
786
|
}
|
|
665
787
|
|
|
666
788
|
fun setWindowSafeInsets(left: Int, top: Int, right: Int, bottom: Int) {
|
|
@@ -672,6 +794,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
672
794
|
if ("bottom" in safeAreaEdges) insets[3] else 0,
|
|
673
795
|
)
|
|
674
796
|
applySafeAreaPadding()
|
|
797
|
+
invalidateMeasurements()
|
|
675
798
|
}
|
|
676
799
|
|
|
677
800
|
private fun applySafeAreaPadding() {
|
|
@@ -720,6 +843,31 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
720
843
|
private fun probedWidth(size: Long): Int = (size shr 32).toInt()
|
|
721
844
|
private fun probedHeight(size: Long): Int = size.toInt()
|
|
722
845
|
|
|
846
|
+
/**
|
|
847
|
+
* Reactive style and inset mutations can arrive during one frame. The
|
|
848
|
+
* memo is intentionally frame-scoped for performance, but a mutation
|
|
849
|
+
* must invalidate it immediately or the next layout can reuse a child's
|
|
850
|
+
* old intrinsic width/height.
|
|
851
|
+
*/
|
|
852
|
+
private fun invalidateMeasurements() {
|
|
853
|
+
memos.clear()
|
|
854
|
+
lastProbe.clear()
|
|
855
|
+
forceLayout()
|
|
856
|
+
requestLayout()
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
private fun invalidateMeasurementAncestors(start: ViewParent?) {
|
|
860
|
+
var ancestor = start
|
|
861
|
+
while (ancestor != null) {
|
|
862
|
+
when (ancestor) {
|
|
863
|
+
is CraftNativeFlexLayout -> ancestor.invalidateMeasurements()
|
|
864
|
+
is CraftNativeKeyboardAvoidingView -> ancestor.content.invalidateMeasurements()
|
|
865
|
+
is CraftNativeScrollView -> ancestor.invalidateContentLayout()
|
|
866
|
+
}
|
|
867
|
+
ancestor = (ancestor as? View)?.parent
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
723
871
|
/**
|
|
724
872
|
* Lays a child out at its final frame, measuring it again at exactly
|
|
725
873
|
* that size first when the measure pass left it another size (a
|
|
@@ -757,34 +905,50 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
757
905
|
alignItems = style.optString("alignItems", "stretch")
|
|
758
906
|
alignContent = style.optString("alignContent", "flex-start")
|
|
759
907
|
justifyContent = style.optString("justifyContent", "flex-start")
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
908
|
+
fun spacing(name: String, fallback: Float): Float {
|
|
909
|
+
val value = style.optDouble(name, Double.NaN)
|
|
910
|
+
val pixels = value * density
|
|
911
|
+
val narrowed = pixels.toFloat()
|
|
912
|
+
return if (value.isFinite() && value >= 0 && pixels.isFinite() && narrowed.isFinite()) narrowed else fallback
|
|
913
|
+
}
|
|
914
|
+
val gap = spacing("gap", 0f)
|
|
915
|
+
rowGap = spacing("rowGap", gap)
|
|
916
|
+
columnGap = spacing("columnGap", gap)
|
|
763
917
|
grid = style.optString("display") == "grid"
|
|
764
918
|
gridColumns = craftNativeGridColumnCount(
|
|
765
919
|
if (style.has("gridTemplateColumns")) style.opt("gridTemplateColumns") else style.opt("gridColumns")
|
|
766
920
|
)
|
|
767
921
|
gridAutoRows = CraftNativeLength.from(style.opt("gridAutoRows"), density)
|
|
768
|
-
val
|
|
769
|
-
val horizontal = (style
|
|
770
|
-
val vertical = (style
|
|
922
|
+
val all = craftNativePadding(style, "padding", 0, density)
|
|
923
|
+
val horizontal = craftNativePadding(style, "paddingHorizontal", all, density)
|
|
924
|
+
val vertical = craftNativePadding(style, "paddingVertical", all, density)
|
|
771
925
|
setPadding(
|
|
772
|
-
(style
|
|
773
|
-
(style
|
|
774
|
-
(style
|
|
775
|
-
(style
|
|
926
|
+
craftNativePadding(style, "paddingLeft", horizontal, density),
|
|
927
|
+
craftNativePadding(style, "paddingTop", vertical, density),
|
|
928
|
+
craftNativePadding(style, "paddingRight", horizontal, density),
|
|
929
|
+
craftNativePadding(style, "paddingBottom", vertical, density),
|
|
776
930
|
)
|
|
777
|
-
|
|
931
|
+
invalidateMeasurements()
|
|
778
932
|
}
|
|
779
933
|
|
|
780
934
|
fun setChildStyle(view: View, style: JSONObject, density: Float) {
|
|
781
935
|
styles[view] = CraftNativeLayoutStyle.from(style, density)
|
|
782
|
-
|
|
936
|
+
invalidateMeasurements()
|
|
937
|
+
invalidateMeasurementAncestors(parent)
|
|
783
938
|
}
|
|
784
939
|
|
|
785
940
|
override fun onViewRemoved(child: View) {
|
|
941
|
+
val parentBeforeRemoval = parent
|
|
786
942
|
styles.remove(child)
|
|
787
943
|
super.onViewRemoved(child)
|
|
944
|
+
invalidateMeasurements()
|
|
945
|
+
invalidateMeasurementAncestors(parentBeforeRemoval)
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
override fun onViewAdded(child: View) {
|
|
949
|
+
super.onViewAdded(child)
|
|
950
|
+
invalidateMeasurements()
|
|
951
|
+
invalidateMeasurementAncestors(parent)
|
|
788
952
|
}
|
|
789
953
|
|
|
790
954
|
private data class Entry(val view: View, val style: CraftNativeLayoutStyle, val width: Int, val height: Int)
|
|
@@ -1038,6 +1202,116 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1038
1202
|
else -> 0.0
|
|
1039
1203
|
}
|
|
1040
1204
|
|
|
1205
|
+
private fun sharedAlignItemsCode(value: String): Int = when (value) {
|
|
1206
|
+
"center" -> 1
|
|
1207
|
+
"flex-end" -> 2
|
|
1208
|
+
"stretch" -> 3
|
|
1209
|
+
"baseline" -> 4
|
|
1210
|
+
else -> 0
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
private fun sharedAlignSelfCode(value: String?): Int = when (value) {
|
|
1214
|
+
"flex-start" -> 1
|
|
1215
|
+
"center" -> 2
|
|
1216
|
+
"flex-end" -> 3
|
|
1217
|
+
"stretch" -> 4
|
|
1218
|
+
"baseline" -> 5
|
|
1219
|
+
else -> 0
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
private fun sharedLength(value: CraftNativeLength?, base: Int?): Float = value?.resolve(base)?.toFloat() ?: Float.NaN
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* Use the host-neutral tree ABI when platform measurements are complete:
|
|
1226
|
+
* definite, non-grid direct-child containers, including wrapped flows with
|
|
1227
|
+
* independent row and column gaps. Reverse flows, absolute children,
|
|
1228
|
+
* missing runtimes, and malformed results deliberately fall through to
|
|
1229
|
+
* the established Kotlin layout below.
|
|
1230
|
+
*/
|
|
1231
|
+
private fun sharedDirectFrames(entries: List<Entry>, width: Int, height: Int): FloatArray? {
|
|
1232
|
+
if (entries.isEmpty() || width < 0 || height < 0 || grid || reverse || wrapReverse) return null
|
|
1233
|
+
val count = entries.size + 1
|
|
1234
|
+
val fields = 32
|
|
1235
|
+
val nodeData = FloatArray(count * fields) { Float.NaN }
|
|
1236
|
+
val treeMeta = FloatArray(count * 2)
|
|
1237
|
+
fun enumBits(value: Int): Float = Float.fromBits(value)
|
|
1238
|
+
fun put(base: Int, index: Int, value: Float) { nodeData[base + index] = value }
|
|
1239
|
+
fun putEnum(base: Int, index: Int, value: Int) { put(base, index, enumBits(value)) }
|
|
1240
|
+
val contentWidth = max(0, width - paddingLeft - paddingRight)
|
|
1241
|
+
val contentHeight = max(0, height - paddingTop - paddingBottom)
|
|
1242
|
+
val mainSize = if (orientation == HORIZONTAL) contentWidth else contentHeight
|
|
1243
|
+
fun writeStyle(
|
|
1244
|
+
base: Int,
|
|
1245
|
+
style: CraftNativeLayoutStyle,
|
|
1246
|
+
intrinsicWidth: Float,
|
|
1247
|
+
intrinsicHeight: Float,
|
|
1248
|
+
baseline: Float,
|
|
1249
|
+
margins: IntArray,
|
|
1250
|
+
direction: Int,
|
|
1251
|
+
padding: IntArray,
|
|
1252
|
+
gap: Float,
|
|
1253
|
+
rowGapValue: Float,
|
|
1254
|
+
columnGapValue: Float,
|
|
1255
|
+
root: Boolean,
|
|
1256
|
+
view: View?,
|
|
1257
|
+
) {
|
|
1258
|
+
putEnum(base, 0, direction)
|
|
1259
|
+
put(base, 1, if (root) width.toFloat() else sharedLength(style.width, contentWidth))
|
|
1260
|
+
put(base, 2, if (root) height.toFloat() else sharedLength(style.height, contentHeight))
|
|
1261
|
+
put(base, 3, if (root) Float.NaN else intrinsicWidth)
|
|
1262
|
+
put(base, 4, if (root) Float.NaN else intrinsicHeight)
|
|
1263
|
+
put(base, 5, if (root) Float.NaN else baseline)
|
|
1264
|
+
put(base, 6, style.aspectRatio?.toFloat() ?: Float.NaN)
|
|
1265
|
+
put(base, 7, sharedLength(style.flexBasis, mainSize))
|
|
1266
|
+
put(base, 8, if (root) 0f else style.flexGrow.toFloat().coerceAtLeast(0f))
|
|
1267
|
+
put(base, 9, if (root) 0f else (style.flexShrink ?: view?.let { defaultFlexShrink(it) } ?: 0.0).toFloat().coerceAtLeast(0f))
|
|
1268
|
+
put(base, 10, sharedLength(style.minWidth, contentWidth))
|
|
1269
|
+
put(base, 11, sharedLength(style.maxWidth, contentWidth))
|
|
1270
|
+
put(base, 12, sharedLength(style.minHeight, contentHeight))
|
|
1271
|
+
put(base, 13, sharedLength(style.maxHeight, contentHeight))
|
|
1272
|
+
put(base, 14, if (root) gap else 0f)
|
|
1273
|
+
putEnum(base, 15, if (root) justifyCode() else 0)
|
|
1274
|
+
putEnum(base, 16, if (root) sharedAlignItemsCode(alignItems) else 3)
|
|
1275
|
+
putEnum(base, 17, if (root) 0 else sharedAlignSelfCode(style.alignSelf))
|
|
1276
|
+
putEnum(base, 18, if (root && wrap) 1 else 0)
|
|
1277
|
+
putEnum(base, 19, if (root) alignContentCode() else 6)
|
|
1278
|
+
put(base, 20, margins[2].toFloat())
|
|
1279
|
+
put(base, 21, margins[1].toFloat())
|
|
1280
|
+
put(base, 22, margins[3].toFloat())
|
|
1281
|
+
put(base, 23, margins[0].toFloat())
|
|
1282
|
+
put(base, 24, padding[2].toFloat())
|
|
1283
|
+
put(base, 25, padding[1].toFloat())
|
|
1284
|
+
put(base, 26, padding[3].toFloat())
|
|
1285
|
+
put(base, 27, padding[0].toFloat())
|
|
1286
|
+
putEnum(base, 28, 0)
|
|
1287
|
+
putEnum(base, 29, 1)
|
|
1288
|
+
put(base, 30, if (root) rowGapValue else 0f)
|
|
1289
|
+
put(base, 31, if (root) columnGapValue else 0f)
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
treeMeta[0] = 1f
|
|
1293
|
+
treeMeta[1] = entries.size.toFloat()
|
|
1294
|
+
val rootMargins = intArrayOf(0, 0, 0, 0)
|
|
1295
|
+
val rootPadding = intArrayOf(paddingLeft, paddingRight, paddingTop, paddingBottom)
|
|
1296
|
+
val mainGap = if (orientation == HORIZONTAL) columnGap else rowGap
|
|
1297
|
+
writeStyle(
|
|
1298
|
+
0, CraftNativeLayoutStyle(), 0f, 0f, 0f, rootMargins,
|
|
1299
|
+
if (orientation == HORIZONTAL) 0 else 1, rootPadding, mainGap, rowGap, columnGap, true, null,
|
|
1300
|
+
)
|
|
1301
|
+
entries.forEachIndexed { index, entry ->
|
|
1302
|
+
val base = (index + 1) * fields
|
|
1303
|
+
val lp = entry.view.layoutParams as? MarginLayoutParams
|
|
1304
|
+
val margins = intArrayOf(lp?.leftMargin ?: 0, lp?.rightMargin ?: 0, lp?.topMargin ?: 0, lp?.bottomMargin ?: 0)
|
|
1305
|
+
treeMeta[(index + 1) * 2] = 0f
|
|
1306
|
+
treeMeta[(index + 1) * 2 + 1] = 0f
|
|
1307
|
+
writeStyle(
|
|
1308
|
+
base, entry.style, entry.width.toFloat(), entry.height.toFloat(), baselineOffset(entry.view, entry.height).toFloat(),
|
|
1309
|
+
margins, 0, intArrayOf(0, 0, 0, 0), 0f, 0f, 0f, false, entry.view,
|
|
1310
|
+
)
|
|
1311
|
+
}
|
|
1312
|
+
return CraftNative.layoutTree(width.toFloat(), height.toFloat(), nodeData, treeMeta)
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1041
1315
|
private fun justifyCode(): Int = when (justifyContent) {
|
|
1042
1316
|
"center" -> 1
|
|
1043
1317
|
"flex-end" -> 2
|
|
@@ -1165,11 +1439,11 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1165
1439
|
contentWidth.toFloat(), contentHeight.toFloat(), gridColumns, columnGap, rowGap,
|
|
1166
1440
|
flow.map { it.width.toFloat() }.toFloatArray(), flow.map { it.height.toFloat() }.toFloatArray(),
|
|
1167
1441
|
flow.map { it.style.width?.resolve(layout.cellWidth.roundToInt())?.toFloat() ?: Float.NaN }.toFloatArray(),
|
|
1168
|
-
flow.map { it.style.height?.resolve(
|
|
1442
|
+
flow.map { it.style.height?.resolve(contentHeight)?.toFloat() ?: Float.NaN }.toFloatArray(),
|
|
1169
1443
|
flow.map { it.style.minWidth?.resolve(layout.cellWidth.roundToInt())?.toFloat() ?: Float.NaN }.toFloatArray(),
|
|
1170
1444
|
flow.map { it.style.maxWidth?.resolve(layout.cellWidth.roundToInt())?.toFloat() ?: Float.NaN }.toFloatArray(),
|
|
1171
|
-
flow.map { it.style.minHeight?.resolve(
|
|
1172
|
-
flow.map { it.style.maxHeight?.resolve(
|
|
1445
|
+
flow.map { it.style.minHeight?.resolve(contentHeight)?.toFloat() ?: Float.NaN }.toFloatArray(),
|
|
1446
|
+
flow.map { it.style.maxHeight?.resolve(contentHeight)?.toFloat() ?: Float.NaN }.toFloatArray(),
|
|
1173
1447
|
flow.flatMap {
|
|
1174
1448
|
val lp = it.view.layoutParams as? MarginLayoutParams
|
|
1175
1449
|
listOf((lp?.leftMargin ?: 0).toFloat(), (lp?.rightMargin ?: 0).toFloat(), (lp?.topMargin ?: 0).toFloat(), (lp?.bottomMargin ?: 0).toFloat())
|
|
@@ -1255,6 +1529,22 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1255
1529
|
val contentWidth = width - paddingLeft - paddingRight
|
|
1256
1530
|
val contentHeight = height - paddingTop - paddingBottom
|
|
1257
1531
|
val (lines, absolute) = entries(width, height)
|
|
1532
|
+
if (absolute.isEmpty() && lines.isNotEmpty() && (lines.size == 1 || wrap)) {
|
|
1533
|
+
val sharedEntries = if (wrap) lines.flatMap { it.entries } else lines[0].entries
|
|
1534
|
+
val sharedFrames = sharedDirectFrames(sharedEntries, width, height)
|
|
1535
|
+
if (sharedFrames != null) {
|
|
1536
|
+
for ((index, entry) in sharedEntries.withIndex()) {
|
|
1537
|
+
val offset = (index + 1) * 4
|
|
1538
|
+
val x = sharedFrames[offset].roundToInt()
|
|
1539
|
+
val y = sharedFrames[offset + 1].roundToInt()
|
|
1540
|
+
val childWidth = sharedFrames[offset + 2].roundToInt()
|
|
1541
|
+
val childHeight = sharedFrames[offset + 3].roundToInt()
|
|
1542
|
+
place(entry.view, x, y, x + childWidth, y + childHeight)
|
|
1543
|
+
}
|
|
1544
|
+
onLayoutChanged?.invoke()
|
|
1545
|
+
return
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1258
1548
|
val gap = if (orientation == HORIZONTAL) columnGap else rowGap
|
|
1259
1549
|
val crossGap = if (orientation == HORIZONTAL) rowGap else columnGap
|
|
1260
1550
|
val availableMain = if (orientation == HORIZONTAL) contentWidth else contentHeight
|
|
@@ -1449,7 +1739,9 @@ private class CraftNativeKeyboardAvoidingView(context: Context) : FrameLayout(co
|
|
|
1449
1739
|
|
|
1450
1740
|
fun configure(props: JSONObject, style: JSONObject, density: Float) {
|
|
1451
1741
|
behavior = props.optString("behavior", "padding").takeIf { it in setOf("height", "position", "padding") } ?: "padding"
|
|
1452
|
-
|
|
1742
|
+
val authoredOffset = craftNativeFiniteDouble(props, "keyboardVerticalOffset", 0.0)
|
|
1743
|
+
val pixelOffset = (authoredOffset * density.toDouble()).takeIf { it.isFinite() } ?: 0.0
|
|
1744
|
+
keyboardVerticalOffset = pixelOffset.roundToInt()
|
|
1453
1745
|
content.configure(style, density)
|
|
1454
1746
|
basePadding = intArrayOf(content.paddingLeft, content.paddingTop, content.paddingRight, content.paddingBottom)
|
|
1455
1747
|
applyKeyboardInset()
|
|
@@ -1551,10 +1843,10 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
1551
1843
|
}
|
|
1552
1844
|
|
|
1553
1845
|
fun setRefreshAppearance(props: JSONObject, density: Float) {
|
|
1554
|
-
val tint = props.
|
|
1846
|
+
val tint = craftNativeOptionalString(props.opt("tintColor"))
|
|
1555
1847
|
if (tint != null) setColorSchemeColors(craftNativeColor(tint))
|
|
1556
1848
|
else setColorSchemeColors(Color.WHITE)
|
|
1557
|
-
val background = props.
|
|
1849
|
+
val background = craftNativeOptionalString(props.opt("progressBackgroundColor"))
|
|
1558
1850
|
if (background != null) setProgressBackgroundColorSchemeColor(craftNativeColor(background))
|
|
1559
1851
|
else setProgressBackgroundColorSchemeColor(Color.TRANSPARENT)
|
|
1560
1852
|
val offset = props.optDouble("progressViewOffset", Double.NaN)
|
|
@@ -1915,6 +2207,7 @@ private class CraftNativeDatePickerView(context: Context) : FrameLayout(context)
|
|
|
1915
2207
|
private var pickerMode: PickerMode? = null
|
|
1916
2208
|
private var datePicker: DatePicker? = null
|
|
1917
2209
|
private var timePicker: TimePicker? = null
|
|
2210
|
+
private var defaultDateBounds: Pair<Long, Long>? = null
|
|
1918
2211
|
private var minuteInterval = 1
|
|
1919
2212
|
private var countdownSecond = 0
|
|
1920
2213
|
private var suppressChanges = true
|
|
@@ -1942,6 +2235,7 @@ private class CraftNativeDatePickerView(context: Context) : FrameLayout(context)
|
|
|
1942
2235
|
}
|
|
1943
2236
|
if (requestedMode == PickerMode.DATE || requestedMode == PickerMode.DATE_TIME) {
|
|
1944
2237
|
datePicker = DatePicker(context).also { picker ->
|
|
2238
|
+
defaultDateBounds = picker.minDate to picker.maxDate
|
|
1945
2239
|
val today = Calendar.getInstance()
|
|
1946
2240
|
picker.init(today.get(Calendar.YEAR), today.get(Calendar.MONTH), today.get(Calendar.DAY_OF_MONTH)) { _, _, _, _ -> changed() }
|
|
1947
2241
|
content.addView(picker, LinearLayout.LayoutParams(
|
|
@@ -1965,8 +2259,9 @@ private class CraftNativeDatePickerView(context: Context) : FrameLayout(context)
|
|
|
1965
2259
|
|
|
1966
2260
|
suppressChanges = true
|
|
1967
2261
|
minuteInterval = props.optInt("minuteInterval", 1).takeIf { it in 1..30 && 60 % it == 0 } ?: 1
|
|
1968
|
-
|
|
1969
|
-
pickerDate(props.opt("
|
|
2262
|
+
val defaults = defaultDateBounds
|
|
2263
|
+
datePicker?.minDate = pickerDate(props.opt("minimumDate"))?.timeInMillis ?: defaults?.first ?: datePicker?.minDate ?: 0L
|
|
2264
|
+
datePicker?.maxDate = pickerDate(props.opt("maximumDate"))?.timeInMillis ?: defaults?.second ?: datePicker?.maxDate ?: Long.MAX_VALUE
|
|
1970
2265
|
if (requestedMode == PickerMode.COUNTDOWN) {
|
|
1971
2266
|
countdownSecond = 0
|
|
1972
2267
|
countdownSeconds(props.opt("value"))?.let { applyCountdown(it) }
|
|
@@ -2051,7 +2346,11 @@ private class CraftNativeDatePickerView(context: Context) : FrameLayout(context)
|
|
|
2051
2346
|
}
|
|
2052
2347
|
|
|
2053
2348
|
private fun pickerDate(value: Any?): Calendar? {
|
|
2054
|
-
if (value is Number && value !is Boolean)
|
|
2349
|
+
if (value is Number && value !is Boolean) {
|
|
2350
|
+
val milliseconds = value.toDouble()
|
|
2351
|
+
if (!milliseconds.isFinite()) return null
|
|
2352
|
+
return runCatching { Calendar.getInstance().apply { timeInMillis = milliseconds.toLong() } }.getOrNull()
|
|
2353
|
+
}
|
|
2055
2354
|
val text = value?.toString()?.trim().orEmpty()
|
|
2056
2355
|
if (text.isBlank() || text == JSONObject.NULL.toString()) return null
|
|
2057
2356
|
val formats = listOf(
|
|
@@ -2069,7 +2368,11 @@ private class CraftNativeDatePickerView(context: Context) : FrameLayout(context)
|
|
|
2069
2368
|
}
|
|
2070
2369
|
|
|
2071
2370
|
private fun countdownSeconds(value: Any?): Int? {
|
|
2072
|
-
if (value is Number && value !is Boolean)
|
|
2371
|
+
if (value is Number && value !is Boolean) {
|
|
2372
|
+
val seconds = value.toDouble()
|
|
2373
|
+
if (!seconds.isFinite()) return null
|
|
2374
|
+
return seconds.coerceIn(0.0, maxCountdownSeconds.toDouble()).roundToInt()
|
|
2375
|
+
}
|
|
2073
2376
|
val text = value?.toString()?.trim().orEmpty()
|
|
2074
2377
|
if (text.isBlank() || text == JSONObject.NULL.toString()) return null
|
|
2075
2378
|
text.toDoubleOrNull()?.takeIf { it.isFinite() }?.let { return it.coerceIn(0.0, maxCountdownSeconds.toDouble()).roundToInt() }
|
|
@@ -2078,7 +2381,8 @@ private class CraftNativeDatePickerView(context: Context) : FrameLayout(context)
|
|
|
2078
2381
|
val hours = parts[0].toDoubleOrNull() ?: return null
|
|
2079
2382
|
val minutes = parts[1].toDoubleOrNull() ?: return null
|
|
2080
2383
|
val seconds = if (parts.size == 3) parts[2].toDoubleOrNull() ?: return null else 0.0
|
|
2081
|
-
if (hours
|
|
2384
|
+
if (!hours.isFinite() || !minutes.isFinite() || !seconds.isFinite() ||
|
|
2385
|
+
hours < 0 || minutes !in 0.0..<60.0 || seconds !in 0.0..<60.0) return null
|
|
2082
2386
|
return (hours * 3600 + minutes * 60 + seconds).coerceIn(0.0, maxCountdownSeconds.toDouble()).roundToInt()
|
|
2083
2387
|
}
|
|
2084
2388
|
}
|
|
@@ -2088,26 +2392,33 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
|
|
|
2088
2392
|
private var shouldLoop = false
|
|
2089
2393
|
private var shouldPlay = false
|
|
2090
2394
|
private var muted = false
|
|
2395
|
+
private var resizeMode = "contain"
|
|
2396
|
+
private var preparedPlayer: MediaPlayer? = null
|
|
2091
2397
|
|
|
2092
|
-
fun configure(source: Any?, autoplay: Boolean, paused: Boolean, loop: Boolean, muted: Boolean, controls: Boolean) {
|
|
2398
|
+
fun configure(source: Any?, autoplay: Boolean, paused: Boolean, loop: Boolean, muted: Boolean, controls: Boolean, resizeMode: String?) {
|
|
2093
2399
|
val uri = when (source) {
|
|
2094
2400
|
is String -> source
|
|
2095
|
-
is JSONObject -> source.
|
|
2401
|
+
is JSONObject -> craftNativeOptionalString(source.opt("uri")).orEmpty()
|
|
2096
2402
|
else -> ""
|
|
2097
2403
|
}
|
|
2098
2404
|
shouldLoop = loop
|
|
2099
2405
|
shouldPlay = autoplay && !paused
|
|
2100
2406
|
this.muted = muted
|
|
2407
|
+
this.resizeMode = resizeMode?.lowercase()?.takeIf { it in setOf("cover", "contain", "stretch", "center") } ?: "contain"
|
|
2408
|
+
applyScalingMode()
|
|
2101
2409
|
setMediaController(if (controls) MediaController(context).also { it.setAnchorView(this) } else null)
|
|
2102
2410
|
if (uri != sourceUri) {
|
|
2103
2411
|
sourceUri = uri
|
|
2104
2412
|
stopPlayback()
|
|
2413
|
+
preparedPlayer = null
|
|
2105
2414
|
if (uri.isBlank()) return
|
|
2106
2415
|
// A copy kept on the phone (craft.media.keep) plays in place of
|
|
2107
2416
|
// the address: offline in a gym with no signal.
|
|
2108
2417
|
val kept = CraftNativeMedia.local(context, uri)
|
|
2109
2418
|
setVideoURI(if (kept != null) Uri.fromFile(kept) else Uri.parse(uri))
|
|
2110
2419
|
setOnPreparedListener { mediaPlayer ->
|
|
2420
|
+
preparedPlayer = mediaPlayer
|
|
2421
|
+
applyScalingMode()
|
|
2111
2422
|
mediaPlayer.isLooping = shouldLoop
|
|
2112
2423
|
val volume = if (this.muted) 0f else 1f
|
|
2113
2424
|
mediaPlayer.setVolume(volume, volume)
|
|
@@ -2125,12 +2436,27 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
|
|
|
2125
2436
|
fun stop() {
|
|
2126
2437
|
stopPlayback()
|
|
2127
2438
|
sourceUri = ""
|
|
2439
|
+
preparedPlayer = null
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
private fun applyScalingMode() {
|
|
2443
|
+
preparedPlayer?.setVideoScalingMode(
|
|
2444
|
+
if (resizeMode == "cover") MediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING
|
|
2445
|
+
else MediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT,
|
|
2446
|
+
)
|
|
2128
2447
|
}
|
|
2129
2448
|
}
|
|
2130
2449
|
|
|
2131
|
-
/**
|
|
2450
|
+
/**
|
|
2451
|
+
* The native-screen Web Audio graph. Android renders the same useful graph
|
|
2452
|
+
* shape as iOS: oscillators and buffer sources can pass through gain, filter,
|
|
2453
|
+
* delay, compressor, and convolver nodes before reaching the destination.
|
|
2454
|
+
* Effects deliberately remain conservative in this first Android renderer;
|
|
2455
|
+
* the graph and its timing are native, while the existing WebView remains the
|
|
2456
|
+
* fallback for unsupported media decoding.
|
|
2457
|
+
*/
|
|
2132
2458
|
private class CraftNativeAudio {
|
|
2133
|
-
private data class ParameterEvent(val time: Double, val value: Double, val
|
|
2459
|
+
private data class ParameterEvent(val time: Double, val value: Double, val mode: String)
|
|
2134
2460
|
|
|
2135
2461
|
private class Parameter(initial: Double) {
|
|
2136
2462
|
private var current = initial
|
|
@@ -2141,8 +2467,8 @@ private class CraftNativeAudio {
|
|
|
2141
2467
|
events.clear()
|
|
2142
2468
|
}
|
|
2143
2469
|
|
|
2144
|
-
@Synchronized fun schedule(value: Double, time: Double,
|
|
2145
|
-
events += ParameterEvent(time.coerceAtLeast(0.0), value,
|
|
2470
|
+
@Synchronized fun schedule(value: Double, time: Double, mode: String) {
|
|
2471
|
+
events += ParameterEvent(time.coerceAtLeast(0.0), value, mode)
|
|
2146
2472
|
events.sortBy { it.time }
|
|
2147
2473
|
}
|
|
2148
2474
|
|
|
@@ -2151,9 +2477,20 @@ private class CraftNativeAudio {
|
|
|
2151
2477
|
var previousValue = current
|
|
2152
2478
|
for (event in events) {
|
|
2153
2479
|
if (time < event.time) {
|
|
2154
|
-
if (
|
|
2480
|
+
if (event.time <= previousTime) return previousValue
|
|
2155
2481
|
val fraction = (time - previousTime) / (event.time - previousTime)
|
|
2156
|
-
return
|
|
2482
|
+
return when (event.mode) {
|
|
2483
|
+
"exponential" -> {
|
|
2484
|
+
val start = previousValue.takeIf { it > 0 } ?: 0.000001
|
|
2485
|
+
val end = event.value.takeIf { it > 0 } ?: 0.000001
|
|
2486
|
+
start * (end / start).pow(fraction)
|
|
2487
|
+
}
|
|
2488
|
+
"target" -> {
|
|
2489
|
+
previousValue + (event.value - previousValue) * (1 - exp(-fraction * 5))
|
|
2490
|
+
}
|
|
2491
|
+
"linear" -> previousValue + (event.value - previousValue) * fraction
|
|
2492
|
+
else -> previousValue
|
|
2493
|
+
}
|
|
2157
2494
|
}
|
|
2158
2495
|
previousTime = event.time
|
|
2159
2496
|
previousValue = event.value
|
|
@@ -2162,73 +2499,182 @@ private class CraftNativeAudio {
|
|
|
2162
2499
|
}
|
|
2163
2500
|
}
|
|
2164
2501
|
|
|
2165
|
-
private class
|
|
2502
|
+
private class NodeState(
|
|
2503
|
+
val contextId: String,
|
|
2504
|
+
val id: String,
|
|
2505
|
+
val kind: String,
|
|
2506
|
+
) {
|
|
2166
2507
|
val frequency = Parameter(440.0)
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
var
|
|
2171
|
-
|
|
2508
|
+
val detune = Parameter(0.0)
|
|
2509
|
+
val gain = Parameter(1.0)
|
|
2510
|
+
val playbackRate = Parameter(1.0)
|
|
2511
|
+
var waveform = 0
|
|
2512
|
+
var bufferId: String? = null
|
|
2513
|
+
var bufferLoop = false
|
|
2514
|
+
var loopStart = 0.0
|
|
2515
|
+
var loopEnd = 0.0
|
|
2516
|
+
var startOffset = 0.0
|
|
2517
|
+
var startAt = Double.POSITIVE_INFINITY
|
|
2518
|
+
var stopAt = Double.POSITIVE_INFINITY
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
private data class BufferSpec(
|
|
2522
|
+
val length: Int,
|
|
2523
|
+
val sampleRate: Double,
|
|
2524
|
+
val channels: List<FloatArray> = emptyList(),
|
|
2525
|
+
)
|
|
2172
2526
|
|
|
2173
|
-
private val
|
|
2174
|
-
private val
|
|
2175
|
-
private val
|
|
2527
|
+
private val nodes = mutableMapOf<String, NodeState>()
|
|
2528
|
+
private val inputs = mutableMapOf<String, MutableSet<String>>()
|
|
2529
|
+
private val buffers = mutableMapOf<String, BufferSpec>()
|
|
2176
2530
|
private val contextStarts = mutableMapOf<String, Double>()
|
|
2531
|
+
private val suspended = mutableSetOf<String>()
|
|
2177
2532
|
private var track: AudioTrack? = null
|
|
2178
2533
|
@Volatile private var running = false
|
|
2179
2534
|
private var renderThread: Thread? = null
|
|
2180
2535
|
|
|
2536
|
+
private fun key(contextId: String, nodeId: String): String = "$contextId:$nodeId"
|
|
2537
|
+
|
|
2538
|
+
private fun node(contextId: String, nodeId: String): NodeState? = nodes[key(contextId, nodeId)]
|
|
2539
|
+
|
|
2181
2540
|
@Synchronized fun handle(payload: JSONObject) {
|
|
2541
|
+
val contextId = payload.optString("contextId")
|
|
2182
2542
|
when (payload.optString("action")) {
|
|
2183
2543
|
"createContext" -> {
|
|
2184
|
-
|
|
2544
|
+
contextId.takeIf { it.isNotBlank() }?.let {
|
|
2185
2545
|
contextStarts[it] = System.nanoTime() / 1_000_000_000.0
|
|
2546
|
+
suspended.remove(it)
|
|
2186
2547
|
}
|
|
2187
2548
|
}
|
|
2188
|
-
"createOscillator"
|
|
2549
|
+
"createOscillator", "createGain", "createBufferSource", "createBiquadFilter",
|
|
2550
|
+
"createDelay", "createDynamicsCompressor", "createConvolver" -> {
|
|
2189
2551
|
val id = payload.optString("nodeId")
|
|
2190
|
-
if (
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2552
|
+
if (contextId.isBlank() || id.isBlank()) return
|
|
2553
|
+
val state = NodeState(contextId, id, payload.optString("action").removePrefix("create"))
|
|
2554
|
+
state.frequency.set(payload.optDouble("frequency", 440.0).coerceAtLeast(0.0))
|
|
2555
|
+
state.gain.set(payload.optDouble("gain", 1.0).coerceAtLeast(0.0))
|
|
2556
|
+
nodes[key(contextId, id)] = state
|
|
2557
|
+
}
|
|
2558
|
+
"createBuffer" -> {
|
|
2559
|
+
val id = payload.optString("bufferId")
|
|
2560
|
+
if (id.isNotBlank()) {
|
|
2561
|
+
val length = payload.optInt("length", 1).coerceAtLeast(1)
|
|
2562
|
+
val channelCount = payload.optInt("channels", 1).coerceIn(1, 2)
|
|
2563
|
+
buffers[id] = BufferSpec(
|
|
2564
|
+
length,
|
|
2565
|
+
payload.optDouble("sampleRate", 44_100.0).coerceAtLeast(3_000.0),
|
|
2566
|
+
List(channelCount) { FloatArray(length) },
|
|
2567
|
+
)
|
|
2194
2568
|
}
|
|
2195
2569
|
}
|
|
2196
|
-
"
|
|
2197
|
-
val id = payload.optString("
|
|
2198
|
-
|
|
2570
|
+
"bufferData" -> {
|
|
2571
|
+
val id = payload.optString("bufferId")
|
|
2572
|
+
val existing = buffers[id] ?: return
|
|
2573
|
+
val rawChannels = payload.optJSONArray("channels") ?: return
|
|
2574
|
+
val channels = (0 until min(existing.channels.size, rawChannels.length())).map { channelIndex ->
|
|
2575
|
+
val raw = rawChannels.optJSONArray(channelIndex)
|
|
2576
|
+
FloatArray(existing.length) { sampleIndex ->
|
|
2577
|
+
val value = raw?.optDouble(sampleIndex, 0.0) ?: 0.0
|
|
2578
|
+
value.takeIf { it.isFinite() }?.toFloat()?.coerceIn(-1f, 1f) ?: 0f
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
buffers[id] = existing.copy(channels = channels)
|
|
2199
2582
|
}
|
|
2200
2583
|
"connect" -> {
|
|
2201
2584
|
val source = payload.optString("sourceId")
|
|
2202
|
-
|
|
2585
|
+
val destination = payload.optString("destinationId", "destination")
|
|
2586
|
+
if (contextId.isNotBlank() && source.isNotBlank()) {
|
|
2587
|
+
inputs.getOrPut(key(contextId, destination)) { mutableSetOf() }.add(key(contextId, source))
|
|
2588
|
+
}
|
|
2203
2589
|
}
|
|
2204
|
-
"
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
)
|
|
2210
|
-
"
|
|
2211
|
-
"
|
|
2212
|
-
payload.
|
|
2213
|
-
payload.optDouble("
|
|
2214
|
-
payload.
|
|
2215
|
-
|
|
2216
|
-
"
|
|
2217
|
-
val state =
|
|
2590
|
+
"connectParam" -> {
|
|
2591
|
+
// Parameter modulation is accepted by the protocol. The
|
|
2592
|
+
// Android renderer keeps the target's scheduled value stable
|
|
2593
|
+
// rather than dropping the graph connection or throwing.
|
|
2594
|
+
}
|
|
2595
|
+
"type" -> node(contextId, payload.optString("nodeId"))?.waveform = payload.optInt("value", 0)
|
|
2596
|
+
"buffer" -> node(contextId, payload.optString("nodeId"))?.bufferId = payload.optString("bufferId").takeIf { it.isNotBlank() }
|
|
2597
|
+
"loop" -> node(contextId, payload.optString("nodeId"))?.let {
|
|
2598
|
+
it.bufferLoop = payload.optBoolean("loop", false)
|
|
2599
|
+
it.loopStart = payload.optDouble("loopStart", 0.0).coerceAtLeast(0.0)
|
|
2600
|
+
it.loopEnd = payload.optDouble("loopEnd", 0.0).coerceAtLeast(0.0)
|
|
2601
|
+
}
|
|
2602
|
+
"frequency", "gain", "detune", "playbackRate" -> {
|
|
2603
|
+
val state = node(contextId, payload.optString("nodeId")) ?: return
|
|
2604
|
+
parameter(state, payload.optString("action"))?.set(payload.optDouble("value", 0.0))
|
|
2605
|
+
}
|
|
2606
|
+
"frequencyAtTime", "frequencyRampToTime", "frequencyExponentialRampToTime",
|
|
2607
|
+
"gainAtTime", "gainRampToTime", "gainExponentialRampToTime",
|
|
2608
|
+
"detuneAtTime", "detuneRampToTime", "detuneExponentialRampToTime",
|
|
2609
|
+
"playbackRateAtTime", "playbackRateRampToTime", "playbackRateExponentialRampToTime",
|
|
2610
|
+
"gainTargetAtTime" -> {
|
|
2611
|
+
val state = node(contextId, payload.optString("nodeId")) ?: return
|
|
2612
|
+
val action = payload.optString("action")
|
|
2613
|
+
val parameterName = when {
|
|
2614
|
+
action.contains("TargetAtTime") -> action.substringBefore("TargetAtTime")
|
|
2615
|
+
action.contains("ExponentialRampToTime") -> action.substringBefore("ExponentialRampToTime")
|
|
2616
|
+
action.contains("RampToTime") -> action.substringBefore("RampToTime")
|
|
2617
|
+
else -> action.substringBefore("AtTime")
|
|
2618
|
+
}
|
|
2619
|
+
parameter(state, parameterName)?.schedule(
|
|
2620
|
+
payload.optDouble("value", 0.0), payload.optDouble("time", 0.0),
|
|
2621
|
+
when {
|
|
2622
|
+
action.contains("Exponential") -> "exponential"
|
|
2623
|
+
action.contains("Target") -> "target"
|
|
2624
|
+
action.contains("Ramp") -> "linear"
|
|
2625
|
+
else -> "set"
|
|
2626
|
+
},
|
|
2627
|
+
)
|
|
2628
|
+
}
|
|
2629
|
+
"start" -> {
|
|
2630
|
+
val state = node(contextId, payload.optString("nodeId")) ?: return
|
|
2218
2631
|
ensureTrack()
|
|
2219
|
-
val
|
|
2220
|
-
|
|
2221
|
-
|
|
2632
|
+
val now = contextTime(contextId)
|
|
2633
|
+
state.startAt = now + payload.optDouble("delay", 0.0).coerceAtLeast(0.0)
|
|
2634
|
+
state.startOffset = payload.optDouble("offset", 0.0).coerceAtLeast(0.0)
|
|
2635
|
+
val duration = payload.optDouble("duration", -1.0)
|
|
2636
|
+
if (duration > 0) state.stopAt = state.startAt + duration
|
|
2637
|
+
}
|
|
2638
|
+
"stop" -> {
|
|
2639
|
+
val state = node(contextId, payload.optString("nodeId")) ?: return
|
|
2640
|
+
state.stopAt = contextTime(contextId) + payload.optDouble("delay", 0.0).coerceAtLeast(0.0)
|
|
2641
|
+
}
|
|
2642
|
+
"state" -> when (payload.optString("state")) {
|
|
2643
|
+
"suspended" -> suspended.add(contextId)
|
|
2644
|
+
"running" -> suspended.remove(contextId)
|
|
2645
|
+
"closed" -> closeContext(contextId)
|
|
2222
2646
|
}
|
|
2223
2647
|
"close" -> close()
|
|
2224
2648
|
}
|
|
2225
2649
|
}
|
|
2226
2650
|
|
|
2651
|
+
private fun parameter(state: NodeState, name: String): Parameter? = when (name) {
|
|
2652
|
+
"frequency" -> state.frequency
|
|
2653
|
+
"gain" -> state.gain
|
|
2654
|
+
"detune" -> state.detune
|
|
2655
|
+
"playbackRate" -> state.playbackRate
|
|
2656
|
+
else -> null
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
private fun contextTime(contextId: String): Double {
|
|
2660
|
+
return (System.nanoTime() / 1_000_000_000.0) - (contextStarts[contextId] ?: return 0.0)
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
private fun closeContext(contextId: String) {
|
|
2664
|
+
val prefix = "$contextId:"
|
|
2665
|
+
nodes.keys.removeAll { it.startsWith(prefix) }
|
|
2666
|
+
inputs.keys.removeAll { it.startsWith(prefix) }
|
|
2667
|
+
inputs.values.forEach { it.removeAll { source -> source.startsWith(prefix) } }
|
|
2668
|
+
contextStarts.remove(contextId)
|
|
2669
|
+
suspended.remove(contextId)
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2227
2672
|
@Synchronized fun close() {
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2673
|
+
nodes.clear()
|
|
2674
|
+
inputs.clear()
|
|
2675
|
+
buffers.clear()
|
|
2231
2676
|
contextStarts.clear()
|
|
2677
|
+
suspended.clear()
|
|
2232
2678
|
running = false
|
|
2233
2679
|
renderThread?.interrupt()
|
|
2234
2680
|
renderThread = null
|
|
@@ -2269,31 +2715,66 @@ private class CraftNativeAudio {
|
|
|
2269
2715
|
|
|
2270
2716
|
private fun render(output: AudioTrack, sampleRate: Int) {
|
|
2271
2717
|
val buffer = ShortArray(1024 * 2)
|
|
2718
|
+
val stereo = DoubleArray(2)
|
|
2272
2719
|
while (running && !Thread.currentThread().isInterrupted) {
|
|
2273
|
-
val snapshot = synchronized(this) {
|
|
2274
|
-
oscillators.mapValues { (id, state) ->
|
|
2275
|
-
Triple(state, state.gain, state.active)
|
|
2276
|
-
}
|
|
2277
|
-
}
|
|
2278
|
-
val now = System.nanoTime() / 1_000_000_000.0
|
|
2279
2720
|
for (frame in 0 until 1024) {
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2721
|
+
synchronized(this) {
|
|
2722
|
+
stereo[0] = contextStarts.keys.sumOf { contextId ->
|
|
2723
|
+
if (contextId in suspended) 0.0
|
|
2724
|
+
else renderNode(key(contextId, "destination"), contextTime(contextId) + frame.toDouble() / sampleRate, 0, mutableSetOf())
|
|
2725
|
+
}
|
|
2726
|
+
stereo[1] = contextStarts.keys.sumOf { contextId ->
|
|
2727
|
+
if (contextId in suspended) 0.0
|
|
2728
|
+
else renderNode(key(contextId, "destination"), contextTime(contextId) + frame.toDouble() / sampleRate, 1, mutableSetOf())
|
|
2729
|
+
}
|
|
2289
2730
|
}
|
|
2290
|
-
|
|
2291
|
-
buffer[frame * 2] =
|
|
2292
|
-
buffer[frame * 2 + 1] = value
|
|
2731
|
+
buffer[frame * 2] = (stereo[0].coerceIn(-1.0, 1.0) * Short.MAX_VALUE).toInt().toShort()
|
|
2732
|
+
buffer[frame * 2 + 1] = (stereo[1].coerceIn(-1.0, 1.0) * Short.MAX_VALUE).toInt().toShort()
|
|
2293
2733
|
}
|
|
2294
2734
|
if (output.write(buffer, 0, buffer.size, AudioTrack.WRITE_BLOCKING) < 0) break
|
|
2295
2735
|
}
|
|
2296
2736
|
}
|
|
2737
|
+
|
|
2738
|
+
private fun renderNode(nodeKey: String, time: Double, channel: Int, visited: MutableSet<String>): Double {
|
|
2739
|
+
if (!visited.add(nodeKey)) return 0.0
|
|
2740
|
+
val state = nodes[nodeKey]
|
|
2741
|
+
var value = 0.0
|
|
2742
|
+
if (state == null || state.kind == "destination" || state.kind == "Gain" || state.kind == "BiquadFilter" || state.kind == "Delay" || state.kind == "DynamicsCompressor" || state.kind == "Convolver") {
|
|
2743
|
+
for (source in inputs[nodeKey].orEmpty()) value += renderNode(source, time, channel, visited)
|
|
2744
|
+
} else if (state.kind == "Oscillator") {
|
|
2745
|
+
if (time >= state.startAt && time < state.stopAt) {
|
|
2746
|
+
val elapsed = (time - state.startAt).coerceAtLeast(0.0)
|
|
2747
|
+
val frequency = (state.frequency.valueAt(time) * 2.0.pow(state.detune.valueAt(time) / 1200.0)).coerceAtLeast(0.0)
|
|
2748
|
+
val phase = (elapsed * frequency) % 1.0
|
|
2749
|
+
value = when (state.waveform) {
|
|
2750
|
+
1 -> if (phase < 0.5) 1.0 else -1.0
|
|
2751
|
+
2 -> 2.0 * phase - 1.0
|
|
2752
|
+
3 -> 1.0 - 4.0 * kotlin.math.abs(phase - 0.5)
|
|
2753
|
+
else -> sin(phase * 2.0 * PI)
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
} else if (state.kind == "BufferSource") {
|
|
2757
|
+
val buffer = state.bufferId?.let(buffers::get)
|
|
2758
|
+
if (buffer != null && time >= state.startAt && time < state.stopAt) {
|
|
2759
|
+
val elapsed = (time - state.startAt) * state.playbackRate.valueAt(time).coerceAtLeast(0.01)
|
|
2760
|
+
val length = buffer.length.toDouble() / buffer.sampleRate
|
|
2761
|
+
val loopEnd = if (state.loopEnd > state.loopStart) state.loopEnd else length
|
|
2762
|
+
val position = if (state.bufferLoop && loopEnd > state.loopStart) {
|
|
2763
|
+
state.loopStart + ((elapsed + state.startOffset - state.loopStart) % (loopEnd - state.loopStart))
|
|
2764
|
+
} else elapsed + state.startOffset
|
|
2765
|
+
if (state.bufferLoop || position < length) {
|
|
2766
|
+
val sampleIndex = (position * buffer.sampleRate).toLong().coerceAtLeast(0)
|
|
2767
|
+
value = (buffer.channels.getOrNull(channel) ?: buffer.channels.firstOrNull())
|
|
2768
|
+
?.getOrNull(sampleIndex.toInt())
|
|
2769
|
+
?.toDouble()
|
|
2770
|
+
?: 0.0
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
if (state?.kind == "Gain") value *= state.gain.valueAt(time).coerceAtLeast(0.0)
|
|
2775
|
+
visited.remove(nodeKey)
|
|
2776
|
+
return value
|
|
2777
|
+
}
|
|
2297
2778
|
}
|
|
2298
2779
|
|
|
2299
2780
|
private class CraftTextDecorationSpan(
|
|
@@ -2576,6 +3057,16 @@ class MainActivity : AppCompatActivity() {
|
|
|
2576
3057
|
}.getOrDefault(JSONObject())
|
|
2577
3058
|
hapticsEnabled = config.optBoolean("enableHaptics", false)
|
|
2578
3059
|
clipboardEnabled = config.optBoolean("enableClipboard", false)
|
|
3060
|
+
// Normally started already by its content provider; this covers a
|
|
3061
|
+
// process that skipped providers (instrumentation).
|
|
3062
|
+
CraftCrashReporter.start(this)
|
|
3063
|
+
if (BuildConfig.DEBUG && savedInstanceState == null) {
|
|
3064
|
+
CraftCrashReporter.debugCrashTestRequested(intent?.getStringExtra("craftCrashTest")) { message ->
|
|
3065
|
+
appScreen?.takeIf { it.active }?.let { app ->
|
|
3066
|
+
reportJavaScriptFailure(app, app.isolate.evaluateJavaScriptAsync("(function crashReporterTest() { throw new TypeError(${JSONObject.quote(message)}); })()"))
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
2579
3070
|
capabilities = CraftNativeCapabilities(this, config)
|
|
2580
3071
|
routes = CraftNativeRoutes(config)
|
|
2581
3072
|
stores = CraftNativeStores.get(this, config)
|
|
@@ -2904,18 +3395,36 @@ class MainActivity : AppCompatActivity() {
|
|
|
2904
3395
|
audioMessage({ action: action, contextId: contextId, nodeId: nodeId, value: current });
|
|
2905
3396
|
}
|
|
2906
3397
|
});
|
|
2907
|
-
|
|
3398
|
+
param._paramOwner = nodeId;
|
|
3399
|
+
param._paramName = action;
|
|
3400
|
+
function schedule(value, when, suffix) {
|
|
2908
3401
|
current = Number(value) || 0;
|
|
2909
3402
|
var time = Number(when);
|
|
2910
|
-
audioMessage({ action: action +
|
|
3403
|
+
audioMessage({ action: action + suffix, contextId: contextId, nodeId: nodeId, value: current, time: isFinite(time) ? Math.max(0, time) : 0 });
|
|
2911
3404
|
}
|
|
2912
|
-
param.setValueAtTime = function(value, when) { schedule(value, when,
|
|
2913
|
-
param.linearRampToValueAtTime = function(value, when) { schedule(value, when,
|
|
3405
|
+
param.setValueAtTime = function(value, when) { schedule(value, when, 'AtTime'); return param; };
|
|
3406
|
+
param.linearRampToValueAtTime = function(value, when) { schedule(value, when, 'RampToTime'); return param; };
|
|
3407
|
+
param.exponentialRampToValueAtTime = function(value, when) { schedule(value, when, 'ExponentialRampToTime'); return param; };
|
|
3408
|
+
param.setTargetAtTime = function(value, when) { schedule(value, when, 'TargetAtTime'); return param; };
|
|
2914
3409
|
return param;
|
|
2915
3410
|
}
|
|
3411
|
+
function makeNode(context, kind) {
|
|
3412
|
+
var value = { _contextId: context._id, _nodeId: context._id + '-' + kind + '-' + (++nextAudioId) };
|
|
3413
|
+
value.connect = function(destination) {
|
|
3414
|
+
if (destination && destination._paramOwner) {
|
|
3415
|
+
audioMessage({ action: 'connectParam', contextId: context._id, sourceId: value._nodeId, destinationId: destination._paramOwner, parameter: destination._paramName || '' });
|
|
3416
|
+
} else {
|
|
3417
|
+
audioMessage({ action: 'connect', contextId: context._id, sourceId: value._nodeId, destinationId: destination && destination._nodeId || 'destination' });
|
|
3418
|
+
}
|
|
3419
|
+
return destination;
|
|
3420
|
+
};
|
|
3421
|
+
audioMessage({ action: 'create' + kind, contextId: context._id, nodeId: value._nodeId });
|
|
3422
|
+
return value;
|
|
3423
|
+
}
|
|
2916
3424
|
function AudioContext() {
|
|
2917
3425
|
this._id = 'audio-' + (++nextAudioId);
|
|
2918
3426
|
this._started = Date.now() / 1000;
|
|
3427
|
+
this.sampleRate = 44100;
|
|
2919
3428
|
this.state = 'running';
|
|
2920
3429
|
this.destination = { _contextId: this._id, _nodeId: 'destination' };
|
|
2921
3430
|
audioMessage({ action: 'createContext', contextId: this._id });
|
|
@@ -2925,37 +3434,101 @@ class MainActivity : AppCompatActivity() {
|
|
|
2925
3434
|
});
|
|
2926
3435
|
AudioContext.prototype.createOscillator = function() {
|
|
2927
3436
|
var context = this;
|
|
2928
|
-
var node =
|
|
3437
|
+
var node = makeNode(context, 'Oscillator');
|
|
2929
3438
|
node.frequency = audioParam(context._id, node._nodeId, 'frequency', 440);
|
|
2930
|
-
node.connect = function(destination) {
|
|
2931
|
-
audioMessage({ action: 'connect', contextId: context._id, sourceId: node._nodeId, destinationId: destination && destination._nodeId || 'destination' });
|
|
2932
|
-
return destination;
|
|
2933
|
-
};
|
|
2934
3439
|
node.start = function(when) {
|
|
2935
3440
|
audioMessage({ action: 'start', contextId: context._id, nodeId: node._nodeId, delay: Math.max(0, (Number(when) || 0) - context.currentTime) });
|
|
2936
3441
|
};
|
|
2937
3442
|
node.stop = function(when) {
|
|
2938
3443
|
audioMessage({ action: 'stop', contextId: context._id, nodeId: node._nodeId, delay: Math.max(0, (Number(when) || 0) - context.currentTime) });
|
|
2939
3444
|
};
|
|
2940
|
-
audioMessage({ action: '
|
|
3445
|
+
audioMessage({ action: 'frequency', contextId: context._id, nodeId: node._nodeId, value: node.frequency.value });
|
|
2941
3446
|
return node;
|
|
2942
3447
|
};
|
|
2943
3448
|
AudioContext.prototype.createGain = function() {
|
|
2944
3449
|
var context = this;
|
|
2945
|
-
var node =
|
|
3450
|
+
var node = makeNode(context, 'Gain');
|
|
2946
3451
|
node.gain = audioParam(context._id, node._nodeId, 'gain', 1);
|
|
2947
|
-
node.
|
|
2948
|
-
audioMessage({ action: 'connect', contextId: context._id, sourceId: node._nodeId, destinationId: destination && destination._nodeId || 'destination' });
|
|
2949
|
-
return destination;
|
|
2950
|
-
};
|
|
2951
|
-
audioMessage({ action: 'createGain', contextId: context._id, nodeId: node._nodeId, gain: node.gain.value });
|
|
3452
|
+
audioMessage({ action: 'gain', contextId: context._id, nodeId: node._nodeId, value: node.gain.value });
|
|
2952
3453
|
return node;
|
|
2953
3454
|
};
|
|
2954
|
-
AudioContext.prototype.
|
|
2955
|
-
|
|
2956
|
-
|
|
3455
|
+
AudioContext.prototype.createBuffer = function(channels, length, sampleRate) {
|
|
3456
|
+
var buffer = { _id: 'buffer-' + (++nextAudioId), numberOfChannels: channels, length: length, sampleRate: sampleRate, duration: length / sampleRate, _channels: [] };
|
|
3457
|
+
for (var index = 0; index < channels; index++) buffer._channels.push(new Float32Array(length));
|
|
3458
|
+
buffer.getChannelData = function(channel) { return buffer._channels[channel]; };
|
|
3459
|
+
audioMessage({ action: 'createBuffer', contextId: this._id, bufferId: buffer._id, channels: channels, length: length, sampleRate: sampleRate });
|
|
3460
|
+
return buffer;
|
|
3461
|
+
};
|
|
3462
|
+
function uploadBuffer(contextId, buffer) {
|
|
3463
|
+
var channels = [];
|
|
3464
|
+
for (var index = 0; index < buffer._channels.length; index++) {
|
|
3465
|
+
var source = buffer._channels[index];
|
|
3466
|
+
var values = [];
|
|
3467
|
+
for (var sample = 0; sample < source.length; sample++) values.push(Number(source[sample]) || 0);
|
|
3468
|
+
channels.push(values);
|
|
3469
|
+
}
|
|
3470
|
+
audioMessage({ action: 'bufferData', contextId: contextId, bufferId: buffer._id, channels: channels });
|
|
3471
|
+
}
|
|
3472
|
+
AudioContext.prototype.createBufferSource = function() {
|
|
3473
|
+
var context = this;
|
|
3474
|
+
var value = makeNode(context, 'BufferSource');
|
|
3475
|
+
value.playbackRate = audioParam(context._id, value._nodeId, 'playbackRate', 1);
|
|
3476
|
+
value.detune = audioParam(context._id, value._nodeId, 'detune', 0);
|
|
3477
|
+
value.buffer = null;
|
|
3478
|
+
value.loop = false;
|
|
3479
|
+
value.loopStart = 0;
|
|
3480
|
+
value.loopEnd = 0;
|
|
3481
|
+
value.start = function(when, offset, duration) {
|
|
3482
|
+
if (value.buffer) {
|
|
3483
|
+
uploadBuffer(context._id, value.buffer);
|
|
3484
|
+
audioMessage({ action: 'buffer', contextId: context._id, nodeId: value._nodeId, bufferId: value.buffer._id });
|
|
3485
|
+
}
|
|
3486
|
+
audioMessage({ action: 'loop', contextId: context._id, nodeId: value._nodeId, loop: value.loop, loopStart: value.loopStart, loopEnd: value.loopEnd });
|
|
3487
|
+
audioMessage({ action: 'start', contextId: context._id, nodeId: value._nodeId, delay: Math.max(0, (Number(when) || 0) - context.currentTime), offset: Number(offset) || 0, duration: Number(duration) || -1 });
|
|
3488
|
+
};
|
|
3489
|
+
value.stop = function(when) { audioMessage({ action: 'stop', contextId: context._id, nodeId: value._nodeId, delay: Math.max(0, (Number(when) || 0) - context.currentTime) }); };
|
|
3490
|
+
return value;
|
|
3491
|
+
};
|
|
3492
|
+
AudioContext.prototype.createBiquadFilter = function() {
|
|
3493
|
+
var context = this;
|
|
3494
|
+
var value = makeNode(context, 'BiquadFilter');
|
|
3495
|
+
value.frequency = audioParam(context._id, value._nodeId, 'frequency', 350);
|
|
3496
|
+
value.Q = audioParam(context._id, value._nodeId, 'Q', 1);
|
|
3497
|
+
value.gain = audioParam(context._id, value._nodeId, 'gain', 0);
|
|
3498
|
+
value.type = 'lowpass';
|
|
3499
|
+
return value;
|
|
3500
|
+
};
|
|
3501
|
+
AudioContext.prototype.createDelay = function() {
|
|
3502
|
+
var context = this;
|
|
3503
|
+
var value = makeNode(context, 'Delay');
|
|
3504
|
+
value.delayTime = audioParam(context._id, value._nodeId, 'delayTime', 0);
|
|
3505
|
+
return value;
|
|
3506
|
+
};
|
|
3507
|
+
AudioContext.prototype.createDynamicsCompressor = function() {
|
|
3508
|
+
var context = this;
|
|
3509
|
+
var value = makeNode(context, 'DynamicsCompressor');
|
|
3510
|
+
value.threshold = audioParam(context._id, value._nodeId, 'threshold', -24);
|
|
3511
|
+
value.knee = audioParam(context._id, value._nodeId, 'knee', 30);
|
|
3512
|
+
value.ratio = audioParam(context._id, value._nodeId, 'ratio', 12);
|
|
3513
|
+
value.attack = audioParam(context._id, value._nodeId, 'attack', 0.003);
|
|
3514
|
+
value.release = audioParam(context._id, value._nodeId, 'release', 0.25);
|
|
3515
|
+
value.reduction = 0;
|
|
3516
|
+
return value;
|
|
3517
|
+
};
|
|
3518
|
+
AudioContext.prototype.createConvolver = function() {
|
|
3519
|
+
var context = this;
|
|
3520
|
+
var value = makeNode(context, 'Convolver');
|
|
3521
|
+
value.normalize = true;
|
|
3522
|
+
value.buffer = null;
|
|
3523
|
+
return value;
|
|
3524
|
+
};
|
|
3525
|
+
AudioContext.prototype.resume = function() { this.state = 'running'; audioMessage({ action: 'state', contextId: this._id, state: this.state }); return Promise.resolve(); };
|
|
3526
|
+
AudioContext.prototype.suspend = function() { this.state = 'suspended'; audioMessage({ action: 'state', contextId: this._id, state: this.state }); return Promise.resolve(); };
|
|
3527
|
+
AudioContext.prototype.close = function() { this.state = 'closed'; audioMessage({ action: 'state', contextId: this._id, state: this.state }); audioMessage({ action: 'close', contextId: this._id }); return Promise.resolve(); };
|
|
2957
3528
|
globalThis.AudioContext = AudioContext;
|
|
2958
3529
|
globalThis.webkitAudioContext = AudioContext;
|
|
3530
|
+
globalThis.BaseAudioContext = AudioContext;
|
|
3531
|
+
globalThis.AudioBuffer = function() {};
|
|
2959
3532
|
}
|
|
2960
3533
|
})()
|
|
2961
3534
|
""".trimIndent()
|
|
@@ -3029,6 +3602,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3029
3602
|
}
|
|
3030
3603
|
} catch (error: Exception) {
|
|
3031
3604
|
Log.e(TAG, "Native STX bundle failed", error)
|
|
3605
|
+
reportJavaScriptError(activeScreen, error)
|
|
3032
3606
|
if (activeScreen.active) showError("Native STX bundle failed: ${error.message}")
|
|
3033
3607
|
}
|
|
3034
3608
|
}, executor)
|
|
@@ -3046,6 +3620,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
3046
3620
|
* screen shown before hears true (its `navigation.onAppear`).
|
|
3047
3621
|
*/
|
|
3048
3622
|
private fun show(screen: Screen) {
|
|
3623
|
+
// A navigation breadcrumb: the trail a crash report carries.
|
|
3624
|
+
if (visibleScreen !== screen) CraftCrashReporter.screenAppeared(screen.route ?: "home")
|
|
3049
3625
|
val bars = chrome
|
|
3050
3626
|
val host: ViewGroup = bars?.content ?: root
|
|
3051
3627
|
host.removeAllViews()
|
|
@@ -3258,8 +3834,13 @@ class MainActivity : AppCompatActivity() {
|
|
|
3258
3834
|
}
|
|
3259
3835
|
"NAVIGATE_OPEN" -> payload.optString("path").takeIf { it.isNotBlank() }?.let(::navigateOpen)
|
|
3260
3836
|
"NAVIGATION_SET_OPTIONS" -> {
|
|
3261
|
-
|
|
3262
|
-
|
|
3837
|
+
// The app script has no navigation bar of its own. Screen
|
|
3838
|
+
// options remain valid while that screen is off-stack, so
|
|
3839
|
+
// only reject the headless app isolate here.
|
|
3840
|
+
if (screen !== appScreen) {
|
|
3841
|
+
payload.keys().forEach { key -> screen.options.put(key, payload.opt(key)) }
|
|
3842
|
+
if (visibleScreen === screen) applyChrome(screen)
|
|
3843
|
+
}
|
|
3263
3844
|
}
|
|
3264
3845
|
"SYNC_WRITE" -> syncWrite(screen, payload)
|
|
3265
3846
|
"TIMER" -> {
|
|
@@ -3275,15 +3856,20 @@ class MainActivity : AppCompatActivity() {
|
|
|
3275
3856
|
"TIMER_CLEAR" -> screen.timers.remove(payload.optInt("id"))?.let(capabilityHandler::removeCallbacks)
|
|
3276
3857
|
"FRAME" -> requestFrame(screen)
|
|
3277
3858
|
"NAVIGATE", "NAVIGATE_REPLACE" -> {
|
|
3278
|
-
|
|
3859
|
+
// Navigation belongs to the route that is actually visible.
|
|
3860
|
+
// The stack list can be temporarily detached while a tab or
|
|
3861
|
+
// back transition restores its root; using its last entry
|
|
3862
|
+
// silently drops a valid push from that visible screen. The
|
|
3863
|
+
// app script has no stack of its own; it uses NAVIGATE_OPEN.
|
|
3864
|
+
if (visibleScreen !== screen) return
|
|
3279
3865
|
val route = payload.optString("screen").takeIf { it.isNotBlank() } ?: return
|
|
3280
3866
|
if (message.optString("type") == "NAVIGATE_REPLACE") {
|
|
3281
3867
|
closeScreen(screens.removeAt(screens.lastIndex))
|
|
3282
3868
|
}
|
|
3283
3869
|
openScreen(route, payload.optJSONObject("params") ?: JSONObject())
|
|
3284
3870
|
}
|
|
3285
|
-
"NAVIGATE_BACK" -> if (
|
|
3286
|
-
"NAVIGATE_POP_TO_ROOT" -> if (
|
|
3871
|
+
"NAVIGATE_BACK" -> if (visibleScreen === screen) back()
|
|
3872
|
+
"NAVIGATE_POP_TO_ROOT" -> if (visibleScreen === screen) popToRoot()
|
|
3287
3873
|
"API_REQUEST" -> answerAPI(screen, message, payload)
|
|
3288
3874
|
"API_CANCEL" -> {
|
|
3289
3875
|
val id = payload.optString("requestId")
|
|
@@ -3324,7 +3910,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3324
3910
|
private fun applyTargetedUpdates(screen: Screen, ids: List<String>): Boolean {
|
|
3325
3911
|
val applied = mutableSetOf<String>()
|
|
3326
3912
|
for (id in ids) {
|
|
3327
|
-
val target = screen
|
|
3913
|
+
val target = drawnTarget(screen, id) ?: return false
|
|
3328
3914
|
if (!applied.add(target)) continue
|
|
3329
3915
|
val document = screen.mutations.node(target) ?: return false
|
|
3330
3916
|
val path = screen.nodePaths[target] ?: return false
|
|
@@ -3340,6 +3926,17 @@ class MainActivity : AppCompatActivity() {
|
|
|
3340
3926
|
return true
|
|
3341
3927
|
}
|
|
3342
3928
|
|
|
3929
|
+
/** Find the nearest rendered ancestor for protocol nodes with no view. */
|
|
3930
|
+
private fun drawnTarget(screen: Screen, id: String): String? {
|
|
3931
|
+
var current: String? = id
|
|
3932
|
+
while (current != null) {
|
|
3933
|
+
val target = screen.flatListOwners[current] ?: current
|
|
3934
|
+
if (screen.nodeViews[target] != null) return target
|
|
3935
|
+
current = screen.mutations.parentId(current)
|
|
3936
|
+
}
|
|
3937
|
+
return null
|
|
3938
|
+
}
|
|
3939
|
+
|
|
3343
3940
|
private fun scheduleLayoutEvents(screen: Screen) {
|
|
3344
3941
|
if (!screen.active) return
|
|
3345
3942
|
val observer = screen.view.viewTreeObserver
|
|
@@ -3478,8 +4075,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
3478
4075
|
configureText(this, childText(children), style)
|
|
3479
4076
|
}
|
|
3480
4077
|
"StatusBar" -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
|
|
3481
|
-
screen.statusBarStyle = props.
|
|
3482
|
-
screen.statusBarHidden = if (props.has("hidden")) props.optBoolean("hidden") else null
|
|
4078
|
+
screen.statusBarStyle = craftNativeOptionalString(props.opt("barStyle"))
|
|
4079
|
+
screen.statusBarHidden = if (props.has("hidden") && !props.isNull("hidden")) props.optBoolean("hidden") else null
|
|
3483
4080
|
visibility = View.GONE
|
|
3484
4081
|
}
|
|
3485
4082
|
"RefreshControl" -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
|
|
@@ -3487,19 +4084,19 @@ class MainActivity : AppCompatActivity() {
|
|
|
3487
4084
|
}
|
|
3488
4085
|
"Link" -> ((previous as? TextView)?.takeUnless { it is Button } ?: TextView(this)).apply {
|
|
3489
4086
|
includeFontPadding = false
|
|
3490
|
-
configureText(this, props.
|
|
3491
|
-
val linkColor = props.
|
|
4087
|
+
configureText(this, craftNativeOptionalString(props.opt("title")) ?: childText(children), style)
|
|
4088
|
+
val linkColor = craftNativeOptionalString(props.opt("color")) ?: craftNativeOptionalString(style.opt("color"))
|
|
3492
4089
|
setTextColor(color(linkColor, Color.rgb(33, 150, 243)))
|
|
3493
4090
|
paintFlags = paintFlags or Paint.UNDERLINE_TEXT_FLAG
|
|
3494
4091
|
isEnabled = !props.optBoolean("disabled", false)
|
|
3495
4092
|
isClickable = true
|
|
3496
4093
|
isFocusable = true
|
|
3497
|
-
val target = listOf(props.
|
|
3498
|
-
.
|
|
4094
|
+
val target = listOf(props.opt("href"), props.opt("to"))
|
|
4095
|
+
.firstNotNullOfOrNull(::craftNativeOptionalString)
|
|
3499
4096
|
?.trim()
|
|
3500
4097
|
?.takeIf { !props.optBoolean("disabled", false) }
|
|
3501
4098
|
if (target != null) screen.linkTargets[this] = target else screen.linkTargets.remove(this)
|
|
3502
|
-
val handler = events
|
|
4099
|
+
val handler = craftNativeEventName(events, "onPress", "onClick")
|
|
3503
4100
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
3504
4101
|
setOnClickListener {
|
|
3505
4102
|
screen.linkTargets[it]?.let { target -> navigateOpen(target); return@setOnClickListener }
|
|
@@ -3518,12 +4115,12 @@ class MainActivity : AppCompatActivity() {
|
|
|
3518
4115
|
minimumHeight = 0
|
|
3519
4116
|
setPadding(0, 0, 0, 0)
|
|
3520
4117
|
}).apply {
|
|
3521
|
-
val title = props.
|
|
4118
|
+
val title = craftNativeOptionalString(props.opt("title")) ?: childText(children)
|
|
3522
4119
|
isAllCaps = false
|
|
3523
4120
|
configureText(this, title, style)
|
|
3524
|
-
props.
|
|
4121
|
+
craftNativeOptionalString(props.opt("color"))?.let { setTextColor(color(it, defaultTextColor)) }
|
|
3525
4122
|
isEnabled = !props.optBoolean("disabled", false)
|
|
3526
|
-
val handler = events
|
|
4123
|
+
val handler = craftNativeEventName(events, "onPress", "onClick")
|
|
3527
4124
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
3528
4125
|
setOnClickListener {
|
|
3529
4126
|
val current = screen.handlers[this] ?: return@setOnClickListener
|
|
@@ -3531,7 +4128,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3531
4128
|
}
|
|
3532
4129
|
}
|
|
3533
4130
|
"Checkbox" -> (previous as? CheckBox ?: CheckBox(this)).apply {
|
|
3534
|
-
text = props.
|
|
4131
|
+
text = craftNativeOptionalString(props.opt("label")) ?: childText(children)
|
|
3535
4132
|
configureText(this, text.toString(), style)
|
|
3536
4133
|
val requested = props.opt("checked") ?: props.opt("value")
|
|
3537
4134
|
val checked = when (requested) {
|
|
@@ -3547,8 +4144,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
3547
4144
|
isChecked = checked
|
|
3548
4145
|
isActivated = props.optBoolean("indeterminate", false)
|
|
3549
4146
|
isEnabled = !props.optBoolean("disabled", false)
|
|
3550
|
-
|
|
3551
|
-
|
|
4147
|
+
val checkboxColor = craftNativeOptionalString(props.opt("color"))
|
|
4148
|
+
?: craftNativeOptionalString(style.opt("color"))
|
|
4149
|
+
buttonTintList = checkboxColor?.let { ColorStateList.valueOf(color(it, defaultTextColor)) }
|
|
4150
|
+
val handler = craftNativeEventName(events, "onValueChange", "onChange")
|
|
3552
4151
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
3553
4152
|
setOnCheckedChangeListener { _, value ->
|
|
3554
4153
|
screen.handlers[this]?.let { current ->
|
|
@@ -3569,13 +4168,13 @@ class MainActivity : AppCompatActivity() {
|
|
|
3569
4168
|
hintTextColors ?: ColorStateList.valueOf(Color.GRAY)
|
|
3570
4169
|
}
|
|
3571
4170
|
val defaultHighlightColor = inputHighlightDefaults.getOrPut(this) { highlightColor }
|
|
3572
|
-
hint = props.
|
|
4171
|
+
hint = craftNativeOptionalString(props.opt("placeholder")).orEmpty()
|
|
3573
4172
|
setHintTextColor(
|
|
3574
|
-
props.
|
|
4173
|
+
craftNativeOptionalString(props.opt("placeholderTextColor"))
|
|
3575
4174
|
?.let { ColorStateList.valueOf(color(it, Color.GRAY)) }
|
|
3576
4175
|
?: defaultHintColors,
|
|
3577
4176
|
)
|
|
3578
|
-
highlightColor = props.
|
|
4177
|
+
highlightColor = craftNativeOptionalString(props.opt("selectionColor"))
|
|
3579
4178
|
?.let { color(it, defaultHighlightColor) }
|
|
3580
4179
|
?: defaultHighlightColor
|
|
3581
4180
|
val maxLength = props.optInt("maxLength", 0)
|
|
@@ -3597,14 +4196,14 @@ class MainActivity : AppCompatActivity() {
|
|
|
3597
4196
|
// the web; without one the field keeps its own.
|
|
3598
4197
|
if (listOf("padding", "paddingHorizontal", "paddingVertical", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom").any { style.has(it) }) {
|
|
3599
4198
|
val density = resources.displayMetrics.density
|
|
3600
|
-
val all = style
|
|
3601
|
-
val horizontal = style
|
|
3602
|
-
val vertical = style
|
|
4199
|
+
val all = craftNativePadding(style, "padding", 0, density)
|
|
4200
|
+
val horizontal = craftNativePadding(style, "paddingHorizontal", all, density)
|
|
4201
|
+
val vertical = craftNativePadding(style, "paddingVertical", all, density)
|
|
3603
4202
|
setPadding(
|
|
3604
|
-
(style
|
|
3605
|
-
(style
|
|
3606
|
-
(style
|
|
3607
|
-
(style
|
|
4203
|
+
craftNativePadding(style, "paddingLeft", horizontal, density),
|
|
4204
|
+
craftNativePadding(style, "paddingTop", vertical, density),
|
|
4205
|
+
craftNativePadding(style, "paddingRight", horizontal, density),
|
|
4206
|
+
craftNativePadding(style, "paddingBottom", vertical, density),
|
|
3608
4207
|
)
|
|
3609
4208
|
}
|
|
3610
4209
|
// Reused EditTexts must forget the previous field's hint as
|
|
@@ -3625,15 +4224,15 @@ class MainActivity : AppCompatActivity() {
|
|
|
3625
4224
|
else if (this !== previous && props.has("defaultValue") && !props.isNull("defaultValue")) setText(props.optString("defaultValue"))
|
|
3626
4225
|
applyTextDecoration(this, style)
|
|
3627
4226
|
screen.inputIdentities[this] = identity
|
|
3628
|
-
val handler = events
|
|
4227
|
+
val handler = craftNativeEventName(events, "onChange", "onChangeText")
|
|
3629
4228
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
3630
|
-
val onFocus = events
|
|
4229
|
+
val onFocus = craftNativeEventName(events, "onFocus")
|
|
3631
4230
|
if (onFocus.isNotBlank()) screen.focusHandlers[this] = onFocus else screen.focusHandlers.remove(this)
|
|
3632
|
-
val onBlur = events
|
|
4231
|
+
val onBlur = craftNativeEventName(events, "onBlur")
|
|
3633
4232
|
if (onBlur.isNotBlank()) screen.blurHandlers[this] = onBlur else screen.blurHandlers.remove(this)
|
|
3634
|
-
val onEndEditing = events
|
|
4233
|
+
val onEndEditing = craftNativeEventName(events, "onEndEditing")
|
|
3635
4234
|
if (onEndEditing.isNotBlank()) screen.endEditingHandlers[this] = onEndEditing else screen.endEditingHandlers.remove(this)
|
|
3636
|
-
val onSubmit = events
|
|
4235
|
+
val onSubmit = craftNativeEventName(events, "onSubmitEditing")
|
|
3637
4236
|
if (onSubmit.isNotBlank()) screen.submitHandlers[this] = onSubmit else screen.submitHandlers.remove(this)
|
|
3638
4237
|
setOnFocusChangeListener { view, focused ->
|
|
3639
4238
|
val nativeEvent = JSONObject().put("text", text.toString())
|
|
@@ -3694,10 +4293,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
3694
4293
|
}
|
|
3695
4294
|
if (label.isBlank()) return@mapNotNull null
|
|
3696
4295
|
val optionProps = option.optJSONObject("props")
|
|
3697
|
-
val value = optionProps?.
|
|
4296
|
+
val value = craftNativeOptionalString(optionProps?.opt("value")) ?: label
|
|
3698
4297
|
value to label
|
|
3699
4298
|
}
|
|
3700
|
-
val handler = events
|
|
4299
|
+
val handler = craftNativeEventName(events, "onValueChange", "onChange")
|
|
3701
4300
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
3702
4301
|
// Detach the old listener before replacing the adapter or selection.
|
|
3703
4302
|
// Both operations can synchronously dispatch an Android selection
|
|
@@ -3706,7 +4305,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
3706
4305
|
adapter = ArrayAdapter(this@MainActivity, android.R.layout.simple_spinner_item, options.map { it.second }).also {
|
|
3707
4306
|
it.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
|
3708
4307
|
}
|
|
3709
|
-
val requested = props.
|
|
4308
|
+
val requested = craftNativeOptionalString(props.opt("selectedValue"))
|
|
4309
|
+
?: craftNativeOptionalString(props.opt("value")).orEmpty()
|
|
3710
4310
|
val selected = options.indexOfFirst { it.first == requested }.takeIf { it >= 0 } ?: 0
|
|
3711
4311
|
isEnabled = !props.optBoolean("disabled", false)
|
|
3712
4312
|
// Mounting a controlled picker must not look like a user
|
|
@@ -3729,7 +4329,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3729
4329
|
}
|
|
3730
4330
|
"DatePicker" -> (previous as? CraftNativeDatePickerView ?: CraftNativeDatePickerView(this)).apply {
|
|
3731
4331
|
configure(props)
|
|
3732
|
-
val handler = events
|
|
4332
|
+
val handler = craftNativeEventName(events, "onValueChange", "onChange")
|
|
3733
4333
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
3734
4334
|
onValueChanged = {
|
|
3735
4335
|
screen.handlers[this]?.let { current ->
|
|
@@ -3743,7 +4343,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3743
4343
|
reconcileChildren(screen, content, children, path, next, style, previousControls)
|
|
3744
4344
|
// Dynamic :visible bindings arrive as a display style mutation;
|
|
3745
4345
|
// static visible props remain authoritative when present.
|
|
3746
|
-
val visible = if (props.has("visible")) {
|
|
4346
|
+
val visible = if (props.has("visible") && !props.isNull("visible")) {
|
|
3747
4347
|
// An explicit visible prop owns modal visibility. The
|
|
3748
4348
|
// binding runtime may leave a stale display:none style
|
|
3749
4349
|
// from the previous hidden state when it patches props.
|
|
@@ -3753,18 +4353,22 @@ class MainActivity : AppCompatActivity() {
|
|
|
3753
4353
|
}
|
|
3754
4354
|
val wasVisible = screen.modalVisibility[this]
|
|
3755
4355
|
screen.modalVisibility[this] = visible
|
|
3756
|
-
val requestClose = events.
|
|
4356
|
+
val requestClose = craftNativeOptionalString(events.opt("onRequestClose")).orEmpty()
|
|
3757
4357
|
if (requestClose.isNotBlank()) screen.modalCloseHandlers[this] = requestClose else screen.modalCloseHandlers.remove(this)
|
|
3758
4358
|
onRequestClose = if (requestClose.isNotBlank()) {
|
|
3759
4359
|
{ send(screen, "EVENT", JSONObject().put("handlerName", requestClose).put("nativeEvent", JSONObject())) }
|
|
3760
4360
|
} else {
|
|
3761
4361
|
null
|
|
3762
4362
|
}
|
|
3763
|
-
setVisible(visible, style
|
|
4363
|
+
setVisible(visible, craftNativeFiniteFloat(style, "opacity", 1f), craftNativeOptionalString(props.opt("animationType")))
|
|
3764
4364
|
if (visible) clearDismissSuppression()
|
|
3765
4365
|
val suppressDismiss = !visible && consumeDismissSuppression()
|
|
3766
4366
|
if (wasVisible != visible && (visible || wasVisible == true)) {
|
|
3767
|
-
val lifecycleHandler = if (visible)
|
|
4367
|
+
val lifecycleHandler = if (visible) {
|
|
4368
|
+
craftNativeOptionalString(events.opt("onShow"))
|
|
4369
|
+
} else {
|
|
4370
|
+
craftNativeOptionalString(events.opt("onDismiss"))
|
|
4371
|
+
}
|
|
3768
4372
|
if (lifecycleHandler.isNotBlank() && !suppressDismiss) {
|
|
3769
4373
|
// Deliver lifecycle callbacks after reconciliation returns to
|
|
3770
4374
|
// the queue; the handler may immediately schedule another
|
|
@@ -3779,8 +4383,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
3779
4383
|
}
|
|
3780
4384
|
"TouchableOpacity", "TouchableHighlight", "Pressable" -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
|
|
3781
4385
|
configureContainer(this, style)
|
|
3782
|
-
screen.pressActiveOpacities[this] = props
|
|
3783
|
-
screen.pressBaseOpacities[this] = style
|
|
4386
|
+
screen.pressActiveOpacities[this] = craftNativeFiniteFloat(props, "activeOpacity", 0.5f)
|
|
4387
|
+
screen.pressBaseOpacities[this] = craftNativeFiniteFloat(style, "opacity", 1f)
|
|
3784
4388
|
reconcileChildren(screen, this, children, path, next, style, previousControls)
|
|
3785
4389
|
}
|
|
3786
4390
|
"Switch" -> (previous as? Switch ?: Switch(this)).apply {
|
|
@@ -3804,9 +4408,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
3804
4408
|
intArrayOf(trueColor, falseColor),
|
|
3805
4409
|
)
|
|
3806
4410
|
} ?: run { trackTintList = null }
|
|
3807
|
-
|
|
4411
|
+
craftNativeOptionalString(props.opt("thumbColor"))
|
|
3808
4412
|
?.let { ColorStateList.valueOf(color(it, Color.WHITE)) }
|
|
3809
|
-
|
|
4413
|
+
?: run { thumbTintList = null }
|
|
4414
|
+
val handler = craftNativeEventName(events, "onValueChange", "onChange")
|
|
3810
4415
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
3811
4416
|
setOnCheckedChangeListener { _, checked ->
|
|
3812
4417
|
val current = screen.handlers[this] ?: return@setOnCheckedChangeListener
|
|
@@ -3814,11 +4419,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
3814
4419
|
}
|
|
3815
4420
|
}
|
|
3816
4421
|
"Slider" -> (previous as? SeekBar ?: SeekBar(this)).apply {
|
|
3817
|
-
val minimum = props
|
|
3818
|
-
val maximumCandidate = props
|
|
4422
|
+
val minimum = craftNativeFiniteDouble(props, "minimumValue", 0.0)
|
|
4423
|
+
val maximumCandidate = craftNativeFiniteDouble(props, "maximumValue", 1.0)
|
|
3819
4424
|
val maximum = if (maximumCandidate > minimum) maximumCandidate else minimum + Double.MIN_VALUE
|
|
3820
4425
|
setOnSeekBarChangeListener(null)
|
|
3821
|
-
val step = props
|
|
4426
|
+
val step = craftNativeFiniteDouble(props, "step", 0.0).takeIf { it > 0 }
|
|
3822
4427
|
// A stepped slider needs one native position per authored
|
|
3823
4428
|
// step; a fixed 1,000-position SeekBar silently made large
|
|
3824
4429
|
// ranges unable to represent most valid values. Continuous
|
|
@@ -3832,7 +4437,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3832
4437
|
.toInt()
|
|
3833
4438
|
} ?: 1000
|
|
3834
4439
|
max = progressMax
|
|
3835
|
-
val requested = props
|
|
4440
|
+
val requested = craftNativeFiniteDouble(props, "value", minimum)
|
|
3836
4441
|
val value = (if (step != null) minimum + ((requested - minimum) / step).roundToInt() * step else requested)
|
|
3837
4442
|
.coerceIn(minimum, maximum)
|
|
3838
4443
|
progress = if (step != null) {
|
|
@@ -3841,16 +4446,19 @@ class MainActivity : AppCompatActivity() {
|
|
|
3841
4446
|
(((value - minimum) / (maximum - minimum)) * max).roundToInt().coerceIn(0, max)
|
|
3842
4447
|
}
|
|
3843
4448
|
if (step != null) screen.sliderSteps[this] = step else screen.sliderSteps.remove(this)
|
|
3844
|
-
|
|
4449
|
+
craftNativeOptionalString(props.opt("minimumTrackTintColor"))
|
|
3845
4450
|
?.let { ColorStateList.valueOf(color(it, Color.WHITE)) }
|
|
3846
|
-
|
|
4451
|
+
?: run { progressTintList = null }
|
|
4452
|
+
craftNativeOptionalString(props.opt("maximumTrackTintColor"))
|
|
3847
4453
|
?.let { ColorStateList.valueOf(color(it, Color.GRAY)) }
|
|
3848
|
-
|
|
4454
|
+
?: run { progressBackgroundTintList = null }
|
|
4455
|
+
craftNativeOptionalString(props.opt("thumbTintColor"))
|
|
3849
4456
|
?.let { ColorStateList.valueOf(color(it, Color.WHITE)) }
|
|
4457
|
+
?: run { thumbTintList = null }
|
|
3850
4458
|
isEnabled = !props.optBoolean("disabled", false)
|
|
3851
|
-
val handler = events
|
|
4459
|
+
val handler = craftNativeEventName(events, "onValueChange", "onChange")
|
|
3852
4460
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
3853
|
-
val complete = events
|
|
4461
|
+
val complete = craftNativeEventName(events, "onSlidingComplete")
|
|
3854
4462
|
if (complete.isNotBlank()) screen.sliderCompleteHandlers[this] = complete else screen.sliderCompleteHandlers.remove(this)
|
|
3855
4463
|
setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
|
3856
4464
|
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
|
@@ -3877,7 +4485,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3877
4485
|
val scale = when (requestedSize) {
|
|
3878
4486
|
"large" -> 1.35f
|
|
3879
4487
|
"small" -> 0.75f
|
|
3880
|
-
is Number -> (requestedSize.toDouble() / 24.0).toFloat()
|
|
4488
|
+
is Number -> (requestedSize.toDouble() / 24.0).takeIf { it.isFinite() }?.toFloat()?.coerceAtLeast(0.5f) ?: 1f
|
|
3881
4489
|
else -> 1f
|
|
3882
4490
|
}
|
|
3883
4491
|
scaleX = scale
|
|
@@ -3885,44 +4493,45 @@ class MainActivity : AppCompatActivity() {
|
|
|
3885
4493
|
val animating = props.optBoolean("animating", true)
|
|
3886
4494
|
isIndeterminate = animating
|
|
3887
4495
|
visibility = if (!animating && props.optBoolean("hidesWhenStopped", true)) View.GONE else View.VISIBLE
|
|
3888
|
-
|
|
4496
|
+
craftNativeOptionalString(props.opt("color"))
|
|
3889
4497
|
?.let { ColorStateList.valueOf(color(it, Color.WHITE)) }
|
|
4498
|
+
?: run { indeterminateTintList = null }
|
|
3890
4499
|
}
|
|
3891
4500
|
"Icon" -> (previous as? CraftNativeIconView ?: CraftNativeIconView(this)).apply {
|
|
3892
4501
|
configureIcon(this, props, style)
|
|
3893
4502
|
}
|
|
3894
4503
|
"Image" -> (previous as? ImageView ?: ImageView(this)).apply {
|
|
3895
|
-
scaleType = when (style
|
|
4504
|
+
scaleType = when (craftNativeResizeMode(style, props)) {
|
|
3896
4505
|
"cover" -> ImageView.ScaleType.CENTER_CROP
|
|
3897
4506
|
"stretch" -> ImageView.ScaleType.FIT_XY
|
|
3898
4507
|
"center" -> ImageView.ScaleType.CENTER
|
|
3899
4508
|
else -> ImageView.ScaleType.CENTER_INSIDE
|
|
3900
4509
|
}
|
|
3901
4510
|
adjustViewBounds = false
|
|
3902
|
-
val onLoadStart = events
|
|
4511
|
+
val onLoadStart = craftNativeEventName(events, "onLoadStart")
|
|
3903
4512
|
if (onLoadStart.isNotBlank()) screen.imageLoadStartHandlers[this] = onLoadStart else screen.imageLoadStartHandlers.remove(this)
|
|
3904
|
-
val onLoad = events
|
|
4513
|
+
val onLoad = craftNativeEventName(events, "onLoad")
|
|
3905
4514
|
if (onLoad.isNotBlank()) screen.imageLoadHandlers[this] = onLoad else screen.imageLoadHandlers.remove(this)
|
|
3906
|
-
val onLoadEnd = events
|
|
4515
|
+
val onLoadEnd = craftNativeEventName(events, "onLoadEnd")
|
|
3907
4516
|
if (onLoadEnd.isNotBlank()) screen.imageLoadEndHandlers[this] = onLoadEnd else screen.imageLoadEndHandlers.remove(this)
|
|
3908
|
-
val onError = events
|
|
4517
|
+
val onError = craftNativeEventName(events, "onError")
|
|
3909
4518
|
if (onError.isNotBlank()) screen.imageErrorHandlers[this] = onError else screen.imageErrorHandlers.remove(this)
|
|
3910
4519
|
updateImage(screen, this, props.opt("source"), props.opt("defaultSource"))
|
|
3911
4520
|
}
|
|
3912
4521
|
"ImageBackground" -> (previous as? CraftNativeImageBackgroundView ?: CraftNativeImageBackgroundView(this)).apply {
|
|
3913
|
-
image.scaleType = when (style
|
|
4522
|
+
image.scaleType = when (craftNativeResizeMode(style, props)) {
|
|
3914
4523
|
"cover" -> ImageView.ScaleType.CENTER_CROP
|
|
3915
4524
|
"stretch" -> ImageView.ScaleType.FIT_XY
|
|
3916
4525
|
"center" -> ImageView.ScaleType.CENTER
|
|
3917
4526
|
else -> ImageView.ScaleType.CENTER_INSIDE
|
|
3918
4527
|
}
|
|
3919
|
-
val onLoadStart = events
|
|
4528
|
+
val onLoadStart = craftNativeEventName(events, "onLoadStart")
|
|
3920
4529
|
if (onLoadStart.isNotBlank()) screen.imageLoadStartHandlers[image] = onLoadStart else screen.imageLoadStartHandlers.remove(image)
|
|
3921
|
-
val onLoad = events
|
|
4530
|
+
val onLoad = craftNativeEventName(events, "onLoad")
|
|
3922
4531
|
if (onLoad.isNotBlank()) screen.imageLoadHandlers[image] = onLoad else screen.imageLoadHandlers.remove(image)
|
|
3923
|
-
val onLoadEnd = events
|
|
4532
|
+
val onLoadEnd = craftNativeEventName(events, "onLoadEnd")
|
|
3924
4533
|
if (onLoadEnd.isNotBlank()) screen.imageLoadEndHandlers[image] = onLoadEnd else screen.imageLoadEndHandlers.remove(image)
|
|
3925
|
-
val onError = events
|
|
4534
|
+
val onError = craftNativeEventName(events, "onError")
|
|
3926
4535
|
if (onError.isNotBlank()) screen.imageErrorHandlers[image] = onError else screen.imageErrorHandlers.remove(image)
|
|
3927
4536
|
updateImage(screen, image, props.opt("source"), props.opt("defaultSource"))
|
|
3928
4537
|
configureContainer(content, style)
|
|
@@ -3936,6 +4545,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3936
4545
|
loop = props.optBoolean("loop", false),
|
|
3937
4546
|
muted = props.optBoolean("muted", false),
|
|
3938
4547
|
controls = props.optBoolean("controls", false),
|
|
4548
|
+
resizeMode = craftNativeResizeMode(style, props),
|
|
3939
4549
|
)
|
|
3940
4550
|
}
|
|
3941
4551
|
"Map" -> (previous as? CraftNativeMapView ?: CraftNativeMapView(this)).apply {
|
|
@@ -3969,22 +4579,23 @@ class MainActivity : AppCompatActivity() {
|
|
|
3969
4579
|
.firstOrNull { canonicalType(it.optString("type")) == "RefreshControl" }
|
|
3970
4580
|
val refreshProps = refreshNode?.optJSONObject("props") ?: JSONObject()
|
|
3971
4581
|
val refreshEvents = refreshNode?.optJSONObject("events") ?: JSONObject()
|
|
3972
|
-
val refreshHandlerName = refreshEvents
|
|
3973
|
-
|
|
4582
|
+
val refreshHandlerName = craftNativeEventName(refreshEvents, "onRefresh")
|
|
4583
|
+
.takeIf { it.isNotBlank() }
|
|
4584
|
+
?: craftNativeEventName(events, "onRefresh").takeIf { it.isNotBlank() }
|
|
3974
4585
|
val refreshHandler = refreshHandlerName?.let { handler ->
|
|
3975
4586
|
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject())) }
|
|
3976
4587
|
}
|
|
3977
4588
|
setRefreshHandler(refreshHandler, if (refreshNode == null) props.optBoolean("refreshing", false) else refreshProps.optBoolean("refreshing", false))
|
|
3978
4589
|
setRefreshAppearance(refreshProps, resources.displayMetrics.density)
|
|
3979
|
-
val onScroll = events
|
|
4590
|
+
val onScroll = craftNativeEventName(events, "onScroll")
|
|
3980
4591
|
if (onScroll.isNotBlank()) screen.scrollHandlers[this] = onScroll else screen.scrollHandlers.remove(this)
|
|
3981
|
-
val onBegin = events
|
|
4592
|
+
val onBegin = craftNativeEventName(events, "onScrollBeginDrag")
|
|
3982
4593
|
if (onBegin.isNotBlank()) screen.scrollBeginHandlers[this] = onBegin else screen.scrollBeginHandlers.remove(this)
|
|
3983
|
-
val onEnd = events
|
|
4594
|
+
val onEnd = craftNativeEventName(events, "onScrollEndDrag")
|
|
3984
4595
|
if (onEnd.isNotBlank()) screen.scrollEndHandlers[this] = onEnd else screen.scrollEndHandlers.remove(this)
|
|
3985
|
-
val onMomentumBegin = events
|
|
4596
|
+
val onMomentumBegin = craftNativeEventName(events, "onMomentumScrollBegin")
|
|
3986
4597
|
if (onMomentumBegin.isNotBlank()) screen.scrollMomentumBeginHandlers[this] = onMomentumBegin else screen.scrollMomentumBeginHandlers.remove(this)
|
|
3987
|
-
val onMomentumEnd = events
|
|
4598
|
+
val onMomentumEnd = craftNativeEventName(events, "onMomentumScrollEnd")
|
|
3988
4599
|
if (onMomentumEnd.isNotBlank()) screen.scrollMomentumEndHandlers[this] = onMomentumEnd else screen.scrollMomentumEndHandlers.remove(this)
|
|
3989
4600
|
val nativeScroll = this
|
|
3990
4601
|
setScrollListeners(
|
|
@@ -4038,7 +4649,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4038
4649
|
}
|
|
4039
4650
|
"FlatList" -> (previous as? CraftNativeRefreshableFlatList ?: CraftNativeRefreshableFlatList(this)).apply {
|
|
4040
4651
|
val nativeList = list
|
|
4041
|
-
configureRefresh(events
|
|
4652
|
+
configureRefresh(craftNativeEventName(events, "onRefresh").takeIf { it.isNotBlank() }?.let { handler ->
|
|
4042
4653
|
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject())) }
|
|
4043
4654
|
}, props.optBoolean("refreshing", false))
|
|
4044
4655
|
nativeList.setKeyboardShouldPersistTaps(props.optString("keyboardShouldPersistTaps", "never"))
|
|
@@ -4070,15 +4681,15 @@ class MainActivity : AppCompatActivity() {
|
|
|
4070
4681
|
} else if (control is CraftNativeKeyboardAvoidingView) {
|
|
4071
4682
|
control.content.onLayoutChanged = { if (screen.active) emitLayoutEvents(screen) }
|
|
4072
4683
|
}
|
|
4073
|
-
val pressInHandler = events
|
|
4074
|
-
val pressOutHandler = events
|
|
4684
|
+
val pressInHandler = craftNativeEventName(events, "onPressIn").takeIf { it.isNotBlank() }
|
|
4685
|
+
val pressOutHandler = craftNativeEventName(events, "onPressOut").takeIf { it.isNotBlank() }
|
|
4075
4686
|
if (pressInHandler != null) screen.pressInHandlers[control] = pressInHandler else screen.pressInHandlers.remove(control)
|
|
4076
4687
|
if (pressOutHandler != null) screen.pressOutHandlers[control] = pressOutHandler else screen.pressOutHandlers.remove(control)
|
|
4077
4688
|
if (type != "Button" && type != "Link" && type != "TextInput" && type != "Picker" && type != "DatePicker" && type != "Modal" && type != "Switch" && type != "Slider" && type != "ActivityIndicator" && type != "KeyboardAvoidingView" && type != "StatusBar" && type != "RefreshControl" && type != "Map") {
|
|
4078
|
-
val handler = events
|
|
4689
|
+
val handler = craftNativeEventName(events, "onPress", "onClick")
|
|
4079
4690
|
val disabled = props.optBoolean("disabled", false)
|
|
4080
|
-
val target = listOf(props.
|
|
4081
|
-
.
|
|
4691
|
+
val target = listOf(props.opt("href"), props.opt("to"))
|
|
4692
|
+
.firstNotNullOfOrNull(::craftNativeOptionalString)
|
|
4082
4693
|
?.trim()
|
|
4083
4694
|
?.takeIf { !disabled && type in setOf("TouchableOpacity", "TouchableHighlight", "Pressable") }
|
|
4084
4695
|
if (target != null) screen.linkTargets[control] = target else screen.linkTargets.remove(control)
|
|
@@ -4139,7 +4750,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4139
4750
|
}
|
|
4140
4751
|
val longPressDisabled = props.optBoolean("disabled", false) ||
|
|
4141
4752
|
props.optJSONObject("accessibilityState")?.optBoolean("disabled", false) == true
|
|
4142
|
-
val longPressHandler = events
|
|
4753
|
+
val longPressHandler = craftNativeEventName(events, "onLongPress").takeUnless { longPressDisabled } ?: ""
|
|
4143
4754
|
if (longPressHandler.isNotBlank()) {
|
|
4144
4755
|
screen.longPressHandlers[control] = longPressHandler
|
|
4145
4756
|
control.setOnLongClickListener {
|
|
@@ -4153,7 +4764,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4153
4764
|
control.isLongClickable = false
|
|
4154
4765
|
}
|
|
4155
4766
|
applyAccessibility(control, props, type, explicitKey ?: protocolId ?: path)
|
|
4156
|
-
val layoutHandler = events
|
|
4767
|
+
val layoutHandler = craftNativeEventName(events, "onLayout")
|
|
4157
4768
|
if (layoutHandler.isNotBlank()) {
|
|
4158
4769
|
if (screen.layoutHandlers[control] != layoutHandler) screen.lastLayoutBounds.remove(control)
|
|
4159
4770
|
screen.layoutHandlers[control] = layoutHandler
|
|
@@ -4189,20 +4800,20 @@ class MainActivity : AppCompatActivity() {
|
|
|
4189
4800
|
View.VISIBLE
|
|
4190
4801
|
}
|
|
4191
4802
|
}
|
|
4192
|
-
val width = style
|
|
4193
|
-
val height = style
|
|
4803
|
+
val width = craftNativeFiniteDouble(style, "width", Double.NaN).takeUnless { it.isNaN() }?.let(::dp)
|
|
4804
|
+
val height = craftNativeFiniteDouble(style, "height", Double.NaN).takeUnless { it.isNaN() }?.let(::dp)
|
|
4194
4805
|
control.layoutParams = LinearLayout.LayoutParams(
|
|
4195
4806
|
width ?: ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
4196
4807
|
height ?: ViewGroup.LayoutParams.WRAP_CONTENT
|
|
4197
4808
|
).apply {
|
|
4198
|
-
val margin = style
|
|
4199
|
-
val horizontal = style
|
|
4200
|
-
val vertical = style
|
|
4809
|
+
val margin = craftNativeFiniteDouble(style, "margin", 0.0)
|
|
4810
|
+
val horizontal = craftNativeFiniteDouble(style, "marginHorizontal", margin)
|
|
4811
|
+
val vertical = craftNativeFiniteDouble(style, "marginVertical", margin)
|
|
4201
4812
|
setMargins(
|
|
4202
|
-
dp(style
|
|
4203
|
-
dp(style
|
|
4204
|
-
dp(style
|
|
4205
|
-
dp(style
|
|
4813
|
+
dp(craftNativeFiniteDouble(style, "marginLeft", horizontal)),
|
|
4814
|
+
dp(craftNativeFiniteDouble(style, "marginTop", vertical)),
|
|
4815
|
+
dp(craftNativeFiniteDouble(style, "marginRight", horizontal)),
|
|
4816
|
+
dp(craftNativeFiniteDouble(style, "marginBottom", vertical))
|
|
4206
4817
|
)
|
|
4207
4818
|
}
|
|
4208
4819
|
return control
|
|
@@ -4251,19 +4862,18 @@ class MainActivity : AppCompatActivity() {
|
|
|
4251
4862
|
val direct = listOf(node.optString("key"), props.optString("key"), props.optString("testID"))
|
|
4252
4863
|
.firstOrNull { it.isNotBlank() }
|
|
4253
4864
|
if (direct != null) return direct
|
|
4254
|
-
|
|
4255
|
-
for (index in 0 until children.length()) {
|
|
4256
|
-
val child = children.optJSONObject(index) ?: continue
|
|
4257
|
-
val childProps = child.optJSONObject("props") ?: JSONObject()
|
|
4258
|
-
authoredKey(child, childProps)?.let { return it }
|
|
4259
|
-
}
|
|
4260
|
-
return node.optString("id").takeIf { it.isNotBlank() }
|
|
4865
|
+
return null
|
|
4261
4866
|
}
|
|
4262
4867
|
|
|
4263
4868
|
private fun configureContainer(layout: CraftNativeFlexLayout, style: JSONObject) {
|
|
4264
4869
|
layout.configure(style, resources.displayMetrics.density)
|
|
4265
4870
|
}
|
|
4266
4871
|
|
|
4872
|
+
private fun craftNativeResizeMode(style: JSONObject, props: JSONObject): String {
|
|
4873
|
+
return craftNativeOptionalString(style.opt("resizeMode"))
|
|
4874
|
+
?: craftNativeOptionalString(props.opt("resizeMode")).orEmpty()
|
|
4875
|
+
}
|
|
4876
|
+
|
|
4267
4877
|
private fun flatListScrollEvent(list: CraftNativeFlatList): JSONObject = JSONObject()
|
|
4268
4878
|
.put("contentOffset", JSONObject()
|
|
4269
4879
|
.put("x", list.computeHorizontalScrollOffset())
|
|
@@ -4336,30 +4946,29 @@ class MainActivity : AppCompatActivity() {
|
|
|
4336
4946
|
}
|
|
4337
4947
|
val nodes = (0 until children.length()).mapNotNull { children.optJSONObject(it) }
|
|
4338
4948
|
nodes.forEach { registerFlatListOwnership(screen, it, owner) }
|
|
4339
|
-
list.onScrollEvent = events
|
|
4949
|
+
list.onScrollEvent = craftNativeEventName(events, "onScroll").takeIf { it.isNotBlank() }?.let { handler ->
|
|
4340
4950
|
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", flatListScrollEvent(list))) }
|
|
4341
4951
|
}
|
|
4342
|
-
list.onScrollBeginDrag = events
|
|
4952
|
+
list.onScrollBeginDrag = craftNativeEventName(events, "onScrollBeginDrag").takeIf { it.isNotBlank() }?.let { handler ->
|
|
4343
4953
|
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", flatListScrollEvent(list))) }
|
|
4344
4954
|
}
|
|
4345
|
-
list.onScrollEndDrag = events
|
|
4955
|
+
list.onScrollEndDrag = craftNativeEventName(events, "onScrollEndDrag").takeIf { it.isNotBlank() }?.let { handler ->
|
|
4346
4956
|
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", flatListScrollEvent(list))) }
|
|
4347
4957
|
}
|
|
4348
|
-
list.onMomentumScrollBegin = events
|
|
4958
|
+
list.onMomentumScrollBegin = craftNativeEventName(events, "onMomentumScrollBegin").takeIf { it.isNotBlank() }?.let { handler ->
|
|
4349
4959
|
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", flatListScrollEvent(list))) }
|
|
4350
4960
|
}
|
|
4351
|
-
list.onMomentumScrollEnd = events
|
|
4961
|
+
list.onMomentumScrollEnd = craftNativeEventName(events, "onMomentumScrollEnd").takeIf { it.isNotBlank() }?.let { handler ->
|
|
4352
4962
|
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", flatListScrollEvent(list))) }
|
|
4353
4963
|
}
|
|
4964
|
+
val threshold = craftNativeNonNegativeDouble(props, "threshold", 0.1)
|
|
4965
|
+
val authoredThreshold = craftNativeNonNegativeDouble(props, "onEndReachedThreshold", threshold)
|
|
4354
4966
|
list.apply(
|
|
4355
4967
|
nodes = nodes,
|
|
4356
4968
|
horizontal = props.optBoolean("horizontal", false),
|
|
4357
4969
|
columns = props.optInt("numColumns", 1),
|
|
4358
4970
|
inverted = props.optBoolean("inverted", false),
|
|
4359
|
-
endReachedThreshold =
|
|
4360
|
-
?.takeUnless { it == JSONObject.NULL }
|
|
4361
|
-
?.let { props.optDouble("onEndReachedThreshold", 0.1) }
|
|
4362
|
-
?: props.optDouble("threshold", 0.1),
|
|
4971
|
+
endReachedThreshold = authoredThreshold,
|
|
4363
4972
|
renderItem = { node, itemIdentity, _ ->
|
|
4364
4973
|
val rows = screen.flatListRows.getOrPut(list) { mutableMapOf() }
|
|
4365
4974
|
val previous = rows[itemIdentity].orEmpty()
|
|
@@ -4373,7 +4982,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4373
4982
|
val controls = screen.flatListRows[list]?.remove(itemIdentity).orEmpty()
|
|
4374
4983
|
releaseViewState(screen, controls.values.toList(), preservingInputDrafts = true)
|
|
4375
4984
|
},
|
|
4376
|
-
endReached = events
|
|
4985
|
+
endReached = craftNativeEventName(events, "onEndReached").takeIf { it.isNotBlank() }?.let { handler ->
|
|
4377
4986
|
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject())) }
|
|
4378
4987
|
},
|
|
4379
4988
|
)
|
|
@@ -4393,20 +5002,21 @@ class MainActivity : AppCompatActivity() {
|
|
|
4393
5002
|
private fun addJustificationSpacers(layout: CraftNativeFlexLayout, value: String) { }
|
|
4394
5003
|
|
|
4395
5004
|
private fun applyViewStyle(view: View, style: JSONObject) {
|
|
4396
|
-
view.alpha = style
|
|
5005
|
+
view.alpha = craftNativeFiniteFloat(style, "opacity", 1f)
|
|
4397
5006
|
// A Text's padding (`px-4` on a line of text) is where its text
|
|
4398
5007
|
// starts, as on iOS; containers and fields lay out their own.
|
|
4399
5008
|
if (view is TextView && view !is EditText && view !is CraftNativeIconView) {
|
|
4400
5009
|
val keys = listOf("padding", "paddingHorizontal", "paddingVertical", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom")
|
|
4401
5010
|
if (keys.any(style::has)) {
|
|
4402
|
-
val
|
|
4403
|
-
val
|
|
4404
|
-
val
|
|
5011
|
+
val density = resources.displayMetrics.density
|
|
5012
|
+
val all = craftNativePadding(style, "padding", 0, density)
|
|
5013
|
+
val horizontal = craftNativePadding(style, "paddingHorizontal", all, density)
|
|
5014
|
+
val vertical = craftNativePadding(style, "paddingVertical", all, density)
|
|
4405
5015
|
view.setPadding(
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
5016
|
+
craftNativePadding(style, "paddingLeft", horizontal, density),
|
|
5017
|
+
craftNativePadding(style, "paddingTop", vertical, density),
|
|
5018
|
+
craftNativePadding(style, "paddingRight", horizontal, density),
|
|
5019
|
+
craftNativePadding(style, "paddingBottom", vertical, density),
|
|
4410
5020
|
)
|
|
4411
5021
|
} else if (view.javaClass == TextView::class.java) {
|
|
4412
5022
|
view.setPadding(0, 0, 0, 0)
|
|
@@ -4416,18 +5026,18 @@ class MainActivity : AppCompatActivity() {
|
|
|
4416
5026
|
if (view !is CraftNativeModalView) {
|
|
4417
5027
|
view.visibility = if (style.optString("display") == "none") View.GONE else View.VISIBLE
|
|
4418
5028
|
}
|
|
4419
|
-
view.elevation = dp(style
|
|
4420
|
-
view.z = style
|
|
5029
|
+
view.elevation = dp(craftNativeNonNegativeDouble(style, "elevation", 0.0)).toFloat()
|
|
5030
|
+
view.z = craftNativeFiniteFloat(style, "zIndex", 0f)
|
|
4421
5031
|
val background = style.has("backgroundColor") && !style.isNull("backgroundColor")
|
|
4422
|
-
val border = style
|
|
4423
|
-
val radius = style
|
|
5032
|
+
val border = craftNativeNonNegativeDouble(style, "borderWidth", 0.0)
|
|
5033
|
+
val radius = craftNativeNonNegativeDouble(style, "borderRadius", 0.0)
|
|
4424
5034
|
val sideBorderKeys = listOf("borderTopWidth", "borderRightWidth", "borderBottomWidth", "borderLeftWidth")
|
|
4425
5035
|
val hasSideBorders = (sideBorderKeys + listOf("borderTopColor", "borderRightColor", "borderBottomColor", "borderLeftColor")).any(style::has)
|
|
4426
|
-
val sideWidths = sideBorderKeys.map { dp(style
|
|
5036
|
+
val sideWidths = sideBorderKeys.map { dp(craftNativeNonNegativeDouble(style, it, border)) }.toIntArray()
|
|
4427
5037
|
val cornerRadiusKeys = listOf("borderTopLeftRadius", "borderTopRightRadius", "borderBottomRightRadius", "borderBottomLeftRadius")
|
|
4428
5038
|
val hasCornerRadii = cornerRadiusKeys.any(style::has)
|
|
4429
5039
|
val cornerRadii = cornerRadiusKeys.flatMap { value ->
|
|
4430
|
-
val radius = dp(style
|
|
5040
|
+
val radius = dp(craftNativeNonNegativeDouble(style, value, radius)).toFloat()
|
|
4431
5041
|
listOf(radius, radius)
|
|
4432
5042
|
}.toFloatArray()
|
|
4433
5043
|
val sideColors = listOf("borderTopColor", "borderRightColor", "borderBottomColor", "borderLeftColor")
|
|
@@ -4470,19 +5080,20 @@ class MainActivity : AppCompatActivity() {
|
|
|
4470
5080
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P &&
|
|
4471
5081
|
(style.has("shadowColor") || style.has("shadowOpacity") || style.has("shadowRadius"))
|
|
4472
5082
|
) {
|
|
4473
|
-
val shadowOpacity = style
|
|
5083
|
+
val shadowOpacity = craftNativeFiniteFloat(style, "shadowOpacity", 1f).coerceIn(0f, 1f)
|
|
4474
5084
|
val shadow = color(style.optString("shadowColor"), Color.BLACK)
|
|
4475
5085
|
val alphaShadow = Color.argb((Color.alpha(shadow) * shadowOpacity).roundToInt(), Color.red(shadow), Color.green(shadow), Color.blue(shadow))
|
|
4476
5086
|
view.outlineAmbientShadowColor = alphaShadow
|
|
4477
5087
|
view.outlineSpotShadowColor = alphaShadow
|
|
4478
|
-
|
|
5088
|
+
val shadowRadius = craftNativeNonNegativeDouble(style, "shadowRadius", 0.0)
|
|
5089
|
+
view.elevation = max(view.elevation, dp(shadowRadius * 2.0).toFloat())
|
|
4479
5090
|
val offset = style.optJSONObject("shadowOffset")
|
|
4480
|
-
val offsetX = dp(offset
|
|
4481
|
-
val offsetY = dp(offset
|
|
4482
|
-
val
|
|
5091
|
+
val offsetX = dp(craftNativeFiniteDouble(offset, "width", 0.0))
|
|
5092
|
+
val offsetY = dp(craftNativeFiniteDouble(offset, "height", 0.0))
|
|
5093
|
+
val radius = dp(shadowRadius).toFloat()
|
|
4483
5094
|
view.outlineProvider = object : ViewOutlineProvider() {
|
|
4484
5095
|
override fun getOutline(target: View, outline: Outline) {
|
|
4485
|
-
outline.setRoundRect(offsetX, offsetY, target.width + offsetX, target.height + offsetY,
|
|
5096
|
+
outline.setRoundRect(offsetX, offsetY, target.width + offsetX, target.height + offsetY, radius)
|
|
4486
5097
|
}
|
|
4487
5098
|
}
|
|
4488
5099
|
view.invalidateOutline()
|
|
@@ -4515,11 +5126,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
4515
5126
|
val transform = transforms.optJSONObject(index) ?: continue
|
|
4516
5127
|
val key = transform.keys().asSequence().firstOrNull() ?: continue
|
|
4517
5128
|
when (key) {
|
|
4518
|
-
"translateX" -> view.translationX += dp(transform
|
|
4519
|
-
"translateY" -> view.translationY += dp(transform
|
|
4520
|
-
"scale" -> transform
|
|
4521
|
-
"scaleX" -> view.scaleX *= transform
|
|
4522
|
-
"scaleY" -> view.scaleY *= transform
|
|
5129
|
+
"translateX" -> view.translationX += dp(craftNativeFiniteDouble(transform, key, 0.0)).toFloat()
|
|
5130
|
+
"translateY" -> view.translationY += dp(craftNativeFiniteDouble(transform, key, 0.0)).toFloat()
|
|
5131
|
+
"scale" -> craftNativeFiniteDouble(transform, key, 1.0).toFloat().let { view.scaleX *= it; view.scaleY *= it }
|
|
5132
|
+
"scaleX" -> view.scaleX *= craftNativeFiniteDouble(transform, key, 1.0).toFloat()
|
|
5133
|
+
"scaleY" -> view.scaleY *= craftNativeFiniteDouble(transform, key, 1.0).toFloat()
|
|
4523
5134
|
"rotate", "rotateZ" -> view.rotation += angleDegrees(transform.opt(key))
|
|
4524
5135
|
"rotateX" -> view.rotationX += angleDegrees(transform.opt(key))
|
|
4525
5136
|
"rotateY" -> view.rotationY += angleDegrees(transform.opt(key))
|
|
@@ -4531,15 +5142,16 @@ class MainActivity : AppCompatActivity() {
|
|
|
4531
5142
|
}
|
|
4532
5143
|
|
|
4533
5144
|
private fun skewFactor(value: Any?): Float =
|
|
4534
|
-
kotlin.math.tan(angleDegrees(value).toDouble() * PI / 180.0).toFloat()
|
|
5145
|
+
kotlin.math.tan(angleDegrees(value).toDouble() * PI / 180.0).toFloat().takeIf { it.isFinite() } ?: 0f
|
|
4535
5146
|
|
|
4536
5147
|
private fun angleDegrees(value: Any?): Float {
|
|
4537
5148
|
val text = value?.toString()?.trim() ?: return 0f
|
|
4538
|
-
|
|
5149
|
+
val result = when {
|
|
4539
5150
|
text.endsWith("rad") -> Math.toDegrees(text.dropLast(3).toDoubleOrNull() ?: 0.0).toFloat()
|
|
4540
5151
|
text.endsWith("deg") -> text.dropLast(3).toFloatOrNull() ?: 0f
|
|
4541
5152
|
else -> text.toFloatOrNull() ?: 0f
|
|
4542
5153
|
}
|
|
5154
|
+
return result.takeIf { it.isFinite() } ?: 0f
|
|
4543
5155
|
}
|
|
4544
5156
|
|
|
4545
5157
|
private fun ensureLayoutListener(screen: Screen, view: View) {
|
|
@@ -4562,15 +5174,22 @@ class MainActivity : AppCompatActivity() {
|
|
|
4562
5174
|
}
|
|
4563
5175
|
|
|
4564
5176
|
private fun configureIcon(view: CraftNativeIconView, props: JSONObject, style: JSONObject) {
|
|
4565
|
-
val symbol = props.
|
|
4566
|
-
|
|
5177
|
+
val symbol = craftNativeOptionalString(props.opt("symbol"))
|
|
5178
|
+
?: craftNativeOptionalString(props.opt("name")).orEmpty()
|
|
5179
|
+
val size = craftNativeFiniteFloat(style, "fontSize", 20f).coerceAtLeast(1f)
|
|
4567
5180
|
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, size)
|
|
4568
|
-
val foreground = style.
|
|
5181
|
+
val foreground = craftNativeOptionalString(style.opt("color"))
|
|
5182
|
+
?: craftNativeOptionalString(props.opt("color")).orEmpty()
|
|
4569
5183
|
val tint = color(foreground, defaultTextColor)
|
|
4570
5184
|
view.setTextColor(tint)
|
|
4571
5185
|
// The app's own vector for it (craft-icons.json, the icon the web
|
|
4572
5186
|
// shows), else the closest glyph Android's fonts have.
|
|
4573
|
-
val vector = CraftNativeIcons.drawable(
|
|
5187
|
+
val vector = CraftNativeIcons.drawable(
|
|
5188
|
+
this,
|
|
5189
|
+
craftNativeOptionalString(props.opt("name")).orEmpty(),
|
|
5190
|
+
craftNativeOptionalString(props.opt("symbol")).orEmpty(),
|
|
5191
|
+
tint,
|
|
5192
|
+
)
|
|
4574
5193
|
view.vector = vector
|
|
4575
5194
|
view.text = if (vector != null) "" else androidIconGlyph(symbol)
|
|
4576
5195
|
val weight = when (style.optString("fontWeight")) {
|
|
@@ -4621,7 +5240,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4621
5240
|
|
|
4622
5241
|
private fun configureTextStyle(view: TextView, style: JSONObject) {
|
|
4623
5242
|
view.setTextColor(color(style.optString("color"), defaultTextColor))
|
|
4624
|
-
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, style
|
|
5243
|
+
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, craftNativeFiniteFloat(style, "fontSize", 16f))
|
|
4625
5244
|
val weight = when (style.optString("fontWeight")) {
|
|
4626
5245
|
"bold" -> 700
|
|
4627
5246
|
"100", "200", "300", "400", "500", "600", "700", "800", "900" -> style.optInt("fontWeight")
|
|
@@ -4648,11 +5267,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
4648
5267
|
}
|
|
4649
5268
|
}
|
|
4650
5269
|
view.letterSpacing = style.optDouble("letterSpacing", Double.NaN)
|
|
4651
|
-
.
|
|
5270
|
+
.takeIf { it.isFinite() }
|
|
4652
5271
|
?.let { if (view.textSize > 0) dp(it).toFloat() / view.textSize else 0f }
|
|
4653
5272
|
?: 0f
|
|
4654
5273
|
view.setLineSpacing(
|
|
4655
|
-
style.optDouble("lineHeight", Double.NaN).
|
|
5274
|
+
style.optDouble("lineHeight", Double.NaN).takeIf { it.isFinite() }?.let { dp(it) - view.textSize } ?: 0f,
|
|
4656
5275
|
1f
|
|
4657
5276
|
)
|
|
4658
5277
|
val shadowColor = style.optString("textShadowColor").takeIf { it.isNotBlank() }
|
|
@@ -4661,9 +5280,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
4661
5280
|
} else {
|
|
4662
5281
|
val offset = style.optJSONObject("textShadowOffset")
|
|
4663
5282
|
view.setShadowLayer(
|
|
4664
|
-
dp(style
|
|
4665
|
-
dp(offset
|
|
4666
|
-
dp(offset
|
|
5283
|
+
dp(craftNativeFiniteDouble(style, "textShadowRadius", 0.0)).toFloat().coerceAtLeast(0f),
|
|
5284
|
+
dp(craftNativeFiniteDouble(offset, "width", 0.0)).toFloat(),
|
|
5285
|
+
dp(craftNativeFiniteDouble(offset, "height", 0.0)).toFloat(),
|
|
4667
5286
|
color(shadowColor, Color.TRANSPARENT),
|
|
4668
5287
|
)
|
|
4669
5288
|
}
|
|
@@ -4727,7 +5346,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4727
5346
|
|
|
4728
5347
|
private fun imageUri(source: Any?): String = when (source) {
|
|
4729
5348
|
is String -> source
|
|
4730
|
-
is JSONObject -> source.
|
|
5349
|
+
is JSONObject -> craftNativeOptionalString(source.opt("uri")).orEmpty()
|
|
4731
5350
|
else -> ""
|
|
4732
5351
|
}
|
|
4733
5352
|
|
|
@@ -4748,6 +5367,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4748
5367
|
imageSources[view] = ""
|
|
4749
5368
|
imageErrors[view] = "Image source is missing"
|
|
4750
5369
|
view.setImageDrawable(null)
|
|
5370
|
+
imageDidChange(view)
|
|
4751
5371
|
imageFailure(screen, view, uri, "Image source is missing")
|
|
4752
5372
|
return
|
|
4753
5373
|
}
|
|
@@ -4756,7 +5376,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
4756
5376
|
imageSources[view] = uri
|
|
4757
5377
|
imageErrors.remove(view)
|
|
4758
5378
|
view.setImageDrawable(null)
|
|
5379
|
+
imageDidChange(view)
|
|
4759
5380
|
immediateImage(imageUri(defaultSource))?.let(view::setImageBitmap)
|
|
5381
|
+
if (view.drawable != null) imageDidChange(view)
|
|
4760
5382
|
emitImageEvent(screen, view, screen.imageLoadStartHandlers[view])
|
|
4761
5383
|
|
|
4762
5384
|
if (uri.startsWith("data:image/") && uri.contains(',')) {
|
|
@@ -4764,6 +5386,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4764
5386
|
val bitmap = bytes?.let { BitmapFactory.decodeByteArray(it, 0, it.size) }
|
|
4765
5387
|
if (bitmap != null) {
|
|
4766
5388
|
view.setImageBitmap(bitmap)
|
|
5389
|
+
imageDidChange(view)
|
|
4767
5390
|
emitImageEvent(screen, view, screen.imageLoadHandlers[view])
|
|
4768
5391
|
emitImageEvent(screen, view, screen.imageLoadEndHandlers[view])
|
|
4769
5392
|
} else imageFailure(screen, view, uri, "Image data is invalid")
|
|
@@ -4773,6 +5396,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4773
5396
|
val bitmap = runCatching { assets.open(uri).use { BitmapFactory.decodeStream(it) } }.getOrNull()
|
|
4774
5397
|
if (bitmap != null) {
|
|
4775
5398
|
view.setImageBitmap(bitmap)
|
|
5399
|
+
imageDidChange(view)
|
|
4776
5400
|
emitImageEvent(screen, view, screen.imageLoadHandlers[view])
|
|
4777
5401
|
emitImageEvent(screen, view, screen.imageLoadEndHandlers[view])
|
|
4778
5402
|
} else imageFailure(screen, view, uri, "Bundled image was not found")
|
|
@@ -4795,6 +5419,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4795
5419
|
imageJobs.remove(view)
|
|
4796
5420
|
if (bitmap != null) {
|
|
4797
5421
|
view.setImageBitmap(bitmap)
|
|
5422
|
+
imageDidChange(view)
|
|
4798
5423
|
emitImageEvent(screen, view, screen.imageLoadHandlers[view])
|
|
4799
5424
|
emitImageEvent(screen, view, screen.imageLoadEndHandlers[view])
|
|
4800
5425
|
} else imageFailure(screen, view, uri, "Image download failed")
|
|
@@ -4805,6 +5430,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4805
5430
|
private fun imageFailure(screen: Screen, view: ImageView, uri: String, message: String) {
|
|
4806
5431
|
imageErrors[view] = message
|
|
4807
5432
|
view.setImageDrawable(null)
|
|
5433
|
+
imageDidChange(view)
|
|
4808
5434
|
val current = view.contentDescription?.toString().orEmpty()
|
|
4809
5435
|
if (!current.contains(message)) {
|
|
4810
5436
|
view.contentDescription = listOf(
|
|
@@ -4817,6 +5443,18 @@ class MainActivity : AppCompatActivity() {
|
|
|
4817
5443
|
Log.e(TAG, "$message: $uri")
|
|
4818
5444
|
}
|
|
4819
5445
|
|
|
5446
|
+
/**
|
|
5447
|
+
* An ImageView invalidates its own measure when its drawable changes, but
|
|
5448
|
+
* the native flex parent also memoizes intrinsic probes for the frame.
|
|
5449
|
+
* Clear that memo and walk through image-background wrappers so a source
|
|
5450
|
+
* swap can change an un-sized image's layout on the next pass.
|
|
5451
|
+
*/
|
|
5452
|
+
private fun imageDidChange(view: ImageView) {
|
|
5453
|
+
view.invalidate()
|
|
5454
|
+
view.requestLayout()
|
|
5455
|
+
invalidateMeasurementAncestors(view.parent)
|
|
5456
|
+
}
|
|
5457
|
+
|
|
4820
5458
|
private fun emitImageEvent(screen: Screen, view: ImageView, handler: String?, nativeEvent: JSONObject = JSONObject()) {
|
|
4821
5459
|
if (handler.isNullOrBlank()) return
|
|
4822
5460
|
send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", nativeEvent))
|
|
@@ -4837,17 +5475,17 @@ class MainActivity : AppCompatActivity() {
|
|
|
4837
5475
|
|
|
4838
5476
|
private fun applyAccessibility(view: View, props: JSONObject, type: String, fallbackLabel: String) {
|
|
4839
5477
|
view.tag = fallbackLabel
|
|
4840
|
-
val label = props.
|
|
4841
|
-
?: props.
|
|
4842
|
-
val value = props.opt("accessibilityValue")
|
|
5478
|
+
val label = craftNativeOptionalString(props.opt("accessibilityLabel"))
|
|
5479
|
+
?: craftNativeOptionalString(props.opt("alt"))?.takeIf { type == "Image" || type == "ImageBackground" }
|
|
5480
|
+
val value = craftNativeOptionalString(props.opt("accessibilityValue"))
|
|
4843
5481
|
view.contentDescription = when {
|
|
4844
5482
|
label != null && value != null -> "$label, $value"
|
|
4845
5483
|
label != null -> label
|
|
4846
5484
|
value != null -> value
|
|
4847
5485
|
else -> null
|
|
4848
5486
|
}
|
|
4849
|
-
view.tooltipText = props.
|
|
4850
|
-
val role = props.
|
|
5487
|
+
view.tooltipText = craftNativeOptionalString(props.opt("accessibilityHint"))
|
|
5488
|
+
val role = craftNativeOptionalString(props.opt("accessibilityRole")) ?: run {
|
|
4851
5489
|
if (type == "Icon") "image" else type.lowercase()
|
|
4852
5490
|
}
|
|
4853
5491
|
val state = props.optJSONObject("accessibilityState")
|
|
@@ -4885,7 +5523,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
4885
5523
|
)
|
|
4886
5524
|
}
|
|
4887
5525
|
|
|
4888
|
-
private fun dp(value: Double)
|
|
5526
|
+
private fun dp(value: Double): Int {
|
|
5527
|
+
val pixels = value * resources.displayMetrics.density
|
|
5528
|
+
return if (pixels.isFinite()) pixels.toInt() else 0
|
|
5529
|
+
}
|
|
4889
5530
|
private fun color(raw: String, fallback: Int) = runCatching { craftNativeColor(raw) }.getOrDefault(fallback)
|
|
4890
5531
|
|
|
4891
5532
|
private fun inputTypeFor(props: JSONObject): Int {
|
|
@@ -5089,6 +5730,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
5089
5730
|
super.onStop()
|
|
5090
5731
|
}
|
|
5091
5732
|
|
|
5733
|
+
override fun onLowMemory() {
|
|
5734
|
+
mapViews().forEach { it.lowMemory() }
|
|
5735
|
+
super.onLowMemory()
|
|
5736
|
+
}
|
|
5737
|
+
|
|
5092
5738
|
override fun onNewIntent(intent: Intent) {
|
|
5093
5739
|
super.onNewIntent(intent)
|
|
5094
5740
|
setIntent(intent)
|
|
@@ -5187,10 +5833,40 @@ class MainActivity : AppCompatActivity() {
|
|
|
5187
5833
|
screen.port.postMessage(Message.createStringMessage(message.toString()))
|
|
5188
5834
|
} else {
|
|
5189
5835
|
val script = "globalThis.__stxNativeCallback && globalThis.__stxNativeCallback(${JSONObject.quote(message.toString())})"
|
|
5190
|
-
screen.isolate.evaluateJavaScriptAsync(script)
|
|
5836
|
+
reportJavaScriptFailure(screen, screen.isolate.evaluateJavaScriptAsync(script))
|
|
5191
5837
|
}
|
|
5192
5838
|
}
|
|
5193
5839
|
|
|
5840
|
+
/**
|
|
5841
|
+
* An error JavaScript threw and nothing caught, to the crash reporter. A
|
|
5842
|
+
* sandbox has no global error event: an uncaught error surfaces only as
|
|
5843
|
+
* the evaluation's future failing, so every evaluation that runs a
|
|
5844
|
+
* screen's code reports through here.
|
|
5845
|
+
*/
|
|
5846
|
+
private fun reportJavaScriptFailure(screen: Screen, future: com.google.common.util.concurrent.ListenableFuture<String>) {
|
|
5847
|
+
if (!CraftCrashReporter.enabled) return
|
|
5848
|
+
future.addListener({
|
|
5849
|
+
try {
|
|
5850
|
+
future.get()
|
|
5851
|
+
} catch (error: Exception) {
|
|
5852
|
+
reportJavaScriptError(screen, error)
|
|
5853
|
+
}
|
|
5854
|
+
}, java.util.concurrent.Executor { command -> command.run() })
|
|
5855
|
+
}
|
|
5856
|
+
|
|
5857
|
+
private fun reportJavaScriptError(screen: Screen, error: Throwable) {
|
|
5858
|
+
val cause = (error as? java.util.concurrent.ExecutionException)?.cause ?: error
|
|
5859
|
+
if (cause is java.util.concurrent.CancellationException || cause is androidx.javascriptengine.IsolateTerminatedException) return
|
|
5860
|
+
val text = cause.message ?: cause.javaClass.simpleName
|
|
5861
|
+
// JavaScriptSandbox reports "Uncaught TypeError: message" and, when it
|
|
5862
|
+
// has one, the stack after a newline.
|
|
5863
|
+
val firstLine = text.lineSequence().firstOrNull()?.removePrefix("Uncaught ")?.trim() ?: text
|
|
5864
|
+
val name = Regex("^([A-Z][A-Za-z]*Error|Error):").find(firstLine)?.groupValues?.get(1)
|
|
5865
|
+
val message = if (name != null) firstLine.removePrefix("$name:").trim() else firstLine
|
|
5866
|
+
val stack = text.lineSequence().drop(1).joinToString("\n").ifBlank { null }
|
|
5867
|
+
CraftCrashReporter.captureJavaScript(name, message, stack, screen.route ?: "home", screen.headless)
|
|
5868
|
+
}
|
|
5869
|
+
|
|
5194
5870
|
/** Back to the first screen in one move. */
|
|
5195
5871
|
private fun popToRoot() {
|
|
5196
5872
|
while (screens.size > 1) closeScreen(screens.removeAt(screens.lastIndex))
|
|
@@ -5229,6 +5905,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
5229
5905
|
screen.flatListRows.values.forEach { rows -> rows.values.forEach { addAll(it.values) } }
|
|
5230
5906
|
}
|
|
5231
5907
|
|
|
5908
|
+
private fun mapViews(): Set<CraftNativeMapView> = allScreens()
|
|
5909
|
+
.flatMap { retainedViews(it) }
|
|
5910
|
+
.filterIsInstance<CraftNativeMapView>()
|
|
5911
|
+
.toSet()
|
|
5912
|
+
|
|
5232
5913
|
private fun releaseFlatList(
|
|
5233
5914
|
screen: Screen,
|
|
5234
5915
|
list: CraftNativeFlatList,
|
|
@@ -5260,6 +5941,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5260
5941
|
}
|
|
5261
5942
|
released.filterIsInstance<CraftNativeModalView>().forEach { it.resetForTeardown() }
|
|
5262
5943
|
released.filterIsInstance<CraftNativeVideoView>().forEach { it.stop() }
|
|
5944
|
+
released.filterIsInstance<CraftNativeMapView>().forEach { it.destroy() }
|
|
5263
5945
|
released.filterIsInstance<CraftNativeFlexLayout>().forEach { it.onLayoutChanged = null }
|
|
5264
5946
|
released.filterIsInstance<CraftNativeKeyboardAvoidingView>().forEach { it.content.onLayoutChanged = null }
|
|
5265
5947
|
released.filterIsInstance<CraftNativeScrollView>().forEach { it.setRefreshHandler(null, false) }
|