craft-native 0.0.164 → 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 +416 -36
- package/dist/api/mobile.d.ts +25 -1
- 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 +63 -2
- package/dist/ios/templates/CraftHealth.swift +214 -39
- package/dist/ios/templates/CraftHeartRate.swift +34 -4
- package/dist/ios/templates/CraftHybrid.swift +140 -17
- package/dist/ios/templates/CraftNativeScreen.swift +320 -56
- package/dist/mobile.js +29 -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,6 +96,7 @@ 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
|
|
@@ -178,6 +190,32 @@ private fun craftNativeGridColumnCount(value: Any?): Int {
|
|
|
178
190
|
return max(1, trimmed.split(Regex("\\s+")).count { it.isNotBlank() })
|
|
179
191
|
}
|
|
180
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
|
+
|
|
181
219
|
/** A DOM-free SVG surface for the geometry emitted by the shared STX native protocol. */
|
|
182
220
|
private class CraftNativeSvgView(context: Context) : View(context) {
|
|
183
221
|
var drawing: JSONObject = JSONObject()
|
|
@@ -403,11 +441,23 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
403
441
|
private var gridColumns = 1
|
|
404
442
|
private var gridAutoRows: CraftNativeLength? = null
|
|
405
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
|
+
}
|
|
406
454
|
|
|
407
455
|
fun configure(style: JSONObject, density: Float) {
|
|
408
456
|
orientation = if (style.optString("flexDirection") in setOf("row", "row-reverse")) HORIZONTAL else VERTICAL
|
|
409
457
|
reverse = style.optString("flexDirection").endsWith("-reverse")
|
|
410
|
-
|
|
458
|
+
val flexWrap = style.optString("flexWrap")
|
|
459
|
+
wrap = flexWrap == "wrap" || flexWrap == "wrap-reverse" || style.optBoolean("flexWrap", false)
|
|
460
|
+
wrapReverse = flexWrap == "wrap-reverse"
|
|
411
461
|
alignItems = style.optString("alignItems", "stretch")
|
|
412
462
|
alignContent = style.optString("alignContent", "flex-start")
|
|
413
463
|
justifyContent = style.optString("justifyContent", "flex-start")
|
|
@@ -895,9 +945,12 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
895
945
|
crossFree / lines.size.toFloat()
|
|
896
946
|
else 0f
|
|
897
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()
|
|
898
950
|
for ((lineIndex, line) in lines.withIndex()) {
|
|
899
951
|
val lineCross = line.cross.toFloat() + stretchExtra
|
|
900
|
-
|
|
952
|
+
val lineOffset = sharedCross?.first?.get(lineIndex)?.let { crossOrigin + it } ?: crossOffset
|
|
953
|
+
val physicalLineOffset = if (wrapReverse && wrap) crossOrigin + crossExtent - (lineOffset - crossOrigin) - lineCross else lineOffset
|
|
901
954
|
val fallbackWidths = fallbackFlexWidths(line, availableMain, gap)
|
|
902
955
|
val sharedPrimary = CraftNative.flexRow(
|
|
903
956
|
availableMain.toFloat(),
|
|
@@ -940,6 +993,12 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
940
993
|
val free = max(0f, availableMain.toFloat() - distributedMain.toFloat())
|
|
941
994
|
val (leading, between) = distribution(free, line.entries.size)
|
|
942
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
|
|
943
1002
|
for ((index, entry) in line.entries.withIndex()) {
|
|
944
1003
|
val lp = entry.view.layoutParams as? MarginLayoutParams
|
|
945
1004
|
val marginStart = if (orientation == HORIZONTAL) lp?.leftMargin ?: 0 else lp?.topMargin ?: 0
|
|
@@ -950,18 +1009,26 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
950
1009
|
val align = entry.style.alignSelf ?: alignItems
|
|
951
1010
|
val crossLeading = (if (orientation == HORIZONTAL) lp?.topMargin ?: 0 else lp?.leftMargin ?: 0).toFloat()
|
|
952
1011
|
val crossTrailing = (if (orientation == HORIZONTAL) lp?.bottomMargin ?: 0 else lp?.rightMargin ?: 0).toFloat()
|
|
953
|
-
val
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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
|
+
}
|
|
964
1030
|
}
|
|
1031
|
+
val physicalCrossPosition = if (wrapReverse && wrap) lineCross - crossPosition - crossSize else crossPosition
|
|
965
1032
|
// Shared positions cover every non-reverse justification; the
|
|
966
1033
|
// local main-axis rule remains the fallback for old runtimes
|
|
967
1034
|
// that return widths only.
|
|
@@ -970,8 +1037,8 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
970
1037
|
val sharedPosition = if (!reverse) sharedPositions?.get(line.entries.size + index) else null
|
|
971
1038
|
val positionedMain = sharedPosition?.let { (if (orientation == HORIZONTAL) paddingLeft else paddingTop).toFloat() + it }
|
|
972
1039
|
?: mainOffset + marginStart
|
|
973
|
-
val x = if (orientation == HORIZONTAL) positionedMain.roundToInt() else (
|
|
974
|
-
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()
|
|
975
1042
|
val distributedPrimary = sharedPrimary?.get(index)?.roundToInt() ?: fallbackWidths[index]
|
|
976
1043
|
val w = if (orientation == HORIZONTAL) distributedPrimary else crossSize
|
|
977
1044
|
val h = if (orientation == HORIZONTAL) crossSize else distributedPrimary
|
|
@@ -1002,6 +1069,9 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1002
1069
|
else -> 0f to 0f
|
|
1003
1070
|
}
|
|
1004
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
|
|
1005
1075
|
}
|
|
1006
1076
|
|
|
1007
1077
|
/** An image-backed flex container with ordinary native children over the image. */
|
|
@@ -1024,6 +1094,12 @@ private class CraftNativeKeyboardAvoidingView(context: Context) : FrameLayout(co
|
|
|
1024
1094
|
private var behavior = "padding"
|
|
1025
1095
|
private var keyboardVerticalOffset = 0
|
|
1026
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
|
+
}
|
|
1027
1103
|
private var basePadding = intArrayOf(0, 0, 0, 0)
|
|
1028
1104
|
|
|
1029
1105
|
init {
|
|
@@ -1432,6 +1508,7 @@ private class CraftNativeDatePickerView(context: Context) : FrameLayout(context)
|
|
|
1432
1508
|
private var datePicker: DatePicker? = null
|
|
1433
1509
|
private var timePicker: TimePicker? = null
|
|
1434
1510
|
private var minuteInterval = 1
|
|
1511
|
+
private var countdownSecond = 0
|
|
1435
1512
|
private var suppressChanges = true
|
|
1436
1513
|
var onValueChanged: (() -> Unit)? = null
|
|
1437
1514
|
|
|
@@ -1483,6 +1560,7 @@ private class CraftNativeDatePickerView(context: Context) : FrameLayout(context)
|
|
|
1483
1560
|
pickerDate(props.opt("minimumDate"))?.let { datePicker?.minDate = it.timeInMillis }
|
|
1484
1561
|
pickerDate(props.opt("maximumDate"))?.let { datePicker?.maxDate = it.timeInMillis }
|
|
1485
1562
|
if (requestedMode == PickerMode.COUNTDOWN) {
|
|
1563
|
+
countdownSecond = 0
|
|
1486
1564
|
countdownSeconds(props.opt("value"))?.let { applyCountdown(it) }
|
|
1487
1565
|
} else {
|
|
1488
1566
|
pickerDate(props.opt("value"))?.let { applyDate(it) }
|
|
@@ -1533,11 +1611,12 @@ private class CraftNativeDatePickerView(context: Context) : FrameLayout(context)
|
|
|
1533
1611
|
}
|
|
1534
1612
|
|
|
1535
1613
|
private fun applyCountdown(seconds: Int) {
|
|
1614
|
+
countdownSecond = seconds % 60
|
|
1536
1615
|
timePicker?.hour = (seconds / 3600).coerceIn(0, 23)
|
|
1537
1616
|
timePicker?.minute = (seconds % 3600 / 60).coerceIn(0, 59)
|
|
1538
1617
|
}
|
|
1539
1618
|
|
|
1540
|
-
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
|
|
1541
1620
|
|
|
1542
1621
|
private fun formatCountdown(seconds: Int): String = "%02d:%02d:%02d".format(
|
|
1543
1622
|
Locale.US,
|
|
@@ -1583,11 +1662,11 @@ private class CraftNativeDatePickerView(context: Context) : FrameLayout(context)
|
|
|
1583
1662
|
val text = value?.toString()?.trim().orEmpty()
|
|
1584
1663
|
if (text.isBlank() || text == JSONObject.NULL.toString()) return null
|
|
1585
1664
|
text.toDoubleOrNull()?.takeIf { it.isFinite() }?.let { return it.coerceIn(0.0, maxCountdownSeconds.toDouble()).roundToInt() }
|
|
1586
|
-
val parts = text.split(':')
|
|
1665
|
+
val parts = text.split(':')
|
|
1587
1666
|
if (parts.size != 2 && parts.size != 3) return null
|
|
1588
|
-
val hours = parts[0]
|
|
1589
|
-
val minutes = parts[1]
|
|
1590
|
-
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
|
|
1591
1670
|
if (hours < 0 || minutes !in 0.0..<60.0 || seconds !in 0.0..<60.0) return null
|
|
1592
1671
|
return (hours * 3600 + minutes * 60 + seconds).coerceIn(0.0, maxCountdownSeconds.toDouble()).roundToInt()
|
|
1593
1672
|
}
|
|
@@ -1803,6 +1882,101 @@ private class CraftNativeAudio {
|
|
|
1803
1882
|
}
|
|
1804
1883
|
}
|
|
1805
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
|
+
|
|
1806
1980
|
/** WebView-free host for a route bundle compiled by `stx native compile`. */
|
|
1807
1981
|
class MainActivity : AppCompatActivity() {
|
|
1808
1982
|
private class Screen(
|
|
@@ -2124,6 +2298,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
2124
2298
|
capabilities: ${capabilities.names()},
|
|
2125
2299
|
initialAppState: ${JSONObject.quote(nativeAppState)},
|
|
2126
2300
|
colorScheme: ${JSONObject.quote(colorScheme())},
|
|
2301
|
+
appearance: ${appearance()},
|
|
2127
2302
|
postMessage: function(value) { port.postMessage(value); },
|
|
2128
2303
|
onMessage: function(callback) { port.onmessage = function(event) { callback(event.data); }; }
|
|
2129
2304
|
};
|
|
@@ -2139,6 +2314,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
2139
2314
|
capabilities: ${capabilities.names()},
|
|
2140
2315
|
initialAppState: ${JSONObject.quote(nativeAppState)},
|
|
2141
2316
|
colorScheme: ${JSONObject.quote(colorScheme())},
|
|
2317
|
+
appearance: ${appearance()},
|
|
2142
2318
|
postMessage: function(value) { console.log(${JSONObject.quote(prefix)} + value); },
|
|
2143
2319
|
onMessage: function(callback) { globalThis.__stxNativeCallback = callback; }
|
|
2144
2320
|
};
|
|
@@ -2190,15 +2366,17 @@ class MainActivity : AppCompatActivity() {
|
|
|
2190
2366
|
|
|
2191
2367
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
|
2192
2368
|
super.onConfigurationChanged(newConfig)
|
|
2369
|
+
publishAppearance()
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
private fun publishAppearance() {
|
|
2193
2373
|
if (!::root.isInitialized) return
|
|
2194
2374
|
refreshThemeDefaults()
|
|
2195
2375
|
ViewCompat.requestApplyInsets(root)
|
|
2196
2376
|
refreshInputDefaultsForTraitChange()
|
|
2197
2377
|
screens.lastOrNull()?.let { screen ->
|
|
2198
2378
|
refreshScreenTheme(screen)
|
|
2199
|
-
if (screen.active)
|
|
2200
|
-
send(screen, "APPEARANCE", JSONObject().put("colorScheme", colorScheme()))
|
|
2201
|
-
}
|
|
2379
|
+
if (screen.active) send(screen, "APPEARANCE", appearance())
|
|
2202
2380
|
}
|
|
2203
2381
|
}
|
|
2204
2382
|
|
|
@@ -2223,6 +2401,25 @@ class MainActivity : AppCompatActivity() {
|
|
|
2223
2401
|
resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
|
|
2224
2402
|
) "dark" else "light"
|
|
2225
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
|
+
|
|
2226
2423
|
private fun refreshInputDefaultsForTraitChange() {
|
|
2227
2424
|
if (inputHintDefaults.isEmpty() && inputHighlightDefaults.isEmpty()) return
|
|
2228
2425
|
val probe = EditText(this)
|
|
@@ -2540,7 +2737,12 @@ class MainActivity : AppCompatActivity() {
|
|
|
2540
2737
|
(style.optDouble("paddingBottom", vertical) * density).toInt(),
|
|
2541
2738
|
)
|
|
2542
2739
|
}
|
|
2543
|
-
|
|
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)
|
|
2544
2746
|
val controlledValue = props.opt("value")?.takeUnless { it == JSONObject.NULL }?.toString()
|
|
2545
2747
|
if (controlledValue != null && text.toString() != controlledValue) {
|
|
2546
2748
|
screen.suppressedTextChanges.add(this)
|
|
@@ -2548,6 +2750,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
2548
2750
|
}
|
|
2549
2751
|
else if (this !== previous && screen.drafts.containsKey(identity)) setText(screen.drafts[identity])
|
|
2550
2752
|
else if (this !== previous && props.has("defaultValue") && !props.isNull("defaultValue")) setText(props.optString("defaultValue"))
|
|
2753
|
+
applyTextDecoration(this, style)
|
|
2551
2754
|
screen.inputIdentities[this] = identity
|
|
2552
2755
|
val handler = events.optString("onChange").ifBlank { events.optString("onChangeText") }
|
|
2553
2756
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
@@ -2741,7 +2944,12 @@ class MainActivity : AppCompatActivity() {
|
|
|
2741
2944
|
// ranges unable to represent most valid values. Continuous
|
|
2742
2945
|
// sliders retain the bounded scale to avoid an enormous max.
|
|
2743
2946
|
val progressMax = step?.let {
|
|
2744
|
-
|
|
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()
|
|
2745
2953
|
} ?: 1000
|
|
2746
2954
|
max = progressMax
|
|
2747
2955
|
val requested = props.optDouble("value", minimum)
|
|
@@ -3270,27 +3478,79 @@ class MainActivity : AppCompatActivity() {
|
|
|
3270
3478
|
|
|
3271
3479
|
private fun applyViewStyle(view: View, style: JSONObject) {
|
|
3272
3480
|
view.alpha = style.optDouble("opacity", 1.0).toFloat()
|
|
3481
|
+
applyTransform(view, style.optJSONArray("transform"))
|
|
3273
3482
|
if (view !is CraftNativeModalView) {
|
|
3274
3483
|
view.visibility = if (style.optString("display") == "none") View.GONE else View.VISIBLE
|
|
3275
3484
|
}
|
|
3276
3485
|
view.elevation = dp(style.optDouble("elevation", 0.0)).toFloat()
|
|
3486
|
+
view.z = style.optDouble("zIndex", 0.0).toFloat()
|
|
3277
3487
|
val background = style.has("backgroundColor") && !style.isNull("backgroundColor")
|
|
3278
3488
|
val border = style.optDouble("borderWidth", 0.0)
|
|
3279
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()
|
|
3280
3502
|
val defaultBackground = if (defaultBackgrounds.containsKey(view)) {
|
|
3281
3503
|
defaultBackgrounds[view]
|
|
3282
3504
|
} else {
|
|
3283
3505
|
view.background.also { defaultBackgrounds[view] = it }
|
|
3284
3506
|
}
|
|
3285
|
-
if (background || border > 0 || radius > 0) {
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
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
|
+
}
|
|
3290
3528
|
}
|
|
3291
3529
|
} else view.background = defaultBackground
|
|
3292
3530
|
view.clipToOutline = style.optString("overflow") == "hidden"
|
|
3293
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
|
+
}
|
|
3294
3554
|
if (view is ImageView) {
|
|
3295
3555
|
val tint = style.opt("tintColor") as? String
|
|
3296
3556
|
if (tint.isNullOrBlank() || tint == "null" || tint == "undefined") {
|
|
@@ -3301,6 +3561,49 @@ class MainActivity : AppCompatActivity() {
|
|
|
3301
3561
|
}
|
|
3302
3562
|
}
|
|
3303
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
|
+
|
|
3304
3607
|
private fun ensureLayoutListener(screen: Screen, view: View) {
|
|
3305
3608
|
if (screen.layoutListeners.containsKey(view)) return
|
|
3306
3609
|
val listener = View.OnLayoutChangeListener { changed, left, top, right, bottom, _, _, _, _ ->
|
|
@@ -3409,13 +3712,59 @@ class MainActivity : AppCompatActivity() {
|
|
|
3409
3712
|
style.optDouble("lineHeight", Double.NaN).takeUnless { it.isNaN() }?.let { dp(it) - view.textSize } ?: 0f,
|
|
3410
3713
|
1f
|
|
3411
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
|
+
}
|
|
3412
3727
|
view.paintFlags = view.paintFlags and Paint.UNDERLINE_TEXT_FLAG.inv() and Paint.STRIKE_THRU_TEXT_FLAG.inv()
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
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
|
|
3418
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")
|
|
3419
3768
|
}
|
|
3420
3769
|
|
|
3421
3770
|
private fun updateImage(screen: Screen, view: ImageView, source: Any?) {
|
|
@@ -3459,7 +3808,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3459
3808
|
} else imageFailure(screen, view, uri, "Bundled image was not found")
|
|
3460
3809
|
return
|
|
3461
3810
|
}
|
|
3462
|
-
if (!
|
|
3811
|
+
if (!loadsImage(Uri.parse(uri))) {
|
|
3463
3812
|
imageFailure(screen, view, uri, "Unsupported image source")
|
|
3464
3813
|
return
|
|
3465
3814
|
}
|
|
@@ -3599,14 +3948,30 @@ class MainActivity : AppCompatActivity() {
|
|
|
3599
3948
|
val raw = props.optString("textContentType").ifBlank { props.optString("autoComplete") }
|
|
3600
3949
|
return when (raw.replace("-", "").replace("_", "").lowercase()) {
|
|
3601
3950
|
"username" -> View.AUTOFILL_HINT_USERNAME
|
|
3951
|
+
"nickname" -> "personNickname"
|
|
3602
3952
|
"password", "currentpassword" -> View.AUTOFILL_HINT_PASSWORD
|
|
3603
3953
|
"newpassword" -> "newPassword"
|
|
3604
3954
|
"onetimecode" -> "smsOTPCode"
|
|
3605
3955
|
"emailaddress", "email" -> View.AUTOFILL_HINT_EMAIL_ADDRESS
|
|
3606
3956
|
"telephonenumber", "tel", "phone" -> View.AUTOFILL_HINT_PHONE
|
|
3607
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"
|
|
3608
3966
|
"postalcode" -> View.AUTOFILL_HINT_POSTAL_CODE
|
|
3609
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"
|
|
3610
3975
|
else -> null
|
|
3611
3976
|
}
|
|
3612
3977
|
}
|
|
@@ -3617,6 +3982,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
3617
3982
|
"next" -> EditorInfo.IME_ACTION_NEXT
|
|
3618
3983
|
"search" -> EditorInfo.IME_ACTION_SEARCH
|
|
3619
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
|
|
3620
3990
|
else -> EditorInfo.IME_ACTION_NONE
|
|
3621
3991
|
}
|
|
3622
3992
|
|
|
@@ -3710,6 +4080,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
3710
4080
|
|
|
3711
4081
|
override fun onStart() {
|
|
3712
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
|
+
}
|
|
3713
4088
|
nativeAppState = "active"
|
|
3714
4089
|
publish("APP_STATE", JSONObject().put("state", nativeAppState))
|
|
3715
4090
|
}
|
|
@@ -3721,6 +4096,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
3721
4096
|
}
|
|
3722
4097
|
|
|
3723
4098
|
override fun onStop() {
|
|
4099
|
+
if (animationScaleObserverRegistered) {
|
|
4100
|
+
contentResolver.unregisterContentObserver(animationScaleObserver)
|
|
4101
|
+
animationScaleObserverRegistered = false
|
|
4102
|
+
}
|
|
3724
4103
|
nativeAppState = "background"
|
|
3725
4104
|
publish("APP_STATE", JSONObject().put("state", nativeAppState))
|
|
3726
4105
|
super.onStop()
|
|
@@ -3964,6 +4343,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3964
4343
|
}
|
|
3965
4344
|
|
|
3966
4345
|
override fun onDestroy() {
|
|
4346
|
+
if (animationScaleObserverRegistered) contentResolver.unregisterContentObserver(animationScaleObserver)
|
|
3967
4347
|
screens.forEach { closeScreen(it) }
|
|
3968
4348
|
screens.clear()
|
|
3969
4349
|
imageJobs.values.forEach { it.cancel(true) }
|