@craft-native/android 0.0.174 → 0.0.176
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 +998 -303
- package/templates/build.gradle.kts.app.template +4 -2
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
package {{PACKAGE_NAME}}
|
|
2
2
|
|
|
3
3
|
import android.animation.ValueAnimator
|
|
4
|
+
import android.Manifest
|
|
4
5
|
import android.content.ClipData
|
|
5
6
|
import android.content.ClipboardManager
|
|
6
7
|
import android.content.Context
|
|
8
|
+
import android.content.pm.PackageManager
|
|
7
9
|
import android.content.res.ColorStateList
|
|
8
10
|
import android.content.res.Configuration
|
|
9
11
|
import android.database.ContentObserver
|
|
10
12
|
import android.content.Intent
|
|
11
13
|
import android.media.AudioAttributes
|
|
12
14
|
import android.media.AudioFormat
|
|
15
|
+
import android.media.MediaPlayer
|
|
13
16
|
import android.media.AudioTrack
|
|
17
|
+
import android.graphics.Bitmap
|
|
14
18
|
import android.graphics.BitmapFactory
|
|
15
19
|
import android.graphics.Canvas
|
|
16
20
|
import android.graphics.Color
|
|
@@ -52,6 +56,7 @@ import android.text.Layout
|
|
|
52
56
|
import android.view.View
|
|
53
57
|
import android.view.ViewGroup
|
|
54
58
|
import android.view.ViewOutlineProvider
|
|
59
|
+
import android.view.ViewParent
|
|
55
60
|
import android.view.ViewTreeObserver
|
|
56
61
|
import android.widget.AdapterView
|
|
57
62
|
import android.widget.ArrayAdapter
|
|
@@ -78,6 +83,7 @@ import androidx.core.content.ContextCompat
|
|
|
78
83
|
import androidx.core.view.AccessibilityDelegateCompat
|
|
79
84
|
import androidx.core.view.WindowInsetsCompat
|
|
80
85
|
import androidx.core.view.ViewCompat
|
|
86
|
+
import androidx.core.view.WindowCompat
|
|
81
87
|
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
|
|
82
88
|
import androidx.javascriptengine.JavaScriptIsolate
|
|
83
89
|
import androidx.javascriptengine.JavaScriptConsoleCallback
|
|
@@ -111,26 +117,63 @@ import kotlin.math.max
|
|
|
111
117
|
import kotlin.math.min
|
|
112
118
|
import kotlin.math.PI
|
|
113
119
|
import kotlin.math.ceil
|
|
120
|
+
import kotlin.math.abs
|
|
121
|
+
import kotlin.math.exp
|
|
114
122
|
import kotlin.math.roundToInt
|
|
115
123
|
import kotlin.math.roundToLong
|
|
116
124
|
import kotlin.math.sin
|
|
125
|
+
import kotlin.math.pow
|
|
117
126
|
|
|
118
127
|
private const val CRAFT_NATIVE_CAPABILITY_TIMEOUT_MS = 30_000L
|
|
119
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
|
+
|
|
120
160
|
private data class CraftNativeLength(
|
|
121
161
|
val points: Int? = null,
|
|
122
162
|
val fraction: Double? = null,
|
|
123
163
|
) {
|
|
124
|
-
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()) } }
|
|
125
165
|
|
|
126
166
|
companion object {
|
|
127
167
|
fun from(value: Any?, density: Float): CraftNativeLength? {
|
|
128
168
|
if (value == null || value == JSONObject.NULL) return null
|
|
129
|
-
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
|
+
}
|
|
130
173
|
val text = value.toString().trim()
|
|
131
|
-
if (text.endsWith('%')) return text.dropLast(1).toDoubleOrNull()?.let { CraftNativeLength(fraction = it / 100) }
|
|
132
|
-
if (text.endsWith("px")) return text.dropLast(2).toDoubleOrNull()?.let { CraftNativeLength(points = (it * density).roundToInt()) }
|
|
133
|
-
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()) }
|
|
134
177
|
}
|
|
135
178
|
}
|
|
136
179
|
}
|
|
@@ -158,11 +201,13 @@ private data class CraftNativeLayoutStyle(
|
|
|
158
201
|
fun length(name: String): CraftNativeLength? = CraftNativeLength.from(raw.opt(name), density)
|
|
159
202
|
fun number(name: String): Int? {
|
|
160
203
|
if (!raw.has(name) || raw.isNull(name)) return null
|
|
161
|
-
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
|
+
}
|
|
162
207
|
}
|
|
163
208
|
fun decimal(name: String): Double? {
|
|
164
209
|
if (!raw.has(name) || raw.isNull(name)) return null
|
|
165
|
-
return raw.optDouble(name, Double.NaN).
|
|
210
|
+
return raw.optDouble(name, Double.NaN).takeIf { it.isFinite() }
|
|
166
211
|
}
|
|
167
212
|
val flex = decimal("flex")
|
|
168
213
|
return CraftNativeLayoutStyle(
|
|
@@ -275,6 +320,10 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
275
320
|
val color: String = "#000000",
|
|
276
321
|
val opacity: Float = 1f,
|
|
277
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,
|
|
278
327
|
) {
|
|
279
328
|
fun merge(node: JSONObject): SvgPaint {
|
|
280
329
|
val props = node.optJSONObject("props") ?: JSONObject()
|
|
@@ -284,17 +333,32 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
284
333
|
props.has(name) && !props.isNull(name) -> props.optString(name)
|
|
285
334
|
else -> null
|
|
286
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
|
+
}
|
|
287
347
|
// Keep the cascade explicit: computed style wins over a raw prop
|
|
288
348
|
// when both provide the same SVG paint value.
|
|
289
349
|
val weight = value("fontWeight")
|
|
290
|
-
val ownOpacity = value("opacity")?.toFloatOrNull() ?: 1f
|
|
350
|
+
val ownOpacity = value("opacity")?.toFloatOrNull()?.takeIf { it.isFinite() } ?: 1f
|
|
291
351
|
return copy(
|
|
292
352
|
fill = value("fill") ?: fill,
|
|
293
353
|
stroke = value("stroke") ?: stroke,
|
|
294
|
-
strokeWidth = value("strokeWidth")?.toFloatOrNull() ?: strokeWidth,
|
|
354
|
+
strokeWidth = value("strokeWidth")?.toFloatOrNull()?.takeIf { it.isFinite() } ?: strokeWidth,
|
|
295
355
|
color = value("color") ?: color,
|
|
296
356
|
opacity = ownOpacity * opacity,
|
|
297
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,
|
|
298
362
|
)
|
|
299
363
|
}
|
|
300
364
|
}
|
|
@@ -352,7 +416,7 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
352
416
|
val y = values.optDouble(1, Double.NaN).toFloat()
|
|
353
417
|
val width = values.optDouble(2, Double.NaN).toFloat()
|
|
354
418
|
val height = values.optDouble(3, Double.NaN).toFloat()
|
|
355
|
-
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
|
|
356
420
|
return Quadruple(width, height, x, y)
|
|
357
421
|
}
|
|
358
422
|
|
|
@@ -373,6 +437,7 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
373
437
|
private fun drawPath(canvas: Canvas, node: JSONObject, style: SvgPaint) {
|
|
374
438
|
val props = node.optJSONObject("props") ?: JSONObject()
|
|
375
439
|
val geometry = props.optJSONArray("geometry") ?: return
|
|
440
|
+
if ((0 until geometry.length()).any { !geometry.optDouble(it, Double.NaN).toFloat().isFinite() }) return
|
|
376
441
|
val path = Path()
|
|
377
442
|
var index = 0
|
|
378
443
|
while (index < geometry.length()) {
|
|
@@ -399,34 +464,29 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
399
464
|
alpha = (style.opacity * 255f).roundToInt().coerceIn(0, 255)
|
|
400
465
|
this.style = Paint.Style.STROKE
|
|
401
466
|
strokeWidth = style.strokeWidth
|
|
402
|
-
strokeCap = when (
|
|
467
|
+
strokeCap = when (style.strokeLinecap) {
|
|
403
468
|
"round" -> Paint.Cap.ROUND
|
|
404
469
|
"square" -> Paint.Cap.SQUARE
|
|
405
470
|
else -> Paint.Cap.BUTT
|
|
406
471
|
}
|
|
407
|
-
strokeJoin = when (
|
|
472
|
+
strokeJoin = when (style.strokeLinejoin) {
|
|
408
473
|
"round" -> Paint.Join.ROUND
|
|
409
474
|
"bevel" -> Paint.Join.BEVEL
|
|
410
475
|
else -> Paint.Join.MITER
|
|
411
476
|
}
|
|
412
|
-
|
|
413
|
-
val
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
val pattern: FloatArray = if (dashPattern.size % 2 == 1) {
|
|
419
|
-
FloatArray(dashPattern.size * 2) { index -> dashPattern[index % dashPattern.size] }
|
|
420
|
-
} else {
|
|
421
|
-
dashPattern
|
|
422
|
-
}
|
|
423
|
-
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
|
|
424
483
|
}
|
|
484
|
+
pathEffect = DashPathEffect(pattern, 0f)
|
|
425
485
|
}
|
|
426
486
|
}
|
|
427
487
|
// `vector-effect="non-scaling-stroke"`: the stroke is in the view's
|
|
428
488
|
// pixels, so a stretched chart's line keeps its width.
|
|
429
|
-
if (
|
|
489
|
+
if (style.vectorEffect == "non-scaling-stroke") {
|
|
430
490
|
val line = Path(path).apply { transform(viewTransform) }
|
|
431
491
|
canvas.save()
|
|
432
492
|
canvas.concat(inverseTransform)
|
|
@@ -447,15 +507,17 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
447
507
|
}
|
|
448
508
|
if (text.isEmpty()) return
|
|
449
509
|
val size = (if (props.has("fontSize")) props.optDouble("fontSize", 12.0) else nodeStyle.optDouble("fontSize", 12.0))
|
|
450
|
-
.
|
|
510
|
+
.takeIf { it.isFinite() }
|
|
511
|
+
?.toFloat()
|
|
512
|
+
?: 12f
|
|
451
513
|
val paint = android.graphics.Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
452
514
|
color = resolveColor(style.fill, style.color, Color.BLACK)
|
|
453
515
|
alpha = (style.opacity * 255f).roundToInt().coerceIn(0, 255)
|
|
454
516
|
textSize = size
|
|
455
517
|
typeface = style.typeface
|
|
456
518
|
}
|
|
457
|
-
var x = props.optDouble("x", 0.0).toFloat()
|
|
458
|
-
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
|
|
459
521
|
when (props.optString("textAnchor")) {
|
|
460
522
|
"middle" -> x -= paint.measureText(text) / 2f
|
|
461
523
|
"end" -> x -= paint.measureText(text)
|
|
@@ -480,6 +542,8 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
480
542
|
private val polylines = mutableMapOf<String, Polyline>()
|
|
481
543
|
private val markerHandlers = mutableMapOf<String, String>()
|
|
482
544
|
private var onMarkerTap: ((String) -> Unit)? = null
|
|
545
|
+
private var lifecycleStarted = false
|
|
546
|
+
private var destroyed = false
|
|
483
547
|
|
|
484
548
|
init {
|
|
485
549
|
addView(mapView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
|
|
@@ -488,11 +552,15 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
488
552
|
}
|
|
489
553
|
|
|
490
554
|
override fun onMapReady(map: GoogleMap) {
|
|
555
|
+
if (destroyed) return
|
|
491
556
|
googleMap = map
|
|
492
557
|
map.setOnMarkerClickListener { marker ->
|
|
493
558
|
val id = markers.entries.firstOrNull { it.value == marker }?.key ?: return@setOnMarkerClickListener false
|
|
494
559
|
onMarkerTap?.invoke(id)
|
|
495
|
-
|
|
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
|
|
496
564
|
}
|
|
497
565
|
applyPending()
|
|
498
566
|
}
|
|
@@ -511,32 +579,49 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
511
579
|
map.uiSettings.isScrollGesturesEnabled = pendingProps.optBoolean("scrollEnabled", true)
|
|
512
580
|
map.uiSettings.isZoomGesturesEnabled = pendingProps.optBoolean("zoomEnabled", true)
|
|
513
581
|
map.uiSettings.isRotateGesturesEnabled = pendingProps.optBoolean("rotateEnabled", false)
|
|
514
|
-
map.mapType = when (pendingProps.
|
|
582
|
+
map.mapType = when (craftNativeOptionalString(pendingProps.opt("mapType")).orEmpty().lowercase()) {
|
|
515
583
|
"satellite" -> GoogleMap.MAP_TYPE_SATELLITE
|
|
516
584
|
"hybrid" -> GoogleMap.MAP_TYPE_HYBRID
|
|
517
585
|
"terrain" -> GoogleMap.MAP_TYPE_TERRAIN
|
|
518
586
|
else -> GoogleMap.MAP_TYPE_NORMAL
|
|
519
587
|
}
|
|
588
|
+
val showUserLocation = pendingProps.optBoolean("showsUserLocation", false) && hasLocationPermission()
|
|
589
|
+
runCatching { map.isMyLocationEnabled = showUserLocation }
|
|
520
590
|
val nextMarkers = mutableMapOf<String, Pair<LatLng, String?>>()
|
|
521
591
|
val nextHandlers = mutableMapOf<String, String>()
|
|
522
|
-
val nextPolylines = mutableMapOf<String,
|
|
592
|
+
val nextPolylines = mutableMapOf<String, Triple<List<LatLng>, Int, Float>>()
|
|
523
593
|
for (index in 0 until pendingChildren.length()) {
|
|
524
594
|
val child = pendingChildren.optJSONObject(index) ?: continue
|
|
525
595
|
val type = child.optString("type").lowercase()
|
|
526
596
|
val props = child.optJSONObject("props") ?: JSONObject()
|
|
527
597
|
val events = child.optJSONObject("events") ?: JSONObject()
|
|
528
|
-
val id = props.opt("id")
|
|
529
|
-
?: child.
|
|
598
|
+
val id = craftNativeOptionalString(props.opt("id"))
|
|
599
|
+
?: craftNativeOptionalString(child.opt("id"))
|
|
530
600
|
?: "overlay-$index"
|
|
531
601
|
val points = coordinates(props.optJSONArray("geometry"))
|
|
532
602
|
if (type == "mapmarker" || type == "marker") {
|
|
533
603
|
points.firstOrNull()?.let { point ->
|
|
534
|
-
nextMarkers[id] = point to props.
|
|
535
|
-
events.
|
|
536
|
-
.
|
|
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 }
|
|
537
608
|
}
|
|
538
609
|
} else if (type == "mappolyline" || type == "polyline") {
|
|
539
|
-
if (points.size > 1)
|
|
610
|
+
if (points.size > 1) {
|
|
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)
|
|
624
|
+
}
|
|
540
625
|
}
|
|
541
626
|
}
|
|
542
627
|
markers.keys.filter { nextMarkers[it] == null }.forEach { markers.remove(it)?.remove() }
|
|
@@ -551,10 +636,18 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
551
636
|
}
|
|
552
637
|
markerHandlers.clear()
|
|
553
638
|
markerHandlers.putAll(nextHandlers)
|
|
554
|
-
polylines.
|
|
555
|
-
|
|
639
|
+
polylines.keys.filter { nextPolylines[it] == null }.forEach { id ->
|
|
640
|
+
polylines.remove(id)?.remove()
|
|
641
|
+
}
|
|
556
642
|
for ((id, value) in nextPolylines) {
|
|
557
|
-
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
|
+
}
|
|
558
651
|
}
|
|
559
652
|
if (pendingProps.optBoolean("fitToBounds", false)) fitToBounds()
|
|
560
653
|
region(pendingProps.optJSONObject("region") ?: pendingProps.optJSONObject("initialRegion"))?.let { bounds ->
|
|
@@ -578,20 +671,56 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
578
671
|
|
|
579
672
|
override fun onAttachedToWindow() {
|
|
580
673
|
super.onAttachedToWindow()
|
|
674
|
+
if (destroyed) return
|
|
675
|
+
mapView.onStart()
|
|
581
676
|
mapView.onResume()
|
|
677
|
+
lifecycleStarted = true
|
|
582
678
|
}
|
|
583
679
|
|
|
584
680
|
override fun onDetachedFromWindow() {
|
|
585
|
-
|
|
681
|
+
if (lifecycleStarted) {
|
|
682
|
+
mapView.onPause()
|
|
683
|
+
mapView.onStop()
|
|
684
|
+
lifecycleStarted = false
|
|
685
|
+
}
|
|
586
686
|
super.onDetachedFromWindow()
|
|
587
687
|
}
|
|
588
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
|
+
|
|
711
|
+
private fun hasLocationPermission(): Boolean =
|
|
712
|
+
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
|
|
713
|
+
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
|
|
714
|
+
|
|
589
715
|
private fun coordinates(geometry: JSONArray?): List<LatLng> {
|
|
590
716
|
if (geometry == null) return emptyList()
|
|
591
717
|
return (0 until geometry.length() - 1 step 2).mapNotNull { index ->
|
|
592
718
|
val latitude = geometry.optDouble(index, Double.NaN)
|
|
593
719
|
val longitude = geometry.optDouble(index + 1, Double.NaN)
|
|
594
|
-
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
|
|
595
724
|
}
|
|
596
725
|
}
|
|
597
726
|
|
|
@@ -602,7 +731,12 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
602
731
|
val longitude = center.optDouble("longitude", Double.NaN)
|
|
603
732
|
val latitudeDelta = span.optDouble("latitudeDelta", span.optDouble("latitude_delta", Double.NaN))
|
|
604
733
|
val longitudeDelta = span.optDouble("longitudeDelta", span.optDouble("longitude_delta", Double.NaN))
|
|
605
|
-
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
|
|
606
740
|
return LatLngBounds(LatLng(latitude - latitudeDelta / 2, longitude - longitudeDelta / 2), LatLng(latitude + latitudeDelta / 2, longitude + longitudeDelta / 2))
|
|
607
741
|
}
|
|
608
742
|
|
|
@@ -648,6 +782,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
648
782
|
(0 until raw.length()).mapNotNull { raw.optString(it).takeIf { edge -> edge in setOf("top", "right", "bottom", "left") } }.toSet()
|
|
649
783
|
}
|
|
650
784
|
applySafeAreaPadding()
|
|
785
|
+
invalidateMeasurements()
|
|
651
786
|
}
|
|
652
787
|
|
|
653
788
|
fun setWindowSafeInsets(left: Int, top: Int, right: Int, bottom: Int) {
|
|
@@ -659,6 +794,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
659
794
|
if ("bottom" in safeAreaEdges) insets[3] else 0,
|
|
660
795
|
)
|
|
661
796
|
applySafeAreaPadding()
|
|
797
|
+
invalidateMeasurements()
|
|
662
798
|
}
|
|
663
799
|
|
|
664
800
|
private fun applySafeAreaPadding() {
|
|
@@ -707,6 +843,31 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
707
843
|
private fun probedWidth(size: Long): Int = (size shr 32).toInt()
|
|
708
844
|
private fun probedHeight(size: Long): Int = size.toInt()
|
|
709
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
|
+
|
|
710
871
|
/**
|
|
711
872
|
* Lays a child out at its final frame, measuring it again at exactly
|
|
712
873
|
* that size first when the measure pass left it another size (a
|
|
@@ -744,34 +905,50 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
744
905
|
alignItems = style.optString("alignItems", "stretch")
|
|
745
906
|
alignContent = style.optString("alignContent", "flex-start")
|
|
746
907
|
justifyContent = style.optString("justifyContent", "flex-start")
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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)
|
|
750
917
|
grid = style.optString("display") == "grid"
|
|
751
918
|
gridColumns = craftNativeGridColumnCount(
|
|
752
919
|
if (style.has("gridTemplateColumns")) style.opt("gridTemplateColumns") else style.opt("gridColumns")
|
|
753
920
|
)
|
|
754
921
|
gridAutoRows = CraftNativeLength.from(style.opt("gridAutoRows"), density)
|
|
755
|
-
val
|
|
756
|
-
val horizontal = (style
|
|
757
|
-
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)
|
|
758
925
|
setPadding(
|
|
759
|
-
(style
|
|
760
|
-
(style
|
|
761
|
-
(style
|
|
762
|
-
(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),
|
|
763
930
|
)
|
|
764
|
-
|
|
931
|
+
invalidateMeasurements()
|
|
765
932
|
}
|
|
766
933
|
|
|
767
934
|
fun setChildStyle(view: View, style: JSONObject, density: Float) {
|
|
768
935
|
styles[view] = CraftNativeLayoutStyle.from(style, density)
|
|
769
|
-
|
|
936
|
+
invalidateMeasurements()
|
|
937
|
+
invalidateMeasurementAncestors(parent)
|
|
770
938
|
}
|
|
771
939
|
|
|
772
940
|
override fun onViewRemoved(child: View) {
|
|
941
|
+
val parentBeforeRemoval = parent
|
|
773
942
|
styles.remove(child)
|
|
774
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)
|
|
775
952
|
}
|
|
776
953
|
|
|
777
954
|
private data class Entry(val view: View, val style: CraftNativeLayoutStyle, val width: Int, val height: Int)
|
|
@@ -1025,6 +1202,116 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1025
1202
|
else -> 0.0
|
|
1026
1203
|
}
|
|
1027
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
|
+
|
|
1028
1315
|
private fun justifyCode(): Int = when (justifyContent) {
|
|
1029
1316
|
"center" -> 1
|
|
1030
1317
|
"flex-end" -> 2
|
|
@@ -1152,11 +1439,11 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1152
1439
|
contentWidth.toFloat(), contentHeight.toFloat(), gridColumns, columnGap, rowGap,
|
|
1153
1440
|
flow.map { it.width.toFloat() }.toFloatArray(), flow.map { it.height.toFloat() }.toFloatArray(),
|
|
1154
1441
|
flow.map { it.style.width?.resolve(layout.cellWidth.roundToInt())?.toFloat() ?: Float.NaN }.toFloatArray(),
|
|
1155
|
-
flow.map { it.style.height?.resolve(
|
|
1442
|
+
flow.map { it.style.height?.resolve(contentHeight)?.toFloat() ?: Float.NaN }.toFloatArray(),
|
|
1156
1443
|
flow.map { it.style.minWidth?.resolve(layout.cellWidth.roundToInt())?.toFloat() ?: Float.NaN }.toFloatArray(),
|
|
1157
1444
|
flow.map { it.style.maxWidth?.resolve(layout.cellWidth.roundToInt())?.toFloat() ?: Float.NaN }.toFloatArray(),
|
|
1158
|
-
flow.map { it.style.minHeight?.resolve(
|
|
1159
|
-
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(),
|
|
1160
1447
|
flow.flatMap {
|
|
1161
1448
|
val lp = it.view.layoutParams as? MarginLayoutParams
|
|
1162
1449
|
listOf((lp?.leftMargin ?: 0).toFloat(), (lp?.rightMargin ?: 0).toFloat(), (lp?.topMargin ?: 0).toFloat(), (lp?.bottomMargin ?: 0).toFloat())
|
|
@@ -1242,6 +1529,22 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1242
1529
|
val contentWidth = width - paddingLeft - paddingRight
|
|
1243
1530
|
val contentHeight = height - paddingTop - paddingBottom
|
|
1244
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
|
+
}
|
|
1245
1548
|
val gap = if (orientation == HORIZONTAL) columnGap else rowGap
|
|
1246
1549
|
val crossGap = if (orientation == HORIZONTAL) rowGap else columnGap
|
|
1247
1550
|
val availableMain = if (orientation == HORIZONTAL) contentWidth else contentHeight
|
|
@@ -1436,7 +1739,9 @@ private class CraftNativeKeyboardAvoidingView(context: Context) : FrameLayout(co
|
|
|
1436
1739
|
|
|
1437
1740
|
fun configure(props: JSONObject, style: JSONObject, density: Float) {
|
|
1438
1741
|
behavior = props.optString("behavior", "padding").takeIf { it in setOf("height", "position", "padding") } ?: "padding"
|
|
1439
|
-
|
|
1742
|
+
val authoredOffset = craftNativeFiniteDouble(props, "keyboardVerticalOffset", 0.0)
|
|
1743
|
+
val pixelOffset = (authoredOffset * density.toDouble()).takeIf { it.isFinite() } ?: 0.0
|
|
1744
|
+
keyboardVerticalOffset = pixelOffset.roundToInt()
|
|
1440
1745
|
content.configure(style, density)
|
|
1441
1746
|
basePadding = intArrayOf(content.paddingLeft, content.paddingTop, content.paddingRight, content.paddingBottom)
|
|
1442
1747
|
applyKeyboardInset()
|
|
@@ -1538,10 +1843,10 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
1538
1843
|
}
|
|
1539
1844
|
|
|
1540
1845
|
fun setRefreshAppearance(props: JSONObject, density: Float) {
|
|
1541
|
-
val tint = props.
|
|
1846
|
+
val tint = craftNativeOptionalString(props.opt("tintColor"))
|
|
1542
1847
|
if (tint != null) setColorSchemeColors(craftNativeColor(tint))
|
|
1543
1848
|
else setColorSchemeColors(Color.WHITE)
|
|
1544
|
-
val background = props.
|
|
1849
|
+
val background = craftNativeOptionalString(props.opt("progressBackgroundColor"))
|
|
1545
1850
|
if (background != null) setProgressBackgroundColorSchemeColor(craftNativeColor(background))
|
|
1546
1851
|
else setProgressBackgroundColorSchemeColor(Color.TRANSPARENT)
|
|
1547
1852
|
val offset = props.optDouble("progressViewOffset", Double.NaN)
|
|
@@ -1902,6 +2207,7 @@ private class CraftNativeDatePickerView(context: Context) : FrameLayout(context)
|
|
|
1902
2207
|
private var pickerMode: PickerMode? = null
|
|
1903
2208
|
private var datePicker: DatePicker? = null
|
|
1904
2209
|
private var timePicker: TimePicker? = null
|
|
2210
|
+
private var defaultDateBounds: Pair<Long, Long>? = null
|
|
1905
2211
|
private var minuteInterval = 1
|
|
1906
2212
|
private var countdownSecond = 0
|
|
1907
2213
|
private var suppressChanges = true
|
|
@@ -1929,6 +2235,7 @@ private class CraftNativeDatePickerView(context: Context) : FrameLayout(context)
|
|
|
1929
2235
|
}
|
|
1930
2236
|
if (requestedMode == PickerMode.DATE || requestedMode == PickerMode.DATE_TIME) {
|
|
1931
2237
|
datePicker = DatePicker(context).also { picker ->
|
|
2238
|
+
defaultDateBounds = picker.minDate to picker.maxDate
|
|
1932
2239
|
val today = Calendar.getInstance()
|
|
1933
2240
|
picker.init(today.get(Calendar.YEAR), today.get(Calendar.MONTH), today.get(Calendar.DAY_OF_MONTH)) { _, _, _, _ -> changed() }
|
|
1934
2241
|
content.addView(picker, LinearLayout.LayoutParams(
|
|
@@ -1952,8 +2259,9 @@ private class CraftNativeDatePickerView(context: Context) : FrameLayout(context)
|
|
|
1952
2259
|
|
|
1953
2260
|
suppressChanges = true
|
|
1954
2261
|
minuteInterval = props.optInt("minuteInterval", 1).takeIf { it in 1..30 && 60 % it == 0 } ?: 1
|
|
1955
|
-
|
|
1956
|
-
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
|
|
1957
2265
|
if (requestedMode == PickerMode.COUNTDOWN) {
|
|
1958
2266
|
countdownSecond = 0
|
|
1959
2267
|
countdownSeconds(props.opt("value"))?.let { applyCountdown(it) }
|
|
@@ -2038,7 +2346,11 @@ private class CraftNativeDatePickerView(context: Context) : FrameLayout(context)
|
|
|
2038
2346
|
}
|
|
2039
2347
|
|
|
2040
2348
|
private fun pickerDate(value: Any?): Calendar? {
|
|
2041
|
-
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
|
+
}
|
|
2042
2354
|
val text = value?.toString()?.trim().orEmpty()
|
|
2043
2355
|
if (text.isBlank() || text == JSONObject.NULL.toString()) return null
|
|
2044
2356
|
val formats = listOf(
|
|
@@ -2056,7 +2368,11 @@ private class CraftNativeDatePickerView(context: Context) : FrameLayout(context)
|
|
|
2056
2368
|
}
|
|
2057
2369
|
|
|
2058
2370
|
private fun countdownSeconds(value: Any?): Int? {
|
|
2059
|
-
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
|
+
}
|
|
2060
2376
|
val text = value?.toString()?.trim().orEmpty()
|
|
2061
2377
|
if (text.isBlank() || text == JSONObject.NULL.toString()) return null
|
|
2062
2378
|
text.toDoubleOrNull()?.takeIf { it.isFinite() }?.let { return it.coerceIn(0.0, maxCountdownSeconds.toDouble()).roundToInt() }
|
|
@@ -2065,7 +2381,8 @@ private class CraftNativeDatePickerView(context: Context) : FrameLayout(context)
|
|
|
2065
2381
|
val hours = parts[0].toDoubleOrNull() ?: return null
|
|
2066
2382
|
val minutes = parts[1].toDoubleOrNull() ?: return null
|
|
2067
2383
|
val seconds = if (parts.size == 3) parts[2].toDoubleOrNull() ?: return null else 0.0
|
|
2068
|
-
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
|
|
2069
2386
|
return (hours * 3600 + minutes * 60 + seconds).coerceIn(0.0, maxCountdownSeconds.toDouble()).roundToInt()
|
|
2070
2387
|
}
|
|
2071
2388
|
}
|
|
@@ -2075,26 +2392,33 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
|
|
|
2075
2392
|
private var shouldLoop = false
|
|
2076
2393
|
private var shouldPlay = false
|
|
2077
2394
|
private var muted = false
|
|
2395
|
+
private var resizeMode = "contain"
|
|
2396
|
+
private var preparedPlayer: MediaPlayer? = null
|
|
2078
2397
|
|
|
2079
|
-
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?) {
|
|
2080
2399
|
val uri = when (source) {
|
|
2081
2400
|
is String -> source
|
|
2082
|
-
is JSONObject -> source.
|
|
2401
|
+
is JSONObject -> craftNativeOptionalString(source.opt("uri")).orEmpty()
|
|
2083
2402
|
else -> ""
|
|
2084
2403
|
}
|
|
2085
2404
|
shouldLoop = loop
|
|
2086
2405
|
shouldPlay = autoplay && !paused
|
|
2087
2406
|
this.muted = muted
|
|
2407
|
+
this.resizeMode = resizeMode?.lowercase()?.takeIf { it in setOf("cover", "contain", "stretch", "center") } ?: "contain"
|
|
2408
|
+
applyScalingMode()
|
|
2088
2409
|
setMediaController(if (controls) MediaController(context).also { it.setAnchorView(this) } else null)
|
|
2089
2410
|
if (uri != sourceUri) {
|
|
2090
2411
|
sourceUri = uri
|
|
2091
2412
|
stopPlayback()
|
|
2413
|
+
preparedPlayer = null
|
|
2092
2414
|
if (uri.isBlank()) return
|
|
2093
2415
|
// A copy kept on the phone (craft.media.keep) plays in place of
|
|
2094
2416
|
// the address: offline in a gym with no signal.
|
|
2095
2417
|
val kept = CraftNativeMedia.local(context, uri)
|
|
2096
2418
|
setVideoURI(if (kept != null) Uri.fromFile(kept) else Uri.parse(uri))
|
|
2097
2419
|
setOnPreparedListener { mediaPlayer ->
|
|
2420
|
+
preparedPlayer = mediaPlayer
|
|
2421
|
+
applyScalingMode()
|
|
2098
2422
|
mediaPlayer.isLooping = shouldLoop
|
|
2099
2423
|
val volume = if (this.muted) 0f else 1f
|
|
2100
2424
|
mediaPlayer.setVolume(volume, volume)
|
|
@@ -2112,12 +2436,27 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
|
|
|
2112
2436
|
fun stop() {
|
|
2113
2437
|
stopPlayback()
|
|
2114
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
|
+
)
|
|
2115
2447
|
}
|
|
2116
2448
|
}
|
|
2117
2449
|
|
|
2118
|
-
/**
|
|
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
|
+
*/
|
|
2119
2458
|
private class CraftNativeAudio {
|
|
2120
|
-
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)
|
|
2121
2460
|
|
|
2122
2461
|
private class Parameter(initial: Double) {
|
|
2123
2462
|
private var current = initial
|
|
@@ -2128,8 +2467,8 @@ private class CraftNativeAudio {
|
|
|
2128
2467
|
events.clear()
|
|
2129
2468
|
}
|
|
2130
2469
|
|
|
2131
|
-
@Synchronized fun schedule(value: Double, time: Double,
|
|
2132
|
-
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)
|
|
2133
2472
|
events.sortBy { it.time }
|
|
2134
2473
|
}
|
|
2135
2474
|
|
|
@@ -2138,9 +2477,20 @@ private class CraftNativeAudio {
|
|
|
2138
2477
|
var previousValue = current
|
|
2139
2478
|
for (event in events) {
|
|
2140
2479
|
if (time < event.time) {
|
|
2141
|
-
if (
|
|
2480
|
+
if (event.time <= previousTime) return previousValue
|
|
2142
2481
|
val fraction = (time - previousTime) / (event.time - previousTime)
|
|
2143
|
-
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
|
+
}
|
|
2144
2494
|
}
|
|
2145
2495
|
previousTime = event.time
|
|
2146
2496
|
previousValue = event.value
|
|
@@ -2149,73 +2499,182 @@ private class CraftNativeAudio {
|
|
|
2149
2499
|
}
|
|
2150
2500
|
}
|
|
2151
2501
|
|
|
2152
|
-
private class
|
|
2502
|
+
private class NodeState(
|
|
2503
|
+
val contextId: String,
|
|
2504
|
+
val id: String,
|
|
2505
|
+
val kind: String,
|
|
2506
|
+
) {
|
|
2153
2507
|
val frequency = Parameter(440.0)
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
var
|
|
2158
|
-
|
|
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
|
+
)
|
|
2159
2526
|
|
|
2160
|
-
private val
|
|
2161
|
-
private val
|
|
2162
|
-
private val
|
|
2527
|
+
private val nodes = mutableMapOf<String, NodeState>()
|
|
2528
|
+
private val inputs = mutableMapOf<String, MutableSet<String>>()
|
|
2529
|
+
private val buffers = mutableMapOf<String, BufferSpec>()
|
|
2163
2530
|
private val contextStarts = mutableMapOf<String, Double>()
|
|
2531
|
+
private val suspended = mutableSetOf<String>()
|
|
2164
2532
|
private var track: AudioTrack? = null
|
|
2165
2533
|
@Volatile private var running = false
|
|
2166
2534
|
private var renderThread: Thread? = null
|
|
2167
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
|
+
|
|
2168
2540
|
@Synchronized fun handle(payload: JSONObject) {
|
|
2541
|
+
val contextId = payload.optString("contextId")
|
|
2169
2542
|
when (payload.optString("action")) {
|
|
2170
2543
|
"createContext" -> {
|
|
2171
|
-
|
|
2544
|
+
contextId.takeIf { it.isNotBlank() }?.let {
|
|
2172
2545
|
contextStarts[it] = System.nanoTime() / 1_000_000_000.0
|
|
2546
|
+
suspended.remove(it)
|
|
2173
2547
|
}
|
|
2174
2548
|
}
|
|
2175
|
-
"createOscillator"
|
|
2549
|
+
"createOscillator", "createGain", "createBufferSource", "createBiquadFilter",
|
|
2550
|
+
"createDelay", "createDynamicsCompressor", "createConvolver" -> {
|
|
2176
2551
|
val id = payload.optString("nodeId")
|
|
2177
|
-
if (
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
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
|
+
)
|
|
2181
2568
|
}
|
|
2182
2569
|
}
|
|
2183
|
-
"
|
|
2184
|
-
val id = payload.optString("
|
|
2185
|
-
|
|
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)
|
|
2186
2582
|
}
|
|
2187
2583
|
"connect" -> {
|
|
2188
2584
|
val source = payload.optString("sourceId")
|
|
2189
|
-
|
|
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
|
+
}
|
|
2190
2589
|
}
|
|
2191
|
-
"
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
)
|
|
2197
|
-
"
|
|
2198
|
-
"
|
|
2199
|
-
payload.
|
|
2200
|
-
payload.optDouble("
|
|
2201
|
-
payload.
|
|
2202
|
-
|
|
2203
|
-
"
|
|
2204
|
-
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
|
|
2205
2631
|
ensureTrack()
|
|
2206
|
-
val
|
|
2207
|
-
|
|
2208
|
-
|
|
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)
|
|
2209
2646
|
}
|
|
2210
2647
|
"close" -> close()
|
|
2211
2648
|
}
|
|
2212
2649
|
}
|
|
2213
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
|
+
|
|
2214
2672
|
@Synchronized fun close() {
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2673
|
+
nodes.clear()
|
|
2674
|
+
inputs.clear()
|
|
2675
|
+
buffers.clear()
|
|
2218
2676
|
contextStarts.clear()
|
|
2677
|
+
suspended.clear()
|
|
2219
2678
|
running = false
|
|
2220
2679
|
renderThread?.interrupt()
|
|
2221
2680
|
renderThread = null
|
|
@@ -2256,31 +2715,66 @@ private class CraftNativeAudio {
|
|
|
2256
2715
|
|
|
2257
2716
|
private fun render(output: AudioTrack, sampleRate: Int) {
|
|
2258
2717
|
val buffer = ShortArray(1024 * 2)
|
|
2718
|
+
val stereo = DoubleArray(2)
|
|
2259
2719
|
while (running && !Thread.currentThread().isInterrupted) {
|
|
2260
|
-
val snapshot = synchronized(this) {
|
|
2261
|
-
oscillators.mapValues { (id, state) ->
|
|
2262
|
-
Triple(state, state.gain, state.active)
|
|
2263
|
-
}
|
|
2264
|
-
}
|
|
2265
|
-
val now = System.nanoTime() / 1_000_000_000.0
|
|
2266
2720
|
for (frame in 0 until 1024) {
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
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
|
+
}
|
|
2276
2730
|
}
|
|
2277
|
-
|
|
2278
|
-
buffer[frame * 2] =
|
|
2279
|
-
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()
|
|
2280
2733
|
}
|
|
2281
2734
|
if (output.write(buffer, 0, buffer.size, AudioTrack.WRITE_BLOCKING) < 0) break
|
|
2282
2735
|
}
|
|
2283
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
|
+
}
|
|
2284
2778
|
}
|
|
2285
2779
|
|
|
2286
2780
|
private class CraftTextDecorationSpan(
|
|
@@ -2563,6 +3057,16 @@ class MainActivity : AppCompatActivity() {
|
|
|
2563
3057
|
}.getOrDefault(JSONObject())
|
|
2564
3058
|
hapticsEnabled = config.optBoolean("enableHaptics", false)
|
|
2565
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
|
+
}
|
|
2566
3070
|
capabilities = CraftNativeCapabilities(this, config)
|
|
2567
3071
|
routes = CraftNativeRoutes(config)
|
|
2568
3072
|
stores = CraftNativeStores.get(this, config)
|
|
@@ -2891,18 +3395,36 @@ class MainActivity : AppCompatActivity() {
|
|
|
2891
3395
|
audioMessage({ action: action, contextId: contextId, nodeId: nodeId, value: current });
|
|
2892
3396
|
}
|
|
2893
3397
|
});
|
|
2894
|
-
|
|
3398
|
+
param._paramOwner = nodeId;
|
|
3399
|
+
param._paramName = action;
|
|
3400
|
+
function schedule(value, when, suffix) {
|
|
2895
3401
|
current = Number(value) || 0;
|
|
2896
3402
|
var time = Number(when);
|
|
2897
|
-
audioMessage({ action: action +
|
|
3403
|
+
audioMessage({ action: action + suffix, contextId: contextId, nodeId: nodeId, value: current, time: isFinite(time) ? Math.max(0, time) : 0 });
|
|
2898
3404
|
}
|
|
2899
|
-
param.setValueAtTime = function(value, when) { schedule(value, when,
|
|
2900
|
-
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; };
|
|
2901
3409
|
return param;
|
|
2902
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
|
+
}
|
|
2903
3424
|
function AudioContext() {
|
|
2904
3425
|
this._id = 'audio-' + (++nextAudioId);
|
|
2905
3426
|
this._started = Date.now() / 1000;
|
|
3427
|
+
this.sampleRate = 44100;
|
|
2906
3428
|
this.state = 'running';
|
|
2907
3429
|
this.destination = { _contextId: this._id, _nodeId: 'destination' };
|
|
2908
3430
|
audioMessage({ action: 'createContext', contextId: this._id });
|
|
@@ -2912,37 +3434,101 @@ class MainActivity : AppCompatActivity() {
|
|
|
2912
3434
|
});
|
|
2913
3435
|
AudioContext.prototype.createOscillator = function() {
|
|
2914
3436
|
var context = this;
|
|
2915
|
-
var node =
|
|
3437
|
+
var node = makeNode(context, 'Oscillator');
|
|
2916
3438
|
node.frequency = audioParam(context._id, node._nodeId, 'frequency', 440);
|
|
2917
|
-
node.connect = function(destination) {
|
|
2918
|
-
audioMessage({ action: 'connect', contextId: context._id, sourceId: node._nodeId, destinationId: destination && destination._nodeId || 'destination' });
|
|
2919
|
-
return destination;
|
|
2920
|
-
};
|
|
2921
3439
|
node.start = function(when) {
|
|
2922
3440
|
audioMessage({ action: 'start', contextId: context._id, nodeId: node._nodeId, delay: Math.max(0, (Number(when) || 0) - context.currentTime) });
|
|
2923
3441
|
};
|
|
2924
3442
|
node.stop = function(when) {
|
|
2925
3443
|
audioMessage({ action: 'stop', contextId: context._id, nodeId: node._nodeId, delay: Math.max(0, (Number(when) || 0) - context.currentTime) });
|
|
2926
3444
|
};
|
|
2927
|
-
audioMessage({ action: '
|
|
3445
|
+
audioMessage({ action: 'frequency', contextId: context._id, nodeId: node._nodeId, value: node.frequency.value });
|
|
2928
3446
|
return node;
|
|
2929
3447
|
};
|
|
2930
3448
|
AudioContext.prototype.createGain = function() {
|
|
2931
3449
|
var context = this;
|
|
2932
|
-
var node =
|
|
3450
|
+
var node = makeNode(context, 'Gain');
|
|
2933
3451
|
node.gain = audioParam(context._id, node._nodeId, 'gain', 1);
|
|
2934
|
-
node.
|
|
2935
|
-
audioMessage({ action: 'connect', contextId: context._id, sourceId: node._nodeId, destinationId: destination && destination._nodeId || 'destination' });
|
|
2936
|
-
return destination;
|
|
2937
|
-
};
|
|
2938
|
-
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 });
|
|
2939
3453
|
return node;
|
|
2940
3454
|
};
|
|
2941
|
-
AudioContext.prototype.
|
|
2942
|
-
|
|
2943
|
-
|
|
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(); };
|
|
2944
3528
|
globalThis.AudioContext = AudioContext;
|
|
2945
3529
|
globalThis.webkitAudioContext = AudioContext;
|
|
3530
|
+
globalThis.BaseAudioContext = AudioContext;
|
|
3531
|
+
globalThis.AudioBuffer = function() {};
|
|
2946
3532
|
}
|
|
2947
3533
|
})()
|
|
2948
3534
|
""".trimIndent()
|
|
@@ -3016,6 +3602,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3016
3602
|
}
|
|
3017
3603
|
} catch (error: Exception) {
|
|
3018
3604
|
Log.e(TAG, "Native STX bundle failed", error)
|
|
3605
|
+
reportJavaScriptError(activeScreen, error)
|
|
3019
3606
|
if (activeScreen.active) showError("Native STX bundle failed: ${error.message}")
|
|
3020
3607
|
}
|
|
3021
3608
|
}, executor)
|
|
@@ -3033,6 +3620,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
3033
3620
|
* screen shown before hears true (its `navigation.onAppear`).
|
|
3034
3621
|
*/
|
|
3035
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")
|
|
3036
3625
|
val bars = chrome
|
|
3037
3626
|
val host: ViewGroup = bars?.content ?: root
|
|
3038
3627
|
host.removeAllViews()
|
|
@@ -3245,8 +3834,13 @@ class MainActivity : AppCompatActivity() {
|
|
|
3245
3834
|
}
|
|
3246
3835
|
"NAVIGATE_OPEN" -> payload.optString("path").takeIf { it.isNotBlank() }?.let(::navigateOpen)
|
|
3247
3836
|
"NAVIGATION_SET_OPTIONS" -> {
|
|
3248
|
-
|
|
3249
|
-
|
|
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
|
+
}
|
|
3250
3844
|
}
|
|
3251
3845
|
"SYNC_WRITE" -> syncWrite(screen, payload)
|
|
3252
3846
|
"TIMER" -> {
|
|
@@ -3262,15 +3856,20 @@ class MainActivity : AppCompatActivity() {
|
|
|
3262
3856
|
"TIMER_CLEAR" -> screen.timers.remove(payload.optInt("id"))?.let(capabilityHandler::removeCallbacks)
|
|
3263
3857
|
"FRAME" -> requestFrame(screen)
|
|
3264
3858
|
"NAVIGATE", "NAVIGATE_REPLACE" -> {
|
|
3265
|
-
|
|
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
|
|
3266
3865
|
val route = payload.optString("screen").takeIf { it.isNotBlank() } ?: return
|
|
3267
3866
|
if (message.optString("type") == "NAVIGATE_REPLACE") {
|
|
3268
3867
|
closeScreen(screens.removeAt(screens.lastIndex))
|
|
3269
3868
|
}
|
|
3270
3869
|
openScreen(route, payload.optJSONObject("params") ?: JSONObject())
|
|
3271
3870
|
}
|
|
3272
|
-
"NAVIGATE_BACK" -> if (
|
|
3273
|
-
"NAVIGATE_POP_TO_ROOT" -> if (
|
|
3871
|
+
"NAVIGATE_BACK" -> if (visibleScreen === screen) back()
|
|
3872
|
+
"NAVIGATE_POP_TO_ROOT" -> if (visibleScreen === screen) popToRoot()
|
|
3274
3873
|
"API_REQUEST" -> answerAPI(screen, message, payload)
|
|
3275
3874
|
"API_CANCEL" -> {
|
|
3276
3875
|
val id = payload.optString("requestId")
|
|
@@ -3311,7 +3910,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3311
3910
|
private fun applyTargetedUpdates(screen: Screen, ids: List<String>): Boolean {
|
|
3312
3911
|
val applied = mutableSetOf<String>()
|
|
3313
3912
|
for (id in ids) {
|
|
3314
|
-
val target = screen
|
|
3913
|
+
val target = drawnTarget(screen, id) ?: return false
|
|
3315
3914
|
if (!applied.add(target)) continue
|
|
3316
3915
|
val document = screen.mutations.node(target) ?: return false
|
|
3317
3916
|
val path = screen.nodePaths[target] ?: return false
|
|
@@ -3327,6 +3926,17 @@ class MainActivity : AppCompatActivity() {
|
|
|
3327
3926
|
return true
|
|
3328
3927
|
}
|
|
3329
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
|
+
|
|
3330
3940
|
private fun scheduleLayoutEvents(screen: Screen) {
|
|
3331
3941
|
if (!screen.active) return
|
|
3332
3942
|
val observer = screen.view.viewTreeObserver
|
|
@@ -3465,8 +4075,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
3465
4075
|
configureText(this, childText(children), style)
|
|
3466
4076
|
}
|
|
3467
4077
|
"StatusBar" -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
|
|
3468
|
-
screen.statusBarStyle = props.
|
|
3469
|
-
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
|
|
3470
4080
|
visibility = View.GONE
|
|
3471
4081
|
}
|
|
3472
4082
|
"RefreshControl" -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
|
|
@@ -3474,19 +4084,19 @@ class MainActivity : AppCompatActivity() {
|
|
|
3474
4084
|
}
|
|
3475
4085
|
"Link" -> ((previous as? TextView)?.takeUnless { it is Button } ?: TextView(this)).apply {
|
|
3476
4086
|
includeFontPadding = false
|
|
3477
|
-
configureText(this, props.
|
|
3478
|
-
val linkColor = props.
|
|
4087
|
+
configureText(this, craftNativeOptionalString(props.opt("title")) ?: childText(children), style)
|
|
4088
|
+
val linkColor = craftNativeOptionalString(props.opt("color")) ?: craftNativeOptionalString(style.opt("color"))
|
|
3479
4089
|
setTextColor(color(linkColor, Color.rgb(33, 150, 243)))
|
|
3480
4090
|
paintFlags = paintFlags or Paint.UNDERLINE_TEXT_FLAG
|
|
3481
4091
|
isEnabled = !props.optBoolean("disabled", false)
|
|
3482
4092
|
isClickable = true
|
|
3483
4093
|
isFocusable = true
|
|
3484
|
-
val target = listOf(props.
|
|
3485
|
-
.
|
|
4094
|
+
val target = listOf(props.opt("href"), props.opt("to"))
|
|
4095
|
+
.firstNotNullOfOrNull(::craftNativeOptionalString)
|
|
3486
4096
|
?.trim()
|
|
3487
4097
|
?.takeIf { !props.optBoolean("disabled", false) }
|
|
3488
4098
|
if (target != null) screen.linkTargets[this] = target else screen.linkTargets.remove(this)
|
|
3489
|
-
val handler = events
|
|
4099
|
+
val handler = craftNativeEventName(events, "onPress", "onClick")
|
|
3490
4100
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
3491
4101
|
setOnClickListener {
|
|
3492
4102
|
screen.linkTargets[it]?.let { target -> navigateOpen(target); return@setOnClickListener }
|
|
@@ -3505,12 +4115,12 @@ class MainActivity : AppCompatActivity() {
|
|
|
3505
4115
|
minimumHeight = 0
|
|
3506
4116
|
setPadding(0, 0, 0, 0)
|
|
3507
4117
|
}).apply {
|
|
3508
|
-
val title = props.
|
|
4118
|
+
val title = craftNativeOptionalString(props.opt("title")) ?: childText(children)
|
|
3509
4119
|
isAllCaps = false
|
|
3510
4120
|
configureText(this, title, style)
|
|
3511
|
-
props.
|
|
4121
|
+
craftNativeOptionalString(props.opt("color"))?.let { setTextColor(color(it, defaultTextColor)) }
|
|
3512
4122
|
isEnabled = !props.optBoolean("disabled", false)
|
|
3513
|
-
val handler = events
|
|
4123
|
+
val handler = craftNativeEventName(events, "onPress", "onClick")
|
|
3514
4124
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
3515
4125
|
setOnClickListener {
|
|
3516
4126
|
val current = screen.handlers[this] ?: return@setOnClickListener
|
|
@@ -3518,7 +4128,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3518
4128
|
}
|
|
3519
4129
|
}
|
|
3520
4130
|
"Checkbox" -> (previous as? CheckBox ?: CheckBox(this)).apply {
|
|
3521
|
-
text = props.
|
|
4131
|
+
text = craftNativeOptionalString(props.opt("label")) ?: childText(children)
|
|
3522
4132
|
configureText(this, text.toString(), style)
|
|
3523
4133
|
val requested = props.opt("checked") ?: props.opt("value")
|
|
3524
4134
|
val checked = when (requested) {
|
|
@@ -3534,8 +4144,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
3534
4144
|
isChecked = checked
|
|
3535
4145
|
isActivated = props.optBoolean("indeterminate", false)
|
|
3536
4146
|
isEnabled = !props.optBoolean("disabled", false)
|
|
3537
|
-
|
|
3538
|
-
|
|
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")
|
|
3539
4151
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
3540
4152
|
setOnCheckedChangeListener { _, value ->
|
|
3541
4153
|
screen.handlers[this]?.let { current ->
|
|
@@ -3556,13 +4168,13 @@ class MainActivity : AppCompatActivity() {
|
|
|
3556
4168
|
hintTextColors ?: ColorStateList.valueOf(Color.GRAY)
|
|
3557
4169
|
}
|
|
3558
4170
|
val defaultHighlightColor = inputHighlightDefaults.getOrPut(this) { highlightColor }
|
|
3559
|
-
hint = props.
|
|
4171
|
+
hint = craftNativeOptionalString(props.opt("placeholder")).orEmpty()
|
|
3560
4172
|
setHintTextColor(
|
|
3561
|
-
props.
|
|
4173
|
+
craftNativeOptionalString(props.opt("placeholderTextColor"))
|
|
3562
4174
|
?.let { ColorStateList.valueOf(color(it, Color.GRAY)) }
|
|
3563
4175
|
?: defaultHintColors,
|
|
3564
4176
|
)
|
|
3565
|
-
highlightColor = props.
|
|
4177
|
+
highlightColor = craftNativeOptionalString(props.opt("selectionColor"))
|
|
3566
4178
|
?.let { color(it, defaultHighlightColor) }
|
|
3567
4179
|
?: defaultHighlightColor
|
|
3568
4180
|
val maxLength = props.optInt("maxLength", 0)
|
|
@@ -3584,14 +4196,14 @@ class MainActivity : AppCompatActivity() {
|
|
|
3584
4196
|
// the web; without one the field keeps its own.
|
|
3585
4197
|
if (listOf("padding", "paddingHorizontal", "paddingVertical", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom").any { style.has(it) }) {
|
|
3586
4198
|
val density = resources.displayMetrics.density
|
|
3587
|
-
val all = style
|
|
3588
|
-
val horizontal = style
|
|
3589
|
-
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)
|
|
3590
4202
|
setPadding(
|
|
3591
|
-
(style
|
|
3592
|
-
(style
|
|
3593
|
-
(style
|
|
3594
|
-
(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),
|
|
3595
4207
|
)
|
|
3596
4208
|
}
|
|
3597
4209
|
// Reused EditTexts must forget the previous field's hint as
|
|
@@ -3612,15 +4224,15 @@ class MainActivity : AppCompatActivity() {
|
|
|
3612
4224
|
else if (this !== previous && props.has("defaultValue") && !props.isNull("defaultValue")) setText(props.optString("defaultValue"))
|
|
3613
4225
|
applyTextDecoration(this, style)
|
|
3614
4226
|
screen.inputIdentities[this] = identity
|
|
3615
|
-
val handler = events
|
|
4227
|
+
val handler = craftNativeEventName(events, "onChange", "onChangeText")
|
|
3616
4228
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
3617
|
-
val onFocus = events
|
|
4229
|
+
val onFocus = craftNativeEventName(events, "onFocus")
|
|
3618
4230
|
if (onFocus.isNotBlank()) screen.focusHandlers[this] = onFocus else screen.focusHandlers.remove(this)
|
|
3619
|
-
val onBlur = events
|
|
4231
|
+
val onBlur = craftNativeEventName(events, "onBlur")
|
|
3620
4232
|
if (onBlur.isNotBlank()) screen.blurHandlers[this] = onBlur else screen.blurHandlers.remove(this)
|
|
3621
|
-
val onEndEditing = events
|
|
4233
|
+
val onEndEditing = craftNativeEventName(events, "onEndEditing")
|
|
3622
4234
|
if (onEndEditing.isNotBlank()) screen.endEditingHandlers[this] = onEndEditing else screen.endEditingHandlers.remove(this)
|
|
3623
|
-
val onSubmit = events
|
|
4235
|
+
val onSubmit = craftNativeEventName(events, "onSubmitEditing")
|
|
3624
4236
|
if (onSubmit.isNotBlank()) screen.submitHandlers[this] = onSubmit else screen.submitHandlers.remove(this)
|
|
3625
4237
|
setOnFocusChangeListener { view, focused ->
|
|
3626
4238
|
val nativeEvent = JSONObject().put("text", text.toString())
|
|
@@ -3681,10 +4293,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
3681
4293
|
}
|
|
3682
4294
|
if (label.isBlank()) return@mapNotNull null
|
|
3683
4295
|
val optionProps = option.optJSONObject("props")
|
|
3684
|
-
val value = optionProps?.
|
|
4296
|
+
val value = craftNativeOptionalString(optionProps?.opt("value")) ?: label
|
|
3685
4297
|
value to label
|
|
3686
4298
|
}
|
|
3687
|
-
val handler = events
|
|
4299
|
+
val handler = craftNativeEventName(events, "onValueChange", "onChange")
|
|
3688
4300
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
3689
4301
|
// Detach the old listener before replacing the adapter or selection.
|
|
3690
4302
|
// Both operations can synchronously dispatch an Android selection
|
|
@@ -3693,7 +4305,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
3693
4305
|
adapter = ArrayAdapter(this@MainActivity, android.R.layout.simple_spinner_item, options.map { it.second }).also {
|
|
3694
4306
|
it.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
|
3695
4307
|
}
|
|
3696
|
-
val requested = props.
|
|
4308
|
+
val requested = craftNativeOptionalString(props.opt("selectedValue"))
|
|
4309
|
+
?: craftNativeOptionalString(props.opt("value")).orEmpty()
|
|
3697
4310
|
val selected = options.indexOfFirst { it.first == requested }.takeIf { it >= 0 } ?: 0
|
|
3698
4311
|
isEnabled = !props.optBoolean("disabled", false)
|
|
3699
4312
|
// Mounting a controlled picker must not look like a user
|
|
@@ -3716,7 +4329,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3716
4329
|
}
|
|
3717
4330
|
"DatePicker" -> (previous as? CraftNativeDatePickerView ?: CraftNativeDatePickerView(this)).apply {
|
|
3718
4331
|
configure(props)
|
|
3719
|
-
val handler = events
|
|
4332
|
+
val handler = craftNativeEventName(events, "onValueChange", "onChange")
|
|
3720
4333
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
3721
4334
|
onValueChanged = {
|
|
3722
4335
|
screen.handlers[this]?.let { current ->
|
|
@@ -3730,7 +4343,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3730
4343
|
reconcileChildren(screen, content, children, path, next, style, previousControls)
|
|
3731
4344
|
// Dynamic :visible bindings arrive as a display style mutation;
|
|
3732
4345
|
// static visible props remain authoritative when present.
|
|
3733
|
-
val visible = if (props.has("visible")) {
|
|
4346
|
+
val visible = if (props.has("visible") && !props.isNull("visible")) {
|
|
3734
4347
|
// An explicit visible prop owns modal visibility. The
|
|
3735
4348
|
// binding runtime may leave a stale display:none style
|
|
3736
4349
|
// from the previous hidden state when it patches props.
|
|
@@ -3740,18 +4353,22 @@ class MainActivity : AppCompatActivity() {
|
|
|
3740
4353
|
}
|
|
3741
4354
|
val wasVisible = screen.modalVisibility[this]
|
|
3742
4355
|
screen.modalVisibility[this] = visible
|
|
3743
|
-
val requestClose = events.
|
|
4356
|
+
val requestClose = craftNativeOptionalString(events.opt("onRequestClose")).orEmpty()
|
|
3744
4357
|
if (requestClose.isNotBlank()) screen.modalCloseHandlers[this] = requestClose else screen.modalCloseHandlers.remove(this)
|
|
3745
4358
|
onRequestClose = if (requestClose.isNotBlank()) {
|
|
3746
4359
|
{ send(screen, "EVENT", JSONObject().put("handlerName", requestClose).put("nativeEvent", JSONObject())) }
|
|
3747
4360
|
} else {
|
|
3748
4361
|
null
|
|
3749
4362
|
}
|
|
3750
|
-
setVisible(visible, style
|
|
4363
|
+
setVisible(visible, craftNativeFiniteFloat(style, "opacity", 1f), craftNativeOptionalString(props.opt("animationType")))
|
|
3751
4364
|
if (visible) clearDismissSuppression()
|
|
3752
4365
|
val suppressDismiss = !visible && consumeDismissSuppression()
|
|
3753
4366
|
if (wasVisible != visible && (visible || wasVisible == true)) {
|
|
3754
|
-
val lifecycleHandler = if (visible)
|
|
4367
|
+
val lifecycleHandler = if (visible) {
|
|
4368
|
+
craftNativeOptionalString(events.opt("onShow"))
|
|
4369
|
+
} else {
|
|
4370
|
+
craftNativeOptionalString(events.opt("onDismiss"))
|
|
4371
|
+
}
|
|
3755
4372
|
if (lifecycleHandler.isNotBlank() && !suppressDismiss) {
|
|
3756
4373
|
// Deliver lifecycle callbacks after reconciliation returns to
|
|
3757
4374
|
// the queue; the handler may immediately schedule another
|
|
@@ -3766,8 +4383,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
3766
4383
|
}
|
|
3767
4384
|
"TouchableOpacity", "TouchableHighlight", "Pressable" -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
|
|
3768
4385
|
configureContainer(this, style)
|
|
3769
|
-
screen.pressActiveOpacities[this] = props
|
|
3770
|
-
screen.pressBaseOpacities[this] = style
|
|
4386
|
+
screen.pressActiveOpacities[this] = craftNativeFiniteFloat(props, "activeOpacity", 0.5f)
|
|
4387
|
+
screen.pressBaseOpacities[this] = craftNativeFiniteFloat(style, "opacity", 1f)
|
|
3771
4388
|
reconcileChildren(screen, this, children, path, next, style, previousControls)
|
|
3772
4389
|
}
|
|
3773
4390
|
"Switch" -> (previous as? Switch ?: Switch(this)).apply {
|
|
@@ -3791,9 +4408,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
3791
4408
|
intArrayOf(trueColor, falseColor),
|
|
3792
4409
|
)
|
|
3793
4410
|
} ?: run { trackTintList = null }
|
|
3794
|
-
|
|
4411
|
+
craftNativeOptionalString(props.opt("thumbColor"))
|
|
3795
4412
|
?.let { ColorStateList.valueOf(color(it, Color.WHITE)) }
|
|
3796
|
-
|
|
4413
|
+
?: run { thumbTintList = null }
|
|
4414
|
+
val handler = craftNativeEventName(events, "onValueChange", "onChange")
|
|
3797
4415
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
3798
4416
|
setOnCheckedChangeListener { _, checked ->
|
|
3799
4417
|
val current = screen.handlers[this] ?: return@setOnCheckedChangeListener
|
|
@@ -3801,11 +4419,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
3801
4419
|
}
|
|
3802
4420
|
}
|
|
3803
4421
|
"Slider" -> (previous as? SeekBar ?: SeekBar(this)).apply {
|
|
3804
|
-
val minimum = props
|
|
3805
|
-
val maximumCandidate = props
|
|
4422
|
+
val minimum = craftNativeFiniteDouble(props, "minimumValue", 0.0)
|
|
4423
|
+
val maximumCandidate = craftNativeFiniteDouble(props, "maximumValue", 1.0)
|
|
3806
4424
|
val maximum = if (maximumCandidate > minimum) maximumCandidate else minimum + Double.MIN_VALUE
|
|
3807
4425
|
setOnSeekBarChangeListener(null)
|
|
3808
|
-
val step = props
|
|
4426
|
+
val step = craftNativeFiniteDouble(props, "step", 0.0).takeIf { it > 0 }
|
|
3809
4427
|
// A stepped slider needs one native position per authored
|
|
3810
4428
|
// step; a fixed 1,000-position SeekBar silently made large
|
|
3811
4429
|
// ranges unable to represent most valid values. Continuous
|
|
@@ -3819,7 +4437,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3819
4437
|
.toInt()
|
|
3820
4438
|
} ?: 1000
|
|
3821
4439
|
max = progressMax
|
|
3822
|
-
val requested = props
|
|
4440
|
+
val requested = craftNativeFiniteDouble(props, "value", minimum)
|
|
3823
4441
|
val value = (if (step != null) minimum + ((requested - minimum) / step).roundToInt() * step else requested)
|
|
3824
4442
|
.coerceIn(minimum, maximum)
|
|
3825
4443
|
progress = if (step != null) {
|
|
@@ -3828,16 +4446,19 @@ class MainActivity : AppCompatActivity() {
|
|
|
3828
4446
|
(((value - minimum) / (maximum - minimum)) * max).roundToInt().coerceIn(0, max)
|
|
3829
4447
|
}
|
|
3830
4448
|
if (step != null) screen.sliderSteps[this] = step else screen.sliderSteps.remove(this)
|
|
3831
|
-
|
|
4449
|
+
craftNativeOptionalString(props.opt("minimumTrackTintColor"))
|
|
3832
4450
|
?.let { ColorStateList.valueOf(color(it, Color.WHITE)) }
|
|
3833
|
-
|
|
4451
|
+
?: run { progressTintList = null }
|
|
4452
|
+
craftNativeOptionalString(props.opt("maximumTrackTintColor"))
|
|
3834
4453
|
?.let { ColorStateList.valueOf(color(it, Color.GRAY)) }
|
|
3835
|
-
|
|
4454
|
+
?: run { progressBackgroundTintList = null }
|
|
4455
|
+
craftNativeOptionalString(props.opt("thumbTintColor"))
|
|
3836
4456
|
?.let { ColorStateList.valueOf(color(it, Color.WHITE)) }
|
|
4457
|
+
?: run { thumbTintList = null }
|
|
3837
4458
|
isEnabled = !props.optBoolean("disabled", false)
|
|
3838
|
-
val handler = events
|
|
4459
|
+
val handler = craftNativeEventName(events, "onValueChange", "onChange")
|
|
3839
4460
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
3840
|
-
val complete = events
|
|
4461
|
+
val complete = craftNativeEventName(events, "onSlidingComplete")
|
|
3841
4462
|
if (complete.isNotBlank()) screen.sliderCompleteHandlers[this] = complete else screen.sliderCompleteHandlers.remove(this)
|
|
3842
4463
|
setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
|
3843
4464
|
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
|
@@ -3864,7 +4485,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3864
4485
|
val scale = when (requestedSize) {
|
|
3865
4486
|
"large" -> 1.35f
|
|
3866
4487
|
"small" -> 0.75f
|
|
3867
|
-
is Number -> (requestedSize.toDouble() / 24.0).toFloat()
|
|
4488
|
+
is Number -> (requestedSize.toDouble() / 24.0).takeIf { it.isFinite() }?.toFloat()?.coerceAtLeast(0.5f) ?: 1f
|
|
3868
4489
|
else -> 1f
|
|
3869
4490
|
}
|
|
3870
4491
|
scaleX = scale
|
|
@@ -3872,44 +4493,45 @@ class MainActivity : AppCompatActivity() {
|
|
|
3872
4493
|
val animating = props.optBoolean("animating", true)
|
|
3873
4494
|
isIndeterminate = animating
|
|
3874
4495
|
visibility = if (!animating && props.optBoolean("hidesWhenStopped", true)) View.GONE else View.VISIBLE
|
|
3875
|
-
|
|
4496
|
+
craftNativeOptionalString(props.opt("color"))
|
|
3876
4497
|
?.let { ColorStateList.valueOf(color(it, Color.WHITE)) }
|
|
4498
|
+
?: run { indeterminateTintList = null }
|
|
3877
4499
|
}
|
|
3878
4500
|
"Icon" -> (previous as? CraftNativeIconView ?: CraftNativeIconView(this)).apply {
|
|
3879
4501
|
configureIcon(this, props, style)
|
|
3880
4502
|
}
|
|
3881
4503
|
"Image" -> (previous as? ImageView ?: ImageView(this)).apply {
|
|
3882
|
-
scaleType = when (style
|
|
4504
|
+
scaleType = when (craftNativeResizeMode(style, props)) {
|
|
3883
4505
|
"cover" -> ImageView.ScaleType.CENTER_CROP
|
|
3884
4506
|
"stretch" -> ImageView.ScaleType.FIT_XY
|
|
3885
4507
|
"center" -> ImageView.ScaleType.CENTER
|
|
3886
4508
|
else -> ImageView.ScaleType.CENTER_INSIDE
|
|
3887
4509
|
}
|
|
3888
4510
|
adjustViewBounds = false
|
|
3889
|
-
val onLoadStart = events
|
|
4511
|
+
val onLoadStart = craftNativeEventName(events, "onLoadStart")
|
|
3890
4512
|
if (onLoadStart.isNotBlank()) screen.imageLoadStartHandlers[this] = onLoadStart else screen.imageLoadStartHandlers.remove(this)
|
|
3891
|
-
val onLoad = events
|
|
4513
|
+
val onLoad = craftNativeEventName(events, "onLoad")
|
|
3892
4514
|
if (onLoad.isNotBlank()) screen.imageLoadHandlers[this] = onLoad else screen.imageLoadHandlers.remove(this)
|
|
3893
|
-
val onLoadEnd = events
|
|
4515
|
+
val onLoadEnd = craftNativeEventName(events, "onLoadEnd")
|
|
3894
4516
|
if (onLoadEnd.isNotBlank()) screen.imageLoadEndHandlers[this] = onLoadEnd else screen.imageLoadEndHandlers.remove(this)
|
|
3895
|
-
val onError = events
|
|
4517
|
+
val onError = craftNativeEventName(events, "onError")
|
|
3896
4518
|
if (onError.isNotBlank()) screen.imageErrorHandlers[this] = onError else screen.imageErrorHandlers.remove(this)
|
|
3897
4519
|
updateImage(screen, this, props.opt("source"), props.opt("defaultSource"))
|
|
3898
4520
|
}
|
|
3899
4521
|
"ImageBackground" -> (previous as? CraftNativeImageBackgroundView ?: CraftNativeImageBackgroundView(this)).apply {
|
|
3900
|
-
image.scaleType = when (style
|
|
4522
|
+
image.scaleType = when (craftNativeResizeMode(style, props)) {
|
|
3901
4523
|
"cover" -> ImageView.ScaleType.CENTER_CROP
|
|
3902
4524
|
"stretch" -> ImageView.ScaleType.FIT_XY
|
|
3903
4525
|
"center" -> ImageView.ScaleType.CENTER
|
|
3904
4526
|
else -> ImageView.ScaleType.CENTER_INSIDE
|
|
3905
4527
|
}
|
|
3906
|
-
val onLoadStart = events
|
|
4528
|
+
val onLoadStart = craftNativeEventName(events, "onLoadStart")
|
|
3907
4529
|
if (onLoadStart.isNotBlank()) screen.imageLoadStartHandlers[image] = onLoadStart else screen.imageLoadStartHandlers.remove(image)
|
|
3908
|
-
val onLoad = events
|
|
4530
|
+
val onLoad = craftNativeEventName(events, "onLoad")
|
|
3909
4531
|
if (onLoad.isNotBlank()) screen.imageLoadHandlers[image] = onLoad else screen.imageLoadHandlers.remove(image)
|
|
3910
|
-
val onLoadEnd = events
|
|
4532
|
+
val onLoadEnd = craftNativeEventName(events, "onLoadEnd")
|
|
3911
4533
|
if (onLoadEnd.isNotBlank()) screen.imageLoadEndHandlers[image] = onLoadEnd else screen.imageLoadEndHandlers.remove(image)
|
|
3912
|
-
val onError = events
|
|
4534
|
+
val onError = craftNativeEventName(events, "onError")
|
|
3913
4535
|
if (onError.isNotBlank()) screen.imageErrorHandlers[image] = onError else screen.imageErrorHandlers.remove(image)
|
|
3914
4536
|
updateImage(screen, image, props.opt("source"), props.opt("defaultSource"))
|
|
3915
4537
|
configureContainer(content, style)
|
|
@@ -3923,6 +4545,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3923
4545
|
loop = props.optBoolean("loop", false),
|
|
3924
4546
|
muted = props.optBoolean("muted", false),
|
|
3925
4547
|
controls = props.optBoolean("controls", false),
|
|
4548
|
+
resizeMode = craftNativeResizeMode(style, props),
|
|
3926
4549
|
)
|
|
3927
4550
|
}
|
|
3928
4551
|
"Map" -> (previous as? CraftNativeMapView ?: CraftNativeMapView(this)).apply {
|
|
@@ -3956,22 +4579,23 @@ class MainActivity : AppCompatActivity() {
|
|
|
3956
4579
|
.firstOrNull { canonicalType(it.optString("type")) == "RefreshControl" }
|
|
3957
4580
|
val refreshProps = refreshNode?.optJSONObject("props") ?: JSONObject()
|
|
3958
4581
|
val refreshEvents = refreshNode?.optJSONObject("events") ?: JSONObject()
|
|
3959
|
-
val refreshHandlerName = refreshEvents
|
|
3960
|
-
|
|
4582
|
+
val refreshHandlerName = craftNativeEventName(refreshEvents, "onRefresh")
|
|
4583
|
+
.takeIf { it.isNotBlank() }
|
|
4584
|
+
?: craftNativeEventName(events, "onRefresh").takeIf { it.isNotBlank() }
|
|
3961
4585
|
val refreshHandler = refreshHandlerName?.let { handler ->
|
|
3962
4586
|
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject())) }
|
|
3963
4587
|
}
|
|
3964
4588
|
setRefreshHandler(refreshHandler, if (refreshNode == null) props.optBoolean("refreshing", false) else refreshProps.optBoolean("refreshing", false))
|
|
3965
4589
|
setRefreshAppearance(refreshProps, resources.displayMetrics.density)
|
|
3966
|
-
val onScroll = events
|
|
4590
|
+
val onScroll = craftNativeEventName(events, "onScroll")
|
|
3967
4591
|
if (onScroll.isNotBlank()) screen.scrollHandlers[this] = onScroll else screen.scrollHandlers.remove(this)
|
|
3968
|
-
val onBegin = events
|
|
4592
|
+
val onBegin = craftNativeEventName(events, "onScrollBeginDrag")
|
|
3969
4593
|
if (onBegin.isNotBlank()) screen.scrollBeginHandlers[this] = onBegin else screen.scrollBeginHandlers.remove(this)
|
|
3970
|
-
val onEnd = events
|
|
4594
|
+
val onEnd = craftNativeEventName(events, "onScrollEndDrag")
|
|
3971
4595
|
if (onEnd.isNotBlank()) screen.scrollEndHandlers[this] = onEnd else screen.scrollEndHandlers.remove(this)
|
|
3972
|
-
val onMomentumBegin = events
|
|
4596
|
+
val onMomentumBegin = craftNativeEventName(events, "onMomentumScrollBegin")
|
|
3973
4597
|
if (onMomentumBegin.isNotBlank()) screen.scrollMomentumBeginHandlers[this] = onMomentumBegin else screen.scrollMomentumBeginHandlers.remove(this)
|
|
3974
|
-
val onMomentumEnd = events
|
|
4598
|
+
val onMomentumEnd = craftNativeEventName(events, "onMomentumScrollEnd")
|
|
3975
4599
|
if (onMomentumEnd.isNotBlank()) screen.scrollMomentumEndHandlers[this] = onMomentumEnd else screen.scrollMomentumEndHandlers.remove(this)
|
|
3976
4600
|
val nativeScroll = this
|
|
3977
4601
|
setScrollListeners(
|
|
@@ -4025,7 +4649,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4025
4649
|
}
|
|
4026
4650
|
"FlatList" -> (previous as? CraftNativeRefreshableFlatList ?: CraftNativeRefreshableFlatList(this)).apply {
|
|
4027
4651
|
val nativeList = list
|
|
4028
|
-
configureRefresh(events
|
|
4652
|
+
configureRefresh(craftNativeEventName(events, "onRefresh").takeIf { it.isNotBlank() }?.let { handler ->
|
|
4029
4653
|
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject())) }
|
|
4030
4654
|
}, props.optBoolean("refreshing", false))
|
|
4031
4655
|
nativeList.setKeyboardShouldPersistTaps(props.optString("keyboardShouldPersistTaps", "never"))
|
|
@@ -4057,15 +4681,15 @@ class MainActivity : AppCompatActivity() {
|
|
|
4057
4681
|
} else if (control is CraftNativeKeyboardAvoidingView) {
|
|
4058
4682
|
control.content.onLayoutChanged = { if (screen.active) emitLayoutEvents(screen) }
|
|
4059
4683
|
}
|
|
4060
|
-
val pressInHandler = events
|
|
4061
|
-
val pressOutHandler = events
|
|
4684
|
+
val pressInHandler = craftNativeEventName(events, "onPressIn").takeIf { it.isNotBlank() }
|
|
4685
|
+
val pressOutHandler = craftNativeEventName(events, "onPressOut").takeIf { it.isNotBlank() }
|
|
4062
4686
|
if (pressInHandler != null) screen.pressInHandlers[control] = pressInHandler else screen.pressInHandlers.remove(control)
|
|
4063
4687
|
if (pressOutHandler != null) screen.pressOutHandlers[control] = pressOutHandler else screen.pressOutHandlers.remove(control)
|
|
4064
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") {
|
|
4065
|
-
val handler = events
|
|
4689
|
+
val handler = craftNativeEventName(events, "onPress", "onClick")
|
|
4066
4690
|
val disabled = props.optBoolean("disabled", false)
|
|
4067
|
-
val target = listOf(props.
|
|
4068
|
-
.
|
|
4691
|
+
val target = listOf(props.opt("href"), props.opt("to"))
|
|
4692
|
+
.firstNotNullOfOrNull(::craftNativeOptionalString)
|
|
4069
4693
|
?.trim()
|
|
4070
4694
|
?.takeIf { !disabled && type in setOf("TouchableOpacity", "TouchableHighlight", "Pressable") }
|
|
4071
4695
|
if (target != null) screen.linkTargets[control] = target else screen.linkTargets.remove(control)
|
|
@@ -4126,7 +4750,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4126
4750
|
}
|
|
4127
4751
|
val longPressDisabled = props.optBoolean("disabled", false) ||
|
|
4128
4752
|
props.optJSONObject("accessibilityState")?.optBoolean("disabled", false) == true
|
|
4129
|
-
val longPressHandler = events
|
|
4753
|
+
val longPressHandler = craftNativeEventName(events, "onLongPress").takeUnless { longPressDisabled } ?: ""
|
|
4130
4754
|
if (longPressHandler.isNotBlank()) {
|
|
4131
4755
|
screen.longPressHandlers[control] = longPressHandler
|
|
4132
4756
|
control.setOnLongClickListener {
|
|
@@ -4140,7 +4764,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4140
4764
|
control.isLongClickable = false
|
|
4141
4765
|
}
|
|
4142
4766
|
applyAccessibility(control, props, type, explicitKey ?: protocolId ?: path)
|
|
4143
|
-
val layoutHandler = events
|
|
4767
|
+
val layoutHandler = craftNativeEventName(events, "onLayout")
|
|
4144
4768
|
if (layoutHandler.isNotBlank()) {
|
|
4145
4769
|
if (screen.layoutHandlers[control] != layoutHandler) screen.lastLayoutBounds.remove(control)
|
|
4146
4770
|
screen.layoutHandlers[control] = layoutHandler
|
|
@@ -4176,20 +4800,20 @@ class MainActivity : AppCompatActivity() {
|
|
|
4176
4800
|
View.VISIBLE
|
|
4177
4801
|
}
|
|
4178
4802
|
}
|
|
4179
|
-
val width = style
|
|
4180
|
-
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)
|
|
4181
4805
|
control.layoutParams = LinearLayout.LayoutParams(
|
|
4182
4806
|
width ?: ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
4183
4807
|
height ?: ViewGroup.LayoutParams.WRAP_CONTENT
|
|
4184
4808
|
).apply {
|
|
4185
|
-
val margin = style
|
|
4186
|
-
val horizontal = style
|
|
4187
|
-
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)
|
|
4188
4812
|
setMargins(
|
|
4189
|
-
dp(style
|
|
4190
|
-
dp(style
|
|
4191
|
-
dp(style
|
|
4192
|
-
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))
|
|
4193
4817
|
)
|
|
4194
4818
|
}
|
|
4195
4819
|
return control
|
|
@@ -4238,19 +4862,18 @@ class MainActivity : AppCompatActivity() {
|
|
|
4238
4862
|
val direct = listOf(node.optString("key"), props.optString("key"), props.optString("testID"))
|
|
4239
4863
|
.firstOrNull { it.isNotBlank() }
|
|
4240
4864
|
if (direct != null) return direct
|
|
4241
|
-
|
|
4242
|
-
for (index in 0 until children.length()) {
|
|
4243
|
-
val child = children.optJSONObject(index) ?: continue
|
|
4244
|
-
val childProps = child.optJSONObject("props") ?: JSONObject()
|
|
4245
|
-
authoredKey(child, childProps)?.let { return it }
|
|
4246
|
-
}
|
|
4247
|
-
return node.optString("id").takeIf { it.isNotBlank() }
|
|
4865
|
+
return null
|
|
4248
4866
|
}
|
|
4249
4867
|
|
|
4250
4868
|
private fun configureContainer(layout: CraftNativeFlexLayout, style: JSONObject) {
|
|
4251
4869
|
layout.configure(style, resources.displayMetrics.density)
|
|
4252
4870
|
}
|
|
4253
4871
|
|
|
4872
|
+
private fun craftNativeResizeMode(style: JSONObject, props: JSONObject): String {
|
|
4873
|
+
return craftNativeOptionalString(style.opt("resizeMode"))
|
|
4874
|
+
?: craftNativeOptionalString(props.opt("resizeMode")).orEmpty()
|
|
4875
|
+
}
|
|
4876
|
+
|
|
4254
4877
|
private fun flatListScrollEvent(list: CraftNativeFlatList): JSONObject = JSONObject()
|
|
4255
4878
|
.put("contentOffset", JSONObject()
|
|
4256
4879
|
.put("x", list.computeHorizontalScrollOffset())
|
|
@@ -4323,30 +4946,29 @@ class MainActivity : AppCompatActivity() {
|
|
|
4323
4946
|
}
|
|
4324
4947
|
val nodes = (0 until children.length()).mapNotNull { children.optJSONObject(it) }
|
|
4325
4948
|
nodes.forEach { registerFlatListOwnership(screen, it, owner) }
|
|
4326
|
-
list.onScrollEvent = events
|
|
4949
|
+
list.onScrollEvent = craftNativeEventName(events, "onScroll").takeIf { it.isNotBlank() }?.let { handler ->
|
|
4327
4950
|
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", flatListScrollEvent(list))) }
|
|
4328
4951
|
}
|
|
4329
|
-
list.onScrollBeginDrag = events
|
|
4952
|
+
list.onScrollBeginDrag = craftNativeEventName(events, "onScrollBeginDrag").takeIf { it.isNotBlank() }?.let { handler ->
|
|
4330
4953
|
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", flatListScrollEvent(list))) }
|
|
4331
4954
|
}
|
|
4332
|
-
list.onScrollEndDrag = events
|
|
4955
|
+
list.onScrollEndDrag = craftNativeEventName(events, "onScrollEndDrag").takeIf { it.isNotBlank() }?.let { handler ->
|
|
4333
4956
|
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", flatListScrollEvent(list))) }
|
|
4334
4957
|
}
|
|
4335
|
-
list.onMomentumScrollBegin = events
|
|
4958
|
+
list.onMomentumScrollBegin = craftNativeEventName(events, "onMomentumScrollBegin").takeIf { it.isNotBlank() }?.let { handler ->
|
|
4336
4959
|
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", flatListScrollEvent(list))) }
|
|
4337
4960
|
}
|
|
4338
|
-
list.onMomentumScrollEnd = events
|
|
4961
|
+
list.onMomentumScrollEnd = craftNativeEventName(events, "onMomentumScrollEnd").takeIf { it.isNotBlank() }?.let { handler ->
|
|
4339
4962
|
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", flatListScrollEvent(list))) }
|
|
4340
4963
|
}
|
|
4964
|
+
val threshold = craftNativeNonNegativeDouble(props, "threshold", 0.1)
|
|
4965
|
+
val authoredThreshold = craftNativeNonNegativeDouble(props, "onEndReachedThreshold", threshold)
|
|
4341
4966
|
list.apply(
|
|
4342
4967
|
nodes = nodes,
|
|
4343
4968
|
horizontal = props.optBoolean("horizontal", false),
|
|
4344
4969
|
columns = props.optInt("numColumns", 1),
|
|
4345
4970
|
inverted = props.optBoolean("inverted", false),
|
|
4346
|
-
endReachedThreshold =
|
|
4347
|
-
?.takeUnless { it == JSONObject.NULL }
|
|
4348
|
-
?.let { props.optDouble("onEndReachedThreshold", 0.1) }
|
|
4349
|
-
?: props.optDouble("threshold", 0.1),
|
|
4971
|
+
endReachedThreshold = authoredThreshold,
|
|
4350
4972
|
renderItem = { node, itemIdentity, _ ->
|
|
4351
4973
|
val rows = screen.flatListRows.getOrPut(list) { mutableMapOf() }
|
|
4352
4974
|
val previous = rows[itemIdentity].orEmpty()
|
|
@@ -4360,7 +4982,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4360
4982
|
val controls = screen.flatListRows[list]?.remove(itemIdentity).orEmpty()
|
|
4361
4983
|
releaseViewState(screen, controls.values.toList(), preservingInputDrafts = true)
|
|
4362
4984
|
},
|
|
4363
|
-
endReached = events
|
|
4985
|
+
endReached = craftNativeEventName(events, "onEndReached").takeIf { it.isNotBlank() }?.let { handler ->
|
|
4364
4986
|
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject())) }
|
|
4365
4987
|
},
|
|
4366
4988
|
)
|
|
@@ -4380,20 +5002,21 @@ class MainActivity : AppCompatActivity() {
|
|
|
4380
5002
|
private fun addJustificationSpacers(layout: CraftNativeFlexLayout, value: String) { }
|
|
4381
5003
|
|
|
4382
5004
|
private fun applyViewStyle(view: View, style: JSONObject) {
|
|
4383
|
-
view.alpha = style
|
|
5005
|
+
view.alpha = craftNativeFiniteFloat(style, "opacity", 1f)
|
|
4384
5006
|
// A Text's padding (`px-4` on a line of text) is where its text
|
|
4385
5007
|
// starts, as on iOS; containers and fields lay out their own.
|
|
4386
5008
|
if (view is TextView && view !is EditText && view !is CraftNativeIconView) {
|
|
4387
5009
|
val keys = listOf("padding", "paddingHorizontal", "paddingVertical", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom")
|
|
4388
5010
|
if (keys.any(style::has)) {
|
|
4389
|
-
val
|
|
4390
|
-
val
|
|
4391
|
-
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)
|
|
4392
5015
|
view.setPadding(
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
5016
|
+
craftNativePadding(style, "paddingLeft", horizontal, density),
|
|
5017
|
+
craftNativePadding(style, "paddingTop", vertical, density),
|
|
5018
|
+
craftNativePadding(style, "paddingRight", horizontal, density),
|
|
5019
|
+
craftNativePadding(style, "paddingBottom", vertical, density),
|
|
4397
5020
|
)
|
|
4398
5021
|
} else if (view.javaClass == TextView::class.java) {
|
|
4399
5022
|
view.setPadding(0, 0, 0, 0)
|
|
@@ -4403,18 +5026,18 @@ class MainActivity : AppCompatActivity() {
|
|
|
4403
5026
|
if (view !is CraftNativeModalView) {
|
|
4404
5027
|
view.visibility = if (style.optString("display") == "none") View.GONE else View.VISIBLE
|
|
4405
5028
|
}
|
|
4406
|
-
view.elevation = dp(style
|
|
4407
|
-
view.z = style
|
|
5029
|
+
view.elevation = dp(craftNativeNonNegativeDouble(style, "elevation", 0.0)).toFloat()
|
|
5030
|
+
view.z = craftNativeFiniteFloat(style, "zIndex", 0f)
|
|
4408
5031
|
val background = style.has("backgroundColor") && !style.isNull("backgroundColor")
|
|
4409
|
-
val border = style
|
|
4410
|
-
val radius = style
|
|
5032
|
+
val border = craftNativeNonNegativeDouble(style, "borderWidth", 0.0)
|
|
5033
|
+
val radius = craftNativeNonNegativeDouble(style, "borderRadius", 0.0)
|
|
4411
5034
|
val sideBorderKeys = listOf("borderTopWidth", "borderRightWidth", "borderBottomWidth", "borderLeftWidth")
|
|
4412
5035
|
val hasSideBorders = (sideBorderKeys + listOf("borderTopColor", "borderRightColor", "borderBottomColor", "borderLeftColor")).any(style::has)
|
|
4413
|
-
val sideWidths = sideBorderKeys.map { dp(style
|
|
5036
|
+
val sideWidths = sideBorderKeys.map { dp(craftNativeNonNegativeDouble(style, it, border)) }.toIntArray()
|
|
4414
5037
|
val cornerRadiusKeys = listOf("borderTopLeftRadius", "borderTopRightRadius", "borderBottomRightRadius", "borderBottomLeftRadius")
|
|
4415
5038
|
val hasCornerRadii = cornerRadiusKeys.any(style::has)
|
|
4416
5039
|
val cornerRadii = cornerRadiusKeys.flatMap { value ->
|
|
4417
|
-
val radius = dp(style
|
|
5040
|
+
val radius = dp(craftNativeNonNegativeDouble(style, value, radius)).toFloat()
|
|
4418
5041
|
listOf(radius, radius)
|
|
4419
5042
|
}.toFloatArray()
|
|
4420
5043
|
val sideColors = listOf("borderTopColor", "borderRightColor", "borderBottomColor", "borderLeftColor")
|
|
@@ -4457,19 +5080,20 @@ class MainActivity : AppCompatActivity() {
|
|
|
4457
5080
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P &&
|
|
4458
5081
|
(style.has("shadowColor") || style.has("shadowOpacity") || style.has("shadowRadius"))
|
|
4459
5082
|
) {
|
|
4460
|
-
val shadowOpacity = style
|
|
5083
|
+
val shadowOpacity = craftNativeFiniteFloat(style, "shadowOpacity", 1f).coerceIn(0f, 1f)
|
|
4461
5084
|
val shadow = color(style.optString("shadowColor"), Color.BLACK)
|
|
4462
5085
|
val alphaShadow = Color.argb((Color.alpha(shadow) * shadowOpacity).roundToInt(), Color.red(shadow), Color.green(shadow), Color.blue(shadow))
|
|
4463
5086
|
view.outlineAmbientShadowColor = alphaShadow
|
|
4464
5087
|
view.outlineSpotShadowColor = alphaShadow
|
|
4465
|
-
|
|
5088
|
+
val shadowRadius = craftNativeNonNegativeDouble(style, "shadowRadius", 0.0)
|
|
5089
|
+
view.elevation = max(view.elevation, dp(shadowRadius * 2.0).toFloat())
|
|
4466
5090
|
val offset = style.optJSONObject("shadowOffset")
|
|
4467
|
-
val offsetX = dp(offset
|
|
4468
|
-
val offsetY = dp(offset
|
|
4469
|
-
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()
|
|
4470
5094
|
view.outlineProvider = object : ViewOutlineProvider() {
|
|
4471
5095
|
override fun getOutline(target: View, outline: Outline) {
|
|
4472
|
-
outline.setRoundRect(offsetX, offsetY, target.width + offsetX, target.height + offsetY,
|
|
5096
|
+
outline.setRoundRect(offsetX, offsetY, target.width + offsetX, target.height + offsetY, radius)
|
|
4473
5097
|
}
|
|
4474
5098
|
}
|
|
4475
5099
|
view.invalidateOutline()
|
|
@@ -4502,11 +5126,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
4502
5126
|
val transform = transforms.optJSONObject(index) ?: continue
|
|
4503
5127
|
val key = transform.keys().asSequence().firstOrNull() ?: continue
|
|
4504
5128
|
when (key) {
|
|
4505
|
-
"translateX" -> view.translationX += dp(transform
|
|
4506
|
-
"translateY" -> view.translationY += dp(transform
|
|
4507
|
-
"scale" -> transform
|
|
4508
|
-
"scaleX" -> view.scaleX *= transform
|
|
4509
|
-
"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()
|
|
4510
5134
|
"rotate", "rotateZ" -> view.rotation += angleDegrees(transform.opt(key))
|
|
4511
5135
|
"rotateX" -> view.rotationX += angleDegrees(transform.opt(key))
|
|
4512
5136
|
"rotateY" -> view.rotationY += angleDegrees(transform.opt(key))
|
|
@@ -4518,15 +5142,16 @@ class MainActivity : AppCompatActivity() {
|
|
|
4518
5142
|
}
|
|
4519
5143
|
|
|
4520
5144
|
private fun skewFactor(value: Any?): Float =
|
|
4521
|
-
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
|
|
4522
5146
|
|
|
4523
5147
|
private fun angleDegrees(value: Any?): Float {
|
|
4524
5148
|
val text = value?.toString()?.trim() ?: return 0f
|
|
4525
|
-
|
|
5149
|
+
val result = when {
|
|
4526
5150
|
text.endsWith("rad") -> Math.toDegrees(text.dropLast(3).toDoubleOrNull() ?: 0.0).toFloat()
|
|
4527
5151
|
text.endsWith("deg") -> text.dropLast(3).toFloatOrNull() ?: 0f
|
|
4528
5152
|
else -> text.toFloatOrNull() ?: 0f
|
|
4529
5153
|
}
|
|
5154
|
+
return result.takeIf { it.isFinite() } ?: 0f
|
|
4530
5155
|
}
|
|
4531
5156
|
|
|
4532
5157
|
private fun ensureLayoutListener(screen: Screen, view: View) {
|
|
@@ -4549,15 +5174,22 @@ class MainActivity : AppCompatActivity() {
|
|
|
4549
5174
|
}
|
|
4550
5175
|
|
|
4551
5176
|
private fun configureIcon(view: CraftNativeIconView, props: JSONObject, style: JSONObject) {
|
|
4552
|
-
val symbol = props.
|
|
4553
|
-
|
|
5177
|
+
val symbol = craftNativeOptionalString(props.opt("symbol"))
|
|
5178
|
+
?: craftNativeOptionalString(props.opt("name")).orEmpty()
|
|
5179
|
+
val size = craftNativeFiniteFloat(style, "fontSize", 20f).coerceAtLeast(1f)
|
|
4554
5180
|
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, size)
|
|
4555
|
-
val foreground = style.
|
|
5181
|
+
val foreground = craftNativeOptionalString(style.opt("color"))
|
|
5182
|
+
?: craftNativeOptionalString(props.opt("color")).orEmpty()
|
|
4556
5183
|
val tint = color(foreground, defaultTextColor)
|
|
4557
5184
|
view.setTextColor(tint)
|
|
4558
5185
|
// The app's own vector for it (craft-icons.json, the icon the web
|
|
4559
5186
|
// shows), else the closest glyph Android's fonts have.
|
|
4560
|
-
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
|
+
)
|
|
4561
5193
|
view.vector = vector
|
|
4562
5194
|
view.text = if (vector != null) "" else androidIconGlyph(symbol)
|
|
4563
5195
|
val weight = when (style.optString("fontWeight")) {
|
|
@@ -4608,7 +5240,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4608
5240
|
|
|
4609
5241
|
private fun configureTextStyle(view: TextView, style: JSONObject) {
|
|
4610
5242
|
view.setTextColor(color(style.optString("color"), defaultTextColor))
|
|
4611
|
-
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, style
|
|
5243
|
+
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, craftNativeFiniteFloat(style, "fontSize", 16f))
|
|
4612
5244
|
val weight = when (style.optString("fontWeight")) {
|
|
4613
5245
|
"bold" -> 700
|
|
4614
5246
|
"100", "200", "300", "400", "500", "600", "700", "800", "900" -> style.optInt("fontWeight")
|
|
@@ -4635,11 +5267,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
4635
5267
|
}
|
|
4636
5268
|
}
|
|
4637
5269
|
view.letterSpacing = style.optDouble("letterSpacing", Double.NaN)
|
|
4638
|
-
.
|
|
5270
|
+
.takeIf { it.isFinite() }
|
|
4639
5271
|
?.let { if (view.textSize > 0) dp(it).toFloat() / view.textSize else 0f }
|
|
4640
5272
|
?: 0f
|
|
4641
5273
|
view.setLineSpacing(
|
|
4642
|
-
style.optDouble("lineHeight", Double.NaN).
|
|
5274
|
+
style.optDouble("lineHeight", Double.NaN).takeIf { it.isFinite() }?.let { dp(it) - view.textSize } ?: 0f,
|
|
4643
5275
|
1f
|
|
4644
5276
|
)
|
|
4645
5277
|
val shadowColor = style.optString("textShadowColor").takeIf { it.isNotBlank() }
|
|
@@ -4648,9 +5280,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
4648
5280
|
} else {
|
|
4649
5281
|
val offset = style.optJSONObject("textShadowOffset")
|
|
4650
5282
|
view.setShadowLayer(
|
|
4651
|
-
dp(style
|
|
4652
|
-
dp(offset
|
|
4653
|
-
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(),
|
|
4654
5286
|
color(shadowColor, Color.TRANSPARENT),
|
|
4655
5287
|
)
|
|
4656
5288
|
}
|
|
@@ -4714,7 +5346,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4714
5346
|
|
|
4715
5347
|
private fun imageUri(source: Any?): String = when (source) {
|
|
4716
5348
|
is String -> source
|
|
4717
|
-
is JSONObject -> source.
|
|
5349
|
+
is JSONObject -> craftNativeOptionalString(source.opt("uri")).orEmpty()
|
|
4718
5350
|
else -> ""
|
|
4719
5351
|
}
|
|
4720
5352
|
|
|
@@ -4735,6 +5367,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4735
5367
|
imageSources[view] = ""
|
|
4736
5368
|
imageErrors[view] = "Image source is missing"
|
|
4737
5369
|
view.setImageDrawable(null)
|
|
5370
|
+
imageDidChange(view)
|
|
4738
5371
|
imageFailure(screen, view, uri, "Image source is missing")
|
|
4739
5372
|
return
|
|
4740
5373
|
}
|
|
@@ -4743,7 +5376,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
4743
5376
|
imageSources[view] = uri
|
|
4744
5377
|
imageErrors.remove(view)
|
|
4745
5378
|
view.setImageDrawable(null)
|
|
5379
|
+
imageDidChange(view)
|
|
4746
5380
|
immediateImage(imageUri(defaultSource))?.let(view::setImageBitmap)
|
|
5381
|
+
if (view.drawable != null) imageDidChange(view)
|
|
4747
5382
|
emitImageEvent(screen, view, screen.imageLoadStartHandlers[view])
|
|
4748
5383
|
|
|
4749
5384
|
if (uri.startsWith("data:image/") && uri.contains(',')) {
|
|
@@ -4751,6 +5386,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4751
5386
|
val bitmap = bytes?.let { BitmapFactory.decodeByteArray(it, 0, it.size) }
|
|
4752
5387
|
if (bitmap != null) {
|
|
4753
5388
|
view.setImageBitmap(bitmap)
|
|
5389
|
+
imageDidChange(view)
|
|
4754
5390
|
emitImageEvent(screen, view, screen.imageLoadHandlers[view])
|
|
4755
5391
|
emitImageEvent(screen, view, screen.imageLoadEndHandlers[view])
|
|
4756
5392
|
} else imageFailure(screen, view, uri, "Image data is invalid")
|
|
@@ -4760,6 +5396,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4760
5396
|
val bitmap = runCatching { assets.open(uri).use { BitmapFactory.decodeStream(it) } }.getOrNull()
|
|
4761
5397
|
if (bitmap != null) {
|
|
4762
5398
|
view.setImageBitmap(bitmap)
|
|
5399
|
+
imageDidChange(view)
|
|
4763
5400
|
emitImageEvent(screen, view, screen.imageLoadHandlers[view])
|
|
4764
5401
|
emitImageEvent(screen, view, screen.imageLoadEndHandlers[view])
|
|
4765
5402
|
} else imageFailure(screen, view, uri, "Bundled image was not found")
|
|
@@ -4782,6 +5419,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4782
5419
|
imageJobs.remove(view)
|
|
4783
5420
|
if (bitmap != null) {
|
|
4784
5421
|
view.setImageBitmap(bitmap)
|
|
5422
|
+
imageDidChange(view)
|
|
4785
5423
|
emitImageEvent(screen, view, screen.imageLoadHandlers[view])
|
|
4786
5424
|
emitImageEvent(screen, view, screen.imageLoadEndHandlers[view])
|
|
4787
5425
|
} else imageFailure(screen, view, uri, "Image download failed")
|
|
@@ -4792,6 +5430,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4792
5430
|
private fun imageFailure(screen: Screen, view: ImageView, uri: String, message: String) {
|
|
4793
5431
|
imageErrors[view] = message
|
|
4794
5432
|
view.setImageDrawable(null)
|
|
5433
|
+
imageDidChange(view)
|
|
4795
5434
|
val current = view.contentDescription?.toString().orEmpty()
|
|
4796
5435
|
if (!current.contains(message)) {
|
|
4797
5436
|
view.contentDescription = listOf(
|
|
@@ -4804,6 +5443,18 @@ class MainActivity : AppCompatActivity() {
|
|
|
4804
5443
|
Log.e(TAG, "$message: $uri")
|
|
4805
5444
|
}
|
|
4806
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
|
+
|
|
4807
5458
|
private fun emitImageEvent(screen: Screen, view: ImageView, handler: String?, nativeEvent: JSONObject = JSONObject()) {
|
|
4808
5459
|
if (handler.isNullOrBlank()) return
|
|
4809
5460
|
send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", nativeEvent))
|
|
@@ -4824,17 +5475,17 @@ class MainActivity : AppCompatActivity() {
|
|
|
4824
5475
|
|
|
4825
5476
|
private fun applyAccessibility(view: View, props: JSONObject, type: String, fallbackLabel: String) {
|
|
4826
5477
|
view.tag = fallbackLabel
|
|
4827
|
-
val label = props.
|
|
4828
|
-
?: props.
|
|
4829
|
-
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"))
|
|
4830
5481
|
view.contentDescription = when {
|
|
4831
5482
|
label != null && value != null -> "$label, $value"
|
|
4832
5483
|
label != null -> label
|
|
4833
5484
|
value != null -> value
|
|
4834
5485
|
else -> null
|
|
4835
5486
|
}
|
|
4836
|
-
view.tooltipText = props.
|
|
4837
|
-
val role = props.
|
|
5487
|
+
view.tooltipText = craftNativeOptionalString(props.opt("accessibilityHint"))
|
|
5488
|
+
val role = craftNativeOptionalString(props.opt("accessibilityRole")) ?: run {
|
|
4838
5489
|
if (type == "Icon") "image" else type.lowercase()
|
|
4839
5490
|
}
|
|
4840
5491
|
val state = props.optJSONObject("accessibilityState")
|
|
@@ -4872,7 +5523,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
4872
5523
|
)
|
|
4873
5524
|
}
|
|
4874
5525
|
|
|
4875
|
-
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
|
+
}
|
|
4876
5530
|
private fun color(raw: String, fallback: Int) = runCatching { craftNativeColor(raw) }.getOrDefault(fallback)
|
|
4877
5531
|
|
|
4878
5532
|
private fun inputTypeFor(props: JSONObject): Int {
|
|
@@ -5076,6 +5730,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
5076
5730
|
super.onStop()
|
|
5077
5731
|
}
|
|
5078
5732
|
|
|
5733
|
+
override fun onLowMemory() {
|
|
5734
|
+
mapViews().forEach { it.lowMemory() }
|
|
5735
|
+
super.onLowMemory()
|
|
5736
|
+
}
|
|
5737
|
+
|
|
5079
5738
|
override fun onNewIntent(intent: Intent) {
|
|
5080
5739
|
super.onNewIntent(intent)
|
|
5081
5740
|
setIntent(intent)
|
|
@@ -5174,10 +5833,40 @@ class MainActivity : AppCompatActivity() {
|
|
|
5174
5833
|
screen.port.postMessage(Message.createStringMessage(message.toString()))
|
|
5175
5834
|
} else {
|
|
5176
5835
|
val script = "globalThis.__stxNativeCallback && globalThis.__stxNativeCallback(${JSONObject.quote(message.toString())})"
|
|
5177
|
-
screen.isolate.evaluateJavaScriptAsync(script)
|
|
5836
|
+
reportJavaScriptFailure(screen, screen.isolate.evaluateJavaScriptAsync(script))
|
|
5178
5837
|
}
|
|
5179
5838
|
}
|
|
5180
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
|
+
|
|
5181
5870
|
/** Back to the first screen in one move. */
|
|
5182
5871
|
private fun popToRoot() {
|
|
5183
5872
|
while (screens.size > 1) closeScreen(screens.removeAt(screens.lastIndex))
|
|
@@ -5216,6 +5905,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
5216
5905
|
screen.flatListRows.values.forEach { rows -> rows.values.forEach { addAll(it.values) } }
|
|
5217
5906
|
}
|
|
5218
5907
|
|
|
5908
|
+
private fun mapViews(): Set<CraftNativeMapView> = allScreens()
|
|
5909
|
+
.flatMap { retainedViews(it) }
|
|
5910
|
+
.filterIsInstance<CraftNativeMapView>()
|
|
5911
|
+
.toSet()
|
|
5912
|
+
|
|
5219
5913
|
private fun releaseFlatList(
|
|
5220
5914
|
screen: Screen,
|
|
5221
5915
|
list: CraftNativeFlatList,
|
|
@@ -5247,6 +5941,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5247
5941
|
}
|
|
5248
5942
|
released.filterIsInstance<CraftNativeModalView>().forEach { it.resetForTeardown() }
|
|
5249
5943
|
released.filterIsInstance<CraftNativeVideoView>().forEach { it.stop() }
|
|
5944
|
+
released.filterIsInstance<CraftNativeMapView>().forEach { it.destroy() }
|
|
5250
5945
|
released.filterIsInstance<CraftNativeFlexLayout>().forEach { it.onLayoutChanged = null }
|
|
5251
5946
|
released.filterIsInstance<CraftNativeKeyboardAvoidingView>().forEach { it.content.onLayoutChanged = null }
|
|
5252
5947
|
released.filterIsInstance<CraftNativeScrollView>().forEach { it.setRefreshHandler(null, false) }
|