craft-native 0.0.163 → 0.0.166
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/android/templates/AndroidManifest.xml.template +1 -1
- package/dist/android/templates/MainActivityNative.kt.template +516 -119
- package/dist/api/mobile.d.ts +34 -2
- package/dist/cli.js +1 -1
- package/dist/components/index.d.ts +2 -2
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.d.ts +1 -0
- package/dist/ios/src/index.js +138 -88
- package/dist/ios/src/source-audit.d.ts +8 -0
- package/dist/ios/templates/CraftApp.swift +94 -2
- package/dist/ios/templates/CraftHealth.swift +261 -40
- package/dist/ios/templates/CraftHeartRate.swift +34 -4
- package/dist/ios/templates/CraftHybrid.swift +174 -2
- package/dist/ios/templates/CraftNativeActions.swift +111 -1
- package/dist/ios/templates/CraftNativeFiles.swift +107 -0
- package/dist/ios/templates/CraftNativeScreen.swift +354 -59
- package/dist/mobile.js +35 -0
- package/package.json +1 -1
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
package {{PACKAGE_NAME}}
|
|
2
2
|
|
|
3
|
+
import android.animation.ValueAnimator
|
|
3
4
|
import android.content.ClipData
|
|
4
5
|
import android.content.ClipboardManager
|
|
5
6
|
import android.content.Context
|
|
6
7
|
import android.content.res.ColorStateList
|
|
7
8
|
import android.content.res.Configuration
|
|
9
|
+
import android.database.ContentObserver
|
|
8
10
|
import android.content.Intent
|
|
9
11
|
import android.media.AudioAttributes
|
|
10
12
|
import android.media.AudioFormat
|
|
@@ -14,9 +16,13 @@ import android.graphics.Canvas
|
|
|
14
16
|
import android.graphics.Color
|
|
15
17
|
import android.graphics.DashPathEffect
|
|
16
18
|
import android.graphics.Matrix
|
|
19
|
+
import android.graphics.Outline
|
|
17
20
|
import android.graphics.Paint
|
|
18
21
|
import android.graphics.Path
|
|
22
|
+
import android.graphics.RectF
|
|
19
23
|
import android.graphics.Typeface
|
|
24
|
+
import android.graphics.ColorFilter
|
|
25
|
+
import android.graphics.PixelFormat
|
|
20
26
|
import android.graphics.drawable.Drawable
|
|
21
27
|
import android.graphics.drawable.GradientDrawable
|
|
22
28
|
import android.net.Uri
|
|
@@ -26,10 +32,14 @@ import android.os.Handler
|
|
|
26
32
|
import android.os.Looper
|
|
27
33
|
import android.os.VibrationEffect
|
|
28
34
|
import android.os.Vibrator
|
|
35
|
+
import android.provider.Settings
|
|
29
36
|
import android.text.Editable
|
|
30
37
|
import android.text.InputFilter
|
|
31
38
|
import android.text.TextUtils
|
|
32
39
|
import android.text.TextWatcher
|
|
40
|
+
import android.text.Spannable
|
|
41
|
+
import android.text.SpannableString
|
|
42
|
+
import android.text.style.ReplacementSpan
|
|
33
43
|
import android.util.Log
|
|
34
44
|
import android.util.Base64
|
|
35
45
|
import android.util.TypedValue
|
|
@@ -41,6 +51,7 @@ import android.text.InputType
|
|
|
41
51
|
import android.text.Layout
|
|
42
52
|
import android.view.View
|
|
43
53
|
import android.view.ViewGroup
|
|
54
|
+
import android.view.ViewOutlineProvider
|
|
44
55
|
import android.view.ViewTreeObserver
|
|
45
56
|
import android.widget.AdapterView
|
|
46
57
|
import android.widget.ArrayAdapter
|
|
@@ -85,9 +96,11 @@ import java.util.Locale
|
|
|
85
96
|
import java.util.concurrent.Executors
|
|
86
97
|
import java.util.concurrent.Future
|
|
87
98
|
import java.util.UUID
|
|
99
|
+
import java.util.WeakHashMap
|
|
88
100
|
import kotlin.math.max
|
|
89
101
|
import kotlin.math.min
|
|
90
102
|
import kotlin.math.PI
|
|
103
|
+
import kotlin.math.ceil
|
|
91
104
|
import kotlin.math.roundToInt
|
|
92
105
|
import kotlin.math.roundToLong
|
|
93
106
|
import kotlin.math.sin
|
|
@@ -177,6 +190,32 @@ private fun craftNativeGridColumnCount(value: Any?): Int {
|
|
|
177
190
|
return max(1, trimmed.split(Regex("\\s+")).count { it.isNotBlank() })
|
|
178
191
|
}
|
|
179
192
|
|
|
193
|
+
/** Android View has no skew setters before API 29; draw the shared skew around the view's center. */
|
|
194
|
+
private object CraftNativeSkewTransforms {
|
|
195
|
+
private val values = WeakHashMap<View, Pair<Float, Float>>()
|
|
196
|
+
|
|
197
|
+
fun set(view: View, x: Float, y: Float) {
|
|
198
|
+
if (x == 0f && y == 0f) values.remove(view) else values[view] = x to y
|
|
199
|
+
view.invalidate()
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
fun clear(view: View) {
|
|
203
|
+
values.remove(view)
|
|
204
|
+
view.invalidate()
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
fun matrix(view: View): Matrix? {
|
|
208
|
+
val (x, y) = values[view] ?: return null
|
|
209
|
+
val pivotX = view.width / 2f
|
|
210
|
+
val pivotY = view.height / 2f
|
|
211
|
+
return Matrix().apply {
|
|
212
|
+
setTranslate(-pivotX, -pivotY)
|
|
213
|
+
postSkew(x, y)
|
|
214
|
+
postTranslate(pivotX, pivotY)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
180
219
|
/** A DOM-free SVG surface for the geometry emitted by the shared STX native protocol. */
|
|
181
220
|
private class CraftNativeSvgView(context: Context) : View(context) {
|
|
182
221
|
var drawing: JSONObject = JSONObject()
|
|
@@ -202,13 +241,16 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
202
241
|
props.has(name) && !props.isNull(name) -> props.optString(name)
|
|
203
242
|
else -> null
|
|
204
243
|
}
|
|
244
|
+
// Keep the cascade explicit: computed style wins over a raw prop
|
|
245
|
+
// when both provide the same SVG paint value.
|
|
205
246
|
val weight = value("fontWeight")
|
|
247
|
+
val ownOpacity = value("opacity")?.toFloatOrNull() ?: 1f
|
|
206
248
|
return copy(
|
|
207
249
|
fill = value("fill") ?: fill,
|
|
208
250
|
stroke = value("stroke") ?: stroke,
|
|
209
251
|
strokeWidth = value("strokeWidth")?.toFloatOrNull() ?: strokeWidth,
|
|
210
252
|
color = value("color") ?: color,
|
|
211
|
-
opacity =
|
|
253
|
+
opacity = ownOpacity * opacity,
|
|
212
254
|
typeface = if (weight in setOf("bold", "700")) Typeface.DEFAULT_BOLD else typeface,
|
|
213
255
|
)
|
|
214
256
|
}
|
|
@@ -393,23 +435,35 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
393
435
|
private var alignItems = "stretch"
|
|
394
436
|
private var alignContent = "flex-start"
|
|
395
437
|
private var justifyContent = "flex-start"
|
|
396
|
-
private var rowGap =
|
|
397
|
-
private var columnGap =
|
|
438
|
+
private var rowGap = 0f
|
|
439
|
+
private var columnGap = 0f
|
|
398
440
|
private var grid = false
|
|
399
441
|
private var gridColumns = 1
|
|
400
442
|
private var gridAutoRows: CraftNativeLength? = null
|
|
401
443
|
private var reverse = false
|
|
444
|
+
private var wrapReverse = false
|
|
445
|
+
|
|
446
|
+
override fun drawChild(canvas: Canvas, child: View, drawingTime: Long): Boolean {
|
|
447
|
+
val skew = CraftNativeSkewTransforms.matrix(child) ?: return super.drawChild(canvas, child, drawingTime)
|
|
448
|
+
val save = canvas.save()
|
|
449
|
+
canvas.concat(skew)
|
|
450
|
+
val drawn = super.drawChild(canvas, child, drawingTime)
|
|
451
|
+
canvas.restoreToCount(save)
|
|
452
|
+
return drawn
|
|
453
|
+
}
|
|
402
454
|
|
|
403
455
|
fun configure(style: JSONObject, density: Float) {
|
|
404
456
|
orientation = if (style.optString("flexDirection") in setOf("row", "row-reverse")) HORIZONTAL else VERTICAL
|
|
405
457
|
reverse = style.optString("flexDirection").endsWith("-reverse")
|
|
406
|
-
|
|
458
|
+
val flexWrap = style.optString("flexWrap")
|
|
459
|
+
wrap = flexWrap == "wrap" || flexWrap == "wrap-reverse" || style.optBoolean("flexWrap", false)
|
|
460
|
+
wrapReverse = flexWrap == "wrap-reverse"
|
|
407
461
|
alignItems = style.optString("alignItems", "stretch")
|
|
408
462
|
alignContent = style.optString("alignContent", "flex-start")
|
|
409
463
|
justifyContent = style.optString("justifyContent", "flex-start")
|
|
410
|
-
val gap =
|
|
411
|
-
rowGap =
|
|
412
|
-
columnGap =
|
|
464
|
+
val gap = style.optDouble("gap", 0.0) * density
|
|
465
|
+
rowGap = style.optDouble("rowGap", gap / density.toDouble()).toFloat() * density
|
|
466
|
+
columnGap = style.optDouble("columnGap", gap / density.toDouble()).toFloat() * density
|
|
413
467
|
grid = style.optString("display") == "grid"
|
|
414
468
|
gridColumns = craftNativeGridColumnCount(
|
|
415
469
|
if (style.has("gridTemplateColumns")) style.opt("gridTemplateColumns") else style.opt("gridColumns")
|
|
@@ -438,8 +492,8 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
438
492
|
}
|
|
439
493
|
|
|
440
494
|
private data class Entry(val view: View, val style: CraftNativeLayoutStyle, val width: Int, val height: Int)
|
|
441
|
-
private data class Line(val entries: MutableList<Entry> = mutableListOf(), var main:
|
|
442
|
-
private data class GridLayout(val rows: List<List<Entry>>, val absolute: List<Entry>, val cellWidth:
|
|
495
|
+
private data class Line(val entries: MutableList<Entry> = mutableListOf(), var main: Float = 0f, var cross: Float = 0f)
|
|
496
|
+
private data class GridLayout(val rows: List<List<Entry>>, val absolute: List<Entry>, val cellWidth: Float, val rowHeights: List<Float>)
|
|
443
497
|
|
|
444
498
|
private fun entries(width: Int, height: Int): Pair<List<Line>, List<Entry>> {
|
|
445
499
|
val availableMain = if (orientation == HORIZONTAL) max(0, width - paddingLeft - paddingRight)
|
|
@@ -487,8 +541,8 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
487
541
|
}
|
|
488
542
|
var line = Line()
|
|
489
543
|
pending.forEachIndexed { index, entry ->
|
|
490
|
-
val childMain = if (orientation == HORIZONTAL) entry.width +
|
|
491
|
-
else entry.height +
|
|
544
|
+
val childMain = if (orientation == HORIZONTAL) entry.width.toFloat() + margins[index * 2] + margins[index * 2 + 1]
|
|
545
|
+
else entry.height.toFloat() + margins[index * 2] + margins[index * 2 + 1]
|
|
492
546
|
val lp = entry.view.layoutParams as? MarginLayoutParams
|
|
493
547
|
val crossMargins = if (orientation == HORIZONTAL) (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
|
|
494
548
|
else (lp?.leftMargin ?: 0) + (lp?.rightMargin ?: 0)
|
|
@@ -502,17 +556,22 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
502
556
|
}
|
|
503
557
|
line.entries += entry
|
|
504
558
|
line.main = if (line.entries.size == 1) childMain else line.main + gap + childMain
|
|
505
|
-
line.cross = max(line.cross, childCross)
|
|
559
|
+
line.cross = max(line.cross, childCross.toFloat())
|
|
506
560
|
}
|
|
507
561
|
if (line.entries.isNotEmpty()) lines += line
|
|
508
562
|
}
|
|
563
|
+
// Reconciliation reverses the child order so the main axis runs in
|
|
564
|
+
// the requested direction. With wrapping, that also reverses the
|
|
565
|
+
// line order; flexbox keeps wrap lines in cross-axis order, so restore
|
|
566
|
+
// the groups while retaining the reversed entries inside each line.
|
|
567
|
+
if (reverse && wrap && lines.size > 1) lines.reverse()
|
|
509
568
|
return lines to absolute
|
|
510
569
|
}
|
|
511
570
|
|
|
512
571
|
private fun gridLayout(width: Int, height: Int): GridLayout {
|
|
513
572
|
val availableWidth = max(0, width - paddingLeft - paddingRight)
|
|
514
573
|
val gap = columnGap
|
|
515
|
-
val cellWidth = max(
|
|
574
|
+
val cellWidth = max(0f, (availableWidth.toFloat() - max(0, gridColumns - 1) * gap) / max(1, gridColumns).toFloat())
|
|
516
575
|
val rows = mutableListOf<MutableList<Entry>>()
|
|
517
576
|
val absolute = mutableListOf<Entry>()
|
|
518
577
|
for (index in 0 until childCount) {
|
|
@@ -532,7 +591,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
532
591
|
absolute += Entry(child, style, modalWidth, modalHeight)
|
|
533
592
|
continue
|
|
534
593
|
}
|
|
535
|
-
val childWidth = measureChildWidth(child, style, cellWidth, horizontalMargins)
|
|
594
|
+
val childWidth = measureChildWidth(child, style, cellWidth.roundToInt(), horizontalMargins)
|
|
536
595
|
val childHeight = measureChildHeight(child, style, height - paddingTop - paddingBottom, verticalMargins)
|
|
537
596
|
val entry = Entry(child, style, childWidth, childHeight)
|
|
538
597
|
if (style.position == "absolute") absolute += entry
|
|
@@ -542,10 +601,10 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
542
601
|
}
|
|
543
602
|
}
|
|
544
603
|
val rowHeights = rows.map { row ->
|
|
545
|
-
max(gridAutoRows?.resolve(height - paddingTop - paddingBottom) ?: 0, row.maxOfOrNull { entry ->
|
|
604
|
+
max((gridAutoRows?.resolve(height - paddingTop - paddingBottom) ?: 0).toFloat(), row.maxOfOrNull { entry ->
|
|
546
605
|
val lp = entry.view.layoutParams as? MarginLayoutParams
|
|
547
|
-
entry.height + (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
|
|
548
|
-
} ?:
|
|
606
|
+
(entry.height + (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)).toFloat()
|
|
607
|
+
} ?: 0f)
|
|
549
608
|
}
|
|
550
609
|
return GridLayout(rows, absolute, cellWidth, rowHeights)
|
|
551
610
|
}
|
|
@@ -573,9 +632,9 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
573
632
|
flowIndex += 1
|
|
574
633
|
}
|
|
575
634
|
val cellWidth = columnWidths.maxOrNull() ?: 0
|
|
576
|
-
val width = paddingLeft + paddingRight + cellWidth * columns + max(0, columns - 1) * columnGap
|
|
635
|
+
val width = paddingLeft + paddingRight + cellWidth * columns + (max(0, columns - 1) * columnGap).roundToInt()
|
|
577
636
|
val rowGapTotal = max(0, rowHeights.size - 1) * rowGap
|
|
578
|
-
val measuredHeight = paddingTop + paddingBottom + rowHeights.sum() + rowGapTotal
|
|
637
|
+
val measuredHeight = paddingTop + paddingBottom + rowHeights.sum() + rowGapTotal.roundToInt()
|
|
579
638
|
return width to measuredHeight
|
|
580
639
|
}
|
|
581
640
|
|
|
@@ -658,15 +717,15 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
658
717
|
}
|
|
659
718
|
|
|
660
719
|
/** Keep the old-runtime shim on the same bounded-flex water-fill rule as the Zig bridge. */
|
|
661
|
-
private fun fallbackFlexWidths(line: Line, available: Int, gap:
|
|
720
|
+
private fun fallbackFlexWidths(line: Line, available: Int, gap: Float): IntArray {
|
|
662
721
|
val horizontal = orientation == HORIZONTAL
|
|
663
722
|
val hypothetical = line.entries.sumOf { entry ->
|
|
664
723
|
val lp = entry.view.layoutParams as? MarginLayoutParams
|
|
665
724
|
val margins = if (horizontal) (lp?.leftMargin ?: 0) + (lp?.rightMargin ?: 0)
|
|
666
725
|
else (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
|
|
667
|
-
(if (horizontal) entry.width else entry.height) + margins
|
|
668
|
-
} + max(0, line.entries.size - 1) * gap
|
|
669
|
-
val free = available - hypothetical
|
|
726
|
+
((if (horizontal) entry.width else entry.height) + margins).toDouble()
|
|
727
|
+
} + max(0, line.entries.size - 1) * gap.toDouble()
|
|
728
|
+
val free = available.toDouble() - hypothetical
|
|
670
729
|
val growTotal = line.entries.sumOf { it.style.flexGrow }
|
|
671
730
|
val shrinkTotal = line.entries.sumOf {
|
|
672
731
|
(it.style.flexShrink ?: defaultFlexShrink(it.view)) * (if (horizontal) it.width else it.height)
|
|
@@ -695,7 +754,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
695
754
|
}
|
|
696
755
|
}
|
|
697
756
|
if (factor <= 0) return@repeat
|
|
698
|
-
val remaining = available - fixed - activeBasis
|
|
757
|
+
val remaining = available.toDouble() - fixed - activeBasis
|
|
699
758
|
var froze = false
|
|
700
759
|
for ((index, entry) in line.entries.withIndex()) {
|
|
701
760
|
if (frozen[index]) continue
|
|
@@ -733,16 +792,16 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
733
792
|
return
|
|
734
793
|
}
|
|
735
794
|
val layout = gridLayout(width, height)
|
|
736
|
-
val desiredWidth = paddingLeft + paddingRight + layout.cellWidth * gridColumns + max(0, gridColumns - 1) * columnGap
|
|
737
|
-
val desiredHeight = paddingTop + paddingBottom + layout.rowHeights.sum() + max(0, layout.rowHeights.size - 1) * rowGap
|
|
795
|
+
val desiredWidth = paddingLeft + paddingRight + (layout.cellWidth * gridColumns + max(0, gridColumns - 1) * columnGap).roundToInt()
|
|
796
|
+
val desiredHeight = paddingTop + paddingBottom + layout.rowHeights.sum().roundToInt() + (max(0, layout.rowHeights.size - 1) * rowGap).roundToInt()
|
|
738
797
|
setMeasuredDimension(resolveSize(desiredWidth, widthMeasureSpec), resolveSize(desiredHeight, heightMeasureSpec))
|
|
739
798
|
return
|
|
740
799
|
}
|
|
741
800
|
val (lines, absolute) = entries(width, height)
|
|
742
|
-
val main = lines.maxOfOrNull { it.main } ?:
|
|
743
|
-
val cross = lines.sumOf { it.cross } + max(0, lines.size - 1) * (if (orientation == HORIZONTAL) rowGap else columnGap)
|
|
744
|
-
val desiredWidth = if (orientation == HORIZONTAL) main else cross
|
|
745
|
-
val desiredHeight = if (orientation == HORIZONTAL) cross else main
|
|
801
|
+
val main = lines.maxOfOrNull { it.main } ?: 0f
|
|
802
|
+
val cross = lines.sumOf { it.cross.toDouble() } + max(0, lines.size - 1) * (if (orientation == HORIZONTAL) rowGap else columnGap).toDouble()
|
|
803
|
+
val desiredWidth = if (orientation == HORIZONTAL) main.roundToInt() else cross.roundToInt()
|
|
804
|
+
val desiredHeight = if (orientation == HORIZONTAL) cross.roundToInt() else main.roundToInt()
|
|
746
805
|
val absWidth = absolute.maxOfOrNull { it.width } ?: 0
|
|
747
806
|
val absHeight = absolute.maxOfOrNull { it.height } ?: 0
|
|
748
807
|
setMeasuredDimension(
|
|
@@ -760,12 +819,12 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
760
819
|
// Older runtimes return null for the optional symbol; the local
|
|
761
820
|
// layout below remains the compatibility path in that case.
|
|
762
821
|
val sharedFrames = CraftNative.grid(
|
|
763
|
-
contentWidth.toFloat(), contentHeight.toFloat(), gridColumns, columnGap
|
|
822
|
+
contentWidth.toFloat(), contentHeight.toFloat(), gridColumns, columnGap, rowGap,
|
|
764
823
|
flow.map { it.width.toFloat() }.toFloatArray(), flow.map { it.height.toFloat() }.toFloatArray(),
|
|
765
|
-
flow.map { it.style.width?.resolve(layout.cellWidth)?.toFloat() ?: Float.NaN }.toFloatArray(),
|
|
824
|
+
flow.map { it.style.width?.resolve(layout.cellWidth.roundToInt())?.toFloat() ?: Float.NaN }.toFloatArray(),
|
|
766
825
|
flow.map { it.style.height?.resolve(null)?.toFloat() ?: Float.NaN }.toFloatArray(),
|
|
767
|
-
flow.map { it.style.minWidth?.resolve(layout.cellWidth)?.toFloat() ?: Float.NaN }.toFloatArray(),
|
|
768
|
-
flow.map { it.style.maxWidth?.resolve(layout.cellWidth)?.toFloat() ?: Float.NaN }.toFloatArray(),
|
|
826
|
+
flow.map { it.style.minWidth?.resolve(layout.cellWidth.roundToInt())?.toFloat() ?: Float.NaN }.toFloatArray(),
|
|
827
|
+
flow.map { it.style.maxWidth?.resolve(layout.cellWidth.roundToInt())?.toFloat() ?: Float.NaN }.toFloatArray(),
|
|
769
828
|
flow.map { it.style.minHeight?.resolve(null)?.toFloat() ?: Float.NaN }.toFloatArray(),
|
|
770
829
|
flow.map { it.style.maxHeight?.resolve(null)?.toFloat() ?: Float.NaN }.toFloatArray(),
|
|
771
830
|
flow.flatMap {
|
|
@@ -790,8 +849,8 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
790
849
|
}
|
|
791
850
|
} else {
|
|
792
851
|
val totalRows = layout.rowHeights.sum()
|
|
793
|
-
val rowGaps = max(0, layout.rowHeights.size - 1) * rowGap
|
|
794
|
-
val freeRows = max(
|
|
852
|
+
val rowGaps = max(0, layout.rowHeights.size - 1) * rowGap.toFloat()
|
|
853
|
+
val freeRows = max(0f, contentHeight.toFloat() - totalRows - rowGaps)
|
|
795
854
|
val rowCount = max(1, layout.rowHeights.size)
|
|
796
855
|
var rowLeading = 0f
|
|
797
856
|
var rowBetween = rowGap.toFloat()
|
|
@@ -820,15 +879,15 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
820
879
|
val marginRight = lp?.rightMargin ?: 0
|
|
821
880
|
val marginTop = lp?.topMargin ?: 0
|
|
822
881
|
val marginBottom = lp?.bottomMargin ?: 0
|
|
823
|
-
val availableWidth = max(
|
|
882
|
+
val availableWidth = max(0f, layout.cellWidth - marginLeft - marginRight)
|
|
824
883
|
val availableHeight = max(0, (rowHeight - marginTop - marginBottom).roundToInt())
|
|
825
|
-
val childWidth = if (align == "stretch" && entry.style.width == null) availableWidth else min(availableWidth, entry.width)
|
|
884
|
+
val childWidth = if (align == "stretch" && entry.style.width == null) availableWidth.roundToInt() else min(availableWidth.roundToInt(), entry.width)
|
|
826
885
|
val childHeight = if (align == "stretch" && entry.style.height == null) availableHeight else min(availableHeight, entry.height)
|
|
827
|
-
val x = paddingLeft + column * (layout.cellWidth + columnGap) + marginLeft + when (align) {
|
|
828
|
-
"center" -> (availableWidth - childWidth) /
|
|
886
|
+
val x = (paddingLeft + column * (layout.cellWidth + columnGap) + marginLeft + when (align) {
|
|
887
|
+
"center" -> (availableWidth - childWidth) / 2f
|
|
829
888
|
"flex-end" -> availableWidth - childWidth
|
|
830
|
-
else ->
|
|
831
|
-
}
|
|
889
|
+
else -> 0f
|
|
890
|
+
}).roundToInt()
|
|
832
891
|
val y = (rowTop + marginTop + when (align) {
|
|
833
892
|
"center" -> (availableHeight - childHeight) / 2
|
|
834
893
|
"flex-end" -> availableHeight - childHeight
|
|
@@ -857,42 +916,45 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
857
916
|
val crossGap = if (orientation == HORIZONTAL) rowGap else columnGap
|
|
858
917
|
val availableMain = if (orientation == HORIZONTAL) contentWidth else contentHeight
|
|
859
918
|
val availableCross = if (orientation == HORIZONTAL) contentHeight else contentWidth
|
|
860
|
-
val totalCross = lines.sumOf { it.cross } + max(0, lines.size - 1) * crossGap
|
|
861
|
-
val crossFree = max(
|
|
862
|
-
var crossLeading =
|
|
919
|
+
val totalCross = lines.sumOf { it.cross.toDouble() }.toFloat() + max(0, lines.size - 1) * crossGap
|
|
920
|
+
val crossFree = max(0f, availableCross.toFloat() - totalCross)
|
|
921
|
+
var crossLeading = 0f
|
|
863
922
|
var crossBetween = crossGap
|
|
864
923
|
val sharedCross = if (wrap && lines.size > 1) {
|
|
865
924
|
CraftNative.flexLineDistribution(
|
|
866
|
-
availableCross.toFloat(), crossGap
|
|
925
|
+
availableCross.toFloat(), crossGap, alignContentCode(), lines.map { it.cross }.toFloatArray()
|
|
867
926
|
)
|
|
868
927
|
} else null
|
|
869
|
-
sharedCross?.second?.forEachIndexed { index, size -> lines[index].cross = size
|
|
928
|
+
sharedCross?.second?.forEachIndexed { index, size -> lines[index].cross = size }
|
|
870
929
|
if (wrap && lines.size > 1) {
|
|
871
930
|
if (sharedCross == null) when (alignContent) {
|
|
872
|
-
"center" -> crossLeading = crossFree /
|
|
931
|
+
"center" -> crossLeading = crossFree / 2f
|
|
873
932
|
"flex-end" -> crossLeading = crossFree
|
|
874
|
-
"space-between" -> crossBetween = crossFree / (lines.size - 1) + crossGap
|
|
933
|
+
"space-between" -> crossBetween = crossFree / (lines.size - 1).toFloat() + crossGap
|
|
875
934
|
"space-around" -> {
|
|
876
|
-
crossBetween = crossFree / lines.size + crossGap
|
|
877
|
-
crossLeading = crossFree / (lines.size * 2)
|
|
935
|
+
crossBetween = crossFree / lines.size.toFloat() + crossGap
|
|
936
|
+
crossLeading = crossFree / (lines.size * 2).toFloat()
|
|
878
937
|
}
|
|
879
938
|
"space-evenly" -> {
|
|
880
|
-
crossBetween = crossFree / (lines.size + 1) + crossGap
|
|
881
|
-
crossLeading = crossFree / (lines.size + 1)
|
|
882
|
-
}
|
|
883
|
-
"stretch" -> {
|
|
884
|
-
val extra = crossFree / lines.size
|
|
885
|
-
lines.forEach { it.cross += extra }
|
|
939
|
+
crossBetween = crossFree / (lines.size + 1).toFloat() + crossGap
|
|
940
|
+
crossLeading = crossFree / (lines.size + 1).toFloat()
|
|
886
941
|
}
|
|
887
942
|
}
|
|
888
943
|
}
|
|
889
|
-
|
|
944
|
+
val stretchExtra = if (sharedCross == null && wrap && lines.size > 1 && alignContent == "stretch")
|
|
945
|
+
crossFree / lines.size.toFloat()
|
|
946
|
+
else 0f
|
|
947
|
+
var crossOffset = (if (orientation == HORIZONTAL) paddingTop else paddingLeft).toFloat() + crossLeading
|
|
948
|
+
val crossOrigin = (if (orientation == HORIZONTAL) paddingTop else paddingLeft).toFloat()
|
|
949
|
+
val crossExtent = availableCross.toFloat()
|
|
890
950
|
for ((lineIndex, line) in lines.withIndex()) {
|
|
891
|
-
val
|
|
951
|
+
val lineCross = line.cross.toFloat() + stretchExtra
|
|
952
|
+
val lineOffset = sharedCross?.first?.get(lineIndex)?.let { crossOrigin + it } ?: crossOffset
|
|
953
|
+
val physicalLineOffset = if (wrapReverse && wrap) crossOrigin + crossExtent - (lineOffset - crossOrigin) - lineCross else lineOffset
|
|
892
954
|
val fallbackWidths = fallbackFlexWidths(line, availableMain, gap)
|
|
893
955
|
val sharedPrimary = CraftNative.flexRow(
|
|
894
956
|
availableMain.toFloat(),
|
|
895
|
-
gap
|
|
957
|
+
gap,
|
|
896
958
|
justifyCode(),
|
|
897
959
|
line.entries.map { entry -> if (orientation == HORIZONTAL) entry.width.toFloat() else entry.height.toFloat() }.toFloatArray(),
|
|
898
960
|
line.entries.map { it.style.flexGrow.toFloat() }.toFloatArray(),
|
|
@@ -917,8 +979,8 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
917
979
|
val lp = line.entries[index].view.layoutParams as? MarginLayoutParams
|
|
918
980
|
val margins = if (orientation == HORIZONTAL) (lp?.leftMargin ?: 0) + (lp?.rightMargin ?: 0)
|
|
919
981
|
else (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
|
|
920
|
-
sharedPrimary[index].
|
|
921
|
-
} + max(0, line.entries.size - 1) * gap
|
|
982
|
+
sharedPrimary[index].toDouble() + margins
|
|
983
|
+
} + max(0, line.entries.size - 1) * gap.toDouble()
|
|
922
984
|
} else {
|
|
923
985
|
line.entries.indices.sumOf { index ->
|
|
924
986
|
val constrained = fallbackWidths[index]
|
|
@@ -926,48 +988,64 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
926
988
|
val margins = if (orientation == HORIZONTAL) (lp?.leftMargin ?: 0) + (lp?.rightMargin ?: 0)
|
|
927
989
|
else (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
|
|
928
990
|
constrained + margins
|
|
929
|
-
} + max(0, line.entries.size - 1) * gap
|
|
991
|
+
}.toDouble() + max(0, line.entries.size - 1) * gap.toDouble()
|
|
930
992
|
}
|
|
931
|
-
val free = max(
|
|
993
|
+
val free = max(0f, availableMain.toFloat() - distributedMain.toFloat())
|
|
932
994
|
val (leading, between) = distribution(free, line.entries.size)
|
|
933
995
|
var mainOffset = (if (orientation == HORIZONTAL) paddingLeft else paddingTop).toFloat() + leading
|
|
996
|
+
val baseline = if (orientation == HORIZONTAL) {
|
|
997
|
+
line.entries.filter { (it.style.alignSelf ?: alignItems) == "baseline" }.maxOfOrNull { entry ->
|
|
998
|
+
val lp = entry.view.layoutParams as? MarginLayoutParams
|
|
999
|
+
(lp?.topMargin ?: 0) + baselineOffset(entry.view, entry.height)
|
|
1000
|
+
}
|
|
1001
|
+
} else null
|
|
934
1002
|
for ((index, entry) in line.entries.withIndex()) {
|
|
935
1003
|
val lp = entry.view.layoutParams as? MarginLayoutParams
|
|
936
1004
|
val marginStart = if (orientation == HORIZONTAL) lp?.leftMargin ?: 0 else lp?.topMargin ?: 0
|
|
937
1005
|
val marginEnd = if (orientation == HORIZONTAL) lp?.rightMargin ?: 0 else lp?.bottomMargin ?: 0
|
|
938
1006
|
val crossSize = if ((entry.style.alignSelf ?: alignItems) == "stretch" && (if (orientation == HORIZONTAL) entry.style.height else entry.style.width) == null)
|
|
939
|
-
max(0,
|
|
1007
|
+
max(0, (lineCross - (if (orientation == HORIZONTAL) (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0) else (lp?.leftMargin ?: 0) + (lp?.rightMargin ?: 0))).roundToInt())
|
|
940
1008
|
else if (orientation == HORIZONTAL) entry.height else entry.width
|
|
941
1009
|
val align = entry.style.alignSelf ?: alignItems
|
|
942
|
-
val crossLeading = if (orientation == HORIZONTAL) lp?.topMargin ?: 0 else lp?.leftMargin ?: 0
|
|
943
|
-
val crossTrailing = if (orientation == HORIZONTAL) lp?.bottomMargin ?: 0 else lp?.rightMargin ?: 0
|
|
944
|
-
val
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1010
|
+
val crossLeading = (if (orientation == HORIZONTAL) lp?.topMargin ?: 0 else lp?.leftMargin ?: 0).toFloat()
|
|
1011
|
+
val crossTrailing = (if (orientation == HORIZONTAL) lp?.bottomMargin ?: 0 else lp?.rightMargin ?: 0).toFloat()
|
|
1012
|
+
val crossPosition = if (align == "baseline" && baseline != null) {
|
|
1013
|
+
val childBaseline = baselineOffset(entry.view, crossSize).toFloat()
|
|
1014
|
+
CraftNative.crossAxisPosition(
|
|
1015
|
+
baseline.toFloat(), childBaseline, crossLeading, crossTrailing, 3
|
|
1016
|
+
) ?: max(crossLeading, baseline.toFloat() - childBaseline)
|
|
1017
|
+
} else {
|
|
1018
|
+
val alignment = when (align) {
|
|
1019
|
+
"center" -> 1
|
|
1020
|
+
"flex-end" -> 2
|
|
1021
|
+
else -> 0
|
|
1022
|
+
}
|
|
1023
|
+
CraftNative.crossAxisPosition(
|
|
1024
|
+
lineCross, crossSize.toFloat(), crossLeading, crossTrailing, alignment
|
|
1025
|
+
) ?: when (alignment) {
|
|
1026
|
+
1 -> crossLeading + (lineCross - crossLeading - crossTrailing - crossSize) / 2f
|
|
1027
|
+
2 -> lineCross - crossTrailing - crossSize
|
|
1028
|
+
else -> crossLeading
|
|
1029
|
+
}
|
|
955
1030
|
}
|
|
1031
|
+
val physicalCrossPosition = if (wrapReverse && wrap) lineCross - crossPosition - crossSize else crossPosition
|
|
956
1032
|
// Shared positions cover every non-reverse justification; the
|
|
957
1033
|
// local main-axis rule remains the fallback for old runtimes
|
|
958
1034
|
// that return widths only.
|
|
959
|
-
|
|
1035
|
+
// The shared result stores widths in the first half and
|
|
1036
|
+
// main-axis positions in the second half.
|
|
1037
|
+
val sharedPosition = if (!reverse) sharedPositions?.get(line.entries.size + index) else null
|
|
960
1038
|
val positionedMain = sharedPosition?.let { (if (orientation == HORIZONTAL) paddingLeft else paddingTop).toFloat() + it }
|
|
961
1039
|
?: mainOffset + marginStart
|
|
962
|
-
val x = if (orientation == HORIZONTAL) positionedMain.roundToInt() else
|
|
963
|
-
val y = if (orientation == HORIZONTAL)
|
|
1040
|
+
val x = if (orientation == HORIZONTAL) positionedMain.roundToInt() else (physicalLineOffset + physicalCrossPosition).roundToInt()
|
|
1041
|
+
val y = if (orientation == HORIZONTAL) (physicalLineOffset + physicalCrossPosition).roundToInt() else positionedMain.roundToInt()
|
|
964
1042
|
val distributedPrimary = sharedPrimary?.get(index)?.roundToInt() ?: fallbackWidths[index]
|
|
965
1043
|
val w = if (orientation == HORIZONTAL) distributedPrimary else crossSize
|
|
966
1044
|
val h = if (orientation == HORIZONTAL) crossSize else distributedPrimary
|
|
967
1045
|
entry.view.layout(x, y, x + w, y + h)
|
|
968
1046
|
if (sharedPosition == null) mainOffset += distributedPrimary + marginStart + marginEnd + gap + between
|
|
969
1047
|
}
|
|
970
|
-
if (sharedCross == null) crossOffset +=
|
|
1048
|
+
if (sharedCross == null) crossOffset += lineCross + crossBetween
|
|
971
1049
|
}
|
|
972
1050
|
for (entry in absolute) {
|
|
973
1051
|
if (entry.view is CraftNativeModalView && entry.view.visibility == View.VISIBLE) entry.view.bringToFront()
|
|
@@ -980,8 +1058,8 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
980
1058
|
onLayoutChanged?.invoke()
|
|
981
1059
|
}
|
|
982
1060
|
|
|
983
|
-
private fun distribution(free:
|
|
984
|
-
val available = free
|
|
1061
|
+
private fun distribution(free: Float, count: Int): Pair<Float, Float> {
|
|
1062
|
+
val available = free
|
|
985
1063
|
return when (justifyContent) {
|
|
986
1064
|
"center" -> available / 2f to 0f
|
|
987
1065
|
"flex-end" -> available to 0f
|
|
@@ -991,6 +1069,9 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
991
1069
|
else -> 0f to 0f
|
|
992
1070
|
}
|
|
993
1071
|
}
|
|
1072
|
+
|
|
1073
|
+
/** First text baseline in pixels; non-text views align by their bottom. */
|
|
1074
|
+
private fun baselineOffset(view: View, cross: Int): Int = view.baseline.takeIf { it >= 0 }?.coerceIn(0, cross) ?: cross
|
|
994
1075
|
}
|
|
995
1076
|
|
|
996
1077
|
/** An image-backed flex container with ordinary native children over the image. */
|
|
@@ -1013,6 +1094,12 @@ private class CraftNativeKeyboardAvoidingView(context: Context) : FrameLayout(co
|
|
|
1013
1094
|
private var behavior = "padding"
|
|
1014
1095
|
private var keyboardVerticalOffset = 0
|
|
1015
1096
|
private var keyboardInset = 0
|
|
1097
|
+
private var animationScaleObserverRegistered = false
|
|
1098
|
+
private val animationScaleObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {
|
|
1099
|
+
override fun onChange(selfChange: Boolean, uri: Uri?) {
|
|
1100
|
+
publishAppearance()
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1016
1103
|
private var basePadding = intArrayOf(0, 0, 0, 0)
|
|
1017
1104
|
|
|
1018
1105
|
init {
|
|
@@ -1421,6 +1508,7 @@ private class CraftNativeDatePickerView(context: Context) : FrameLayout(context)
|
|
|
1421
1508
|
private var datePicker: DatePicker? = null
|
|
1422
1509
|
private var timePicker: TimePicker? = null
|
|
1423
1510
|
private var minuteInterval = 1
|
|
1511
|
+
private var countdownSecond = 0
|
|
1424
1512
|
private var suppressChanges = true
|
|
1425
1513
|
var onValueChanged: (() -> Unit)? = null
|
|
1426
1514
|
|
|
@@ -1472,6 +1560,7 @@ private class CraftNativeDatePickerView(context: Context) : FrameLayout(context)
|
|
|
1472
1560
|
pickerDate(props.opt("minimumDate"))?.let { datePicker?.minDate = it.timeInMillis }
|
|
1473
1561
|
pickerDate(props.opt("maximumDate"))?.let { datePicker?.maxDate = it.timeInMillis }
|
|
1474
1562
|
if (requestedMode == PickerMode.COUNTDOWN) {
|
|
1563
|
+
countdownSecond = 0
|
|
1475
1564
|
countdownSeconds(props.opt("value"))?.let { applyCountdown(it) }
|
|
1476
1565
|
} else {
|
|
1477
1566
|
pickerDate(props.opt("value"))?.let { applyDate(it) }
|
|
@@ -1522,11 +1611,12 @@ private class CraftNativeDatePickerView(context: Context) : FrameLayout(context)
|
|
|
1522
1611
|
}
|
|
1523
1612
|
|
|
1524
1613
|
private fun applyCountdown(seconds: Int) {
|
|
1614
|
+
countdownSecond = seconds % 60
|
|
1525
1615
|
timePicker?.hour = (seconds / 3600).coerceIn(0, 23)
|
|
1526
1616
|
timePicker?.minute = (seconds % 3600 / 60).coerceIn(0, 59)
|
|
1527
1617
|
}
|
|
1528
1618
|
|
|
1529
|
-
private fun currentCountdownSeconds(): Int = timePicker?.let { it.hour * 3600 + it.minute * 60 } ?: 0
|
|
1619
|
+
private fun currentCountdownSeconds(): Int = timePicker?.let { it.hour * 3600 + it.minute * 60 + countdownSecond } ?: 0
|
|
1530
1620
|
|
|
1531
1621
|
private fun formatCountdown(seconds: Int): String = "%02d:%02d:%02d".format(
|
|
1532
1622
|
Locale.US,
|
|
@@ -1572,11 +1662,11 @@ private class CraftNativeDatePickerView(context: Context) : FrameLayout(context)
|
|
|
1572
1662
|
val text = value?.toString()?.trim().orEmpty()
|
|
1573
1663
|
if (text.isBlank() || text == JSONObject.NULL.toString()) return null
|
|
1574
1664
|
text.toDoubleOrNull()?.takeIf { it.isFinite() }?.let { return it.coerceIn(0.0, maxCountdownSeconds.toDouble()).roundToInt() }
|
|
1575
|
-
val parts = text.split(':')
|
|
1665
|
+
val parts = text.split(':')
|
|
1576
1666
|
if (parts.size != 2 && parts.size != 3) return null
|
|
1577
|
-
val hours = parts[0]
|
|
1578
|
-
val minutes = parts[1]
|
|
1579
|
-
val seconds = if (parts.size == 3) parts[2] else 0.0
|
|
1667
|
+
val hours = parts[0].toDoubleOrNull() ?: return null
|
|
1668
|
+
val minutes = parts[1].toDoubleOrNull() ?: return null
|
|
1669
|
+
val seconds = if (parts.size == 3) parts[2].toDoubleOrNull() ?: return null else 0.0
|
|
1580
1670
|
if (hours < 0 || minutes !in 0.0..<60.0 || seconds !in 0.0..<60.0) return null
|
|
1581
1671
|
return (hours * 3600 + minutes * 60 + seconds).coerceIn(0.0, maxCountdownSeconds.toDouble()).roundToInt()
|
|
1582
1672
|
}
|
|
@@ -1792,6 +1882,101 @@ private class CraftNativeAudio {
|
|
|
1792
1882
|
}
|
|
1793
1883
|
}
|
|
1794
1884
|
|
|
1885
|
+
private class CraftTextDecorationSpan(
|
|
1886
|
+
private val line: String,
|
|
1887
|
+
private val style: String,
|
|
1888
|
+
private val decorationColor: Int?,
|
|
1889
|
+
) : ReplacementSpan() {
|
|
1890
|
+
override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int {
|
|
1891
|
+
return ceil(paint.measureText(text, start, end).toDouble()).toInt()
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
override fun draw(
|
|
1895
|
+
canvas: Canvas,
|
|
1896
|
+
text: CharSequence,
|
|
1897
|
+
start: Int,
|
|
1898
|
+
end: Int,
|
|
1899
|
+
x: Float,
|
|
1900
|
+
top: Int,
|
|
1901
|
+
y: Int,
|
|
1902
|
+
bottom: Int,
|
|
1903
|
+
paint: Paint,
|
|
1904
|
+
) {
|
|
1905
|
+
val originalColor = paint.color
|
|
1906
|
+
val originalStyle = paint.style
|
|
1907
|
+
val originalStrokeWidth = paint.strokeWidth
|
|
1908
|
+
val originalPathEffect = paint.pathEffect
|
|
1909
|
+
val originalStrokeCap = paint.strokeCap
|
|
1910
|
+
paint.style = Paint.Style.FILL
|
|
1911
|
+
paint.pathEffect = null
|
|
1912
|
+
canvas.drawText(text, start, end, x, y.toFloat(), paint)
|
|
1913
|
+
|
|
1914
|
+
val width = paint.measureText(text, start, end)
|
|
1915
|
+
val thickness = max(1f, paint.textSize / 16f)
|
|
1916
|
+
val underlineY = y + max(1f, paint.descent() * 0.15f)
|
|
1917
|
+
val strikeY = y - paint.textSize * 0.3f
|
|
1918
|
+
val decoration = decorationColor ?: originalColor
|
|
1919
|
+
paint.color = decoration
|
|
1920
|
+
paint.style = Paint.Style.STROKE
|
|
1921
|
+
paint.strokeWidth = thickness
|
|
1922
|
+
paint.strokeCap = if (style == "dotted") Paint.Cap.ROUND else Paint.Cap.BUTT
|
|
1923
|
+
paint.pathEffect = when (style) {
|
|
1924
|
+
"dashed" -> DashPathEffect(floatArrayOf(paint.textSize * 0.35f, paint.textSize * 0.25f), 0f)
|
|
1925
|
+
"dotted" -> DashPathEffect(floatArrayOf(thickness, paint.textSize * 0.22f), 0f)
|
|
1926
|
+
else -> null
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
fun drawLine(position: Float) {
|
|
1930
|
+
canvas.drawLine(x, position, x + width, position, paint)
|
|
1931
|
+
}
|
|
1932
|
+
if (line == "underline" || line == "underline line-through") {
|
|
1933
|
+
drawLine(underlineY)
|
|
1934
|
+
if (style == "double") drawLine(underlineY + thickness * 2.5f)
|
|
1935
|
+
}
|
|
1936
|
+
if (line == "line-through" || line == "underline line-through") {
|
|
1937
|
+
drawLine(strikeY)
|
|
1938
|
+
if (style == "double") drawLine(strikeY + thickness * 2.5f)
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
paint.color = originalColor
|
|
1942
|
+
paint.style = originalStyle
|
|
1943
|
+
paint.strokeWidth = originalStrokeWidth
|
|
1944
|
+
paint.pathEffect = originalPathEffect
|
|
1945
|
+
paint.strokeCap = originalStrokeCap
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
private class CraftNativeSideBorderDrawable(
|
|
1950
|
+
private val fillColor: Int,
|
|
1951
|
+
private val radii: FloatArray,
|
|
1952
|
+
private val widths: IntArray,
|
|
1953
|
+
private val colors: IntArray,
|
|
1954
|
+
) : Drawable() {
|
|
1955
|
+
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
|
1956
|
+
|
|
1957
|
+
override fun draw(canvas: Canvas) {
|
|
1958
|
+
val bounds = RectF(this.bounds)
|
|
1959
|
+
paint.style = Paint.Style.FILL
|
|
1960
|
+
paint.color = fillColor
|
|
1961
|
+
if (Color.alpha(fillColor) > 0) {
|
|
1962
|
+
val path = Path().apply { addRoundRect(bounds, radii, Path.Direction.CW) }
|
|
1963
|
+
canvas.drawPath(path, paint)
|
|
1964
|
+
}
|
|
1965
|
+
val top = widths.getOrElse(0) { 0 }
|
|
1966
|
+
val right = widths.getOrElse(1) { 0 }
|
|
1967
|
+
val bottom = widths.getOrElse(2) { 0 }
|
|
1968
|
+
val left = widths.getOrElse(3) { 0 }
|
|
1969
|
+
if (top > 0) { paint.color = colors.getOrElse(0) { Color.TRANSPARENT }; canvas.drawRect(bounds.left, bounds.top, bounds.right, bounds.top + top, paint) }
|
|
1970
|
+
if (right > 0) { paint.color = colors.getOrElse(1) { Color.TRANSPARENT }; canvas.drawRect(bounds.right - right, bounds.top, bounds.right, bounds.bottom, paint) }
|
|
1971
|
+
if (bottom > 0) { paint.color = colors.getOrElse(2) { Color.TRANSPARENT }; canvas.drawRect(bounds.left, bounds.bottom - bottom, bounds.right, bounds.bottom, paint) }
|
|
1972
|
+
if (left > 0) { paint.color = colors.getOrElse(3) { Color.TRANSPARENT }; canvas.drawRect(bounds.left, bounds.top, bounds.left + left, bounds.bottom, paint) }
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
override fun setAlpha(alpha: Int) { paint.alpha = alpha; invalidateSelf() }
|
|
1976
|
+
override fun setColorFilter(colorFilter: ColorFilter?) { paint.colorFilter = colorFilter; invalidateSelf() }
|
|
1977
|
+
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1795
1980
|
/** WebView-free host for a route bundle compiled by `stx native compile`. */
|
|
1796
1981
|
class MainActivity : AppCompatActivity() {
|
|
1797
1982
|
private class Screen(
|
|
@@ -2113,6 +2298,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
2113
2298
|
capabilities: ${capabilities.names()},
|
|
2114
2299
|
initialAppState: ${JSONObject.quote(nativeAppState)},
|
|
2115
2300
|
colorScheme: ${JSONObject.quote(colorScheme())},
|
|
2301
|
+
appearance: ${appearance()},
|
|
2116
2302
|
postMessage: function(value) { port.postMessage(value); },
|
|
2117
2303
|
onMessage: function(callback) { port.onmessage = function(event) { callback(event.data); }; }
|
|
2118
2304
|
};
|
|
@@ -2128,6 +2314,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
2128
2314
|
capabilities: ${capabilities.names()},
|
|
2129
2315
|
initialAppState: ${JSONObject.quote(nativeAppState)},
|
|
2130
2316
|
colorScheme: ${JSONObject.quote(colorScheme())},
|
|
2317
|
+
appearance: ${appearance()},
|
|
2131
2318
|
postMessage: function(value) { console.log(${JSONObject.quote(prefix)} + value); },
|
|
2132
2319
|
onMessage: function(callback) { globalThis.__stxNativeCallback = callback; }
|
|
2133
2320
|
};
|
|
@@ -2179,15 +2366,17 @@ class MainActivity : AppCompatActivity() {
|
|
|
2179
2366
|
|
|
2180
2367
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
|
2181
2368
|
super.onConfigurationChanged(newConfig)
|
|
2369
|
+
publishAppearance()
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
private fun publishAppearance() {
|
|
2182
2373
|
if (!::root.isInitialized) return
|
|
2183
2374
|
refreshThemeDefaults()
|
|
2184
2375
|
ViewCompat.requestApplyInsets(root)
|
|
2185
2376
|
refreshInputDefaultsForTraitChange()
|
|
2186
2377
|
screens.lastOrNull()?.let { screen ->
|
|
2187
2378
|
refreshScreenTheme(screen)
|
|
2188
|
-
if (screen.active)
|
|
2189
|
-
send(screen, "APPEARANCE", JSONObject().put("colorScheme", colorScheme()))
|
|
2190
|
-
}
|
|
2379
|
+
if (screen.active) send(screen, "APPEARANCE", appearance())
|
|
2191
2380
|
}
|
|
2192
2381
|
}
|
|
2193
2382
|
|
|
@@ -2212,6 +2401,25 @@ class MainActivity : AppCompatActivity() {
|
|
|
2212
2401
|
resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
|
|
2213
2402
|
) "dark" else "light"
|
|
2214
2403
|
|
|
2404
|
+
private fun appearance(): JSONObject {
|
|
2405
|
+
val scale = resources.configuration.fontScale.toDouble().coerceAtLeast(0.1)
|
|
2406
|
+
val contentSize = when {
|
|
2407
|
+
scale >= 1.5 -> "accessibilityExtraExtraExtraLarge"
|
|
2408
|
+
scale >= 1.35 -> "accessibilityExtraExtraLarge"
|
|
2409
|
+
scale >= 1.2 -> "accessibilityExtraLarge"
|
|
2410
|
+
scale >= 1.1 -> "extraLarge"
|
|
2411
|
+
scale <= 0.85 -> "extraSmall"
|
|
2412
|
+
scale <= 0.92 -> "small"
|
|
2413
|
+
else -> "large"
|
|
2414
|
+
}
|
|
2415
|
+
return JSONObject()
|
|
2416
|
+
.put("contentSizeCategory", contentSize)
|
|
2417
|
+
.put("fontScale", (scale * 1000).roundToInt() / 1000.0)
|
|
2418
|
+
.put("reduceMotion", !ValueAnimator.areAnimatorsEnabled())
|
|
2419
|
+
.put("reduceTransparency", false)
|
|
2420
|
+
.put("colorScheme", colorScheme())
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2215
2423
|
private fun refreshInputDefaultsForTraitChange() {
|
|
2216
2424
|
if (inputHintDefaults.isEmpty() && inputHighlightDefaults.isEmpty()) return
|
|
2217
2425
|
val probe = EditText(this)
|
|
@@ -2529,7 +2737,12 @@ class MainActivity : AppCompatActivity() {
|
|
|
2529
2737
|
(style.optDouble("paddingBottom", vertical) * density).toInt(),
|
|
2530
2738
|
)
|
|
2531
2739
|
}
|
|
2532
|
-
|
|
2740
|
+
// Reused EditTexts must forget the previous field's hint as
|
|
2741
|
+
// well as accept a new one. Otherwise a password manager can
|
|
2742
|
+
// offer credentials in a field whose props no longer ask for
|
|
2743
|
+
// them after keyed reconciliation.
|
|
2744
|
+
val autofillHint = autofillHintFor(props)
|
|
2745
|
+
if (autofillHint == null) setAutofillHints(*emptyArray()) else setAutofillHints(autofillHint)
|
|
2533
2746
|
val controlledValue = props.opt("value")?.takeUnless { it == JSONObject.NULL }?.toString()
|
|
2534
2747
|
if (controlledValue != null && text.toString() != controlledValue) {
|
|
2535
2748
|
screen.suppressedTextChanges.add(this)
|
|
@@ -2537,6 +2750,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
2537
2750
|
}
|
|
2538
2751
|
else if (this !== previous && screen.drafts.containsKey(identity)) setText(screen.drafts[identity])
|
|
2539
2752
|
else if (this !== previous && props.has("defaultValue") && !props.isNull("defaultValue")) setText(props.optString("defaultValue"))
|
|
2753
|
+
applyTextDecoration(this, style)
|
|
2540
2754
|
screen.inputIdentities[this] = identity
|
|
2541
2755
|
val handler = events.optString("onChange").ifBlank { events.optString("onChangeText") }
|
|
2542
2756
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
@@ -2723,12 +2937,29 @@ class MainActivity : AppCompatActivity() {
|
|
|
2723
2937
|
val minimum = props.optDouble("minimumValue", 0.0)
|
|
2724
2938
|
val maximumCandidate = props.optDouble("maximumValue", 1.0)
|
|
2725
2939
|
val maximum = if (maximumCandidate > minimum) maximumCandidate else minimum + Double.MIN_VALUE
|
|
2726
|
-
max = 1000
|
|
2727
2940
|
setOnSeekBarChangeListener(null)
|
|
2728
2941
|
val step = props.optDouble("step", 0.0).takeIf { it > 0 }
|
|
2942
|
+
// A stepped slider needs one native position per authored
|
|
2943
|
+
// step; a fixed 1,000-position SeekBar silently made large
|
|
2944
|
+
// ranges unable to represent most valid values. Continuous
|
|
2945
|
+
// sliders retain the bounded scale to avoid an enormous max.
|
|
2946
|
+
val progressMax = step?.let {
|
|
2947
|
+
// SeekBar stores progress as an Int. Do not impose a
|
|
2948
|
+
// smaller arbitrary ceiling: large authored ranges must
|
|
2949
|
+
// still be able to reach their final stepped value.
|
|
2950
|
+
ceil((maximum - minimum) / it).toLong()
|
|
2951
|
+
.coerceIn(1L, Int.MAX_VALUE.toLong())
|
|
2952
|
+
.toInt()
|
|
2953
|
+
} ?: 1000
|
|
2954
|
+
max = progressMax
|
|
2729
2955
|
val requested = props.optDouble("value", minimum)
|
|
2730
|
-
val value = if (step != null) minimum + ((requested - minimum) / step).roundToInt() * step else requested
|
|
2731
|
-
|
|
2956
|
+
val value = (if (step != null) minimum + ((requested - minimum) / step).roundToInt() * step else requested)
|
|
2957
|
+
.coerceIn(minimum, maximum)
|
|
2958
|
+
progress = if (step != null) {
|
|
2959
|
+
((value - minimum) / step).roundToInt().coerceIn(0, max)
|
|
2960
|
+
} else {
|
|
2961
|
+
(((value - minimum) / (maximum - minimum)) * max).roundToInt().coerceIn(0, max)
|
|
2962
|
+
}
|
|
2732
2963
|
if (step != null) screen.sliderSteps[this] = step else screen.sliderSteps.remove(this)
|
|
2733
2964
|
progressTintList = props.optString("minimumTrackTintColor").takeIf { it.isNotBlank() }
|
|
2734
2965
|
?.let { ColorStateList.valueOf(color(it, Color.WHITE)) }
|
|
@@ -2744,25 +2975,19 @@ class MainActivity : AppCompatActivity() {
|
|
|
2744
2975
|
setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
|
2745
2976
|
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
|
2746
2977
|
val current = screen.handlers[seekBar] ?: return
|
|
2747
|
-
val raw = minimum + (maximum - minimum) * progress.toDouble() / max.toDouble()
|
|
2748
2978
|
val step = screen.sliderSteps[seekBar]
|
|
2749
|
-
val value = if (step != null) minimum +
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
.roundToInt().coerceIn(0, max)
|
|
2753
|
-
if (seekBar.progress != snappedProgress) {
|
|
2754
|
-
seekBar.progress = snappedProgress
|
|
2755
|
-
return
|
|
2756
|
-
}
|
|
2757
|
-
}
|
|
2979
|
+
val value = (if (step != null) minimum + progress * step else
|
|
2980
|
+
minimum + (maximum - minimum) * progress.toDouble() / max.toDouble())
|
|
2981
|
+
.coerceIn(minimum, maximum)
|
|
2758
2982
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject().put("value", value)))
|
|
2759
2983
|
}
|
|
2760
2984
|
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
|
2761
2985
|
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
|
2762
2986
|
val current = screen.sliderCompleteHandlers[seekBar] ?: return
|
|
2763
|
-
val raw = minimum + (maximum - minimum) * seekBar.progress.toDouble() / max.toDouble()
|
|
2764
2987
|
val step = screen.sliderSteps[seekBar]
|
|
2765
|
-
val value = if (step != null) minimum +
|
|
2988
|
+
val value = (if (step != null) minimum + seekBar.progress * step else
|
|
2989
|
+
minimum + (maximum - minimum) * seekBar.progress.toDouble() / max.toDouble())
|
|
2990
|
+
.coerceIn(minimum, maximum)
|
|
2766
2991
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject().put("value", value)))
|
|
2767
2992
|
}
|
|
2768
2993
|
})
|
|
@@ -3253,27 +3478,79 @@ class MainActivity : AppCompatActivity() {
|
|
|
3253
3478
|
|
|
3254
3479
|
private fun applyViewStyle(view: View, style: JSONObject) {
|
|
3255
3480
|
view.alpha = style.optDouble("opacity", 1.0).toFloat()
|
|
3481
|
+
applyTransform(view, style.optJSONArray("transform"))
|
|
3256
3482
|
if (view !is CraftNativeModalView) {
|
|
3257
3483
|
view.visibility = if (style.optString("display") == "none") View.GONE else View.VISIBLE
|
|
3258
3484
|
}
|
|
3259
3485
|
view.elevation = dp(style.optDouble("elevation", 0.0)).toFloat()
|
|
3486
|
+
view.z = style.optDouble("zIndex", 0.0).toFloat()
|
|
3260
3487
|
val background = style.has("backgroundColor") && !style.isNull("backgroundColor")
|
|
3261
3488
|
val border = style.optDouble("borderWidth", 0.0)
|
|
3262
3489
|
val radius = style.optDouble("borderRadius", 0.0)
|
|
3490
|
+
val sideBorderKeys = listOf("borderTopWidth", "borderRightWidth", "borderBottomWidth", "borderLeftWidth")
|
|
3491
|
+
val hasSideBorders = (sideBorderKeys + listOf("borderTopColor", "borderRightColor", "borderBottomColor", "borderLeftColor")).any(style::has)
|
|
3492
|
+
val sideWidths = sideBorderKeys.map { dp(style.optDouble(it, border)) }.toIntArray()
|
|
3493
|
+
val cornerRadiusKeys = listOf("borderTopLeftRadius", "borderTopRightRadius", "borderBottomRightRadius", "borderBottomLeftRadius")
|
|
3494
|
+
val hasCornerRadii = cornerRadiusKeys.any(style::has)
|
|
3495
|
+
val cornerRadii = cornerRadiusKeys.flatMap { value ->
|
|
3496
|
+
val radius = dp(style.optDouble(value, radius)).toFloat()
|
|
3497
|
+
listOf(radius, radius)
|
|
3498
|
+
}.toFloatArray()
|
|
3499
|
+
val sideColors = listOf("borderTopColor", "borderRightColor", "borderBottomColor", "borderLeftColor")
|
|
3500
|
+
.map { color(style.optString(it).ifBlank { style.optString("borderColor") }, Color.TRANSPARENT) }
|
|
3501
|
+
.toIntArray()
|
|
3263
3502
|
val defaultBackground = if (defaultBackgrounds.containsKey(view)) {
|
|
3264
3503
|
defaultBackgrounds[view]
|
|
3265
3504
|
} else {
|
|
3266
3505
|
view.background.also { defaultBackgrounds[view] = it }
|
|
3267
3506
|
}
|
|
3268
|
-
if (background || border > 0 || radius > 0) {
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3507
|
+
if (background || border > 0 || radius > 0 || hasSideBorders) {
|
|
3508
|
+
if (hasSideBorders) {
|
|
3509
|
+
view.background = CraftNativeSideBorderDrawable(
|
|
3510
|
+
color(style.optString("backgroundColor"), Color.TRANSPARENT),
|
|
3511
|
+
cornerRadii,
|
|
3512
|
+
sideWidths,
|
|
3513
|
+
sideColors,
|
|
3514
|
+
)
|
|
3515
|
+
} else {
|
|
3516
|
+
view.background = GradientDrawable().apply {
|
|
3517
|
+
setColor(color(style.optString("backgroundColor"), Color.TRANSPARENT))
|
|
3518
|
+
if (hasCornerRadii) setCornerRadii(cornerRadii) else cornerRadius = dp(radius).toFloat()
|
|
3519
|
+
if (border > 0) {
|
|
3520
|
+
val borderColor = color(style.optString("borderColor"), Color.TRANSPARENT)
|
|
3521
|
+
when (style.optString("borderStyle")) {
|
|
3522
|
+
"dashed" -> setStroke(dp(border), borderColor, dp(border * 3).toFloat(), dp(border * 2).toFloat())
|
|
3523
|
+
"dotted" -> setStroke(dp(border), borderColor, dp(border).toFloat(), dp(border * 1.5).toFloat())
|
|
3524
|
+
else -> setStroke(dp(border), borderColor)
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3273
3528
|
}
|
|
3274
3529
|
} else view.background = defaultBackground
|
|
3275
3530
|
view.clipToOutline = style.optString("overflow") == "hidden"
|
|
3276
3531
|
if (view is ViewGroup) view.clipChildren = style.optString("overflow") == "hidden"
|
|
3532
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P &&
|
|
3533
|
+
(style.has("shadowColor") || style.has("shadowOpacity") || style.has("shadowRadius"))
|
|
3534
|
+
) {
|
|
3535
|
+
val shadowOpacity = style.optDouble("shadowOpacity", 1.0).coerceIn(0.0, 1.0)
|
|
3536
|
+
val shadow = color(style.optString("shadowColor"), Color.BLACK)
|
|
3537
|
+
val alphaShadow = Color.argb((Color.alpha(shadow) * shadowOpacity).roundToInt(), Color.red(shadow), Color.green(shadow), Color.blue(shadow))
|
|
3538
|
+
view.outlineAmbientShadowColor = alphaShadow
|
|
3539
|
+
view.outlineSpotShadowColor = alphaShadow
|
|
3540
|
+
view.elevation = max(view.elevation, dp(style.optDouble("shadowRadius", 0.0) * 2.0).toFloat())
|
|
3541
|
+
val offset = style.optJSONObject("shadowOffset")
|
|
3542
|
+
val offsetX = dp(offset?.optDouble("width", 0.0) ?: 0.0)
|
|
3543
|
+
val offsetY = dp(offset?.optDouble("height", 0.0) ?: 0.0)
|
|
3544
|
+
val shadowRadius = dp(style.optDouble("shadowRadius", 0.0)).toFloat()
|
|
3545
|
+
view.outlineProvider = object : ViewOutlineProvider() {
|
|
3546
|
+
override fun getOutline(target: View, outline: Outline) {
|
|
3547
|
+
outline.setRoundRect(offsetX, offsetY, target.width + offsetX, target.height + offsetY, shadowRadius)
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
view.invalidateOutline()
|
|
3551
|
+
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
3552
|
+
view.outlineProvider = ViewOutlineProvider.BACKGROUND
|
|
3553
|
+
}
|
|
3277
3554
|
if (view is ImageView) {
|
|
3278
3555
|
val tint = style.opt("tintColor") as? String
|
|
3279
3556
|
if (tint.isNullOrBlank() || tint == "null" || tint == "undefined") {
|
|
@@ -3284,6 +3561,49 @@ class MainActivity : AppCompatActivity() {
|
|
|
3284
3561
|
}
|
|
3285
3562
|
}
|
|
3286
3563
|
|
|
3564
|
+
private fun applyTransform(view: View, transforms: JSONArray?) {
|
|
3565
|
+
view.translationX = 0f
|
|
3566
|
+
view.translationY = 0f
|
|
3567
|
+
view.scaleX = 1f
|
|
3568
|
+
view.scaleY = 1f
|
|
3569
|
+
view.rotation = 0f
|
|
3570
|
+
view.rotationX = 0f
|
|
3571
|
+
view.rotationY = 0f
|
|
3572
|
+
CraftNativeSkewTransforms.clear(view)
|
|
3573
|
+
if (transforms == null) return
|
|
3574
|
+
var skewX = 0f
|
|
3575
|
+
var skewY = 0f
|
|
3576
|
+
for (index in 0 until transforms.length()) {
|
|
3577
|
+
val transform = transforms.optJSONObject(index) ?: continue
|
|
3578
|
+
val key = transform.keys().asSequence().firstOrNull() ?: continue
|
|
3579
|
+
when (key) {
|
|
3580
|
+
"translateX" -> view.translationX += dp(transform.optDouble(key, 0.0)).toFloat()
|
|
3581
|
+
"translateY" -> view.translationY += dp(transform.optDouble(key, 0.0)).toFloat()
|
|
3582
|
+
"scale" -> transform.optDouble(key, 1.0).toFloat().let { view.scaleX *= it; view.scaleY *= it }
|
|
3583
|
+
"scaleX" -> view.scaleX *= transform.optDouble(key, 1.0).toFloat()
|
|
3584
|
+
"scaleY" -> view.scaleY *= transform.optDouble(key, 1.0).toFloat()
|
|
3585
|
+
"rotate", "rotateZ" -> view.rotation += angleDegrees(transform.opt(key))
|
|
3586
|
+
"rotateX" -> view.rotationX += angleDegrees(transform.opt(key))
|
|
3587
|
+
"rotateY" -> view.rotationY += angleDegrees(transform.opt(key))
|
|
3588
|
+
"skewX" -> skewX += skewFactor(transform.opt(key))
|
|
3589
|
+
"skewY" -> skewY += skewFactor(transform.opt(key))
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
CraftNativeSkewTransforms.set(view, skewX, skewY)
|
|
3593
|
+
}
|
|
3594
|
+
|
|
3595
|
+
private fun skewFactor(value: Any?): Float =
|
|
3596
|
+
kotlin.math.tan(angleDegrees(value).toDouble() * PI / 180.0).toFloat()
|
|
3597
|
+
|
|
3598
|
+
private fun angleDegrees(value: Any?): Float {
|
|
3599
|
+
val text = value?.toString()?.trim() ?: return 0f
|
|
3600
|
+
return when {
|
|
3601
|
+
text.endsWith("rad") -> Math.toDegrees(text.dropLast(3).toDoubleOrNull() ?: 0.0).toFloat()
|
|
3602
|
+
text.endsWith("deg") -> text.dropLast(3).toFloatOrNull() ?: 0f
|
|
3603
|
+
else -> text.toFloatOrNull() ?: 0f
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
|
|
3287
3607
|
private fun ensureLayoutListener(screen: Screen, view: View) {
|
|
3288
3608
|
if (screen.layoutListeners.containsKey(view)) return
|
|
3289
3609
|
val listener = View.OnLayoutChangeListener { changed, left, top, right, bottom, _, _, _, _ ->
|
|
@@ -3392,13 +3712,59 @@ class MainActivity : AppCompatActivity() {
|
|
|
3392
3712
|
style.optDouble("lineHeight", Double.NaN).takeUnless { it.isNaN() }?.let { dp(it) - view.textSize } ?: 0f,
|
|
3393
3713
|
1f
|
|
3394
3714
|
)
|
|
3715
|
+
val shadowColor = style.optString("textShadowColor").takeIf { it.isNotBlank() }
|
|
3716
|
+
if (shadowColor == null) {
|
|
3717
|
+
view.paint.clearShadowLayer()
|
|
3718
|
+
} else {
|
|
3719
|
+
val offset = style.optJSONObject("textShadowOffset")
|
|
3720
|
+
view.setShadowLayer(
|
|
3721
|
+
dp(style.optDouble("textShadowRadius", 0.0)).toFloat().coerceAtLeast(0f),
|
|
3722
|
+
dp(offset?.optDouble("width", 0.0) ?: 0.0).toFloat(),
|
|
3723
|
+
dp(offset?.optDouble("height", 0.0) ?: 0.0).toFloat(),
|
|
3724
|
+
color(shadowColor, Color.TRANSPARENT),
|
|
3725
|
+
)
|
|
3726
|
+
}
|
|
3395
3727
|
view.paintFlags = view.paintFlags and Paint.UNDERLINE_TEXT_FLAG.inv() and Paint.STRIKE_THRU_TEXT_FLAG.inv()
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3728
|
+
applyTextDecoration(view, style)
|
|
3729
|
+
}
|
|
3730
|
+
|
|
3731
|
+
private fun applyTextDecoration(view: TextView, style: JSONObject) {
|
|
3732
|
+
val line = style.optString("textDecorationLine")
|
|
3733
|
+
val text = view.text?.toString() ?: return
|
|
3734
|
+
if (line.isBlank() || line == "none" || text.isEmpty()) {
|
|
3735
|
+
if (view.text is Spannable) view.text = text
|
|
3736
|
+
return
|
|
3401
3737
|
}
|
|
3738
|
+
val decorationColor = style.optString("textDecorationColor")
|
|
3739
|
+
.takeIf { it.isNotBlank() }
|
|
3740
|
+
?.let { color(it, Color.TRANSPARENT) }
|
|
3741
|
+
val decorated = SpannableString(text)
|
|
3742
|
+
decorated.setSpan(
|
|
3743
|
+
CraftTextDecorationSpan(
|
|
3744
|
+
line,
|
|
3745
|
+
style.optString("textDecorationStyle").ifBlank { "solid" },
|
|
3746
|
+
decorationColor,
|
|
3747
|
+
),
|
|
3748
|
+
0,
|
|
3749
|
+
decorated.length,
|
|
3750
|
+
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
|
|
3751
|
+
)
|
|
3752
|
+
view.text = decorated
|
|
3753
|
+
}
|
|
3754
|
+
|
|
3755
|
+
/**
|
|
3756
|
+
* Match iOS image loading: HTTPS is allowed everywhere, while plain HTTP
|
|
3757
|
+
* is limited to loopback/development hosts. Android's generated manifest
|
|
3758
|
+
* enables cleartext traffic only for the corresponding dev-server or
|
|
3759
|
+
* local-network configurations.
|
|
3760
|
+
*/
|
|
3761
|
+
private fun loadsImage(uri: Uri): Boolean {
|
|
3762
|
+
val scheme = uri.scheme?.lowercase() ?: return false
|
|
3763
|
+
if (scheme == "https") return true
|
|
3764
|
+
if (scheme != "http") return false
|
|
3765
|
+
val host = uri.host?.lowercase() ?: return false
|
|
3766
|
+
return host == "localhost" || host == "127.0.0.1" || host == "10.0.2.2" ||
|
|
3767
|
+
host == "::1" || host.endsWith(".localhost")
|
|
3402
3768
|
}
|
|
3403
3769
|
|
|
3404
3770
|
private fun updateImage(screen: Screen, view: ImageView, source: Any?) {
|
|
@@ -3442,7 +3808,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3442
3808
|
} else imageFailure(screen, view, uri, "Bundled image was not found")
|
|
3443
3809
|
return
|
|
3444
3810
|
}
|
|
3445
|
-
if (!
|
|
3811
|
+
if (!loadsImage(Uri.parse(uri))) {
|
|
3446
3812
|
imageFailure(screen, view, uri, "Unsupported image source")
|
|
3447
3813
|
return
|
|
3448
3814
|
}
|
|
@@ -3582,14 +3948,30 @@ class MainActivity : AppCompatActivity() {
|
|
|
3582
3948
|
val raw = props.optString("textContentType").ifBlank { props.optString("autoComplete") }
|
|
3583
3949
|
return when (raw.replace("-", "").replace("_", "").lowercase()) {
|
|
3584
3950
|
"username" -> View.AUTOFILL_HINT_USERNAME
|
|
3951
|
+
"nickname" -> "personNickname"
|
|
3585
3952
|
"password", "currentpassword" -> View.AUTOFILL_HINT_PASSWORD
|
|
3586
3953
|
"newpassword" -> "newPassword"
|
|
3587
3954
|
"onetimecode" -> "smsOTPCode"
|
|
3588
3955
|
"emailaddress", "email" -> View.AUTOFILL_HINT_EMAIL_ADDRESS
|
|
3589
3956
|
"telephonenumber", "tel", "phone" -> View.AUTOFILL_HINT_PHONE
|
|
3590
3957
|
"name" -> View.AUTOFILL_HINT_NAME
|
|
3958
|
+
"givenname", "firstname" -> "personGivenName"
|
|
3959
|
+
"middlename", "additionalname" -> "personMiddleName"
|
|
3960
|
+
"familyname", "lastname" -> "personFamilyName"
|
|
3961
|
+
"nameprefix", "honorificprefix" -> "personNamePrefix"
|
|
3962
|
+
"namesuffix", "honorificsuffix" -> "personNameSuffix"
|
|
3963
|
+
"organizationname", "organization" -> "organizationName"
|
|
3964
|
+
"jobtitle", "organizationtitle" -> "jobTitle"
|
|
3965
|
+
"url" -> "url"
|
|
3591
3966
|
"postalcode" -> View.AUTOFILL_HINT_POSTAL_CODE
|
|
3592
3967
|
"fullstreetaddress", "streetaddress" -> View.AUTOFILL_HINT_POSTAL_ADDRESS
|
|
3968
|
+
"streetaddressline1", "addressline1" -> "streetAddress"
|
|
3969
|
+
"streetaddressline2", "addressline2" -> "streetAddressLine2"
|
|
3970
|
+
"addresscity", "addresslevel2" -> "addressLocality"
|
|
3971
|
+
"addressstate", "addresslevel1" -> "addressRegion"
|
|
3972
|
+
"countryname", "country" -> "addressCountry"
|
|
3973
|
+
"location" -> "location"
|
|
3974
|
+
"creditcardnumber", "ccnumber" -> "creditCardNumber"
|
|
3593
3975
|
else -> null
|
|
3594
3976
|
}
|
|
3595
3977
|
}
|
|
@@ -3600,6 +3982,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
3600
3982
|
"next" -> EditorInfo.IME_ACTION_NEXT
|
|
3601
3983
|
"search" -> EditorInfo.IME_ACTION_SEARCH
|
|
3602
3984
|
"send" -> EditorInfo.IME_ACTION_SEND
|
|
3985
|
+
// Android has no separate JOIN, ROUTE, CONTINUE, or EMERGENCY_CALL
|
|
3986
|
+
// action. GO/NEXT are the closest platform actions and still expose
|
|
3987
|
+
// an actionable IME key instead of silently falling back to NONE.
|
|
3988
|
+
"join", "route", "emergency-call" -> EditorInfo.IME_ACTION_GO
|
|
3989
|
+
"continue" -> EditorInfo.IME_ACTION_NEXT
|
|
3603
3990
|
else -> EditorInfo.IME_ACTION_NONE
|
|
3604
3991
|
}
|
|
3605
3992
|
|
|
@@ -3693,6 +4080,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
3693
4080
|
|
|
3694
4081
|
override fun onStart() {
|
|
3695
4082
|
super.onStart()
|
|
4083
|
+
if (!animationScaleObserverRegistered) {
|
|
4084
|
+
contentResolver.registerContentObserver(Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE), false, animationScaleObserver)
|
|
4085
|
+
contentResolver.registerContentObserver(Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE), false, animationScaleObserver)
|
|
4086
|
+
animationScaleObserverRegistered = true
|
|
4087
|
+
}
|
|
3696
4088
|
nativeAppState = "active"
|
|
3697
4089
|
publish("APP_STATE", JSONObject().put("state", nativeAppState))
|
|
3698
4090
|
}
|
|
@@ -3704,6 +4096,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
3704
4096
|
}
|
|
3705
4097
|
|
|
3706
4098
|
override fun onStop() {
|
|
4099
|
+
if (animationScaleObserverRegistered) {
|
|
4100
|
+
contentResolver.unregisterContentObserver(animationScaleObserver)
|
|
4101
|
+
animationScaleObserverRegistered = false
|
|
4102
|
+
}
|
|
3707
4103
|
nativeAppState = "background"
|
|
3708
4104
|
publish("APP_STATE", JSONObject().put("state", nativeAppState))
|
|
3709
4105
|
super.onStop()
|
|
@@ -3947,6 +4343,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3947
4343
|
}
|
|
3948
4344
|
|
|
3949
4345
|
override fun onDestroy() {
|
|
4346
|
+
if (animationScaleObserverRegistered) contentResolver.unregisterContentObserver(animationScaleObserver)
|
|
3950
4347
|
screens.forEach { closeScreen(it) }
|
|
3951
4348
|
screens.clear()
|
|
3952
4349
|
imageJobs.values.forEach { it.cancel(true) }
|