@craft-native/android 0.0.204 → 0.0.206
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 +24 -8
- package/dist/index.js +91 -1
- package/dist/index.js.map +3 -3
- package/package.json +1 -1
- package/templates/CraftNativeFlatList.kt.template +117 -0
- package/templates/CraftNativeRefreshableFlatList.kt.template +7 -0
- package/templates/CraftNativeShell.kt.template +42 -10
- package/templates/MainActivityNative.kt.template +628 -99
|
@@ -21,15 +21,18 @@ import android.graphics.BitmapFactory
|
|
|
21
21
|
import android.graphics.Canvas
|
|
22
22
|
import android.graphics.Color
|
|
23
23
|
import android.graphics.DashPathEffect
|
|
24
|
+
import android.graphics.LinearGradient
|
|
24
25
|
import android.graphics.Matrix
|
|
25
26
|
import android.graphics.Outline
|
|
26
27
|
import android.graphics.Paint
|
|
27
28
|
import android.graphics.Path
|
|
28
29
|
import android.graphics.RectF
|
|
30
|
+
import android.graphics.Shader
|
|
29
31
|
import android.graphics.Typeface
|
|
30
32
|
import android.graphics.ColorFilter
|
|
31
33
|
import android.graphics.PixelFormat
|
|
32
34
|
import android.graphics.drawable.Drawable
|
|
35
|
+
import android.graphics.drawable.ColorDrawable
|
|
33
36
|
import android.graphics.drawable.GradientDrawable
|
|
34
37
|
import android.net.Uri
|
|
35
38
|
import android.os.Build
|
|
@@ -145,6 +148,12 @@ private fun craftNativeOptionalString(value: Any?): String? = value
|
|
|
145
148
|
?.takeUnless { it == "null" || it == "undefined" }
|
|
146
149
|
?.takeIf { it.isNotBlank() }
|
|
147
150
|
|
|
151
|
+
private fun craftNativePickerValue(value: Any?): String? = when (value) {
|
|
152
|
+
is String -> value.takeIf { it.isNotEmpty() }
|
|
153
|
+
is Number -> value.toDouble().takeIf { it.isFinite() }?.toString()
|
|
154
|
+
else -> null
|
|
155
|
+
}
|
|
156
|
+
|
|
148
157
|
private fun craftNativeEventName(events: JSONObject, vararg names: String): String =
|
|
149
158
|
names.firstNotNullOfOrNull { name -> craftNativeOptionalString(events.opt(name)) }.orEmpty()
|
|
150
159
|
|
|
@@ -332,12 +341,26 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
332
341
|
val strokeWidth: Float = 1f,
|
|
333
342
|
val color: String = "#000000",
|
|
334
343
|
val opacity: Float = 1f,
|
|
344
|
+
val fillOpacity: Float = 1f,
|
|
345
|
+
val strokeOpacity: Float = 1f,
|
|
346
|
+
val strokeDashoffset: Float = 0f,
|
|
347
|
+
val strokeMiterlimit: Float = 4f,
|
|
348
|
+
val fillRule: String = "nonzero",
|
|
335
349
|
val typeface: Typeface = Typeface.DEFAULT,
|
|
336
350
|
val strokeLinecap: String? = null,
|
|
337
351
|
val strokeLinejoin: String? = null,
|
|
338
352
|
val strokeDasharray: List<Float> = emptyList(),
|
|
339
353
|
val vectorEffect: String? = null,
|
|
354
|
+
val dominantBaseline: String? = null,
|
|
340
355
|
) {
|
|
356
|
+
private fun svgTypeface(weight: String?, inherited: Typeface): Typeface = when (weight?.lowercase()) {
|
|
357
|
+
"500" -> Typeface.create("sans-serif-medium", Typeface.NORMAL)
|
|
358
|
+
"600" -> Typeface.create("sans-serif", Typeface.BOLD)
|
|
359
|
+
"bold", "700", "800", "900" -> Typeface.DEFAULT_BOLD
|
|
360
|
+
"normal", "100", "200", "300", "400" -> Typeface.DEFAULT
|
|
361
|
+
else -> inherited
|
|
362
|
+
}
|
|
363
|
+
|
|
341
364
|
fun merge(node: JSONObject): SvgPaint {
|
|
342
365
|
val props = node.optJSONObject("props") ?: JSONObject()
|
|
343
366
|
val style = node.optJSONObject("style") ?: JSONObject()
|
|
@@ -361,17 +384,24 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
361
384
|
// when both provide the same SVG paint value.
|
|
362
385
|
val weight = value("fontWeight")
|
|
363
386
|
val ownOpacity = value("opacity")?.toFloatOrNull()?.takeIf { it.isFinite() } ?: 1f
|
|
387
|
+
fun number(name: String): Float? = value(name)?.toFloatOrNull()?.takeIf { it.isFinite() }
|
|
364
388
|
return copy(
|
|
365
389
|
fill = value("fill") ?: fill,
|
|
366
390
|
stroke = value("stroke") ?: stroke,
|
|
367
391
|
strokeWidth = value("strokeWidth")?.toFloatOrNull()?.takeIf { it.isFinite() } ?: strokeWidth,
|
|
368
392
|
color = value("color") ?: color,
|
|
369
393
|
opacity = ownOpacity * opacity,
|
|
370
|
-
|
|
394
|
+
fillOpacity = number("fillOpacity") ?: fillOpacity,
|
|
395
|
+
strokeOpacity = number("strokeOpacity") ?: strokeOpacity,
|
|
396
|
+
strokeDashoffset = number("strokeDashoffset") ?: strokeDashoffset,
|
|
397
|
+
strokeMiterlimit = number("strokeMiterlimit")?.takeIf { it > 0 } ?: strokeMiterlimit,
|
|
398
|
+
fillRule = value("fillRule") ?: fillRule,
|
|
399
|
+
typeface = svgTypeface(weight, typeface),
|
|
371
400
|
strokeLinecap = value("strokeLinecap") ?: strokeLinecap,
|
|
372
401
|
strokeLinejoin = value("strokeLinejoin") ?: strokeLinejoin,
|
|
373
402
|
strokeDasharray = dashes(raw("strokeDasharray")) ?: strokeDasharray,
|
|
374
403
|
vectorEffect = value("vectorEffect") ?: vectorEffect,
|
|
404
|
+
dominantBaseline = value("dominantBaseline") ?: dominantBaseline,
|
|
375
405
|
)
|
|
376
406
|
}
|
|
377
407
|
}
|
|
@@ -467,16 +497,18 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
467
497
|
}
|
|
468
498
|
val fill = android.graphics.Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
469
499
|
color = resolveColor(style.fill, style.color, Color.TRANSPARENT)
|
|
470
|
-
alpha = (style.opacity * 255f).roundToInt().coerceIn(0, 255)
|
|
500
|
+
alpha = (style.opacity * style.fillOpacity * 255f).roundToInt().coerceIn(0, 255)
|
|
471
501
|
this.style = Paint.Style.FILL
|
|
472
502
|
}
|
|
503
|
+
path.fillType = if (style.fillRule == "evenodd") Path.FillType.EVEN_ODD else Path.FillType.WINDING
|
|
473
504
|
if (style.fill != "none" && style.fill != "transparent") canvas.drawPath(path, fill)
|
|
474
505
|
if (!style.stroke.isNullOrBlank() && style.stroke != "none" && style.stroke != "transparent" && style.strokeWidth > 0) {
|
|
475
506
|
val stroke = android.graphics.Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
476
507
|
color = resolveColor(style.stroke, style.color, Color.TRANSPARENT)
|
|
477
|
-
alpha = (style.opacity * 255f).roundToInt().coerceIn(0, 255)
|
|
508
|
+
alpha = (style.opacity * style.strokeOpacity * 255f).roundToInt().coerceIn(0, 255)
|
|
478
509
|
this.style = Paint.Style.STROKE
|
|
479
510
|
strokeWidth = style.strokeWidth
|
|
511
|
+
strokeMiter = style.strokeMiterlimit
|
|
480
512
|
strokeCap = when (style.strokeLinecap) {
|
|
481
513
|
"round" -> Paint.Cap.ROUND
|
|
482
514
|
"square" -> Paint.Cap.SQUARE
|
|
@@ -494,7 +526,7 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
494
526
|
} else {
|
|
495
527
|
dashPattern
|
|
496
528
|
}
|
|
497
|
-
pathEffect = DashPathEffect(pattern,
|
|
529
|
+
pathEffect = DashPathEffect(pattern, style.strokeDashoffset)
|
|
498
530
|
}
|
|
499
531
|
}
|
|
500
532
|
// `vector-effect="non-scaling-stroke"`: the stroke is in the view's
|
|
@@ -525,7 +557,7 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
525
557
|
?: 12f
|
|
526
558
|
val paint = android.graphics.Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
527
559
|
color = resolveColor(style.fill, style.color, Color.BLACK)
|
|
528
|
-
alpha = (style.opacity * 255f).roundToInt().coerceIn(0, 255)
|
|
560
|
+
alpha = (style.opacity * style.fillOpacity * 255f).roundToInt().coerceIn(0, 255)
|
|
529
561
|
textSize = size
|
|
530
562
|
typeface = style.typeface
|
|
531
563
|
}
|
|
@@ -535,7 +567,14 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
535
567
|
"middle" -> x -= paint.measureText(text) / 2f
|
|
536
568
|
"end" -> x -= paint.measureText(text)
|
|
537
569
|
}
|
|
538
|
-
|
|
570
|
+
val metrics = paint.fontMetrics
|
|
571
|
+
val baseline = when (style.dominantBaseline) {
|
|
572
|
+
"middle", "central", "mathematical" -> y - (metrics.ascent + metrics.descent) / 2f
|
|
573
|
+
"hanging", "text-before-edge", "before-edge" -> y - metrics.ascent
|
|
574
|
+
"text-after-edge", "after-edge" -> y - metrics.descent
|
|
575
|
+
else -> y
|
|
576
|
+
}
|
|
577
|
+
canvas.drawText(text, x, baseline, paint)
|
|
539
578
|
}
|
|
540
579
|
|
|
541
580
|
private fun resolveColor(value: String?, current: String, fallback: Int): Int = runCatching {
|
|
@@ -557,6 +596,7 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
557
596
|
private var onMarkerTap: ((String) -> Unit)? = null
|
|
558
597
|
private var lifecycleStarted = false
|
|
559
598
|
private var destroyed = false
|
|
599
|
+
private var initialCameraKey: String? = null
|
|
560
600
|
|
|
561
601
|
init {
|
|
562
602
|
addView(mapView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
|
|
@@ -631,7 +671,11 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
631
671
|
is Number -> rawWidth.toDouble()
|
|
632
672
|
else -> 3.0
|
|
633
673
|
}
|
|
634
|
-
|
|
674
|
+
// MapKit's lineWidth is expressed in points, while the
|
|
675
|
+
// Google Maps SDK consumes screen pixels. Keep the
|
|
676
|
+
// protocol in logical points and convert only at this
|
|
677
|
+
// Android host boundary so dense displays stay in parity.
|
|
678
|
+
val width = (authoredWidth * resources.displayMetrics.density).toFloat()
|
|
635
679
|
if (!authoredWidth.isFinite() || !width.isFinite() || width <= 0f) continue
|
|
636
680
|
nextPolylines[id] = Triple(points, color(craftNativeOptionalString(props.opt("strokeColor")).orEmpty()), width)
|
|
637
681
|
}
|
|
@@ -662,24 +706,67 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
662
706
|
map.addPolyline(PolylineOptions().addAll(value.first).color(value.second).width(value.third))?.let { polylines[id] = it }
|
|
663
707
|
}
|
|
664
708
|
}
|
|
665
|
-
|
|
666
|
-
region(pendingProps.optJSONObject("
|
|
667
|
-
|
|
668
|
-
|
|
709
|
+
val explicitRegion = region(pendingProps.optJSONObject("region"))
|
|
710
|
+
val initialRegion = region(pendingProps.optJSONObject("initialRegion"))
|
|
711
|
+
if (explicitRegion != null) {
|
|
712
|
+
moveCamera(explicitRegion)
|
|
713
|
+
initialCameraKey = null
|
|
714
|
+
} else if (initialRegion != null) {
|
|
715
|
+
val key = "region:${pendingProps.optJSONObject("initialRegion")}"
|
|
716
|
+
if (initialCameraKey != key) {
|
|
717
|
+
moveCamera(initialRegion)
|
|
718
|
+
initialCameraKey = key
|
|
669
719
|
}
|
|
720
|
+
} else if (pendingProps.optBoolean("fitToBounds", false)) {
|
|
721
|
+
val key = "fit:${overlayKey(nextMarkers, nextPolylines)}"
|
|
722
|
+
if (initialCameraKey != key) {
|
|
723
|
+
if (fitToBounds()) initialCameraKey = key
|
|
724
|
+
}
|
|
725
|
+
} else {
|
|
726
|
+
initialCameraKey = null
|
|
670
727
|
}
|
|
671
728
|
}
|
|
672
729
|
|
|
673
|
-
|
|
674
|
-
|
|
730
|
+
override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
|
|
731
|
+
super.onSizeChanged(width, height, oldWidth, oldHeight)
|
|
732
|
+
// MapView can become ready before its host has a usable viewport. Do
|
|
733
|
+
// not mark a fit as complete until the camera update can be issued.
|
|
734
|
+
if (width > 0 && height > 0 && pendingProps.optBoolean("fitToBounds", false) && initialCameraKey == null) {
|
|
735
|
+
applyPending()
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
private fun moveCamera(bounds: LatLngBounds) {
|
|
740
|
+
post {
|
|
741
|
+
if (width > 0 && height > 0) googleMap?.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 24))
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
private fun overlayKey(
|
|
746
|
+
markers: Map<String, Pair<LatLng, String?>>,
|
|
747
|
+
polylines: Map<String, Triple<List<LatLng>, Int, Float>>,
|
|
748
|
+
): String {
|
|
749
|
+
val markerKey = markers.toSortedMap().entries.joinToString("|") { (id, value) ->
|
|
750
|
+
"$id:${value.first.latitude},${value.first.longitude}"
|
|
751
|
+
}
|
|
752
|
+
val polylineKey = polylines.toSortedMap().entries.joinToString("|") { (id, value) ->
|
|
753
|
+
"$id:" + value.first.joinToString(";") { point -> "${point.latitude},${point.longitude}" }
|
|
754
|
+
}
|
|
755
|
+
return "m[$markerKey]p[$polylineKey]"
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
private fun fitToBounds(): Boolean {
|
|
759
|
+
val map = googleMap ?: return false
|
|
760
|
+
if (width <= 0 || height <= 0) return false
|
|
675
761
|
val builder = LatLngBounds.Builder()
|
|
676
762
|
var count = 0
|
|
677
763
|
markers.values.forEach { builder.include(it.position); count++ }
|
|
678
764
|
polylines.values.flatMap { it.points }.forEach { builder.include(it); count++ }
|
|
679
|
-
if (count == 0) return
|
|
765
|
+
if (count == 0) return false
|
|
680
766
|
post {
|
|
681
767
|
if (width > 0 && height > 0) map.moveCamera(CameraUpdateFactory.newLatLngBounds(builder.build(), 24))
|
|
682
768
|
}
|
|
769
|
+
return true
|
|
683
770
|
}
|
|
684
771
|
|
|
685
772
|
override fun onAttachedToWindow() {
|
|
@@ -728,8 +815,8 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
728
815
|
private fun coordinates(geometry: JSONArray?): List<LatLng> {
|
|
729
816
|
if (geometry == null) return emptyList()
|
|
730
817
|
return (0 until geometry.length() - 1 step 2).mapNotNull { index ->
|
|
731
|
-
val latitude = geometry.
|
|
732
|
-
val longitude = geometry.
|
|
818
|
+
val latitude = finiteNumber(geometry.opt(index)) ?: return@mapNotNull null
|
|
819
|
+
val longitude = finiteNumber(geometry.opt(index + 1)) ?: return@mapNotNull null
|
|
733
820
|
if (latitude.isFinite() && longitude.isFinite() &&
|
|
734
821
|
latitude >= -90.0 && latitude <= 90.0 &&
|
|
735
822
|
longitude >= -180.0 && longitude <= 180.0
|
|
@@ -740,10 +827,14 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
740
827
|
private fun region(value: JSONObject?): LatLngBounds? {
|
|
741
828
|
val center = value?.optJSONObject("center") ?: return null
|
|
742
829
|
val span = value.optJSONObject("span") ?: return null
|
|
743
|
-
val latitude = center.
|
|
744
|
-
val longitude = center.
|
|
745
|
-
val latitudeDelta = span.
|
|
746
|
-
|
|
830
|
+
val latitude = finiteNumber(center.opt("latitude")) ?: return null
|
|
831
|
+
val longitude = finiteNumber(center.opt("longitude")) ?: return null
|
|
832
|
+
val latitudeDelta = finiteNumber(span.opt("latitudeDelta"))
|
|
833
|
+
?: finiteNumber(span.opt("latitude_delta"))
|
|
834
|
+
?: return null
|
|
835
|
+
val longitudeDelta = finiteNumber(span.opt("longitudeDelta"))
|
|
836
|
+
?: finiteNumber(span.opt("longitude_delta"))
|
|
837
|
+
?: return null
|
|
747
838
|
if (!latitude.isFinite() || !longitude.isFinite() || !latitudeDelta.isFinite() || !longitudeDelta.isFinite() ||
|
|
748
839
|
latitude < -90.0 || latitude > 90.0 || longitude < -180.0 || longitude > 180.0 ||
|
|
749
840
|
latitudeDelta <= 0.0 || latitudeDelta > 180.0 ||
|
|
@@ -753,7 +844,10 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
753
844
|
return LatLngBounds(LatLng(latitude - latitudeDelta / 2, longitude - longitudeDelta / 2), LatLng(latitude + latitudeDelta / 2, longitude + longitudeDelta / 2))
|
|
754
845
|
}
|
|
755
846
|
|
|
756
|
-
private fun
|
|
847
|
+
private fun finiteNumber(value: Any?): Double? =
|
|
848
|
+
(value as? Number)?.toDouble()?.takeIf { it.isFinite() }
|
|
849
|
+
|
|
850
|
+
private fun color(value: String): Int = runCatching { craftNativeColor(value) }.getOrDefault(Color.BLUE)
|
|
757
851
|
}
|
|
758
852
|
|
|
759
853
|
/** A deterministic flex-shaped container shared by normal views and scroll content. */
|
|
@@ -2036,6 +2130,9 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
2036
2130
|
private val defaultProgressStart = progressViewStartOffset
|
|
2037
2131
|
private val defaultProgressEnd = progressViewEndOffset
|
|
2038
2132
|
private var configuredProgressOffset: Int? = null
|
|
2133
|
+
private var stickyIndices = emptyList<Int>()
|
|
2134
|
+
private var scrollTargetKey: String? = null
|
|
2135
|
+
private var pendingScrollTarget: Pair<String, Boolean>? = null
|
|
2039
2136
|
|
|
2040
2137
|
init {
|
|
2041
2138
|
vertical.isNestedScrollingEnabled = true
|
|
@@ -2121,13 +2218,13 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
2121
2218
|
}
|
|
2122
2219
|
}
|
|
2123
2220
|
|
|
2124
|
-
fun setRefreshAppearance(props: JSONObject, density: Float) {
|
|
2221
|
+
fun setRefreshAppearance(props: JSONObject, density: Float, fallback: Int) {
|
|
2125
2222
|
val tint = craftNativeOptionalString(props.opt("tintColor"))
|
|
2126
|
-
|
|
2127
|
-
else setColorSchemeColors(Color.WHITE)
|
|
2223
|
+
setColorSchemeColors(tint?.let { runCatching { craftNativeColor(it) }.getOrNull() } ?: fallback)
|
|
2128
2224
|
val background = craftNativeOptionalString(props.opt("progressBackgroundColor"))
|
|
2129
|
-
|
|
2130
|
-
|
|
2225
|
+
setProgressBackgroundColorSchemeColor(
|
|
2226
|
+
background?.let { runCatching { craftNativeColor(it) }.getOrNull() } ?: Color.TRANSPARENT,
|
|
2227
|
+
)
|
|
2131
2228
|
val offset = props.optDouble("progressViewOffset", Double.NaN)
|
|
2132
2229
|
val requested = if (offset.isFinite()) (offset * density).roundToInt() else null
|
|
2133
2230
|
if (configuredProgressOffset != requested) {
|
|
@@ -2247,6 +2344,133 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
2247
2344
|
horizontal.isHorizontalScrollBarEnabled = horizontalVisible
|
|
2248
2345
|
}
|
|
2249
2346
|
|
|
2347
|
+
/** Keep vertical ScrollView headers pinned while preserving their layout slots. */
|
|
2348
|
+
fun setStickyHeaderIndices(value: JSONArray?) {
|
|
2349
|
+
stickyIndices.forEach { index ->
|
|
2350
|
+
content.getChildAt(index)?.apply {
|
|
2351
|
+
translationY = 0f
|
|
2352
|
+
translationZ = 0f
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
stickyIndices = if (value == null) {
|
|
2356
|
+
emptyList()
|
|
2357
|
+
} else {
|
|
2358
|
+
(0 until value.length()).mapNotNull { value.optInt(it, -1).takeIf { index -> index >= 0 } }.distinct().sorted()
|
|
2359
|
+
}
|
|
2360
|
+
post { updateStickyHeaders() }
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
/** Scroll a child identified by its native testID/id after reconciliation. */
|
|
2364
|
+
fun setScrollTarget(value: Any?) {
|
|
2365
|
+
val target: String
|
|
2366
|
+
val animated: Boolean
|
|
2367
|
+
val key: String
|
|
2368
|
+
when (value) {
|
|
2369
|
+
is String -> {
|
|
2370
|
+
target = value.trim()
|
|
2371
|
+
animated = true
|
|
2372
|
+
key = target
|
|
2373
|
+
}
|
|
2374
|
+
is JSONObject -> {
|
|
2375
|
+
target = craftNativeOptionalString(value.opt("id")).orEmpty().trim()
|
|
2376
|
+
animated = value.optBoolean("animated", true)
|
|
2377
|
+
key = value.toString()
|
|
2378
|
+
}
|
|
2379
|
+
else -> {
|
|
2380
|
+
scrollTargetKey = null
|
|
2381
|
+
pendingScrollTarget = null
|
|
2382
|
+
return
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
if (target.isBlank()) {
|
|
2386
|
+
scrollTargetKey = null
|
|
2387
|
+
pendingScrollTarget = null
|
|
2388
|
+
return
|
|
2389
|
+
}
|
|
2390
|
+
if (key == scrollTargetKey) return
|
|
2391
|
+
scrollTargetKey = key
|
|
2392
|
+
pendingScrollTarget = target to animated
|
|
2393
|
+
post { scrollToPendingTarget(0) }
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
private fun updateStickyHeaders() {
|
|
2397
|
+
if (horizontalMode == true) {
|
|
2398
|
+
stickyIndices.forEach { index ->
|
|
2399
|
+
content.getChildAt(index)?.apply {
|
|
2400
|
+
translationY = 0f
|
|
2401
|
+
translationZ = 0f
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
return
|
|
2405
|
+
}
|
|
2406
|
+
val scrollY = vertical.scrollY
|
|
2407
|
+
var pinnedBottom = 0
|
|
2408
|
+
stickyIndices.forEachIndexed { position, index ->
|
|
2409
|
+
val header = content.getChildAt(index)
|
|
2410
|
+
if (header == null || header.visibility != View.VISIBLE || header.height <= 0) return@forEachIndexed
|
|
2411
|
+
val nextTop = stickyIndices.drop(position + 1)
|
|
2412
|
+
.firstNotNullOfOrNull { next -> content.getChildAt(next)?.takeIf { it.visibility == View.VISIBLE }?.top }
|
|
2413
|
+
?: Int.MAX_VALUE
|
|
2414
|
+
val desired = max(0, scrollY + pinnedBottom - header.top)
|
|
2415
|
+
val limit = if (nextTop == Int.MAX_VALUE) Int.MAX_VALUE else max(0, nextTop - header.bottom)
|
|
2416
|
+
val translation = min(desired, limit)
|
|
2417
|
+
header.translationY = translation.toFloat()
|
|
2418
|
+
header.translationZ = if (translation > 0) 1f else 0f
|
|
2419
|
+
if (translation > 0) pinnedBottom = header.top - scrollY + translation + header.height
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
private fun findDescendant(view: View, id: String): View? {
|
|
2424
|
+
if (view.tag?.toString() == id || view.contentDescription?.toString() == id) return view
|
|
2425
|
+
if (view is ViewGroup) {
|
|
2426
|
+
for (index in 0 until view.childCount) findDescendant(view.getChildAt(index), id)?.let { return it }
|
|
2427
|
+
}
|
|
2428
|
+
return null
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
private fun scrollToPendingTarget(attempt: Int) {
|
|
2432
|
+
val target = pendingScrollTarget ?: return
|
|
2433
|
+
val scroller: View = if (horizontalMode == true) horizontal else vertical
|
|
2434
|
+
val view = findDescendant(content, target.first)
|
|
2435
|
+
if (view == null || view.width <= 0 || view.height <= 0 || scroller.width <= 0 || scroller.height <= 0) {
|
|
2436
|
+
if (attempt < 4) postDelayed({ scrollToPendingTarget(attempt + 1) }, 16L)
|
|
2437
|
+
return
|
|
2438
|
+
}
|
|
2439
|
+
val targetLocation = IntArray(2)
|
|
2440
|
+
val scrollerLocation = IntArray(2)
|
|
2441
|
+
view.getLocationOnScreen(targetLocation)
|
|
2442
|
+
scroller.getLocationOnScreen(scrollerLocation)
|
|
2443
|
+
val current = if (horizontalMode == true) horizontal.scrollX else vertical.scrollY
|
|
2444
|
+
val stickyHeight = if (horizontalMode == true) 0 else {
|
|
2445
|
+
val contentLocation = IntArray(2)
|
|
2446
|
+
content.getLocationOnScreen(contentLocation)
|
|
2447
|
+
val targetContentTop = targetLocation[1] - contentLocation[1] + vertical.scrollY
|
|
2448
|
+
stickyIndices.mapNotNull { index -> content.getChildAt(index) }
|
|
2449
|
+
.lastOrNull { it.visibility == View.VISIBLE && it.top <= targetContentTop }
|
|
2450
|
+
?.height ?: 0
|
|
2451
|
+
}
|
|
2452
|
+
val desired = if (horizontalMode == true) {
|
|
2453
|
+
current + targetLocation[0] - scrollerLocation[0]
|
|
2454
|
+
} else {
|
|
2455
|
+
current + targetLocation[1] - scrollerLocation[1] - stickyHeight
|
|
2456
|
+
}
|
|
2457
|
+
val maximum = if (horizontalMode == true) {
|
|
2458
|
+
max(0, content.measuredWidth - scroller.width)
|
|
2459
|
+
} else {
|
|
2460
|
+
max(0, content.measuredHeight - scroller.height)
|
|
2461
|
+
}
|
|
2462
|
+
val bounded = desired.coerceIn(0, maximum)
|
|
2463
|
+
if (target.second) {
|
|
2464
|
+
if (horizontalMode == true) horizontal.smoothScrollTo(bounded, 0) else vertical.smoothScrollTo(0, bounded)
|
|
2465
|
+
} else if (horizontalMode == true) {
|
|
2466
|
+
horizontal.scrollTo(bounded, 0)
|
|
2467
|
+
} else {
|
|
2468
|
+
vertical.scrollTo(0, bounded)
|
|
2469
|
+
}
|
|
2470
|
+
pendingScrollTarget = null
|
|
2471
|
+
updateStickyHeaders()
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2250
2474
|
fun setScrollListeners(
|
|
2251
2475
|
onScroll: ((Int, Int) -> Unit)?,
|
|
2252
2476
|
onBeginDrag: (() -> Unit)?,
|
|
@@ -2277,7 +2501,10 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
2277
2501
|
}
|
|
2278
2502
|
}
|
|
2279
2503
|
|
|
2280
|
-
scroller.setOnScrollChangeListener { _, x, y, _, _ ->
|
|
2504
|
+
scroller.setOnScrollChangeListener { _, x, y, _, _ ->
|
|
2505
|
+
if (!horizontal) updateStickyHeaders()
|
|
2506
|
+
onScroll?.invoke(x, y)
|
|
2507
|
+
}
|
|
2281
2508
|
scroller.setOnTouchListener { _, event ->
|
|
2282
2509
|
when (event.actionMasked) {
|
|
2283
2510
|
android.view.MotionEvent.ACTION_DOWN -> {
|
|
@@ -2644,7 +2871,8 @@ private class CraftNativeDatePickerView(context: Context) : FrameLayout(context)
|
|
|
2644
2871
|
if (text.isBlank() || text == JSONObject.NULL.toString()) return null
|
|
2645
2872
|
val formats = listOf(
|
|
2646
2873
|
"HH:mm", "HH:mm:ss", "yyyy-MM-dd", "yyyy-MM-dd'T'HH:mm",
|
|
2647
|
-
"yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss.
|
|
2874
|
+
"yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss.SSS",
|
|
2875
|
+
"yyyy-MM-dd'T'HH:mm:ss.SSSXXX", "yyyy-MM-dd'T'HH:mm:ssX",
|
|
2648
2876
|
)
|
|
2649
2877
|
formats.firstNotNullOfOrNull { format ->
|
|
2650
2878
|
runCatching {
|
|
@@ -2722,6 +2950,9 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
|
|
|
2722
2950
|
private var sourceUri = ""
|
|
2723
2951
|
private var shouldLoop = false
|
|
2724
2952
|
private var shouldPlay = false
|
|
2953
|
+
private var playIntent: Boolean? = null
|
|
2954
|
+
private var userPaused = false
|
|
2955
|
+
private var ended = false
|
|
2725
2956
|
private var muted = false
|
|
2726
2957
|
private var resizeMode = "contain"
|
|
2727
2958
|
private var preparedPlayer: MediaPlayer? = null
|
|
@@ -2736,6 +2967,16 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
|
|
|
2736
2967
|
private set
|
|
2737
2968
|
/** Playback started or stopped, or the choice changed: the activity's PiP params follow. */
|
|
2738
2969
|
var onPictureInPictureChanged: (() -> Unit)? = null
|
|
2970
|
+
private val progressHandler = Handler(Looper.getMainLooper())
|
|
2971
|
+
private val progressTick = object : Runnable {
|
|
2972
|
+
override fun run() {
|
|
2973
|
+
if (!playing) return
|
|
2974
|
+
fire("onProgress", JSONObject()
|
|
2975
|
+
.put("currentTime", currentPosition.coerceAtLeast(0) / 1000.0)
|
|
2976
|
+
.put("duration", duration.coerceAtLeast(0) / 1000.0))
|
|
2977
|
+
progressHandler.postDelayed(this, 500L)
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2739
2980
|
|
|
2740
2981
|
init {
|
|
2741
2982
|
// Mixed with the music rather than pausing it.
|
|
@@ -2749,7 +2990,13 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
|
|
|
2749
2990
|
else -> ""
|
|
2750
2991
|
}
|
|
2751
2992
|
shouldLoop = loop
|
|
2752
|
-
|
|
2993
|
+
val nextShouldPlay = autoplay && !paused
|
|
2994
|
+
if (playIntent != nextShouldPlay) {
|
|
2995
|
+
userPaused = false
|
|
2996
|
+
ended = false
|
|
2997
|
+
}
|
|
2998
|
+
playIntent = nextShouldPlay
|
|
2999
|
+
shouldPlay = nextShouldPlay
|
|
2753
3000
|
this.muted = muted
|
|
2754
3001
|
val floating = pictureInPicture && !muted
|
|
2755
3002
|
if (floating != this.pictureInPicture) {
|
|
@@ -2761,6 +3008,8 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
|
|
|
2761
3008
|
setMediaController(if (controls) MediaController(context).also { it.setAnchorView(this) } else null)
|
|
2762
3009
|
if (uri != sourceUri) {
|
|
2763
3010
|
sourceUri = uri
|
|
3011
|
+
userPaused = false
|
|
3012
|
+
ended = false
|
|
2764
3013
|
stopPlayback()
|
|
2765
3014
|
setPlaying(false)
|
|
2766
3015
|
preparedPlayer = null
|
|
@@ -2780,7 +3029,10 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
|
|
|
2780
3029
|
}
|
|
2781
3030
|
setOnCompletionListener {
|
|
2782
3031
|
if (shouldLoop && !isPlaying) start()
|
|
2783
|
-
else
|
|
3032
|
+
else {
|
|
3033
|
+
ended = true
|
|
3034
|
+
setPlaying(false)
|
|
3035
|
+
}
|
|
2784
3036
|
fire("onEnd")
|
|
2785
3037
|
}
|
|
2786
3038
|
setOnErrorListener { _, what, extra ->
|
|
@@ -2791,18 +3043,24 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
|
|
|
2791
3043
|
}
|
|
2792
3044
|
if (uri.isBlank()) return
|
|
2793
3045
|
runCatching {
|
|
2794
|
-
if (shouldPlay && !isPlaying) start()
|
|
3046
|
+
if (shouldPlay && !isPlaying && !userPaused && !ended) start()
|
|
2795
3047
|
else if (!shouldPlay && isPlaying) pause()
|
|
2796
3048
|
}
|
|
2797
3049
|
}
|
|
2798
3050
|
|
|
2799
3051
|
override fun start() {
|
|
2800
3052
|
super.start()
|
|
2801
|
-
|
|
3053
|
+
userPaused = false
|
|
3054
|
+
ended = false
|
|
3055
|
+
// VideoView accepts start() before its MediaPlayer is prepared. Do
|
|
3056
|
+
// not announce playback until the player reports that it is actually
|
|
3057
|
+
// running; iOS follows the same rule through AVPlayer's time status.
|
|
3058
|
+
if (runCatching { isPlaying }.getOrDefault(false)) setPlaying(true)
|
|
2802
3059
|
}
|
|
2803
3060
|
|
|
2804
3061
|
override fun pause() {
|
|
2805
3062
|
super.pause()
|
|
3063
|
+
userPaused = true
|
|
2806
3064
|
setPlaying(false)
|
|
2807
3065
|
}
|
|
2808
3066
|
|
|
@@ -2810,6 +3068,8 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
|
|
|
2810
3068
|
if (playing == value) return
|
|
2811
3069
|
playing = value
|
|
2812
3070
|
fire(if (value) "onPlay" else "onPause")
|
|
3071
|
+
progressHandler.removeCallbacks(progressTick)
|
|
3072
|
+
if (value) progressHandler.post(progressTick)
|
|
2813
3073
|
onPictureInPictureChanged?.invoke()
|
|
2814
3074
|
}
|
|
2815
3075
|
|
|
@@ -2834,12 +3094,20 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
|
|
|
2834
3094
|
return runCatching { player.setDisplay(holder ?: this.holder) }.isSuccess
|
|
2835
3095
|
}
|
|
2836
3096
|
|
|
3097
|
+
fun notifyPictureInPicture(active: Boolean) {
|
|
3098
|
+
fire(if (active) "onPictureInPictureStart" else "onPictureInPictureStop")
|
|
3099
|
+
}
|
|
3100
|
+
|
|
2837
3101
|
fun stop() {
|
|
2838
3102
|
// Released with its screen: nothing more is told to it.
|
|
2839
3103
|
emit = null
|
|
3104
|
+
progressHandler.removeCallbacks(progressTick)
|
|
2840
3105
|
stopPlayback()
|
|
2841
3106
|
setPlaying(false)
|
|
2842
3107
|
sourceUri = ""
|
|
3108
|
+
playIntent = null
|
|
3109
|
+
userPaused = false
|
|
3110
|
+
ended = false
|
|
2843
3111
|
preparedPlayer = null
|
|
2844
3112
|
if (pictureInPicture) {
|
|
2845
3113
|
pictureInPicture = false
|
|
@@ -3249,30 +3517,92 @@ private class CraftTextDecorationSpan(
|
|
|
3249
3517
|
}
|
|
3250
3518
|
}
|
|
3251
3519
|
|
|
3520
|
+
private fun craftNativeGradientOrientation(direction: String?): GradientDrawable.Orientation = when (direction) {
|
|
3521
|
+
"to-t" -> GradientDrawable.Orientation.BOTTOM_TOP
|
|
3522
|
+
"to-tr" -> GradientDrawable.Orientation.BL_TR
|
|
3523
|
+
"to-r" -> GradientDrawable.Orientation.LEFT_RIGHT
|
|
3524
|
+
"to-br" -> GradientDrawable.Orientation.TL_BR
|
|
3525
|
+
"to-bl" -> GradientDrawable.Orientation.TR_BL
|
|
3526
|
+
"to-l" -> GradientDrawable.Orientation.RIGHT_LEFT
|
|
3527
|
+
"to-tl" -> GradientDrawable.Orientation.BR_TL
|
|
3528
|
+
else -> GradientDrawable.Orientation.TOP_BOTTOM
|
|
3529
|
+
}
|
|
3530
|
+
|
|
3252
3531
|
private class CraftNativeSideBorderDrawable(
|
|
3253
3532
|
private val fillColor: Int,
|
|
3254
3533
|
private val radii: FloatArray,
|
|
3255
3534
|
private val widths: IntArray,
|
|
3256
3535
|
private val colors: IntArray,
|
|
3536
|
+
private val borderStyle: String?,
|
|
3537
|
+
private val gradientDirection: String?,
|
|
3538
|
+
private val gradientColors: IntArray?,
|
|
3257
3539
|
) : Drawable() {
|
|
3258
3540
|
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
|
3259
3541
|
|
|
3542
|
+
private fun gradient(bounds: RectF): Shader? {
|
|
3543
|
+
val colors = gradientColors ?: return null
|
|
3544
|
+
val points = when (gradientDirection) {
|
|
3545
|
+
"to-t" -> floatArrayOf(0.5f, 1f, 0.5f, 0f)
|
|
3546
|
+
"to-tr" -> floatArrayOf(0f, 1f, 1f, 0f)
|
|
3547
|
+
"to-r" -> floatArrayOf(0f, 0.5f, 1f, 0.5f)
|
|
3548
|
+
"to-br" -> floatArrayOf(0f, 0f, 1f, 1f)
|
|
3549
|
+
"to-bl" -> floatArrayOf(1f, 0f, 0f, 1f)
|
|
3550
|
+
"to-l" -> floatArrayOf(1f, 0.5f, 0f, 0.5f)
|
|
3551
|
+
"to-tl" -> floatArrayOf(1f, 1f, 0f, 0f)
|
|
3552
|
+
else -> floatArrayOf(0.5f, 0f, 0.5f, 1f)
|
|
3553
|
+
}
|
|
3554
|
+
return LinearGradient(
|
|
3555
|
+
bounds.left + bounds.width() * points[0],
|
|
3556
|
+
bounds.top + bounds.height() * points[1],
|
|
3557
|
+
bounds.left + bounds.width() * points[2],
|
|
3558
|
+
bounds.top + bounds.height() * points[3],
|
|
3559
|
+
colors,
|
|
3560
|
+
null,
|
|
3561
|
+
Shader.TileMode.CLAMP,
|
|
3562
|
+
)
|
|
3563
|
+
}
|
|
3564
|
+
|
|
3260
3565
|
override fun draw(canvas: Canvas) {
|
|
3261
3566
|
val bounds = RectF(this.bounds)
|
|
3262
3567
|
paint.style = Paint.Style.FILL
|
|
3263
3568
|
paint.color = fillColor
|
|
3264
|
-
|
|
3569
|
+
paint.shader = gradient(bounds)
|
|
3570
|
+
if (paint.shader != null || Color.alpha(fillColor) > 0) {
|
|
3265
3571
|
val path = Path().apply { addRoundRect(bounds, radii, Path.Direction.CW) }
|
|
3266
3572
|
canvas.drawPath(path, paint)
|
|
3267
3573
|
}
|
|
3574
|
+
paint.shader = null
|
|
3268
3575
|
val top = widths.getOrElse(0) { 0 }
|
|
3269
3576
|
val right = widths.getOrElse(1) { 0 }
|
|
3270
3577
|
val bottom = widths.getOrElse(2) { 0 }
|
|
3271
3578
|
val left = widths.getOrElse(3) { 0 }
|
|
3272
|
-
if (
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3579
|
+
if (borderStyle == "dashed" || borderStyle == "dotted") {
|
|
3580
|
+
paint.style = Paint.Style.STROKE
|
|
3581
|
+
paint.strokeCap = if (borderStyle == "dotted") Paint.Cap.ROUND else Paint.Cap.BUTT
|
|
3582
|
+
paint.pathEffect = null
|
|
3583
|
+
fun drawEdge(index: Int, width: Int, startX: Float, startY: Float, endX: Float, endY: Float) {
|
|
3584
|
+
if (width <= 0) return
|
|
3585
|
+
paint.color = colors.getOrElse(index) { Color.TRANSPARENT }
|
|
3586
|
+
paint.strokeWidth = width.toFloat()
|
|
3587
|
+
paint.pathEffect = if (borderStyle == "dotted") {
|
|
3588
|
+
DashPathEffect(floatArrayOf(width.toFloat(), width * 1.5f), 0f)
|
|
3589
|
+
} else {
|
|
3590
|
+
DashPathEffect(floatArrayOf(width * 3f, width * 2f), 0f)
|
|
3591
|
+
}
|
|
3592
|
+
canvas.drawLine(startX, startY, endX, endY, paint)
|
|
3593
|
+
}
|
|
3594
|
+
drawEdge(0, top, bounds.left, bounds.top + top / 2f, bounds.right, bounds.top + top / 2f)
|
|
3595
|
+
drawEdge(1, right, bounds.right - right / 2f, bounds.top, bounds.right - right / 2f, bounds.bottom)
|
|
3596
|
+
drawEdge(2, bottom, bounds.left, bounds.bottom - bottom / 2f, bounds.right, bounds.bottom - bottom / 2f)
|
|
3597
|
+
drawEdge(3, left, bounds.left + left / 2f, bounds.top, bounds.left + left / 2f, bounds.bottom)
|
|
3598
|
+
paint.pathEffect = null
|
|
3599
|
+
} else {
|
|
3600
|
+
paint.style = Paint.Style.FILL
|
|
3601
|
+
if (top > 0) { paint.color = colors.getOrElse(0) { Color.TRANSPARENT }; canvas.drawRect(bounds.left, bounds.top, bounds.right, bounds.top + top, paint) }
|
|
3602
|
+
if (right > 0) { paint.color = colors.getOrElse(1) { Color.TRANSPARENT }; canvas.drawRect(bounds.right - right, bounds.top, bounds.right, bounds.bottom, paint) }
|
|
3603
|
+
if (bottom > 0) { paint.color = colors.getOrElse(2) { Color.TRANSPARENT }; canvas.drawRect(bounds.left, bounds.bottom - bottom, bounds.right, bounds.bottom, paint) }
|
|
3604
|
+
if (left > 0) { paint.color = colors.getOrElse(3) { Color.TRANSPARENT }; canvas.drawRect(bounds.left, bounds.top, bounds.left + left, bounds.bottom, paint) }
|
|
3605
|
+
}
|
|
3276
3606
|
}
|
|
3277
3607
|
|
|
3278
3608
|
override fun setAlpha(alpha: Int) { paint.alpha = alpha; invalidateSelf() }
|
|
@@ -3326,6 +3656,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
3326
3656
|
val linkTargets = mutableMapOf<View, String>()
|
|
3327
3657
|
val pressInHandlers = mutableMapOf<View, String>()
|
|
3328
3658
|
val pressOutHandlers = mutableMapOf<View, String>()
|
|
3659
|
+
val pressUnderlayShowHandlers = mutableMapOf<View, String>()
|
|
3660
|
+
val pressUnderlayHideHandlers = mutableMapOf<View, String>()
|
|
3329
3661
|
val longPressHandlers = mutableMapOf<View, String>()
|
|
3330
3662
|
val scrollHandlers = mutableMapOf<View, String>()
|
|
3331
3663
|
val scrollBeginHandlers = mutableMapOf<View, String>()
|
|
@@ -3344,6 +3676,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
3344
3676
|
val imageErrorHandlers = mutableMapOf<ImageView, String>()
|
|
3345
3677
|
val pressActiveOpacities = mutableMapOf<View, Float>()
|
|
3346
3678
|
val pressBaseOpacities = mutableMapOf<View, Float>()
|
|
3679
|
+
val pressUnderlayColors = mutableMapOf<View, Int>()
|
|
3680
|
+
val pressBaseBackgrounds = mutableMapOf<View, Drawable?>()
|
|
3347
3681
|
val modalVisibility = mutableMapOf<View, Boolean>()
|
|
3348
3682
|
val modalCloseHandlers = mutableMapOf<View, String>()
|
|
3349
3683
|
val layoutHandlers = mutableMapOf<View, String>()
|
|
@@ -3459,6 +3793,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3459
3793
|
/** Fields that already carry their text watcher. */
|
|
3460
3794
|
private val watchedInputs: MutableSet<EditText> = java.util.Collections.newSetFromMap(WeakHashMap())
|
|
3461
3795
|
private val inputHighlightDefaults = mutableMapOf<EditText, Int>()
|
|
3796
|
+
private val inputPaddingDefaults = mutableMapOf<EditText, IntArray>()
|
|
3462
3797
|
private val defaultBackgrounds = mutableMapOf<View, Drawable?>()
|
|
3463
3798
|
private var keyboardInset = 0
|
|
3464
3799
|
private var animationScaleObserverRegistered = false
|
|
@@ -3670,6 +4005,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3670
4005
|
val target = ensureTabRoot(targetIndex) ?: return false
|
|
3671
4006
|
val host = chrome?.content ?: return false
|
|
3672
4007
|
if (target === current || host.width <= 0) return false
|
|
4008
|
+
updateSafeAreaInsets(target)
|
|
3673
4009
|
val chromeLocation = IntArray(2)
|
|
3674
4010
|
chrome?.getLocationOnScreen(chromeLocation)
|
|
3675
4011
|
if (horizontalScrollCanConsume(current.view, chromeLocation[0] + x, chromeLocation[1] + y, dx)) return false
|
|
@@ -4114,6 +4450,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4114
4450
|
visibleScreen = screen
|
|
4115
4451
|
if (screen.frameRequested) postFrame(screen)
|
|
4116
4452
|
if (bars != null) applyChrome(screen)
|
|
4453
|
+
applyScreenBackground(screen)
|
|
4117
4454
|
if (previous !== screen) {
|
|
4118
4455
|
if (previous != null && previous.active) send(previous, "SCREEN_FOCUS", JSONObject().put("focused", false))
|
|
4119
4456
|
if (screen.shown) send(screen, "SCREEN_FOCUS", JSONObject().put("focused", true))
|
|
@@ -4149,6 +4486,22 @@ class MainActivity : AppCompatActivity() {
|
|
|
4149
4486
|
bars.apply(screen.options, depth, fallback)
|
|
4150
4487
|
}
|
|
4151
4488
|
|
|
4489
|
+
/** Applies a screen-specific background, falling back to the dynamic app colour. */
|
|
4490
|
+
private fun applyScreenBackground(screen: Screen) {
|
|
4491
|
+
val configured = screen.options.opt("backgroundColor")
|
|
4492
|
+
?.takeUnless { it == JSONObject.NULL }
|
|
4493
|
+
?.let { craftNativeOptionalString(it) }
|
|
4494
|
+
?.let { runCatching { craftNativeColor(it) }.getOrNull() }
|
|
4495
|
+
screen.view.setBackgroundColor(configured ?: ContextCompat.getColor(this, R.color.background))
|
|
4496
|
+
}
|
|
4497
|
+
|
|
4498
|
+
/** Refresh controls use a contrasting system tint when no color is authored. */
|
|
4499
|
+
private fun refreshTintFallback(screen: Screen): Int {
|
|
4500
|
+
val background = (screen.view.background as? ColorDrawable)?.color
|
|
4501
|
+
?: ContextCompat.getColor(this, R.color.background)
|
|
4502
|
+
return if (Color.luminance(background) < 0.5f) Color.WHITE else Color.BLACK
|
|
4503
|
+
}
|
|
4504
|
+
|
|
4152
4505
|
/** The bridge's capability names: Craft's core set and this host's services. */
|
|
4153
4506
|
private fun capabilityNames(): JSONArray = capabilities.names().apply {
|
|
4154
4507
|
services.names().forEach { name -> put(name) }
|
|
@@ -4214,6 +4567,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4214
4567
|
}
|
|
4215
4568
|
|
|
4216
4569
|
private fun refreshScreenTheme(screen: Screen) {
|
|
4570
|
+
applyScreenBackground(screen)
|
|
4217
4571
|
screen.mutations.node("root")?.let { renderCommitted(screen, it) }
|
|
4218
4572
|
screen.controls.values.mapNotNull {
|
|
4219
4573
|
when (it) {
|
|
@@ -4236,7 +4590,20 @@ class MainActivity : AppCompatActivity() {
|
|
|
4236
4590
|
if (safeRoot) 0 else windowSafeInsets[2],
|
|
4237
4591
|
if (safeRoot) 0 else windowSafeInsets[3],
|
|
4238
4592
|
)
|
|
4239
|
-
|
|
4593
|
+
active?.let(::updateSafeAreaInsets)
|
|
4594
|
+
}
|
|
4595
|
+
|
|
4596
|
+
/**
|
|
4597
|
+
* Applies window insets to a screen that is mounted for an interactive
|
|
4598
|
+
* transition but is not visibleScreen yet. The tab swipe keeps both
|
|
4599
|
+
* screens attached while it moves the incoming one across the window, so
|
|
4600
|
+
* updating only the active screen leaves a SafeAreaView briefly measured
|
|
4601
|
+
* against zero insets.
|
|
4602
|
+
*/
|
|
4603
|
+
private fun updateSafeAreaInsets(screen: Screen) {
|
|
4604
|
+
val rootNode = screen.mutations.node("root")
|
|
4605
|
+
val safeRoot = rootNode?.let { canonicalType(it.optString("type")) == "SafeAreaView" } == true
|
|
4606
|
+
val tree = screen.view.getChildAt(0) as? CraftNativeFlexLayout
|
|
4240
4607
|
if (safeRoot) tree?.setWindowSafeInsets(windowSafeInsets[0], windowSafeInsets[1], windowSafeInsets[2], windowSafeInsets[3])
|
|
4241
4608
|
else tree?.setWindowSafeInsets(0, 0, 0, 0)
|
|
4242
4609
|
}
|
|
@@ -4325,6 +4692,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4325
4692
|
if (screen !== appScreen) {
|
|
4326
4693
|
payload.keys().forEach { key -> screen.options.put(key, payload.opt(key)) }
|
|
4327
4694
|
if (visibleScreen === screen) applyChrome(screen)
|
|
4695
|
+
if (payload.has("backgroundColor")) applyScreenBackground(screen)
|
|
4328
4696
|
}
|
|
4329
4697
|
}
|
|
4330
4698
|
"ANIMATE_NEXT" -> if (screen !== appScreen && visibleScreen === screen) animateNextChange(screen, payload)
|
|
@@ -4610,7 +4978,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
4610
4978
|
val title = craftNativeOptionalString(props.opt("title")) ?: childText(children)
|
|
4611
4979
|
isAllCaps = false
|
|
4612
4980
|
configureText(this, title, style)
|
|
4613
|
-
craftNativeOptionalString(props.opt("color"))
|
|
4981
|
+
val buttonColor = craftNativeOptionalString(props.opt("color"))
|
|
4982
|
+
?: craftNativeOptionalString(style.opt("color"))
|
|
4983
|
+
setTextColor(buttonColor?.let { color(it, defaultTextColor) } ?: defaultTextColor)
|
|
4614
4984
|
isEnabled = !props.optBoolean("disabled", false)
|
|
4615
4985
|
val handler = craftNativeEventName(events, "onPress", "onClick")
|
|
4616
4986
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
@@ -4622,11 +4992,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
4622
4992
|
"Checkbox" -> (previous as? CheckBox ?: CheckBox(this)).apply {
|
|
4623
4993
|
text = craftNativeOptionalString(props.opt("label")) ?: childText(children)
|
|
4624
4994
|
configureText(this, text.toString(), style)
|
|
4625
|
-
val
|
|
4626
|
-
val
|
|
4627
|
-
|
|
4628
|
-
is
|
|
4629
|
-
is
|
|
4995
|
+
val checkboxChecked = props.opt("checked")
|
|
4996
|
+
val checkboxValue = props.opt("value")
|
|
4997
|
+
val checked = when {
|
|
4998
|
+
checkboxChecked is Boolean -> checkboxChecked
|
|
4999
|
+
checkboxValue is Boolean -> checkboxValue
|
|
4630
5000
|
else -> false
|
|
4631
5001
|
}
|
|
4632
5002
|
setOnCheckedChangeListener(null)
|
|
@@ -4634,11 +5004,14 @@ class MainActivity : AppCompatActivity() {
|
|
|
4634
5004
|
// an indeterminate control must announce "mixed" and its next
|
|
4635
5005
|
// user toggle should transition to checked, not unchecked.
|
|
4636
5006
|
isChecked = checked
|
|
4637
|
-
|
|
5007
|
+
val indeterminate = (props.opt("indeterminate") as? Boolean) == true
|
|
5008
|
+
isActivated = indeterminate
|
|
4638
5009
|
isEnabled = !props.optBoolean("disabled", false)
|
|
4639
5010
|
val checkboxColor = craftNativeOptionalString(props.opt("color"))
|
|
4640
5011
|
?: craftNativeOptionalString(style.opt("color"))
|
|
4641
|
-
buttonTintList = checkboxColor
|
|
5012
|
+
buttonTintList = checkboxColor
|
|
5013
|
+
?.let { runCatching { craftNativeColor(it) }.getOrNull() }
|
|
5014
|
+
?.let { ColorStateList.valueOf(it) }
|
|
4642
5015
|
val handler = craftNativeEventName(events, "onValueChange", "onChange")
|
|
4643
5016
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
4644
5017
|
setOnCheckedChangeListener { _, value ->
|
|
@@ -4659,14 +5032,18 @@ class MainActivity : AppCompatActivity() {
|
|
|
4659
5032
|
hintTextColors ?: ColorStateList.valueOf(Color.GRAY)
|
|
4660
5033
|
}
|
|
4661
5034
|
val defaultHighlightColor = inputHighlightDefaults.getOrPut(this) { highlightColor }
|
|
5035
|
+
val defaultPadding = inputPaddingDefaults.getOrPut(this) {
|
|
5036
|
+
intArrayOf(paddingLeft, paddingTop, paddingRight, paddingBottom)
|
|
5037
|
+
}
|
|
4662
5038
|
hint = craftNativeOptionalString(props.opt("placeholder")).orEmpty()
|
|
4663
5039
|
setHintTextColor(
|
|
4664
5040
|
craftNativeOptionalString(props.opt("placeholderTextColor"))
|
|
4665
|
-
?.let {
|
|
5041
|
+
?.let { runCatching { craftNativeColor(it) }.getOrNull() }
|
|
5042
|
+
?.let { ColorStateList.valueOf(it) }
|
|
4666
5043
|
?: defaultHintColors,
|
|
4667
5044
|
)
|
|
4668
5045
|
highlightColor = craftNativeOptionalString(props.opt("selectionColor"))
|
|
4669
|
-
?.let {
|
|
5046
|
+
?.let { runCatching { craftNativeColor(it) }.getOrNull() }
|
|
4670
5047
|
?: defaultHighlightColor
|
|
4671
5048
|
val maxLength = props.optInt("maxLength", 0)
|
|
4672
5049
|
val currentLength = (filters.firstOrNull { it is InputFilter.LengthFilter } as? InputFilter.LengthFilter)?.max ?: 0
|
|
@@ -4685,7 +5062,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
4685
5062
|
configureTextStyle(this, style)
|
|
4686
5063
|
// The style's padding is where the text starts, as on iOS and
|
|
4687
5064
|
// the web; without one the field keeps its own.
|
|
4688
|
-
|
|
5065
|
+
val paddingKeys = listOf("padding", "paddingHorizontal", "paddingVertical", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom")
|
|
5066
|
+
if (paddingKeys.any(style::has)) {
|
|
4689
5067
|
val density = resources.displayMetrics.density
|
|
4690
5068
|
val all = craftNativePadding(style, "padding", 0, density)
|
|
4691
5069
|
val horizontal = craftNativePadding(style, "paddingHorizontal", all, density)
|
|
@@ -4696,6 +5074,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
4696
5074
|
craftNativePadding(style, "paddingRight", horizontal, density),
|
|
4697
5075
|
craftNativePadding(style, "paddingBottom", vertical, density),
|
|
4698
5076
|
)
|
|
5077
|
+
} else {
|
|
5078
|
+
// A keyed field can lose its authored padding while the
|
|
5079
|
+
// EditText itself is retained. Restore the native baseline
|
|
5080
|
+
// rather than carrying the previous field's spacing.
|
|
5081
|
+
setPadding(defaultPadding[0], defaultPadding[1], defaultPadding[2], defaultPadding[3])
|
|
4699
5082
|
}
|
|
4700
5083
|
// Reused EditTexts must forget the previous field's hint as
|
|
4701
5084
|
// well as accept a new one. Otherwise a password manager can
|
|
@@ -4772,7 +5155,14 @@ class MainActivity : AppCompatActivity() {
|
|
|
4772
5155
|
})
|
|
4773
5156
|
}
|
|
4774
5157
|
"Picker" -> (previous as? Spinner ?: Spinner(this)).apply {
|
|
4775
|
-
val
|
|
5158
|
+
val itemOptions = props.optJSONArray("items")?.let { items ->
|
|
5159
|
+
(0 until items.length()).mapNotNull { index ->
|
|
5160
|
+
val item = items.optJSONObject(index) ?: return@mapNotNull null
|
|
5161
|
+
val label = craftNativeOptionalString(item.opt("label")) ?: return@mapNotNull null
|
|
5162
|
+
craftNativePickerValue(item.opt("value"))?.let { it to label } ?: (label to label)
|
|
5163
|
+
}
|
|
5164
|
+
}.orEmpty()
|
|
5165
|
+
val options = if (itemOptions.isNotEmpty()) itemOptions else (0 until children.length()).mapNotNull { index ->
|
|
4776
5166
|
val option = children.optJSONObject(index) ?: return@mapNotNull null
|
|
4777
5167
|
if (option.optString("type") != "Text") return@mapNotNull null
|
|
4778
5168
|
val label = (option.optJSONArray("children") ?: JSONArray()).let { text ->
|
|
@@ -4780,7 +5170,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4780
5170
|
}
|
|
4781
5171
|
if (label.isBlank()) return@mapNotNull null
|
|
4782
5172
|
val optionProps = option.optJSONObject("props")
|
|
4783
|
-
val value =
|
|
5173
|
+
val value = craftNativePickerValue(optionProps?.opt("value")) ?: label
|
|
4784
5174
|
value to label
|
|
4785
5175
|
}
|
|
4786
5176
|
val handler = craftNativeEventName(events, "onValueChange", "onChange")
|
|
@@ -4792,8 +5182,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
4792
5182
|
adapter = ArrayAdapter(this@MainActivity, android.R.layout.simple_spinner_item, options.map { it.second }).also {
|
|
4793
5183
|
it.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
|
4794
5184
|
}
|
|
4795
|
-
val requested =
|
|
4796
|
-
?:
|
|
5185
|
+
val requested = craftNativePickerValue(props.opt("selectedValue"))
|
|
5186
|
+
?: craftNativePickerValue(props.opt("value")).orEmpty()
|
|
4797
5187
|
val selected = options.indexOfFirst { it.first == requested }.takeIf { it >= 0 } ?: 0
|
|
4798
5188
|
isEnabled = !props.optBoolean("disabled", false)
|
|
4799
5189
|
// Mounting a controlled picker must not look like a user
|
|
@@ -4870,8 +5260,16 @@ class MainActivity : AppCompatActivity() {
|
|
|
4870
5260
|
}
|
|
4871
5261
|
"TouchableOpacity", "TouchableHighlight", "Pressable" -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
|
|
4872
5262
|
configureContainer(this, style)
|
|
5263
|
+
val disabled = props.optBoolean("disabled", false)
|
|
4873
5264
|
screen.pressActiveOpacities[this] = craftNativeFiniteFloat(props, "activeOpacity", 0.5f)
|
|
4874
5265
|
screen.pressBaseOpacities[this] = craftNativeFiniteFloat(style, "opacity", 1f)
|
|
5266
|
+
if (type == "TouchableHighlight" && !disabled) {
|
|
5267
|
+
craftNativeOptionalString(props.opt("underlayColor"))?.let { screen.pressUnderlayColors[this] = color(it, Color.TRANSPARENT) }
|
|
5268
|
+
?: screen.pressUnderlayColors.remove(this)
|
|
5269
|
+
} else {
|
|
5270
|
+
screen.pressUnderlayColors.remove(this)
|
|
5271
|
+
}
|
|
5272
|
+
screen.pressBaseBackgrounds[this] = this.background
|
|
4875
5273
|
reconcileChildren(screen, this, children, identity, next, style, previousControls)
|
|
4876
5274
|
}
|
|
4877
5275
|
"Switch" -> (previous as? Switch ?: Switch(this)).apply {
|
|
@@ -4879,24 +5277,29 @@ class MainActivity : AppCompatActivity() {
|
|
|
4879
5277
|
val requestedValue = props.opt("value")
|
|
4880
5278
|
val requestedChecked = props.opt("checked")
|
|
4881
5279
|
isChecked = when {
|
|
4882
|
-
requestedValue != null && requestedValue != JSONObject.NULL ->
|
|
4883
|
-
requestedChecked == null || requestedChecked == JSONObject.NULL -> false
|
|
5280
|
+
requestedValue != null && requestedValue != JSONObject.NULL -> requestedValue as? Boolean ?: false
|
|
4884
5281
|
requestedChecked is Boolean -> requestedChecked
|
|
4885
|
-
|
|
4886
|
-
requestedChecked is String -> requestedChecked.isBlank() || requestedChecked.equals("true", ignoreCase = true)
|
|
4887
|
-
else -> props.optBoolean("checked", false)
|
|
5282
|
+
else -> false
|
|
4888
5283
|
}
|
|
4889
5284
|
isEnabled = !props.optBoolean("disabled", false)
|
|
4890
5285
|
props.optJSONObject("trackColor")?.let { track ->
|
|
4891
|
-
val
|
|
4892
|
-
val
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
5286
|
+
val falseRaw = craftNativeOptionalString(track.opt("false"))
|
|
5287
|
+
val trueRaw = craftNativeOptionalString(track.opt("true"))
|
|
5288
|
+
val falseColor = falseRaw?.let { runCatching { craftNativeColor(it) }.getOrNull() }
|
|
5289
|
+
val trueColor = trueRaw?.let { runCatching { craftNativeColor(it) }.getOrNull() }
|
|
5290
|
+
trackTintList = if ((falseRaw != null && falseColor == null) || (trueRaw != null && trueColor == null)) {
|
|
5291
|
+
null
|
|
5292
|
+
} else {
|
|
5293
|
+
val resolvedFalse = falseColor ?: Color.GRAY
|
|
5294
|
+
ColorStateList(
|
|
5295
|
+
arrayOf(intArrayOf(android.R.attr.state_checked), intArrayOf()),
|
|
5296
|
+
intArrayOf(trueColor ?: resolvedFalse, resolvedFalse),
|
|
5297
|
+
)
|
|
5298
|
+
}
|
|
4897
5299
|
} ?: run { trackTintList = null }
|
|
4898
5300
|
craftNativeOptionalString(props.opt("thumbColor"))
|
|
4899
|
-
?.let {
|
|
5301
|
+
?.let { runCatching { craftNativeColor(it) }.getOrNull() }
|
|
5302
|
+
?.let { thumbTintList = ColorStateList.valueOf(it) }
|
|
4900
5303
|
?: run { thumbTintList = null }
|
|
4901
5304
|
val handler = craftNativeEventName(events, "onValueChange", "onChange")
|
|
4902
5305
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
@@ -4933,15 +5336,15 @@ class MainActivity : AppCompatActivity() {
|
|
|
4933
5336
|
(((value - minimum) / (maximum - minimum)) * max).roundToInt().coerceIn(0, max)
|
|
4934
5337
|
}
|
|
4935
5338
|
if (step != null) screen.sliderSteps[this] = step else screen.sliderSteps.remove(this)
|
|
4936
|
-
craftNativeOptionalString(props.opt("minimumTrackTintColor"))
|
|
4937
|
-
?.let {
|
|
4938
|
-
|
|
4939
|
-
craftNativeOptionalString(props.opt("maximumTrackTintColor"))
|
|
4940
|
-
?.let {
|
|
4941
|
-
|
|
4942
|
-
craftNativeOptionalString(props.opt("thumbTintColor"))
|
|
4943
|
-
?.let {
|
|
4944
|
-
|
|
5339
|
+
progressTintList = craftNativeOptionalString(props.opt("minimumTrackTintColor"))
|
|
5340
|
+
?.let { runCatching { craftNativeColor(it) }.getOrNull() }
|
|
5341
|
+
?.let { ColorStateList.valueOf(it) }
|
|
5342
|
+
progressBackgroundTintList = craftNativeOptionalString(props.opt("maximumTrackTintColor"))
|
|
5343
|
+
?.let { runCatching { craftNativeColor(it) }.getOrNull() }
|
|
5344
|
+
?.let { ColorStateList.valueOf(it) }
|
|
5345
|
+
thumbTintList = craftNativeOptionalString(props.opt("thumbTintColor"))
|
|
5346
|
+
?.let { runCatching { craftNativeColor(it) }.getOrNull() }
|
|
5347
|
+
?.let { ColorStateList.valueOf(it) }
|
|
4945
5348
|
isEnabled = !props.optBoolean("disabled", false)
|
|
4946
5349
|
val handler = craftNativeEventName(events, "onValueChange", "onChange")
|
|
4947
5350
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
@@ -4979,9 +5382,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
4979
5382
|
val animating = props.optBoolean("animating", true)
|
|
4980
5383
|
isIndeterminate = animating
|
|
4981
5384
|
visibility = if (!animating && props.optBoolean("hidesWhenStopped", true)) View.GONE else View.VISIBLE
|
|
4982
|
-
craftNativeOptionalString(props.opt("color"))
|
|
4983
|
-
?.let {
|
|
4984
|
-
|
|
5385
|
+
indeterminateTintList = craftNativeOptionalString(props.opt("color"))
|
|
5386
|
+
?.let { runCatching { craftNativeColor(it) }.getOrNull() }
|
|
5387
|
+
?.let { ColorStateList.valueOf(it) }
|
|
4985
5388
|
}
|
|
4986
5389
|
"Icon" -> (previous as? CraftNativeIconView ?: CraftNativeIconView(this)).apply {
|
|
4987
5390
|
configureIcon(this, props, style)
|
|
@@ -5011,6 +5414,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
5011
5414
|
"center" -> ImageView.ScaleType.CENTER
|
|
5012
5415
|
else -> ImageView.ScaleType.CENTER_INSIDE
|
|
5013
5416
|
}
|
|
5417
|
+
val tint = craftNativeOptionalString(style.opt("tintColor"))
|
|
5418
|
+
?.let { runCatching { craftNativeColor(it) }.getOrNull() }
|
|
5419
|
+
if (tint == null) image.clearColorFilter() else image.setColorFilter(tint)
|
|
5014
5420
|
val onLoadStart = craftNativeEventName(events, "onLoadStart")
|
|
5015
5421
|
if (onLoadStart.isNotBlank()) screen.imageLoadStartHandlers[image] = onLoadStart else screen.imageLoadStartHandlers.remove(image)
|
|
5016
5422
|
val onLoad = craftNativeEventName(events, "onLoad")
|
|
@@ -5041,8 +5447,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
5041
5447
|
)
|
|
5042
5448
|
}
|
|
5043
5449
|
"Map" -> (previous as? CraftNativeMapView ?: CraftNativeMapView(this)).apply {
|
|
5450
|
+
val mapMarkerHandler = craftNativeOptionalString(events.opt("onMarkerPress"))
|
|
5044
5451
|
configure(props, children) { markerId ->
|
|
5045
|
-
markerHandler(markerId)?.let { handler ->
|
|
5452
|
+
(markerHandler(markerId) ?: mapMarkerHandler)?.let { handler ->
|
|
5046
5453
|
send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject().put("id", markerId)))
|
|
5047
5454
|
}
|
|
5048
5455
|
}
|
|
@@ -5069,7 +5476,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
5069
5476
|
val refreshNode = (0 until children.length()).asSequence()
|
|
5070
5477
|
.mapNotNull { children.optJSONObject(it) }
|
|
5071
5478
|
.firstOrNull { canonicalType(it.optString("type")) == "RefreshControl" }
|
|
5072
|
-
val
|
|
5479
|
+
val propRefresh = props.optJSONObject("refreshControl")
|
|
5480
|
+
val refreshProps = refreshNode?.optJSONObject("props") ?: propRefresh ?: JSONObject()
|
|
5073
5481
|
val refreshEvents = refreshNode?.optJSONObject("events") ?: JSONObject()
|
|
5074
5482
|
val refreshHandlerName = craftNativeEventName(refreshEvents, "onRefresh")
|
|
5075
5483
|
.takeIf { it.isNotBlank() }
|
|
@@ -5077,8 +5485,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
5077
5485
|
val refreshHandler = refreshHandlerName?.let { handler ->
|
|
5078
5486
|
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject())) }
|
|
5079
5487
|
}
|
|
5080
|
-
setRefreshHandler(refreshHandler,
|
|
5081
|
-
setRefreshAppearance(refreshProps, resources.displayMetrics.density)
|
|
5488
|
+
setRefreshHandler(refreshHandler, refreshProps.optBoolean("refreshing", props.optBoolean("refreshing", false)))
|
|
5489
|
+
setRefreshAppearance(refreshProps, resources.displayMetrics.density, refreshTintFallback(screen))
|
|
5082
5490
|
val onScroll = craftNativeEventName(events, "onScroll")
|
|
5083
5491
|
if (onScroll.isNotBlank()) screen.scrollHandlers[this] = onScroll else screen.scrollHandlers.remove(this)
|
|
5084
5492
|
val onBegin = craftNativeEventName(events, "onScrollBeginDrag")
|
|
@@ -5143,19 +5551,26 @@ class MainActivity : AppCompatActivity() {
|
|
|
5143
5551
|
if (child !is JSONObject || canonicalType(child.optString("type")) != "RefreshControl") contentChildren.put(child)
|
|
5144
5552
|
}
|
|
5145
5553
|
reconcileChildren(screen, content, contentChildren, identity, next, contentStyle, previousControls)
|
|
5554
|
+
setStickyHeaderIndices(props.optJSONArray("stickyHeaderIndices"))
|
|
5555
|
+
setScrollTarget(props.opt("scrollTarget"))
|
|
5146
5556
|
}
|
|
5147
5557
|
"FlatList" -> (previous as? CraftNativeRefreshableFlatList ?: CraftNativeRefreshableFlatList(this)).apply {
|
|
5148
5558
|
val nativeList = list
|
|
5149
|
-
|
|
5559
|
+
val propRefresh = props.optJSONObject("refreshControl") ?: JSONObject()
|
|
5560
|
+
val propRefreshEvents = propRefresh.optJSONObject("events") ?: JSONObject()
|
|
5561
|
+
val refreshHandlerName = craftNativeEventName(propRefreshEvents, "onRefresh")
|
|
5562
|
+
.takeIf { it.isNotBlank() }
|
|
5563
|
+
?: craftNativeEventName(events, "onRefresh").takeIf { it.isNotBlank() }
|
|
5564
|
+
configureRefresh(refreshHandlerName?.let { handler ->
|
|
5150
5565
|
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject())) }
|
|
5151
|
-
}, props.optBoolean("refreshing", false))
|
|
5566
|
+
}, propRefresh.optBoolean("refreshing", props.optBoolean("refreshing", false)))
|
|
5567
|
+
setRefreshAppearance(propRefresh, refreshTintFallback(screen))
|
|
5152
5568
|
nativeList.setKeyboardShouldPersistTaps(props.optString("keyboardShouldPersistTaps", "never"))
|
|
5153
5569
|
nativeList.setContentContainerStyle(props.optJSONObject("contentContainerStyle"), resources.displayMetrics.density)
|
|
5154
5570
|
nativeList.setKeyboardDismissMode(props.optString("keyboardDismissMode")) {
|
|
5155
5571
|
(getSystemService(INPUT_METHOD_SERVICE) as? android.view.inputmethod.InputMethodManager)
|
|
5156
5572
|
?.hideSoftInputFromWindow(windowToken, 0)
|
|
5157
5573
|
}
|
|
5158
|
-
nativeList.overScrollMode = if (props.optBoolean("bounces", true)) View.OVER_SCROLL_ALWAYS else View.OVER_SCROLL_NEVER
|
|
5159
5574
|
reconcileFlatList(screen, nativeList, children, identity, protocolId ?: identity, props, events)
|
|
5160
5575
|
}
|
|
5161
5576
|
"View", "SafeAreaView" -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
|
|
@@ -5182,6 +5597,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
5182
5597
|
val pressOutHandler = craftNativeEventName(events, "onPressOut").takeIf { it.isNotBlank() }
|
|
5183
5598
|
if (pressInHandler != null) screen.pressInHandlers[control] = pressInHandler else screen.pressInHandlers.remove(control)
|
|
5184
5599
|
if (pressOutHandler != null) screen.pressOutHandlers[control] = pressOutHandler else screen.pressOutHandlers.remove(control)
|
|
5600
|
+
val underlayShowHandler = if (type == "TouchableHighlight") craftNativeEventName(events, "onShowUnderlay").takeIf { it.isNotBlank() } else null
|
|
5601
|
+
val underlayHideHandler = if (type == "TouchableHighlight") craftNativeEventName(events, "onHideUnderlay").takeIf { it.isNotBlank() } else null
|
|
5602
|
+
if (underlayShowHandler != null) screen.pressUnderlayShowHandlers[control] = underlayShowHandler else screen.pressUnderlayShowHandlers.remove(control)
|
|
5603
|
+
if (underlayHideHandler != null) screen.pressUnderlayHideHandlers[control] = underlayHideHandler else screen.pressUnderlayHideHandlers.remove(control)
|
|
5185
5604
|
if (type != "Button" && type != "Link" && type != "Checkbox" && type != "TextInput" && type != "Picker" && type != "DatePicker" && type != "Modal" && type != "Switch" && type != "Slider" && type != "ActivityIndicator" && type != "KeyboardAvoidingView" && type != "StatusBar" && type != "RefreshControl" && type != "Map") {
|
|
5186
5605
|
val handler = craftNativeEventName(events, "onPress", "onClick")
|
|
5187
5606
|
val disabled = props.optBoolean("disabled", false)
|
|
@@ -5190,7 +5609,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
5190
5609
|
?.trim()
|
|
5191
5610
|
?.takeIf { !disabled && type in setOf("TouchableOpacity", "TouchableHighlight", "Pressable") }
|
|
5192
5611
|
if (target != null) screen.linkTargets[control] = target else screen.linkTargets.remove(control)
|
|
5193
|
-
val
|
|
5612
|
+
val hasUnderlay = type == "TouchableHighlight" && screen.pressUnderlayColors[control] != null
|
|
5613
|
+
val hasPressLifecycle = pressInHandler != null || pressOutHandler != null || hasUnderlay
|
|
5194
5614
|
if ((handler.isNotBlank() || target != null || (type in setOf("TouchableOpacity", "TouchableHighlight", "Pressable") && hasPressLifecycle)) && !disabled) {
|
|
5195
5615
|
if (handler.isNotBlank()) screen.handlers[control] = handler else screen.handlers.remove(control)
|
|
5196
5616
|
control.isClickable = true
|
|
@@ -5206,16 +5626,24 @@ class MainActivity : AppCompatActivity() {
|
|
|
5206
5626
|
val active = screen.pressActiveOpacities[view] ?: 0.5f
|
|
5207
5627
|
val base = screen.pressBaseOpacities[view] ?: 1f
|
|
5208
5628
|
view.alpha = base * active
|
|
5629
|
+
screen.pressUnderlayColors[view]?.let { view.background = ColorDrawable(it) }
|
|
5209
5630
|
screen.pressInHandlers[view]?.let { current ->
|
|
5210
5631
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
5211
5632
|
}
|
|
5633
|
+
screen.pressUnderlayShowHandlers[view]?.let { current ->
|
|
5634
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
5635
|
+
}
|
|
5212
5636
|
true
|
|
5213
5637
|
}
|
|
5214
5638
|
android.view.MotionEvent.ACTION_UP, android.view.MotionEvent.ACTION_CANCEL -> {
|
|
5215
5639
|
view.alpha = screen.pressBaseOpacities[view] ?: 1f
|
|
5640
|
+
if (screen.pressBaseBackgrounds.containsKey(view)) view.background = screen.pressBaseBackgrounds[view]
|
|
5216
5641
|
screen.pressOutHandlers[view]?.let { current ->
|
|
5217
5642
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
5218
5643
|
}
|
|
5644
|
+
screen.pressUnderlayHideHandlers[view]?.let { current ->
|
|
5645
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
5646
|
+
}
|
|
5219
5647
|
if (event.actionMasked == android.view.MotionEvent.ACTION_UP) {
|
|
5220
5648
|
screen.linkTargets[view]?.let { target -> navigateOpen(target); return@setOnTouchListener true }
|
|
5221
5649
|
val current = screen.handlers[view] ?: return@setOnTouchListener true
|
|
@@ -5228,6 +5656,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5228
5656
|
}
|
|
5229
5657
|
} else {
|
|
5230
5658
|
control.setOnTouchListener(null)
|
|
5659
|
+
if (screen.pressBaseBackgrounds.containsKey(control)) control.background = screen.pressBaseBackgrounds[control]
|
|
5231
5660
|
control.setOnClickListener {
|
|
5232
5661
|
screen.linkTargets[control]?.let { target -> navigateOpen(target); return@setOnClickListener }
|
|
5233
5662
|
val current = screen.handlers[control] ?: return@setOnClickListener
|
|
@@ -5405,8 +5834,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5405
5834
|
is JSONObject -> makeView(screen, child, "$path.$index", next, previousControls)
|
|
5406
5835
|
is String -> TextView(this).apply {
|
|
5407
5836
|
includeFontPadding = false
|
|
5408
|
-
|
|
5409
|
-
setTextColor(defaultTextColor)
|
|
5837
|
+
configureText(this, child, craftNativeTypography(style))
|
|
5410
5838
|
}
|
|
5411
5839
|
else -> null
|
|
5412
5840
|
}
|
|
@@ -5464,9 +5892,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
5464
5892
|
}
|
|
5465
5893
|
val threshold = craftNativeNonNegativeDouble(props, "threshold", 0.1)
|
|
5466
5894
|
val authoredThreshold = craftNativeNonNegativeDouble(props, "onEndReachedThreshold", threshold)
|
|
5895
|
+
val horizontal = props.optBoolean("horizontal", false)
|
|
5467
5896
|
list.apply(
|
|
5468
5897
|
nodes = nodes,
|
|
5469
|
-
horizontal =
|
|
5898
|
+
horizontal = horizontal,
|
|
5470
5899
|
columns = props.optInt("numColumns", 1),
|
|
5471
5900
|
inverted = props.optBoolean("inverted", false),
|
|
5472
5901
|
endReachedThreshold = authoredThreshold,
|
|
@@ -5487,6 +5916,18 @@ class MainActivity : AppCompatActivity() {
|
|
|
5487
5916
|
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject())) }
|
|
5488
5917
|
},
|
|
5489
5918
|
)
|
|
5919
|
+
list.setScrollIndicators(
|
|
5920
|
+
props.optBoolean("showsVerticalScrollIndicator", true),
|
|
5921
|
+
props.optBoolean("showsHorizontalScrollIndicator", true),
|
|
5922
|
+
)
|
|
5923
|
+
list.setScrollEnabled(props.optBoolean("scrollEnabled", true))
|
|
5924
|
+
list.setPagingEnabled(props.optBoolean("pagingEnabled", false))
|
|
5925
|
+
list.setScrollTarget(props.opt("scrollTarget"))
|
|
5926
|
+
list.setBounceOptions(
|
|
5927
|
+
props.optBoolean("bounces", true),
|
|
5928
|
+
props.optBoolean("alwaysBounceVertical", !horizontal),
|
|
5929
|
+
props.optBoolean("alwaysBounceHorizontal", horizontal),
|
|
5930
|
+
)
|
|
5490
5931
|
}
|
|
5491
5932
|
|
|
5492
5933
|
private fun registerFlatListOwnership(screen: Screen, node: JSONObject, owner: String) {
|
|
@@ -5519,7 +5960,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
5519
5960
|
craftNativePadding(style, "paddingRight", horizontal, density),
|
|
5520
5961
|
craftNativePadding(style, "paddingBottom", vertical, density),
|
|
5521
5962
|
)
|
|
5522
|
-
} else if (view.javaClass == TextView::class.java) {
|
|
5963
|
+
} else if (view.javaClass == TextView::class.java || view.javaClass == Button::class.java) {
|
|
5964
|
+
// Plain Android buttons start with zero padding in the native
|
|
5965
|
+
// contract. A keyed button may lose a reactive padding style;
|
|
5966
|
+
// clear that old value just as iOS reapplies its padding every
|
|
5967
|
+
// reconciliation.
|
|
5523
5968
|
view.setPadding(0, 0, 0, 0)
|
|
5524
5969
|
}
|
|
5525
5970
|
}
|
|
@@ -5537,6 +5982,17 @@ class MainActivity : AppCompatActivity() {
|
|
|
5537
5982
|
else color(style.optString("backgroundColor"), Color.TRANSPARENT)
|
|
5538
5983
|
val border = craftNativeNonNegativeDouble(style, "borderWidth", 0.0)
|
|
5539
5984
|
val radius = craftNativeNonNegativeDouble(style, "borderRadius", 0.0)
|
|
5985
|
+
val gradientDirection = style.optString("gradientDirection").takeIf { it.isNotBlank() }
|
|
5986
|
+
val gradientFrom = style.optString("gradientFrom").takeIf { it.isNotBlank() }
|
|
5987
|
+
val gradientVia = style.optString("gradientVia").takeIf { it.isNotBlank() }
|
|
5988
|
+
val gradientTo = style.optString("gradientTo").takeIf { it.isNotBlank() }
|
|
5989
|
+
val gradientColors = if (gradientDirection != null && gradientFrom != null && gradientTo != null) {
|
|
5990
|
+
listOfNotNull(
|
|
5991
|
+
color(gradientFrom, Color.TRANSPARENT),
|
|
5992
|
+
gradientVia?.let { color(it, Color.TRANSPARENT) },
|
|
5993
|
+
color(gradientTo, Color.TRANSPARENT),
|
|
5994
|
+
).toIntArray()
|
|
5995
|
+
} else null
|
|
5540
5996
|
val sideBorderKeys = listOf("borderTopWidth", "borderRightWidth", "borderBottomWidth", "borderLeftWidth")
|
|
5541
5997
|
val hasSideBorders = (sideBorderKeys + listOf("borderTopColor", "borderRightColor", "borderBottomColor", "borderLeftColor")).any(style::has)
|
|
5542
5998
|
val sideWidths = sideBorderKeys.map { dp(craftNativeNonNegativeDouble(style, it, border)) }.toIntArray()
|
|
@@ -5554,17 +6010,23 @@ class MainActivity : AppCompatActivity() {
|
|
|
5554
6010
|
} else {
|
|
5555
6011
|
view.background.also { defaultBackgrounds[view] = it }
|
|
5556
6012
|
}
|
|
5557
|
-
if (background || border > 0 || radius > 0 || hasSideBorders) {
|
|
6013
|
+
if (background || border > 0 || radius > 0 || hasSideBorders || gradientColors != null) {
|
|
5558
6014
|
if (hasSideBorders) {
|
|
5559
6015
|
view.background = CraftNativeSideBorderDrawable(
|
|
5560
6016
|
backgroundColor,
|
|
5561
6017
|
cornerRadii,
|
|
5562
6018
|
sideWidths,
|
|
5563
6019
|
sideColors,
|
|
6020
|
+
style.optString("borderStyle").takeIf { it == "dashed" || it == "dotted" },
|
|
6021
|
+
gradientDirection,
|
|
6022
|
+
gradientColors,
|
|
5564
6023
|
)
|
|
5565
6024
|
} else {
|
|
5566
6025
|
view.background = GradientDrawable().apply {
|
|
5567
|
-
|
|
6026
|
+
if (gradientColors != null) {
|
|
6027
|
+
orientation = craftNativeGradientOrientation(gradientDirection)
|
|
6028
|
+
colors = gradientColors
|
|
6029
|
+
} else setColor(backgroundColor)
|
|
5568
6030
|
if (hasCornerRadii) setCornerRadii(cornerRadii) else cornerRadius = dp(radius).toFloat()
|
|
5569
6031
|
if (border > 0) {
|
|
5570
6032
|
val borderColor = color(style.optString("borderColor"), Color.TRANSPARENT)
|
|
@@ -5605,13 +6067,25 @@ class MainActivity : AppCompatActivity() {
|
|
|
5605
6067
|
view.invalidateOutline()
|
|
5606
6068
|
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
5607
6069
|
view.outlineProvider = ViewOutlineProvider.BACKGROUND
|
|
6070
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
6071
|
+
// A retained view may lose an authored shadow while keeping
|
|
6072
|
+
// its native object. Restore Android's default outline colors
|
|
6073
|
+
// too; otherwise a later elevation-only style inherits the
|
|
6074
|
+
// previous shadow's custom ambient/spot tint.
|
|
6075
|
+
view.outlineAmbientShadowColor = Color.BLACK
|
|
6076
|
+
view.outlineSpotShadowColor = Color.BLACK
|
|
6077
|
+
view.invalidateOutline()
|
|
6078
|
+
}
|
|
5608
6079
|
}
|
|
5609
6080
|
if (view is ImageView) {
|
|
5610
6081
|
val tint = style.opt("tintColor") as? String
|
|
5611
|
-
|
|
6082
|
+
val resolvedTint = tint
|
|
6083
|
+
?.takeUnless { it.isBlank() || it == "null" || it == "undefined" }
|
|
6084
|
+
?.let { runCatching { craftNativeColor(it) }.getOrNull() }
|
|
6085
|
+
if (resolvedTint == null) {
|
|
5612
6086
|
view.clearColorFilter()
|
|
5613
6087
|
} else {
|
|
5614
|
-
view.setColorFilter(
|
|
6088
|
+
view.setColorFilter(resolvedTint)
|
|
5615
6089
|
}
|
|
5616
6090
|
}
|
|
5617
6091
|
}
|
|
@@ -5682,6 +6156,22 @@ class MainActivity : AppCompatActivity() {
|
|
|
5682
6156
|
configureTextStyle(view, style)
|
|
5683
6157
|
}
|
|
5684
6158
|
|
|
6159
|
+
/**
|
|
6160
|
+
* Raw strings inside a native container are implicit Text nodes. Keep
|
|
6161
|
+
* their typography, but not the parent's padding or layout, just like
|
|
6162
|
+
* the iOS reconciler does for the same child shape.
|
|
6163
|
+
*/
|
|
6164
|
+
private fun craftNativeTypography(style: JSONObject): JSONObject = JSONObject().also { typography ->
|
|
6165
|
+
listOf(
|
|
6166
|
+
"color", "letterSpacing", "lineHeight", "fontSize", "fontFamily",
|
|
6167
|
+
"fontWeight", "fontStyle", "textAlign", "textTransform",
|
|
6168
|
+
"textDecorationLine", "textDecorationStyle", "textDecorationColor",
|
|
6169
|
+
"textShadowColor", "textShadowRadius", "textShadowOffset",
|
|
6170
|
+
).forEach { key ->
|
|
6171
|
+
if (style.has(key) && !style.isNull(key)) typography.put(key, style.opt(key))
|
|
6172
|
+
}
|
|
6173
|
+
}
|
|
6174
|
+
|
|
5685
6175
|
private fun configureIcon(view: CraftNativeIconView, props: JSONObject, style: JSONObject) {
|
|
5686
6176
|
val symbol = craftNativeOptionalString(props.opt("symbol"))
|
|
5687
6177
|
?: craftNativeOptionalString(props.opt("name")).orEmpty()
|
|
@@ -5706,10 +6196,18 @@ class MainActivity : AppCompatActivity() {
|
|
|
5706
6196
|
else -> Typeface.NORMAL
|
|
5707
6197
|
}
|
|
5708
6198
|
view.typeface = Typeface.create("sans-serif", weight)
|
|
5709
|
-
|
|
6199
|
+
val hasExplicitWidth = style.has("width") && !style.isNull("width")
|
|
6200
|
+
val hasExplicitHeight = style.has("height") && !style.isNull("height")
|
|
6201
|
+
if (!hasExplicitWidth && !hasExplicitHeight) {
|
|
5710
6202
|
val box = dp(size.toDouble())
|
|
5711
6203
|
view.minimumWidth = box
|
|
5712
6204
|
view.minimumHeight = box
|
|
6205
|
+
} else {
|
|
6206
|
+
// A retained icon may move from the implicit square to an
|
|
6207
|
+
// explicitly sized layout. Do not let the old minimum constrain
|
|
6208
|
+
// the authored dimensions.
|
|
6209
|
+
view.minimumWidth = 0
|
|
6210
|
+
view.minimumHeight = 0
|
|
5713
6211
|
}
|
|
5714
6212
|
}
|
|
5715
6213
|
|
|
@@ -5859,6 +6357,17 @@ class MainActivity : AppCompatActivity() {
|
|
|
5859
6357
|
else -> ""
|
|
5860
6358
|
}
|
|
5861
6359
|
|
|
6360
|
+
private fun imageHeaders(source: Any?): Map<String, String> {
|
|
6361
|
+
val objectSource = source as? JSONObject ?: return emptyMap()
|
|
6362
|
+
val raw = objectSource.optJSONObject("headers") ?: return emptyMap()
|
|
6363
|
+
return linkedMapOf<String, String>().apply {
|
|
6364
|
+
raw.keys().forEach { name -> craftNativeOptionalString(raw.opt(name))?.let { put(name, it) } }
|
|
6365
|
+
}
|
|
6366
|
+
}
|
|
6367
|
+
|
|
6368
|
+
private fun imageRequestKey(uri: String, headers: Map<String, String>): String =
|
|
6369
|
+
uri + "\u0000" + headers.toSortedMap().entries.joinToString("\u0000") { "${it.key}=${it.value}" }
|
|
6370
|
+
|
|
5862
6371
|
private fun immediateImage(uri: String): Bitmap? {
|
|
5863
6372
|
if (uri.startsWith("data:image/") && uri.contains(',')) {
|
|
5864
6373
|
val bytes = runCatching { Base64.decode(uri.substringAfter(','), Base64.DEFAULT) }.getOrNull()
|
|
@@ -5870,6 +6379,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
5870
6379
|
|
|
5871
6380
|
private fun updateImage(screen: Screen, view: ImageView, source: Any?, defaultSource: Any? = null) {
|
|
5872
6381
|
val uri = imageUri(source)
|
|
6382
|
+
val headers = imageHeaders(source)
|
|
6383
|
+
val requestKey = imageRequestKey(uri, headers)
|
|
5873
6384
|
if (uri.isBlank()) {
|
|
5874
6385
|
if (imageSources[view] == "") return
|
|
5875
6386
|
imageJobs.remove(view)?.cancel(true)
|
|
@@ -5880,9 +6391,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
5880
6391
|
imageFailure(screen, view, uri, "Image source is missing")
|
|
5881
6392
|
return
|
|
5882
6393
|
}
|
|
5883
|
-
if (imageSources[view] ==
|
|
6394
|
+
if (imageSources[view] == requestKey) return
|
|
5884
6395
|
imageJobs.remove(view)?.cancel(true)
|
|
5885
|
-
imageSources[view] =
|
|
6396
|
+
imageSources[view] = requestKey
|
|
5886
6397
|
imageErrors.remove(view)
|
|
5887
6398
|
view.setImageDrawable(null)
|
|
5888
6399
|
imageDidChange(view)
|
|
@@ -5920,11 +6431,12 @@ class MainActivity : AppCompatActivity() {
|
|
|
5920
6431
|
val connection = URL(uri).openConnection().apply {
|
|
5921
6432
|
connectTimeout = 10_000
|
|
5922
6433
|
readTimeout = 10_000
|
|
6434
|
+
headers.forEach { (name, value) -> setRequestProperty(name, value) }
|
|
5923
6435
|
}
|
|
5924
6436
|
connection.getInputStream().use(BitmapFactory::decodeStream)
|
|
5925
6437
|
}.getOrNull()
|
|
5926
6438
|
runOnUiThread {
|
|
5927
|
-
if (imageSources[view] !=
|
|
6439
|
+
if (imageSources[view] != requestKey) return@runOnUiThread
|
|
5928
6440
|
imageJobs.remove(view)
|
|
5929
6441
|
if (bitmap != null) {
|
|
5930
6442
|
view.setImageBitmap(bitmap)
|
|
@@ -6006,7 +6518,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
6006
6518
|
}
|
|
6007
6519
|
ViewCompat.setStateDescription(
|
|
6008
6520
|
view,
|
|
6009
|
-
if (type == "Checkbox" && props.
|
|
6521
|
+
if (type == "Checkbox" && (props.opt("indeterminate") as? Boolean) == true) "mixed" else null,
|
|
6010
6522
|
)
|
|
6011
6523
|
ViewCompat.setAccessibilityDelegate(view, object : AccessibilityDelegateCompat() {
|
|
6012
6524
|
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
|
|
@@ -6508,7 +7020,13 @@ class MainActivity : AppCompatActivity() {
|
|
|
6508
7020
|
|
|
6509
7021
|
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
|
|
6510
7022
|
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
|
6511
|
-
if (isInPictureInPictureMode)
|
|
7023
|
+
if (isInPictureInPictureMode) {
|
|
7024
|
+
floatingVideo()?.notifyPictureInPicture(true)
|
|
7025
|
+
showVideoAlone()
|
|
7026
|
+
} else {
|
|
7027
|
+
pictureInPictureVideo?.notifyPictureInPicture(false)
|
|
7028
|
+
putVideoBack()
|
|
7029
|
+
}
|
|
6512
7030
|
}
|
|
6513
7031
|
|
|
6514
7032
|
/**
|
|
@@ -6584,6 +7102,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
6584
7102
|
released.filterIsInstance<EditText>().forEach {
|
|
6585
7103
|
inputHintDefaults.remove(it)
|
|
6586
7104
|
inputHighlightDefaults.remove(it)
|
|
7105
|
+
inputPaddingDefaults.remove(it)
|
|
6587
7106
|
screen.suppressedTextChanges.remove(it)
|
|
6588
7107
|
screen.inputEchoes.remove(it)
|
|
6589
7108
|
val identity = screen.inputIdentities.remove(it)
|
|
@@ -6594,6 +7113,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
6594
7113
|
screen.handlers.keys.removeAll(released)
|
|
6595
7114
|
screen.pressInHandlers.keys.removeAll(released)
|
|
6596
7115
|
screen.pressOutHandlers.keys.removeAll(released)
|
|
7116
|
+
screen.pressUnderlayShowHandlers.keys.removeAll(released)
|
|
7117
|
+
screen.pressUnderlayHideHandlers.keys.removeAll(released)
|
|
7118
|
+
screen.pressUnderlayColors.keys.removeAll(released)
|
|
7119
|
+
screen.pressBaseBackgrounds.keys.removeAll(released)
|
|
6597
7120
|
screen.longPressHandlers.keys.removeAll(released)
|
|
6598
7121
|
screen.layoutHandlers.keys.removeAll(released)
|
|
6599
7122
|
screen.layoutListeners.forEach { (view, listener) ->
|
|
@@ -6618,6 +7141,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
6618
7141
|
screen.imageErrorHandlers.keys.removeAll(releasedImages)
|
|
6619
7142
|
screen.pressActiveOpacities.keys.removeAll(released)
|
|
6620
7143
|
screen.pressBaseOpacities.keys.removeAll(released)
|
|
7144
|
+
screen.pressUnderlayColors.keys.removeAll(released)
|
|
7145
|
+
screen.pressBaseBackgrounds.keys.removeAll(released)
|
|
6621
7146
|
screen.modalVisibility.keys.removeAll(released)
|
|
6622
7147
|
screen.modalCloseHandlers.keys.removeAll(released)
|
|
6623
7148
|
}
|
|
@@ -6648,6 +7173,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
6648
7173
|
screen.handlers.clear()
|
|
6649
7174
|
screen.pressInHandlers.clear()
|
|
6650
7175
|
screen.pressOutHandlers.clear()
|
|
7176
|
+
screen.pressUnderlayShowHandlers.clear()
|
|
7177
|
+
screen.pressUnderlayHideHandlers.clear()
|
|
7178
|
+
screen.pressUnderlayColors.clear()
|
|
7179
|
+
screen.pressBaseBackgrounds.clear()
|
|
6651
7180
|
screen.layoutHandlers.clear()
|
|
6652
7181
|
screen.lastLayoutBounds.clear()
|
|
6653
7182
|
screen.scrollHandlers.clear()
|