craft-native 0.0.204 → 0.0.205
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/android/src/index.js +90 -0
- package/dist/android/templates/CraftNativeFlatList.kt.template +117 -0
- package/dist/android/templates/CraftNativeShell.kt.template +42 -10
- package/dist/android/templates/MainActivityNative.kt.template +527 -50
- package/dist/cli.js +1 -1
- package/dist/components/index.d.ts +118 -74
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/testing.d.ts +4 -4
- package/dist/ios/templates/CraftHybrid.swift +17 -3
- package/dist/ios/templates/CraftNativeFlatList.swift +64 -0
- package/dist/ios/templates/CraftNativeScreen.swift +307 -43
- package/dist/ios/templates/testing/DriverTests.swift.template +32 -3
- package/package.json +1 -1
|
@@ -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,11 @@ 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 Number -> value.toDouble().takeIf { it.isFinite() }?.toString()
|
|
153
|
+
else -> craftNativeOptionalString(value)
|
|
154
|
+
}
|
|
155
|
+
|
|
148
156
|
private fun craftNativeEventName(events: JSONObject, vararg names: String): String =
|
|
149
157
|
names.firstNotNullOfOrNull { name -> craftNativeOptionalString(events.opt(name)) }.orEmpty()
|
|
150
158
|
|
|
@@ -332,11 +340,17 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
332
340
|
val strokeWidth: Float = 1f,
|
|
333
341
|
val color: String = "#000000",
|
|
334
342
|
val opacity: Float = 1f,
|
|
343
|
+
val fillOpacity: Float = 1f,
|
|
344
|
+
val strokeOpacity: Float = 1f,
|
|
345
|
+
val strokeDashoffset: Float = 0f,
|
|
346
|
+
val strokeMiterlimit: Float = 4f,
|
|
347
|
+
val fillRule: String = "nonzero",
|
|
335
348
|
val typeface: Typeface = Typeface.DEFAULT,
|
|
336
349
|
val strokeLinecap: String? = null,
|
|
337
350
|
val strokeLinejoin: String? = null,
|
|
338
351
|
val strokeDasharray: List<Float> = emptyList(),
|
|
339
352
|
val vectorEffect: String? = null,
|
|
353
|
+
val dominantBaseline: String? = null,
|
|
340
354
|
) {
|
|
341
355
|
fun merge(node: JSONObject): SvgPaint {
|
|
342
356
|
val props = node.optJSONObject("props") ?: JSONObject()
|
|
@@ -361,17 +375,24 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
361
375
|
// when both provide the same SVG paint value.
|
|
362
376
|
val weight = value("fontWeight")
|
|
363
377
|
val ownOpacity = value("opacity")?.toFloatOrNull()?.takeIf { it.isFinite() } ?: 1f
|
|
378
|
+
fun number(name: String): Float? = value(name)?.toFloatOrNull()?.takeIf { it.isFinite() }
|
|
364
379
|
return copy(
|
|
365
380
|
fill = value("fill") ?: fill,
|
|
366
381
|
stroke = value("stroke") ?: stroke,
|
|
367
382
|
strokeWidth = value("strokeWidth")?.toFloatOrNull()?.takeIf { it.isFinite() } ?: strokeWidth,
|
|
368
383
|
color = value("color") ?: color,
|
|
369
384
|
opacity = ownOpacity * opacity,
|
|
385
|
+
fillOpacity = number("fillOpacity") ?: fillOpacity,
|
|
386
|
+
strokeOpacity = number("strokeOpacity") ?: strokeOpacity,
|
|
387
|
+
strokeDashoffset = number("strokeDashoffset") ?: strokeDashoffset,
|
|
388
|
+
strokeMiterlimit = number("strokeMiterlimit")?.takeIf { it > 0 } ?: strokeMiterlimit,
|
|
389
|
+
fillRule = value("fillRule") ?: fillRule,
|
|
370
390
|
typeface = if (weight in setOf("bold", "700")) Typeface.DEFAULT_BOLD else typeface,
|
|
371
391
|
strokeLinecap = value("strokeLinecap") ?: strokeLinecap,
|
|
372
392
|
strokeLinejoin = value("strokeLinejoin") ?: strokeLinejoin,
|
|
373
393
|
strokeDasharray = dashes(raw("strokeDasharray")) ?: strokeDasharray,
|
|
374
394
|
vectorEffect = value("vectorEffect") ?: vectorEffect,
|
|
395
|
+
dominantBaseline = value("dominantBaseline") ?: dominantBaseline,
|
|
375
396
|
)
|
|
376
397
|
}
|
|
377
398
|
}
|
|
@@ -467,16 +488,18 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
467
488
|
}
|
|
468
489
|
val fill = android.graphics.Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
469
490
|
color = resolveColor(style.fill, style.color, Color.TRANSPARENT)
|
|
470
|
-
alpha = (style.opacity * 255f).roundToInt().coerceIn(0, 255)
|
|
491
|
+
alpha = (style.opacity * style.fillOpacity * 255f).roundToInt().coerceIn(0, 255)
|
|
471
492
|
this.style = Paint.Style.FILL
|
|
472
493
|
}
|
|
494
|
+
path.fillType = if (style.fillRule == "evenodd") Path.FillType.EVEN_ODD else Path.FillType.WINDING
|
|
473
495
|
if (style.fill != "none" && style.fill != "transparent") canvas.drawPath(path, fill)
|
|
474
496
|
if (!style.stroke.isNullOrBlank() && style.stroke != "none" && style.stroke != "transparent" && style.strokeWidth > 0) {
|
|
475
497
|
val stroke = android.graphics.Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
476
498
|
color = resolveColor(style.stroke, style.color, Color.TRANSPARENT)
|
|
477
|
-
alpha = (style.opacity * 255f).roundToInt().coerceIn(0, 255)
|
|
499
|
+
alpha = (style.opacity * style.strokeOpacity * 255f).roundToInt().coerceIn(0, 255)
|
|
478
500
|
this.style = Paint.Style.STROKE
|
|
479
501
|
strokeWidth = style.strokeWidth
|
|
502
|
+
strokeMiter = style.strokeMiterlimit
|
|
480
503
|
strokeCap = when (style.strokeLinecap) {
|
|
481
504
|
"round" -> Paint.Cap.ROUND
|
|
482
505
|
"square" -> Paint.Cap.SQUARE
|
|
@@ -494,7 +517,7 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
494
517
|
} else {
|
|
495
518
|
dashPattern
|
|
496
519
|
}
|
|
497
|
-
pathEffect = DashPathEffect(pattern,
|
|
520
|
+
pathEffect = DashPathEffect(pattern, style.strokeDashoffset)
|
|
498
521
|
}
|
|
499
522
|
}
|
|
500
523
|
// `vector-effect="non-scaling-stroke"`: the stroke is in the view's
|
|
@@ -525,7 +548,7 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
525
548
|
?: 12f
|
|
526
549
|
val paint = android.graphics.Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
527
550
|
color = resolveColor(style.fill, style.color, Color.BLACK)
|
|
528
|
-
alpha = (style.opacity * 255f).roundToInt().coerceIn(0, 255)
|
|
551
|
+
alpha = (style.opacity * style.fillOpacity * 255f).roundToInt().coerceIn(0, 255)
|
|
529
552
|
textSize = size
|
|
530
553
|
typeface = style.typeface
|
|
531
554
|
}
|
|
@@ -535,7 +558,14 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
535
558
|
"middle" -> x -= paint.measureText(text) / 2f
|
|
536
559
|
"end" -> x -= paint.measureText(text)
|
|
537
560
|
}
|
|
538
|
-
|
|
561
|
+
val metrics = paint.fontMetrics
|
|
562
|
+
val baseline = when (style.dominantBaseline) {
|
|
563
|
+
"middle", "central", "mathematical" -> y - (metrics.ascent + metrics.descent) / 2f
|
|
564
|
+
"hanging", "text-before-edge", "before-edge" -> y - metrics.ascent
|
|
565
|
+
"text-after-edge", "after-edge" -> y - metrics.descent
|
|
566
|
+
else -> y
|
|
567
|
+
}
|
|
568
|
+
canvas.drawText(text, x, baseline, paint)
|
|
539
569
|
}
|
|
540
570
|
|
|
541
571
|
private fun resolveColor(value: String?, current: String, fallback: Int): Int = runCatching {
|
|
@@ -557,6 +587,7 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
557
587
|
private var onMarkerTap: ((String) -> Unit)? = null
|
|
558
588
|
private var lifecycleStarted = false
|
|
559
589
|
private var destroyed = false
|
|
590
|
+
private var initialCameraKey: String? = null
|
|
560
591
|
|
|
561
592
|
init {
|
|
562
593
|
addView(mapView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
|
|
@@ -631,7 +662,11 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
631
662
|
is Number -> rawWidth.toDouble()
|
|
632
663
|
else -> 3.0
|
|
633
664
|
}
|
|
634
|
-
|
|
665
|
+
// MapKit's lineWidth is expressed in points, while the
|
|
666
|
+
// Google Maps SDK consumes screen pixels. Keep the
|
|
667
|
+
// protocol in logical points and convert only at this
|
|
668
|
+
// Android host boundary so dense displays stay in parity.
|
|
669
|
+
val width = (authoredWidth * resources.displayMetrics.density).toFloat()
|
|
635
670
|
if (!authoredWidth.isFinite() || !width.isFinite() || width <= 0f) continue
|
|
636
671
|
nextPolylines[id] = Triple(points, color(craftNativeOptionalString(props.opt("strokeColor")).orEmpty()), width)
|
|
637
672
|
}
|
|
@@ -662,24 +697,67 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
662
697
|
map.addPolyline(PolylineOptions().addAll(value.first).color(value.second).width(value.third))?.let { polylines[id] = it }
|
|
663
698
|
}
|
|
664
699
|
}
|
|
665
|
-
|
|
666
|
-
region(pendingProps.optJSONObject("
|
|
667
|
-
|
|
668
|
-
|
|
700
|
+
val explicitRegion = region(pendingProps.optJSONObject("region"))
|
|
701
|
+
val initialRegion = region(pendingProps.optJSONObject("initialRegion"))
|
|
702
|
+
if (explicitRegion != null) {
|
|
703
|
+
moveCamera(explicitRegion)
|
|
704
|
+
initialCameraKey = null
|
|
705
|
+
} else if (initialRegion != null) {
|
|
706
|
+
val key = "region:${pendingProps.optJSONObject("initialRegion")}"
|
|
707
|
+
if (initialCameraKey != key) {
|
|
708
|
+
moveCamera(initialRegion)
|
|
709
|
+
initialCameraKey = key
|
|
669
710
|
}
|
|
711
|
+
} else if (pendingProps.optBoolean("fitToBounds", false)) {
|
|
712
|
+
val key = "fit:${overlayKey(nextMarkers, nextPolylines)}"
|
|
713
|
+
if (initialCameraKey != key) {
|
|
714
|
+
if (fitToBounds()) initialCameraKey = key
|
|
715
|
+
}
|
|
716
|
+
} else {
|
|
717
|
+
initialCameraKey = null
|
|
670
718
|
}
|
|
671
719
|
}
|
|
672
720
|
|
|
673
|
-
|
|
674
|
-
|
|
721
|
+
override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
|
|
722
|
+
super.onSizeChanged(width, height, oldWidth, oldHeight)
|
|
723
|
+
// MapView can become ready before its host has a usable viewport. Do
|
|
724
|
+
// not mark a fit as complete until the camera update can be issued.
|
|
725
|
+
if (width > 0 && height > 0 && pendingProps.optBoolean("fitToBounds", false) && initialCameraKey == null) {
|
|
726
|
+
applyPending()
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
private fun moveCamera(bounds: LatLngBounds) {
|
|
731
|
+
post {
|
|
732
|
+
if (width > 0 && height > 0) googleMap?.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 24))
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
private fun overlayKey(
|
|
737
|
+
markers: Map<String, Pair<LatLng, String?>>,
|
|
738
|
+
polylines: Map<String, Triple<List<LatLng>, Int, Float>>,
|
|
739
|
+
): String {
|
|
740
|
+
val markerKey = markers.toSortedMap().entries.joinToString("|") { (id, value) ->
|
|
741
|
+
"$id:${value.first.latitude},${value.first.longitude}"
|
|
742
|
+
}
|
|
743
|
+
val polylineKey = polylines.toSortedMap().entries.joinToString("|") { (id, value) ->
|
|
744
|
+
"$id:" + value.first.joinToString(";") { point -> "${point.latitude},${point.longitude}" }
|
|
745
|
+
}
|
|
746
|
+
return "m[$markerKey]p[$polylineKey]"
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
private fun fitToBounds(): Boolean {
|
|
750
|
+
val map = googleMap ?: return false
|
|
751
|
+
if (width <= 0 || height <= 0) return false
|
|
675
752
|
val builder = LatLngBounds.Builder()
|
|
676
753
|
var count = 0
|
|
677
754
|
markers.values.forEach { builder.include(it.position); count++ }
|
|
678
755
|
polylines.values.flatMap { it.points }.forEach { builder.include(it); count++ }
|
|
679
|
-
if (count == 0) return
|
|
756
|
+
if (count == 0) return false
|
|
680
757
|
post {
|
|
681
758
|
if (width > 0 && height > 0) map.moveCamera(CameraUpdateFactory.newLatLngBounds(builder.build(), 24))
|
|
682
759
|
}
|
|
760
|
+
return true
|
|
683
761
|
}
|
|
684
762
|
|
|
685
763
|
override fun onAttachedToWindow() {
|
|
@@ -728,8 +806,8 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
728
806
|
private fun coordinates(geometry: JSONArray?): List<LatLng> {
|
|
729
807
|
if (geometry == null) return emptyList()
|
|
730
808
|
return (0 until geometry.length() - 1 step 2).mapNotNull { index ->
|
|
731
|
-
val latitude = geometry.
|
|
732
|
-
val longitude = geometry.
|
|
809
|
+
val latitude = finiteNumber(geometry.opt(index)) ?: return@mapNotNull null
|
|
810
|
+
val longitude = finiteNumber(geometry.opt(index + 1)) ?: return@mapNotNull null
|
|
733
811
|
if (latitude.isFinite() && longitude.isFinite() &&
|
|
734
812
|
latitude >= -90.0 && latitude <= 90.0 &&
|
|
735
813
|
longitude >= -180.0 && longitude <= 180.0
|
|
@@ -740,10 +818,14 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
740
818
|
private fun region(value: JSONObject?): LatLngBounds? {
|
|
741
819
|
val center = value?.optJSONObject("center") ?: return null
|
|
742
820
|
val span = value.optJSONObject("span") ?: return null
|
|
743
|
-
val latitude = center.
|
|
744
|
-
val longitude = center.
|
|
745
|
-
val latitudeDelta = span.
|
|
746
|
-
|
|
821
|
+
val latitude = finiteNumber(center.opt("latitude")) ?: return null
|
|
822
|
+
val longitude = finiteNumber(center.opt("longitude")) ?: return null
|
|
823
|
+
val latitudeDelta = finiteNumber(span.opt("latitudeDelta"))
|
|
824
|
+
?: finiteNumber(span.opt("latitude_delta"))
|
|
825
|
+
?: return null
|
|
826
|
+
val longitudeDelta = finiteNumber(span.opt("longitudeDelta"))
|
|
827
|
+
?: finiteNumber(span.opt("longitude_delta"))
|
|
828
|
+
?: return null
|
|
747
829
|
if (!latitude.isFinite() || !longitude.isFinite() || !latitudeDelta.isFinite() || !longitudeDelta.isFinite() ||
|
|
748
830
|
latitude < -90.0 || latitude > 90.0 || longitude < -180.0 || longitude > 180.0 ||
|
|
749
831
|
latitudeDelta <= 0.0 || latitudeDelta > 180.0 ||
|
|
@@ -753,7 +835,10 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
753
835
|
return LatLngBounds(LatLng(latitude - latitudeDelta / 2, longitude - longitudeDelta / 2), LatLng(latitude + latitudeDelta / 2, longitude + longitudeDelta / 2))
|
|
754
836
|
}
|
|
755
837
|
|
|
756
|
-
private fun
|
|
838
|
+
private fun finiteNumber(value: Any?): Double? =
|
|
839
|
+
(value as? Number)?.toDouble()?.takeIf { it.isFinite() }
|
|
840
|
+
|
|
841
|
+
private fun color(value: String): Int = runCatching { craftNativeColor(value) }.getOrDefault(Color.BLUE)
|
|
757
842
|
}
|
|
758
843
|
|
|
759
844
|
/** A deterministic flex-shaped container shared by normal views and scroll content. */
|
|
@@ -2036,6 +2121,9 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
2036
2121
|
private val defaultProgressStart = progressViewStartOffset
|
|
2037
2122
|
private val defaultProgressEnd = progressViewEndOffset
|
|
2038
2123
|
private var configuredProgressOffset: Int? = null
|
|
2124
|
+
private var stickyIndices = emptyList<Int>()
|
|
2125
|
+
private var scrollTargetKey: String? = null
|
|
2126
|
+
private var pendingScrollTarget: Pair<String, Boolean>? = null
|
|
2039
2127
|
|
|
2040
2128
|
init {
|
|
2041
2129
|
vertical.isNestedScrollingEnabled = true
|
|
@@ -2247,6 +2335,133 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
2247
2335
|
horizontal.isHorizontalScrollBarEnabled = horizontalVisible
|
|
2248
2336
|
}
|
|
2249
2337
|
|
|
2338
|
+
/** Keep vertical ScrollView headers pinned while preserving their layout slots. */
|
|
2339
|
+
fun setStickyHeaderIndices(value: JSONArray?) {
|
|
2340
|
+
stickyIndices.forEach { index ->
|
|
2341
|
+
content.getChildAt(index)?.apply {
|
|
2342
|
+
translationY = 0f
|
|
2343
|
+
translationZ = 0f
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
stickyIndices = if (value == null) {
|
|
2347
|
+
emptyList()
|
|
2348
|
+
} else {
|
|
2349
|
+
(0 until value.length()).mapNotNull { value.optInt(it, -1).takeIf { index -> index >= 0 } }.distinct().sorted()
|
|
2350
|
+
}
|
|
2351
|
+
post { updateStickyHeaders() }
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
/** Scroll a child identified by its native testID/id after reconciliation. */
|
|
2355
|
+
fun setScrollTarget(value: Any?) {
|
|
2356
|
+
val target: String
|
|
2357
|
+
val animated: Boolean
|
|
2358
|
+
val key: String
|
|
2359
|
+
when (value) {
|
|
2360
|
+
is String -> {
|
|
2361
|
+
target = value.trim()
|
|
2362
|
+
animated = true
|
|
2363
|
+
key = target
|
|
2364
|
+
}
|
|
2365
|
+
is JSONObject -> {
|
|
2366
|
+
target = craftNativeOptionalString(value.opt("id")).orEmpty().trim()
|
|
2367
|
+
animated = value.optBoolean("animated", true)
|
|
2368
|
+
key = value.toString()
|
|
2369
|
+
}
|
|
2370
|
+
else -> {
|
|
2371
|
+
scrollTargetKey = null
|
|
2372
|
+
pendingScrollTarget = null
|
|
2373
|
+
return
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
if (target.isBlank()) {
|
|
2377
|
+
scrollTargetKey = null
|
|
2378
|
+
pendingScrollTarget = null
|
|
2379
|
+
return
|
|
2380
|
+
}
|
|
2381
|
+
if (key == scrollTargetKey) return
|
|
2382
|
+
scrollTargetKey = key
|
|
2383
|
+
pendingScrollTarget = target to animated
|
|
2384
|
+
post { scrollToPendingTarget(0) }
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
private fun updateStickyHeaders() {
|
|
2388
|
+
if (horizontalMode == true) {
|
|
2389
|
+
stickyIndices.forEach { index ->
|
|
2390
|
+
content.getChildAt(index)?.apply {
|
|
2391
|
+
translationY = 0f
|
|
2392
|
+
translationZ = 0f
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
return
|
|
2396
|
+
}
|
|
2397
|
+
val scrollY = vertical.scrollY
|
|
2398
|
+
var pinnedBottom = 0
|
|
2399
|
+
stickyIndices.forEachIndexed { position, index ->
|
|
2400
|
+
val header = content.getChildAt(index)
|
|
2401
|
+
if (header == null || header.visibility != View.VISIBLE || header.height <= 0) return@forEachIndexed
|
|
2402
|
+
val nextTop = stickyIndices.drop(position + 1)
|
|
2403
|
+
.firstNotNullOfOrNull { next -> content.getChildAt(next)?.takeIf { it.visibility == View.VISIBLE }?.top }
|
|
2404
|
+
?: Int.MAX_VALUE
|
|
2405
|
+
val desired = max(0, scrollY + pinnedBottom - header.top)
|
|
2406
|
+
val limit = if (nextTop == Int.MAX_VALUE) Int.MAX_VALUE else max(0, nextTop - header.bottom)
|
|
2407
|
+
val translation = min(desired, limit)
|
|
2408
|
+
header.translationY = translation.toFloat()
|
|
2409
|
+
header.translationZ = if (translation > 0) 1f else 0f
|
|
2410
|
+
if (translation > 0) pinnedBottom = header.top - scrollY + translation + header.height
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
private fun findDescendant(view: View, id: String): View? {
|
|
2415
|
+
if (view.tag?.toString() == id || view.contentDescription?.toString() == id) return view
|
|
2416
|
+
if (view is ViewGroup) {
|
|
2417
|
+
for (index in 0 until view.childCount) findDescendant(view.getChildAt(index), id)?.let { return it }
|
|
2418
|
+
}
|
|
2419
|
+
return null
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
private fun scrollToPendingTarget(attempt: Int) {
|
|
2423
|
+
val target = pendingScrollTarget ?: return
|
|
2424
|
+
val scroller: View = if (horizontalMode == true) horizontal else vertical
|
|
2425
|
+
val view = findDescendant(content, target.first)
|
|
2426
|
+
if (view == null || view.width <= 0 || view.height <= 0 || scroller.width <= 0 || scroller.height <= 0) {
|
|
2427
|
+
if (attempt < 4) postDelayed({ scrollToPendingTarget(attempt + 1) }, 16L)
|
|
2428
|
+
return
|
|
2429
|
+
}
|
|
2430
|
+
val targetLocation = IntArray(2)
|
|
2431
|
+
val scrollerLocation = IntArray(2)
|
|
2432
|
+
view.getLocationOnScreen(targetLocation)
|
|
2433
|
+
scroller.getLocationOnScreen(scrollerLocation)
|
|
2434
|
+
val current = if (horizontalMode == true) horizontal.scrollX else vertical.scrollY
|
|
2435
|
+
val stickyHeight = if (horizontalMode == true) 0 else {
|
|
2436
|
+
val contentLocation = IntArray(2)
|
|
2437
|
+
content.getLocationOnScreen(contentLocation)
|
|
2438
|
+
val targetContentTop = targetLocation[1] - contentLocation[1] + vertical.scrollY
|
|
2439
|
+
stickyIndices.mapNotNull { index -> content.getChildAt(index) }
|
|
2440
|
+
.lastOrNull { it.visibility == View.VISIBLE && it.top <= targetContentTop }
|
|
2441
|
+
?.height ?: 0
|
|
2442
|
+
}
|
|
2443
|
+
val desired = if (horizontalMode == true) {
|
|
2444
|
+
current + targetLocation[0] - scrollerLocation[0]
|
|
2445
|
+
} else {
|
|
2446
|
+
current + targetLocation[1] - scrollerLocation[1] - stickyHeight
|
|
2447
|
+
}
|
|
2448
|
+
val maximum = if (horizontalMode == true) {
|
|
2449
|
+
max(0, content.measuredWidth - scroller.width)
|
|
2450
|
+
} else {
|
|
2451
|
+
max(0, content.measuredHeight - scroller.height)
|
|
2452
|
+
}
|
|
2453
|
+
val bounded = desired.coerceIn(0, maximum)
|
|
2454
|
+
if (target.second) {
|
|
2455
|
+
if (horizontalMode == true) horizontal.smoothScrollTo(bounded, 0) else vertical.smoothScrollTo(0, bounded)
|
|
2456
|
+
} else if (horizontalMode == true) {
|
|
2457
|
+
horizontal.scrollTo(bounded, 0)
|
|
2458
|
+
} else {
|
|
2459
|
+
vertical.scrollTo(0, bounded)
|
|
2460
|
+
}
|
|
2461
|
+
pendingScrollTarget = null
|
|
2462
|
+
updateStickyHeaders()
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2250
2465
|
fun setScrollListeners(
|
|
2251
2466
|
onScroll: ((Int, Int) -> Unit)?,
|
|
2252
2467
|
onBeginDrag: (() -> Unit)?,
|
|
@@ -2277,7 +2492,10 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
2277
2492
|
}
|
|
2278
2493
|
}
|
|
2279
2494
|
|
|
2280
|
-
scroller.setOnScrollChangeListener { _, x, y, _, _ ->
|
|
2495
|
+
scroller.setOnScrollChangeListener { _, x, y, _, _ ->
|
|
2496
|
+
if (!horizontal) updateStickyHeaders()
|
|
2497
|
+
onScroll?.invoke(x, y)
|
|
2498
|
+
}
|
|
2281
2499
|
scroller.setOnTouchListener { _, event ->
|
|
2282
2500
|
when (event.actionMasked) {
|
|
2283
2501
|
android.view.MotionEvent.ACTION_DOWN -> {
|
|
@@ -2722,6 +2940,9 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
|
|
|
2722
2940
|
private var sourceUri = ""
|
|
2723
2941
|
private var shouldLoop = false
|
|
2724
2942
|
private var shouldPlay = false
|
|
2943
|
+
private var playIntent: Boolean? = null
|
|
2944
|
+
private var userPaused = false
|
|
2945
|
+
private var ended = false
|
|
2725
2946
|
private var muted = false
|
|
2726
2947
|
private var resizeMode = "contain"
|
|
2727
2948
|
private var preparedPlayer: MediaPlayer? = null
|
|
@@ -2736,6 +2957,16 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
|
|
|
2736
2957
|
private set
|
|
2737
2958
|
/** Playback started or stopped, or the choice changed: the activity's PiP params follow. */
|
|
2738
2959
|
var onPictureInPictureChanged: (() -> Unit)? = null
|
|
2960
|
+
private val progressHandler = Handler(Looper.getMainLooper())
|
|
2961
|
+
private val progressTick = object : Runnable {
|
|
2962
|
+
override fun run() {
|
|
2963
|
+
if (!playing) return
|
|
2964
|
+
fire("onProgress", JSONObject()
|
|
2965
|
+
.put("currentTime", currentPosition.coerceAtLeast(0) / 1000.0)
|
|
2966
|
+
.put("duration", duration.coerceAtLeast(0) / 1000.0))
|
|
2967
|
+
progressHandler.postDelayed(this, 500L)
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2739
2970
|
|
|
2740
2971
|
init {
|
|
2741
2972
|
// Mixed with the music rather than pausing it.
|
|
@@ -2749,7 +2980,13 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
|
|
|
2749
2980
|
else -> ""
|
|
2750
2981
|
}
|
|
2751
2982
|
shouldLoop = loop
|
|
2752
|
-
|
|
2983
|
+
val nextShouldPlay = autoplay && !paused
|
|
2984
|
+
if (playIntent != nextShouldPlay) {
|
|
2985
|
+
userPaused = false
|
|
2986
|
+
ended = false
|
|
2987
|
+
}
|
|
2988
|
+
playIntent = nextShouldPlay
|
|
2989
|
+
shouldPlay = nextShouldPlay
|
|
2753
2990
|
this.muted = muted
|
|
2754
2991
|
val floating = pictureInPicture && !muted
|
|
2755
2992
|
if (floating != this.pictureInPicture) {
|
|
@@ -2761,6 +2998,8 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
|
|
|
2761
2998
|
setMediaController(if (controls) MediaController(context).also { it.setAnchorView(this) } else null)
|
|
2762
2999
|
if (uri != sourceUri) {
|
|
2763
3000
|
sourceUri = uri
|
|
3001
|
+
userPaused = false
|
|
3002
|
+
ended = false
|
|
2764
3003
|
stopPlayback()
|
|
2765
3004
|
setPlaying(false)
|
|
2766
3005
|
preparedPlayer = null
|
|
@@ -2780,7 +3019,10 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
|
|
|
2780
3019
|
}
|
|
2781
3020
|
setOnCompletionListener {
|
|
2782
3021
|
if (shouldLoop && !isPlaying) start()
|
|
2783
|
-
else
|
|
3022
|
+
else {
|
|
3023
|
+
ended = true
|
|
3024
|
+
setPlaying(false)
|
|
3025
|
+
}
|
|
2784
3026
|
fire("onEnd")
|
|
2785
3027
|
}
|
|
2786
3028
|
setOnErrorListener { _, what, extra ->
|
|
@@ -2791,18 +3033,24 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
|
|
|
2791
3033
|
}
|
|
2792
3034
|
if (uri.isBlank()) return
|
|
2793
3035
|
runCatching {
|
|
2794
|
-
if (shouldPlay && !isPlaying) start()
|
|
3036
|
+
if (shouldPlay && !isPlaying && !userPaused && !ended) start()
|
|
2795
3037
|
else if (!shouldPlay && isPlaying) pause()
|
|
2796
3038
|
}
|
|
2797
3039
|
}
|
|
2798
3040
|
|
|
2799
3041
|
override fun start() {
|
|
2800
3042
|
super.start()
|
|
2801
|
-
|
|
3043
|
+
userPaused = false
|
|
3044
|
+
ended = false
|
|
3045
|
+
// VideoView accepts start() before its MediaPlayer is prepared. Do
|
|
3046
|
+
// not announce playback until the player reports that it is actually
|
|
3047
|
+
// running; iOS follows the same rule through AVPlayer's time status.
|
|
3048
|
+
if (runCatching { isPlaying }.getOrDefault(false)) setPlaying(true)
|
|
2802
3049
|
}
|
|
2803
3050
|
|
|
2804
3051
|
override fun pause() {
|
|
2805
3052
|
super.pause()
|
|
3053
|
+
userPaused = true
|
|
2806
3054
|
setPlaying(false)
|
|
2807
3055
|
}
|
|
2808
3056
|
|
|
@@ -2810,6 +3058,8 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
|
|
|
2810
3058
|
if (playing == value) return
|
|
2811
3059
|
playing = value
|
|
2812
3060
|
fire(if (value) "onPlay" else "onPause")
|
|
3061
|
+
progressHandler.removeCallbacks(progressTick)
|
|
3062
|
+
if (value) progressHandler.post(progressTick)
|
|
2813
3063
|
onPictureInPictureChanged?.invoke()
|
|
2814
3064
|
}
|
|
2815
3065
|
|
|
@@ -2834,12 +3084,20 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
|
|
|
2834
3084
|
return runCatching { player.setDisplay(holder ?: this.holder) }.isSuccess
|
|
2835
3085
|
}
|
|
2836
3086
|
|
|
3087
|
+
fun notifyPictureInPicture(active: Boolean) {
|
|
3088
|
+
fire(if (active) "onPictureInPictureStart" else "onPictureInPictureStop")
|
|
3089
|
+
}
|
|
3090
|
+
|
|
2837
3091
|
fun stop() {
|
|
2838
3092
|
// Released with its screen: nothing more is told to it.
|
|
2839
3093
|
emit = null
|
|
3094
|
+
progressHandler.removeCallbacks(progressTick)
|
|
2840
3095
|
stopPlayback()
|
|
2841
3096
|
setPlaying(false)
|
|
2842
3097
|
sourceUri = ""
|
|
3098
|
+
playIntent = null
|
|
3099
|
+
userPaused = false
|
|
3100
|
+
ended = false
|
|
2843
3101
|
preparedPlayer = null
|
|
2844
3102
|
if (pictureInPicture) {
|
|
2845
3103
|
pictureInPicture = false
|
|
@@ -3249,30 +3507,92 @@ private class CraftTextDecorationSpan(
|
|
|
3249
3507
|
}
|
|
3250
3508
|
}
|
|
3251
3509
|
|
|
3510
|
+
private fun craftNativeGradientOrientation(direction: String?): GradientDrawable.Orientation = when (direction) {
|
|
3511
|
+
"to-t" -> GradientDrawable.Orientation.BOTTOM_TOP
|
|
3512
|
+
"to-tr" -> GradientDrawable.Orientation.BL_TR
|
|
3513
|
+
"to-r" -> GradientDrawable.Orientation.LEFT_RIGHT
|
|
3514
|
+
"to-br" -> GradientDrawable.Orientation.TL_BR
|
|
3515
|
+
"to-bl" -> GradientDrawable.Orientation.TR_BL
|
|
3516
|
+
"to-l" -> GradientDrawable.Orientation.RIGHT_LEFT
|
|
3517
|
+
"to-tl" -> GradientDrawable.Orientation.BR_TL
|
|
3518
|
+
else -> GradientDrawable.Orientation.TOP_BOTTOM
|
|
3519
|
+
}
|
|
3520
|
+
|
|
3252
3521
|
private class CraftNativeSideBorderDrawable(
|
|
3253
3522
|
private val fillColor: Int,
|
|
3254
3523
|
private val radii: FloatArray,
|
|
3255
3524
|
private val widths: IntArray,
|
|
3256
3525
|
private val colors: IntArray,
|
|
3526
|
+
private val borderStyle: String?,
|
|
3527
|
+
private val gradientDirection: String?,
|
|
3528
|
+
private val gradientColors: IntArray?,
|
|
3257
3529
|
) : Drawable() {
|
|
3258
3530
|
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
|
3259
3531
|
|
|
3532
|
+
private fun gradient(bounds: RectF): Shader? {
|
|
3533
|
+
val colors = gradientColors ?: return null
|
|
3534
|
+
val points = when (gradientDirection) {
|
|
3535
|
+
"to-t" -> floatArrayOf(0.5f, 1f, 0.5f, 0f)
|
|
3536
|
+
"to-tr" -> floatArrayOf(0f, 1f, 1f, 0f)
|
|
3537
|
+
"to-r" -> floatArrayOf(0f, 0.5f, 1f, 0.5f)
|
|
3538
|
+
"to-br" -> floatArrayOf(0f, 0f, 1f, 1f)
|
|
3539
|
+
"to-bl" -> floatArrayOf(1f, 0f, 0f, 1f)
|
|
3540
|
+
"to-l" -> floatArrayOf(1f, 0.5f, 0f, 0.5f)
|
|
3541
|
+
"to-tl" -> floatArrayOf(1f, 1f, 0f, 0f)
|
|
3542
|
+
else -> floatArrayOf(0.5f, 0f, 0.5f, 1f)
|
|
3543
|
+
}
|
|
3544
|
+
return LinearGradient(
|
|
3545
|
+
bounds.left + bounds.width() * points[0],
|
|
3546
|
+
bounds.top + bounds.height() * points[1],
|
|
3547
|
+
bounds.left + bounds.width() * points[2],
|
|
3548
|
+
bounds.top + bounds.height() * points[3],
|
|
3549
|
+
colors,
|
|
3550
|
+
null,
|
|
3551
|
+
Shader.TileMode.CLAMP,
|
|
3552
|
+
)
|
|
3553
|
+
}
|
|
3554
|
+
|
|
3260
3555
|
override fun draw(canvas: Canvas) {
|
|
3261
3556
|
val bounds = RectF(this.bounds)
|
|
3262
3557
|
paint.style = Paint.Style.FILL
|
|
3263
3558
|
paint.color = fillColor
|
|
3264
|
-
|
|
3559
|
+
paint.shader = gradient(bounds)
|
|
3560
|
+
if (paint.shader != null || Color.alpha(fillColor) > 0) {
|
|
3265
3561
|
val path = Path().apply { addRoundRect(bounds, radii, Path.Direction.CW) }
|
|
3266
3562
|
canvas.drawPath(path, paint)
|
|
3267
3563
|
}
|
|
3564
|
+
paint.shader = null
|
|
3268
3565
|
val top = widths.getOrElse(0) { 0 }
|
|
3269
3566
|
val right = widths.getOrElse(1) { 0 }
|
|
3270
3567
|
val bottom = widths.getOrElse(2) { 0 }
|
|
3271
3568
|
val left = widths.getOrElse(3) { 0 }
|
|
3272
|
-
if (
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3569
|
+
if (borderStyle == "dashed" || borderStyle == "dotted") {
|
|
3570
|
+
paint.style = Paint.Style.STROKE
|
|
3571
|
+
paint.strokeCap = if (borderStyle == "dotted") Paint.Cap.ROUND else Paint.Cap.BUTT
|
|
3572
|
+
paint.pathEffect = null
|
|
3573
|
+
fun drawEdge(index: Int, width: Int, startX: Float, startY: Float, endX: Float, endY: Float) {
|
|
3574
|
+
if (width <= 0) return
|
|
3575
|
+
paint.color = colors.getOrElse(index) { Color.TRANSPARENT }
|
|
3576
|
+
paint.strokeWidth = width.toFloat()
|
|
3577
|
+
paint.pathEffect = if (borderStyle == "dotted") {
|
|
3578
|
+
DashPathEffect(floatArrayOf(width.toFloat(), width * 1.5f), 0f)
|
|
3579
|
+
} else {
|
|
3580
|
+
DashPathEffect(floatArrayOf(width * 3f, width * 2f), 0f)
|
|
3581
|
+
}
|
|
3582
|
+
canvas.drawLine(startX, startY, endX, endY, paint)
|
|
3583
|
+
}
|
|
3584
|
+
drawEdge(0, top, bounds.left, bounds.top + top / 2f, bounds.right, bounds.top + top / 2f)
|
|
3585
|
+
drawEdge(1, right, bounds.right - right / 2f, bounds.top, bounds.right - right / 2f, bounds.bottom)
|
|
3586
|
+
drawEdge(2, bottom, bounds.left, bounds.bottom - bottom / 2f, bounds.right, bounds.bottom - bottom / 2f)
|
|
3587
|
+
drawEdge(3, left, bounds.left + left / 2f, bounds.top, bounds.left + left / 2f, bounds.bottom)
|
|
3588
|
+
paint.pathEffect = null
|
|
3589
|
+
} else {
|
|
3590
|
+
paint.style = Paint.Style.FILL
|
|
3591
|
+
if (top > 0) { paint.color = colors.getOrElse(0) { Color.TRANSPARENT }; canvas.drawRect(bounds.left, bounds.top, bounds.right, bounds.top + top, paint) }
|
|
3592
|
+
if (right > 0) { paint.color = colors.getOrElse(1) { Color.TRANSPARENT }; canvas.drawRect(bounds.right - right, bounds.top, bounds.right, bounds.bottom, paint) }
|
|
3593
|
+
if (bottom > 0) { paint.color = colors.getOrElse(2) { Color.TRANSPARENT }; canvas.drawRect(bounds.left, bounds.bottom - bottom, bounds.right, bounds.bottom, paint) }
|
|
3594
|
+
if (left > 0) { paint.color = colors.getOrElse(3) { Color.TRANSPARENT }; canvas.drawRect(bounds.left, bounds.top, bounds.left + left, bounds.bottom, paint) }
|
|
3595
|
+
}
|
|
3276
3596
|
}
|
|
3277
3597
|
|
|
3278
3598
|
override fun setAlpha(alpha: Int) { paint.alpha = alpha; invalidateSelf() }
|
|
@@ -3326,6 +3646,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
3326
3646
|
val linkTargets = mutableMapOf<View, String>()
|
|
3327
3647
|
val pressInHandlers = mutableMapOf<View, String>()
|
|
3328
3648
|
val pressOutHandlers = mutableMapOf<View, String>()
|
|
3649
|
+
val pressUnderlayShowHandlers = mutableMapOf<View, String>()
|
|
3650
|
+
val pressUnderlayHideHandlers = mutableMapOf<View, String>()
|
|
3329
3651
|
val longPressHandlers = mutableMapOf<View, String>()
|
|
3330
3652
|
val scrollHandlers = mutableMapOf<View, String>()
|
|
3331
3653
|
val scrollBeginHandlers = mutableMapOf<View, String>()
|
|
@@ -3344,6 +3666,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
3344
3666
|
val imageErrorHandlers = mutableMapOf<ImageView, String>()
|
|
3345
3667
|
val pressActiveOpacities = mutableMapOf<View, Float>()
|
|
3346
3668
|
val pressBaseOpacities = mutableMapOf<View, Float>()
|
|
3669
|
+
val pressUnderlayColors = mutableMapOf<View, Int>()
|
|
3670
|
+
val pressBaseBackgrounds = mutableMapOf<View, Drawable?>()
|
|
3347
3671
|
val modalVisibility = mutableMapOf<View, Boolean>()
|
|
3348
3672
|
val modalCloseHandlers = mutableMapOf<View, String>()
|
|
3349
3673
|
val layoutHandlers = mutableMapOf<View, String>()
|
|
@@ -3459,6 +3783,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3459
3783
|
/** Fields that already carry their text watcher. */
|
|
3460
3784
|
private val watchedInputs: MutableSet<EditText> = java.util.Collections.newSetFromMap(WeakHashMap())
|
|
3461
3785
|
private val inputHighlightDefaults = mutableMapOf<EditText, Int>()
|
|
3786
|
+
private val inputPaddingDefaults = mutableMapOf<EditText, IntArray>()
|
|
3462
3787
|
private val defaultBackgrounds = mutableMapOf<View, Drawable?>()
|
|
3463
3788
|
private var keyboardInset = 0
|
|
3464
3789
|
private var animationScaleObserverRegistered = false
|
|
@@ -3670,6 +3995,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3670
3995
|
val target = ensureTabRoot(targetIndex) ?: return false
|
|
3671
3996
|
val host = chrome?.content ?: return false
|
|
3672
3997
|
if (target === current || host.width <= 0) return false
|
|
3998
|
+
updateSafeAreaInsets(target)
|
|
3673
3999
|
val chromeLocation = IntArray(2)
|
|
3674
4000
|
chrome?.getLocationOnScreen(chromeLocation)
|
|
3675
4001
|
if (horizontalScrollCanConsume(current.view, chromeLocation[0] + x, chromeLocation[1] + y, dx)) return false
|
|
@@ -4114,6 +4440,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4114
4440
|
visibleScreen = screen
|
|
4115
4441
|
if (screen.frameRequested) postFrame(screen)
|
|
4116
4442
|
if (bars != null) applyChrome(screen)
|
|
4443
|
+
applyScreenBackground(screen)
|
|
4117
4444
|
if (previous !== screen) {
|
|
4118
4445
|
if (previous != null && previous.active) send(previous, "SCREEN_FOCUS", JSONObject().put("focused", false))
|
|
4119
4446
|
if (screen.shown) send(screen, "SCREEN_FOCUS", JSONObject().put("focused", true))
|
|
@@ -4149,6 +4476,15 @@ class MainActivity : AppCompatActivity() {
|
|
|
4149
4476
|
bars.apply(screen.options, depth, fallback)
|
|
4150
4477
|
}
|
|
4151
4478
|
|
|
4479
|
+
/** Applies a screen-specific background, falling back to the dynamic app colour. */
|
|
4480
|
+
private fun applyScreenBackground(screen: Screen) {
|
|
4481
|
+
val configured = screen.options.opt("backgroundColor")
|
|
4482
|
+
?.takeUnless { it == JSONObject.NULL }
|
|
4483
|
+
?.let { craftNativeOptionalString(it) }
|
|
4484
|
+
?.let { runCatching { craftNativeColor(it) }.getOrNull() }
|
|
4485
|
+
screen.view.setBackgroundColor(configured ?: ContextCompat.getColor(this, R.color.background))
|
|
4486
|
+
}
|
|
4487
|
+
|
|
4152
4488
|
/** The bridge's capability names: Craft's core set and this host's services. */
|
|
4153
4489
|
private fun capabilityNames(): JSONArray = capabilities.names().apply {
|
|
4154
4490
|
services.names().forEach { name -> put(name) }
|
|
@@ -4214,6 +4550,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4214
4550
|
}
|
|
4215
4551
|
|
|
4216
4552
|
private fun refreshScreenTheme(screen: Screen) {
|
|
4553
|
+
applyScreenBackground(screen)
|
|
4217
4554
|
screen.mutations.node("root")?.let { renderCommitted(screen, it) }
|
|
4218
4555
|
screen.controls.values.mapNotNull {
|
|
4219
4556
|
when (it) {
|
|
@@ -4236,7 +4573,20 @@ class MainActivity : AppCompatActivity() {
|
|
|
4236
4573
|
if (safeRoot) 0 else windowSafeInsets[2],
|
|
4237
4574
|
if (safeRoot) 0 else windowSafeInsets[3],
|
|
4238
4575
|
)
|
|
4239
|
-
|
|
4576
|
+
active?.let(::updateSafeAreaInsets)
|
|
4577
|
+
}
|
|
4578
|
+
|
|
4579
|
+
/**
|
|
4580
|
+
* Applies window insets to a screen that is mounted for an interactive
|
|
4581
|
+
* transition but is not visibleScreen yet. The tab swipe keeps both
|
|
4582
|
+
* screens attached while it moves the incoming one across the window, so
|
|
4583
|
+
* updating only the active screen leaves a SafeAreaView briefly measured
|
|
4584
|
+
* against zero insets.
|
|
4585
|
+
*/
|
|
4586
|
+
private fun updateSafeAreaInsets(screen: Screen) {
|
|
4587
|
+
val rootNode = screen.mutations.node("root")
|
|
4588
|
+
val safeRoot = rootNode?.let { canonicalType(it.optString("type")) == "SafeAreaView" } == true
|
|
4589
|
+
val tree = screen.view.getChildAt(0) as? CraftNativeFlexLayout
|
|
4240
4590
|
if (safeRoot) tree?.setWindowSafeInsets(windowSafeInsets[0], windowSafeInsets[1], windowSafeInsets[2], windowSafeInsets[3])
|
|
4241
4591
|
else tree?.setWindowSafeInsets(0, 0, 0, 0)
|
|
4242
4592
|
}
|
|
@@ -4325,6 +4675,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4325
4675
|
if (screen !== appScreen) {
|
|
4326
4676
|
payload.keys().forEach { key -> screen.options.put(key, payload.opt(key)) }
|
|
4327
4677
|
if (visibleScreen === screen) applyChrome(screen)
|
|
4678
|
+
if (payload.has("backgroundColor")) applyScreenBackground(screen)
|
|
4328
4679
|
}
|
|
4329
4680
|
}
|
|
4330
4681
|
"ANIMATE_NEXT" -> if (screen !== appScreen && visibleScreen === screen) animateNextChange(screen, payload)
|
|
@@ -4659,6 +5010,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
4659
5010
|
hintTextColors ?: ColorStateList.valueOf(Color.GRAY)
|
|
4660
5011
|
}
|
|
4661
5012
|
val defaultHighlightColor = inputHighlightDefaults.getOrPut(this) { highlightColor }
|
|
5013
|
+
val defaultPadding = inputPaddingDefaults.getOrPut(this) {
|
|
5014
|
+
intArrayOf(paddingLeft, paddingTop, paddingRight, paddingBottom)
|
|
5015
|
+
}
|
|
4662
5016
|
hint = craftNativeOptionalString(props.opt("placeholder")).orEmpty()
|
|
4663
5017
|
setHintTextColor(
|
|
4664
5018
|
craftNativeOptionalString(props.opt("placeholderTextColor"))
|
|
@@ -4685,7 +5039,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
4685
5039
|
configureTextStyle(this, style)
|
|
4686
5040
|
// The style's padding is where the text starts, as on iOS and
|
|
4687
5041
|
// the web; without one the field keeps its own.
|
|
4688
|
-
|
|
5042
|
+
val paddingKeys = listOf("padding", "paddingHorizontal", "paddingVertical", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom")
|
|
5043
|
+
if (paddingKeys.any(style::has)) {
|
|
4689
5044
|
val density = resources.displayMetrics.density
|
|
4690
5045
|
val all = craftNativePadding(style, "padding", 0, density)
|
|
4691
5046
|
val horizontal = craftNativePadding(style, "paddingHorizontal", all, density)
|
|
@@ -4696,6 +5051,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
4696
5051
|
craftNativePadding(style, "paddingRight", horizontal, density),
|
|
4697
5052
|
craftNativePadding(style, "paddingBottom", vertical, density),
|
|
4698
5053
|
)
|
|
5054
|
+
} else {
|
|
5055
|
+
// A keyed field can lose its authored padding while the
|
|
5056
|
+
// EditText itself is retained. Restore the native baseline
|
|
5057
|
+
// rather than carrying the previous field's spacing.
|
|
5058
|
+
setPadding(defaultPadding[0], defaultPadding[1], defaultPadding[2], defaultPadding[3])
|
|
4699
5059
|
}
|
|
4700
5060
|
// Reused EditTexts must forget the previous field's hint as
|
|
4701
5061
|
// well as accept a new one. Otherwise a password manager can
|
|
@@ -4772,7 +5132,14 @@ class MainActivity : AppCompatActivity() {
|
|
|
4772
5132
|
})
|
|
4773
5133
|
}
|
|
4774
5134
|
"Picker" -> (previous as? Spinner ?: Spinner(this)).apply {
|
|
4775
|
-
val
|
|
5135
|
+
val itemOptions = props.optJSONArray("items")?.let { items ->
|
|
5136
|
+
(0 until items.length()).mapNotNull { index ->
|
|
5137
|
+
val item = items.optJSONObject(index) ?: return@mapNotNull null
|
|
5138
|
+
val label = craftNativeOptionalString(item.opt("label")) ?: return@mapNotNull null
|
|
5139
|
+
craftNativePickerValue(item.opt("value"))?.let { it to label } ?: (label to label)
|
|
5140
|
+
}
|
|
5141
|
+
}.orEmpty()
|
|
5142
|
+
val options = if (itemOptions.isNotEmpty()) itemOptions else (0 until children.length()).mapNotNull { index ->
|
|
4776
5143
|
val option = children.optJSONObject(index) ?: return@mapNotNull null
|
|
4777
5144
|
if (option.optString("type") != "Text") return@mapNotNull null
|
|
4778
5145
|
val label = (option.optJSONArray("children") ?: JSONArray()).let { text ->
|
|
@@ -4780,7 +5147,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4780
5147
|
}
|
|
4781
5148
|
if (label.isBlank()) return@mapNotNull null
|
|
4782
5149
|
val optionProps = option.optJSONObject("props")
|
|
4783
|
-
val value =
|
|
5150
|
+
val value = craftNativePickerValue(optionProps?.opt("value")) ?: label
|
|
4784
5151
|
value to label
|
|
4785
5152
|
}
|
|
4786
5153
|
val handler = craftNativeEventName(events, "onValueChange", "onChange")
|
|
@@ -4792,8 +5159,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
4792
5159
|
adapter = ArrayAdapter(this@MainActivity, android.R.layout.simple_spinner_item, options.map { it.second }).also {
|
|
4793
5160
|
it.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
|
4794
5161
|
}
|
|
4795
|
-
val requested =
|
|
4796
|
-
?:
|
|
5162
|
+
val requested = craftNativePickerValue(props.opt("selectedValue"))
|
|
5163
|
+
?: craftNativePickerValue(props.opt("value")).orEmpty()
|
|
4797
5164
|
val selected = options.indexOfFirst { it.first == requested }.takeIf { it >= 0 } ?: 0
|
|
4798
5165
|
isEnabled = !props.optBoolean("disabled", false)
|
|
4799
5166
|
// Mounting a controlled picker must not look like a user
|
|
@@ -4870,8 +5237,16 @@ class MainActivity : AppCompatActivity() {
|
|
|
4870
5237
|
}
|
|
4871
5238
|
"TouchableOpacity", "TouchableHighlight", "Pressable" -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
|
|
4872
5239
|
configureContainer(this, style)
|
|
5240
|
+
val disabled = props.optBoolean("disabled", false)
|
|
4873
5241
|
screen.pressActiveOpacities[this] = craftNativeFiniteFloat(props, "activeOpacity", 0.5f)
|
|
4874
5242
|
screen.pressBaseOpacities[this] = craftNativeFiniteFloat(style, "opacity", 1f)
|
|
5243
|
+
if (type == "TouchableHighlight" && !disabled) {
|
|
5244
|
+
craftNativeOptionalString(props.opt("underlayColor"))?.let { screen.pressUnderlayColors[this] = color(it, Color.TRANSPARENT) }
|
|
5245
|
+
?: screen.pressUnderlayColors.remove(this)
|
|
5246
|
+
} else {
|
|
5247
|
+
screen.pressUnderlayColors.remove(this)
|
|
5248
|
+
}
|
|
5249
|
+
screen.pressBaseBackgrounds[this] = this.background
|
|
4875
5250
|
reconcileChildren(screen, this, children, identity, next, style, previousControls)
|
|
4876
5251
|
}
|
|
4877
5252
|
"Switch" -> (previous as? Switch ?: Switch(this)).apply {
|
|
@@ -5011,6 +5386,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
5011
5386
|
"center" -> ImageView.ScaleType.CENTER
|
|
5012
5387
|
else -> ImageView.ScaleType.CENTER_INSIDE
|
|
5013
5388
|
}
|
|
5389
|
+
craftNativeOptionalString(style.opt("tintColor"))
|
|
5390
|
+
?.let { image.setColorFilter(color(it, Color.TRANSPARENT)) }
|
|
5391
|
+
?: image.clearColorFilter()
|
|
5014
5392
|
val onLoadStart = craftNativeEventName(events, "onLoadStart")
|
|
5015
5393
|
if (onLoadStart.isNotBlank()) screen.imageLoadStartHandlers[image] = onLoadStart else screen.imageLoadStartHandlers.remove(image)
|
|
5016
5394
|
val onLoad = craftNativeEventName(events, "onLoad")
|
|
@@ -5041,8 +5419,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
5041
5419
|
)
|
|
5042
5420
|
}
|
|
5043
5421
|
"Map" -> (previous as? CraftNativeMapView ?: CraftNativeMapView(this)).apply {
|
|
5422
|
+
val mapMarkerHandler = craftNativeOptionalString(events.opt("onMarkerPress"))
|
|
5044
5423
|
configure(props, children) { markerId ->
|
|
5045
|
-
markerHandler(markerId)?.let { handler ->
|
|
5424
|
+
(markerHandler(markerId) ?: mapMarkerHandler)?.let { handler ->
|
|
5046
5425
|
send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject().put("id", markerId)))
|
|
5047
5426
|
}
|
|
5048
5427
|
}
|
|
@@ -5069,7 +5448,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
5069
5448
|
val refreshNode = (0 until children.length()).asSequence()
|
|
5070
5449
|
.mapNotNull { children.optJSONObject(it) }
|
|
5071
5450
|
.firstOrNull { canonicalType(it.optString("type")) == "RefreshControl" }
|
|
5072
|
-
val
|
|
5451
|
+
val propRefresh = props.optJSONObject("refreshControl")
|
|
5452
|
+
val refreshProps = refreshNode?.optJSONObject("props") ?: propRefresh ?: JSONObject()
|
|
5073
5453
|
val refreshEvents = refreshNode?.optJSONObject("events") ?: JSONObject()
|
|
5074
5454
|
val refreshHandlerName = craftNativeEventName(refreshEvents, "onRefresh")
|
|
5075
5455
|
.takeIf { it.isNotBlank() }
|
|
@@ -5077,7 +5457,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5077
5457
|
val refreshHandler = refreshHandlerName?.let { handler ->
|
|
5078
5458
|
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject())) }
|
|
5079
5459
|
}
|
|
5080
|
-
setRefreshHandler(refreshHandler,
|
|
5460
|
+
setRefreshHandler(refreshHandler, refreshProps.optBoolean("refreshing", props.optBoolean("refreshing", false)))
|
|
5081
5461
|
setRefreshAppearance(refreshProps, resources.displayMetrics.density)
|
|
5082
5462
|
val onScroll = craftNativeEventName(events, "onScroll")
|
|
5083
5463
|
if (onScroll.isNotBlank()) screen.scrollHandlers[this] = onScroll else screen.scrollHandlers.remove(this)
|
|
@@ -5143,6 +5523,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
5143
5523
|
if (child !is JSONObject || canonicalType(child.optString("type")) != "RefreshControl") contentChildren.put(child)
|
|
5144
5524
|
}
|
|
5145
5525
|
reconcileChildren(screen, content, contentChildren, identity, next, contentStyle, previousControls)
|
|
5526
|
+
setStickyHeaderIndices(props.optJSONArray("stickyHeaderIndices"))
|
|
5527
|
+
setScrollTarget(props.opt("scrollTarget"))
|
|
5146
5528
|
}
|
|
5147
5529
|
"FlatList" -> (previous as? CraftNativeRefreshableFlatList ?: CraftNativeRefreshableFlatList(this)).apply {
|
|
5148
5530
|
val nativeList = list
|
|
@@ -5155,7 +5537,6 @@ class MainActivity : AppCompatActivity() {
|
|
|
5155
5537
|
(getSystemService(INPUT_METHOD_SERVICE) as? android.view.inputmethod.InputMethodManager)
|
|
5156
5538
|
?.hideSoftInputFromWindow(windowToken, 0)
|
|
5157
5539
|
}
|
|
5158
|
-
nativeList.overScrollMode = if (props.optBoolean("bounces", true)) View.OVER_SCROLL_ALWAYS else View.OVER_SCROLL_NEVER
|
|
5159
5540
|
reconcileFlatList(screen, nativeList, children, identity, protocolId ?: identity, props, events)
|
|
5160
5541
|
}
|
|
5161
5542
|
"View", "SafeAreaView" -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
|
|
@@ -5182,6 +5563,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
5182
5563
|
val pressOutHandler = craftNativeEventName(events, "onPressOut").takeIf { it.isNotBlank() }
|
|
5183
5564
|
if (pressInHandler != null) screen.pressInHandlers[control] = pressInHandler else screen.pressInHandlers.remove(control)
|
|
5184
5565
|
if (pressOutHandler != null) screen.pressOutHandlers[control] = pressOutHandler else screen.pressOutHandlers.remove(control)
|
|
5566
|
+
val underlayShowHandler = if (type == "TouchableHighlight") craftNativeEventName(events, "onShowUnderlay").takeIf { it.isNotBlank() } else null
|
|
5567
|
+
val underlayHideHandler = if (type == "TouchableHighlight") craftNativeEventName(events, "onHideUnderlay").takeIf { it.isNotBlank() } else null
|
|
5568
|
+
if (underlayShowHandler != null) screen.pressUnderlayShowHandlers[control] = underlayShowHandler else screen.pressUnderlayShowHandlers.remove(control)
|
|
5569
|
+
if (underlayHideHandler != null) screen.pressUnderlayHideHandlers[control] = underlayHideHandler else screen.pressUnderlayHideHandlers.remove(control)
|
|
5185
5570
|
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
5571
|
val handler = craftNativeEventName(events, "onPress", "onClick")
|
|
5187
5572
|
val disabled = props.optBoolean("disabled", false)
|
|
@@ -5190,7 +5575,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
5190
5575
|
?.trim()
|
|
5191
5576
|
?.takeIf { !disabled && type in setOf("TouchableOpacity", "TouchableHighlight", "Pressable") }
|
|
5192
5577
|
if (target != null) screen.linkTargets[control] = target else screen.linkTargets.remove(control)
|
|
5193
|
-
val
|
|
5578
|
+
val hasUnderlay = type == "TouchableHighlight" && screen.pressUnderlayColors[control] != null
|
|
5579
|
+
val hasPressLifecycle = pressInHandler != null || pressOutHandler != null || hasUnderlay
|
|
5194
5580
|
if ((handler.isNotBlank() || target != null || (type in setOf("TouchableOpacity", "TouchableHighlight", "Pressable") && hasPressLifecycle)) && !disabled) {
|
|
5195
5581
|
if (handler.isNotBlank()) screen.handlers[control] = handler else screen.handlers.remove(control)
|
|
5196
5582
|
control.isClickable = true
|
|
@@ -5206,16 +5592,24 @@ class MainActivity : AppCompatActivity() {
|
|
|
5206
5592
|
val active = screen.pressActiveOpacities[view] ?: 0.5f
|
|
5207
5593
|
val base = screen.pressBaseOpacities[view] ?: 1f
|
|
5208
5594
|
view.alpha = base * active
|
|
5595
|
+
screen.pressUnderlayColors[view]?.let { view.background = ColorDrawable(it) }
|
|
5209
5596
|
screen.pressInHandlers[view]?.let { current ->
|
|
5210
5597
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
5211
5598
|
}
|
|
5599
|
+
screen.pressUnderlayShowHandlers[view]?.let { current ->
|
|
5600
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
5601
|
+
}
|
|
5212
5602
|
true
|
|
5213
5603
|
}
|
|
5214
5604
|
android.view.MotionEvent.ACTION_UP, android.view.MotionEvent.ACTION_CANCEL -> {
|
|
5215
5605
|
view.alpha = screen.pressBaseOpacities[view] ?: 1f
|
|
5606
|
+
if (screen.pressBaseBackgrounds.containsKey(view)) view.background = screen.pressBaseBackgrounds[view]
|
|
5216
5607
|
screen.pressOutHandlers[view]?.let { current ->
|
|
5217
5608
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
5218
5609
|
}
|
|
5610
|
+
screen.pressUnderlayHideHandlers[view]?.let { current ->
|
|
5611
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
5612
|
+
}
|
|
5219
5613
|
if (event.actionMasked == android.view.MotionEvent.ACTION_UP) {
|
|
5220
5614
|
screen.linkTargets[view]?.let { target -> navigateOpen(target); return@setOnTouchListener true }
|
|
5221
5615
|
val current = screen.handlers[view] ?: return@setOnTouchListener true
|
|
@@ -5228,6 +5622,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5228
5622
|
}
|
|
5229
5623
|
} else {
|
|
5230
5624
|
control.setOnTouchListener(null)
|
|
5625
|
+
if (screen.pressBaseBackgrounds.containsKey(control)) control.background = screen.pressBaseBackgrounds[control]
|
|
5231
5626
|
control.setOnClickListener {
|
|
5232
5627
|
screen.linkTargets[control]?.let { target -> navigateOpen(target); return@setOnClickListener }
|
|
5233
5628
|
val current = screen.handlers[control] ?: return@setOnClickListener
|
|
@@ -5464,9 +5859,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
5464
5859
|
}
|
|
5465
5860
|
val threshold = craftNativeNonNegativeDouble(props, "threshold", 0.1)
|
|
5466
5861
|
val authoredThreshold = craftNativeNonNegativeDouble(props, "onEndReachedThreshold", threshold)
|
|
5862
|
+
val horizontal = props.optBoolean("horizontal", false)
|
|
5467
5863
|
list.apply(
|
|
5468
5864
|
nodes = nodes,
|
|
5469
|
-
horizontal =
|
|
5865
|
+
horizontal = horizontal,
|
|
5470
5866
|
columns = props.optInt("numColumns", 1),
|
|
5471
5867
|
inverted = props.optBoolean("inverted", false),
|
|
5472
5868
|
endReachedThreshold = authoredThreshold,
|
|
@@ -5487,6 +5883,18 @@ class MainActivity : AppCompatActivity() {
|
|
|
5487
5883
|
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject())) }
|
|
5488
5884
|
},
|
|
5489
5885
|
)
|
|
5886
|
+
list.setScrollIndicators(
|
|
5887
|
+
props.optBoolean("showsVerticalScrollIndicator", true),
|
|
5888
|
+
props.optBoolean("showsHorizontalScrollIndicator", true),
|
|
5889
|
+
)
|
|
5890
|
+
list.setScrollEnabled(props.optBoolean("scrollEnabled", true))
|
|
5891
|
+
list.setPagingEnabled(props.optBoolean("pagingEnabled", false))
|
|
5892
|
+
list.setScrollTarget(props.opt("scrollTarget"))
|
|
5893
|
+
list.setBounceOptions(
|
|
5894
|
+
props.optBoolean("bounces", true),
|
|
5895
|
+
props.optBoolean("alwaysBounceVertical", !horizontal),
|
|
5896
|
+
props.optBoolean("alwaysBounceHorizontal", horizontal),
|
|
5897
|
+
)
|
|
5490
5898
|
}
|
|
5491
5899
|
|
|
5492
5900
|
private fun registerFlatListOwnership(screen: Screen, node: JSONObject, owner: String) {
|
|
@@ -5519,7 +5927,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
5519
5927
|
craftNativePadding(style, "paddingRight", horizontal, density),
|
|
5520
5928
|
craftNativePadding(style, "paddingBottom", vertical, density),
|
|
5521
5929
|
)
|
|
5522
|
-
} else if (view.javaClass == TextView::class.java) {
|
|
5930
|
+
} else if (view.javaClass == TextView::class.java || view.javaClass == Button::class.java) {
|
|
5931
|
+
// Plain Android buttons start with zero padding in the native
|
|
5932
|
+
// contract. A keyed button may lose a reactive padding style;
|
|
5933
|
+
// clear that old value just as iOS reapplies its padding every
|
|
5934
|
+
// reconciliation.
|
|
5523
5935
|
view.setPadding(0, 0, 0, 0)
|
|
5524
5936
|
}
|
|
5525
5937
|
}
|
|
@@ -5537,6 +5949,17 @@ class MainActivity : AppCompatActivity() {
|
|
|
5537
5949
|
else color(style.optString("backgroundColor"), Color.TRANSPARENT)
|
|
5538
5950
|
val border = craftNativeNonNegativeDouble(style, "borderWidth", 0.0)
|
|
5539
5951
|
val radius = craftNativeNonNegativeDouble(style, "borderRadius", 0.0)
|
|
5952
|
+
val gradientDirection = style.optString("gradientDirection").takeIf { it.isNotBlank() }
|
|
5953
|
+
val gradientFrom = style.optString("gradientFrom").takeIf { it.isNotBlank() }
|
|
5954
|
+
val gradientVia = style.optString("gradientVia").takeIf { it.isNotBlank() }
|
|
5955
|
+
val gradientTo = style.optString("gradientTo").takeIf { it.isNotBlank() }
|
|
5956
|
+
val gradientColors = if (gradientDirection != null && gradientFrom != null && gradientTo != null) {
|
|
5957
|
+
listOfNotNull(
|
|
5958
|
+
color(gradientFrom, Color.TRANSPARENT),
|
|
5959
|
+
gradientVia?.let { color(it, Color.TRANSPARENT) },
|
|
5960
|
+
color(gradientTo, Color.TRANSPARENT),
|
|
5961
|
+
).toIntArray()
|
|
5962
|
+
} else null
|
|
5540
5963
|
val sideBorderKeys = listOf("borderTopWidth", "borderRightWidth", "borderBottomWidth", "borderLeftWidth")
|
|
5541
5964
|
val hasSideBorders = (sideBorderKeys + listOf("borderTopColor", "borderRightColor", "borderBottomColor", "borderLeftColor")).any(style::has)
|
|
5542
5965
|
val sideWidths = sideBorderKeys.map { dp(craftNativeNonNegativeDouble(style, it, border)) }.toIntArray()
|
|
@@ -5554,17 +5977,23 @@ class MainActivity : AppCompatActivity() {
|
|
|
5554
5977
|
} else {
|
|
5555
5978
|
view.background.also { defaultBackgrounds[view] = it }
|
|
5556
5979
|
}
|
|
5557
|
-
if (background || border > 0 || radius > 0 || hasSideBorders) {
|
|
5980
|
+
if (background || border > 0 || radius > 0 || hasSideBorders || gradientColors != null) {
|
|
5558
5981
|
if (hasSideBorders) {
|
|
5559
5982
|
view.background = CraftNativeSideBorderDrawable(
|
|
5560
5983
|
backgroundColor,
|
|
5561
5984
|
cornerRadii,
|
|
5562
5985
|
sideWidths,
|
|
5563
5986
|
sideColors,
|
|
5987
|
+
style.optString("borderStyle").takeIf { it == "dashed" || it == "dotted" },
|
|
5988
|
+
gradientDirection,
|
|
5989
|
+
gradientColors,
|
|
5564
5990
|
)
|
|
5565
5991
|
} else {
|
|
5566
5992
|
view.background = GradientDrawable().apply {
|
|
5567
|
-
|
|
5993
|
+
if (gradientColors != null) {
|
|
5994
|
+
orientation = craftNativeGradientOrientation(gradientDirection)
|
|
5995
|
+
colors = gradientColors
|
|
5996
|
+
} else setColor(backgroundColor)
|
|
5568
5997
|
if (hasCornerRadii) setCornerRadii(cornerRadii) else cornerRadius = dp(radius).toFloat()
|
|
5569
5998
|
if (border > 0) {
|
|
5570
5999
|
val borderColor = color(style.optString("borderColor"), Color.TRANSPARENT)
|
|
@@ -5605,6 +6034,15 @@ class MainActivity : AppCompatActivity() {
|
|
|
5605
6034
|
view.invalidateOutline()
|
|
5606
6035
|
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
5607
6036
|
view.outlineProvider = ViewOutlineProvider.BACKGROUND
|
|
6037
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
6038
|
+
// A retained view may lose an authored shadow while keeping
|
|
6039
|
+
// its native object. Restore Android's default outline colors
|
|
6040
|
+
// too; otherwise a later elevation-only style inherits the
|
|
6041
|
+
// previous shadow's custom ambient/spot tint.
|
|
6042
|
+
view.outlineAmbientShadowColor = Color.BLACK
|
|
6043
|
+
view.outlineSpotShadowColor = Color.BLACK
|
|
6044
|
+
view.invalidateOutline()
|
|
6045
|
+
}
|
|
5608
6046
|
}
|
|
5609
6047
|
if (view is ImageView) {
|
|
5610
6048
|
val tint = style.opt("tintColor") as? String
|
|
@@ -5706,10 +6144,18 @@ class MainActivity : AppCompatActivity() {
|
|
|
5706
6144
|
else -> Typeface.NORMAL
|
|
5707
6145
|
}
|
|
5708
6146
|
view.typeface = Typeface.create("sans-serif", weight)
|
|
5709
|
-
|
|
6147
|
+
val hasExplicitWidth = style.has("width") && !style.isNull("width")
|
|
6148
|
+
val hasExplicitHeight = style.has("height") && !style.isNull("height")
|
|
6149
|
+
if (!hasExplicitWidth && !hasExplicitHeight) {
|
|
5710
6150
|
val box = dp(size.toDouble())
|
|
5711
6151
|
view.minimumWidth = box
|
|
5712
6152
|
view.minimumHeight = box
|
|
6153
|
+
} else {
|
|
6154
|
+
// A retained icon may move from the implicit square to an
|
|
6155
|
+
// explicitly sized layout. Do not let the old minimum constrain
|
|
6156
|
+
// the authored dimensions.
|
|
6157
|
+
view.minimumWidth = 0
|
|
6158
|
+
view.minimumHeight = 0
|
|
5713
6159
|
}
|
|
5714
6160
|
}
|
|
5715
6161
|
|
|
@@ -5859,6 +6305,17 @@ class MainActivity : AppCompatActivity() {
|
|
|
5859
6305
|
else -> ""
|
|
5860
6306
|
}
|
|
5861
6307
|
|
|
6308
|
+
private fun imageHeaders(source: Any?): Map<String, String> {
|
|
6309
|
+
val objectSource = source as? JSONObject ?: return emptyMap()
|
|
6310
|
+
val raw = objectSource.optJSONObject("headers") ?: return emptyMap()
|
|
6311
|
+
return linkedMapOf<String, String>().apply {
|
|
6312
|
+
raw.keys().forEach { name -> craftNativeOptionalString(raw.opt(name))?.let { put(name, it) } }
|
|
6313
|
+
}
|
|
6314
|
+
}
|
|
6315
|
+
|
|
6316
|
+
private fun imageRequestKey(uri: String, headers: Map<String, String>): String =
|
|
6317
|
+
uri + "\u0000" + headers.toSortedMap().entries.joinToString("\u0000") { "${it.key}=${it.value}" }
|
|
6318
|
+
|
|
5862
6319
|
private fun immediateImage(uri: String): Bitmap? {
|
|
5863
6320
|
if (uri.startsWith("data:image/") && uri.contains(',')) {
|
|
5864
6321
|
val bytes = runCatching { Base64.decode(uri.substringAfter(','), Base64.DEFAULT) }.getOrNull()
|
|
@@ -5870,6 +6327,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
5870
6327
|
|
|
5871
6328
|
private fun updateImage(screen: Screen, view: ImageView, source: Any?, defaultSource: Any? = null) {
|
|
5872
6329
|
val uri = imageUri(source)
|
|
6330
|
+
val headers = imageHeaders(source)
|
|
6331
|
+
val requestKey = imageRequestKey(uri, headers)
|
|
5873
6332
|
if (uri.isBlank()) {
|
|
5874
6333
|
if (imageSources[view] == "") return
|
|
5875
6334
|
imageJobs.remove(view)?.cancel(true)
|
|
@@ -5880,9 +6339,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
5880
6339
|
imageFailure(screen, view, uri, "Image source is missing")
|
|
5881
6340
|
return
|
|
5882
6341
|
}
|
|
5883
|
-
if (imageSources[view] ==
|
|
6342
|
+
if (imageSources[view] == requestKey) return
|
|
5884
6343
|
imageJobs.remove(view)?.cancel(true)
|
|
5885
|
-
imageSources[view] =
|
|
6344
|
+
imageSources[view] = requestKey
|
|
5886
6345
|
imageErrors.remove(view)
|
|
5887
6346
|
view.setImageDrawable(null)
|
|
5888
6347
|
imageDidChange(view)
|
|
@@ -5920,11 +6379,12 @@ class MainActivity : AppCompatActivity() {
|
|
|
5920
6379
|
val connection = URL(uri).openConnection().apply {
|
|
5921
6380
|
connectTimeout = 10_000
|
|
5922
6381
|
readTimeout = 10_000
|
|
6382
|
+
headers.forEach { (name, value) -> setRequestProperty(name, value) }
|
|
5923
6383
|
}
|
|
5924
6384
|
connection.getInputStream().use(BitmapFactory::decodeStream)
|
|
5925
6385
|
}.getOrNull()
|
|
5926
6386
|
runOnUiThread {
|
|
5927
|
-
if (imageSources[view] !=
|
|
6387
|
+
if (imageSources[view] != requestKey) return@runOnUiThread
|
|
5928
6388
|
imageJobs.remove(view)
|
|
5929
6389
|
if (bitmap != null) {
|
|
5930
6390
|
view.setImageBitmap(bitmap)
|
|
@@ -6508,7 +6968,13 @@ class MainActivity : AppCompatActivity() {
|
|
|
6508
6968
|
|
|
6509
6969
|
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
|
|
6510
6970
|
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
|
6511
|
-
if (isInPictureInPictureMode)
|
|
6971
|
+
if (isInPictureInPictureMode) {
|
|
6972
|
+
floatingVideo()?.notifyPictureInPicture(true)
|
|
6973
|
+
showVideoAlone()
|
|
6974
|
+
} else {
|
|
6975
|
+
pictureInPictureVideo?.notifyPictureInPicture(false)
|
|
6976
|
+
putVideoBack()
|
|
6977
|
+
}
|
|
6512
6978
|
}
|
|
6513
6979
|
|
|
6514
6980
|
/**
|
|
@@ -6584,6 +7050,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
6584
7050
|
released.filterIsInstance<EditText>().forEach {
|
|
6585
7051
|
inputHintDefaults.remove(it)
|
|
6586
7052
|
inputHighlightDefaults.remove(it)
|
|
7053
|
+
inputPaddingDefaults.remove(it)
|
|
6587
7054
|
screen.suppressedTextChanges.remove(it)
|
|
6588
7055
|
screen.inputEchoes.remove(it)
|
|
6589
7056
|
val identity = screen.inputIdentities.remove(it)
|
|
@@ -6594,6 +7061,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
6594
7061
|
screen.handlers.keys.removeAll(released)
|
|
6595
7062
|
screen.pressInHandlers.keys.removeAll(released)
|
|
6596
7063
|
screen.pressOutHandlers.keys.removeAll(released)
|
|
7064
|
+
screen.pressUnderlayShowHandlers.keys.removeAll(released)
|
|
7065
|
+
screen.pressUnderlayHideHandlers.keys.removeAll(released)
|
|
7066
|
+
screen.pressUnderlayColors.keys.removeAll(released)
|
|
7067
|
+
screen.pressBaseBackgrounds.keys.removeAll(released)
|
|
6597
7068
|
screen.longPressHandlers.keys.removeAll(released)
|
|
6598
7069
|
screen.layoutHandlers.keys.removeAll(released)
|
|
6599
7070
|
screen.layoutListeners.forEach { (view, listener) ->
|
|
@@ -6618,6 +7089,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
6618
7089
|
screen.imageErrorHandlers.keys.removeAll(releasedImages)
|
|
6619
7090
|
screen.pressActiveOpacities.keys.removeAll(released)
|
|
6620
7091
|
screen.pressBaseOpacities.keys.removeAll(released)
|
|
7092
|
+
screen.pressUnderlayColors.keys.removeAll(released)
|
|
7093
|
+
screen.pressBaseBackgrounds.keys.removeAll(released)
|
|
6621
7094
|
screen.modalVisibility.keys.removeAll(released)
|
|
6622
7095
|
screen.modalCloseHandlers.keys.removeAll(released)
|
|
6623
7096
|
}
|
|
@@ -6648,6 +7121,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
6648
7121
|
screen.handlers.clear()
|
|
6649
7122
|
screen.pressInHandlers.clear()
|
|
6650
7123
|
screen.pressOutHandlers.clear()
|
|
7124
|
+
screen.pressUnderlayShowHandlers.clear()
|
|
7125
|
+
screen.pressUnderlayHideHandlers.clear()
|
|
7126
|
+
screen.pressUnderlayColors.clear()
|
|
7127
|
+
screen.pressBaseBackgrounds.clear()
|
|
6651
7128
|
screen.layoutHandlers.clear()
|
|
6652
7129
|
screen.lastLayoutBounds.clear()
|
|
6653
7130
|
screen.scrollHandlers.clear()
|