craft-native 0.0.123 → 0.0.125
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/android/src/index.d.ts +3 -0
- package/dist/android/src/index.js +22 -3
- package/dist/android/templates/AndroidManifest.xml.template +1 -1
- package/dist/android/templates/CraftNativeFlatList.kt.template +151 -7
- package/dist/android/templates/CraftNativeRefreshableFlatList.kt.template +29 -0
- package/dist/android/templates/MainActivity.kt.template +17 -0
- package/dist/android/templates/MainActivityNative.kt.template +1085 -84
- package/dist/android/templates/build.gradle.kts.app.template +1 -0
- package/dist/api/mobile.d.ts +250 -2
- package/dist/cli.js +3 -3
- package/dist/index.cjs +22 -1
- package/dist/index.js +22 -1
- package/dist/ios/src/index.d.ts +125 -3
- package/dist/ios/src/index.js +284 -47
- package/dist/ios/templates/CraftApp.swift +1186 -69
- package/dist/ios/templates/CraftHybrid.swift +1196 -0
- package/dist/ios/templates/CraftNativeActions.swift +310 -10
- package/dist/ios/templates/CraftNativeFlatList.swift +226 -17
- package/dist/ios/templates/CraftNativeScreen.swift +2217 -220
- package/dist/ios/templates/CraftNativeUI.swift +562 -0
- package/dist/ios/templates/Info.plist.template +2 -1
- package/dist/mobile.js +204 -0
- package/package.json +1 -1
|
@@ -3,11 +3,14 @@ package {{PACKAGE_NAME}}
|
|
|
3
3
|
import android.content.ClipData
|
|
4
4
|
import android.content.ClipboardManager
|
|
5
5
|
import android.content.Context
|
|
6
|
+
import android.content.res.ColorStateList
|
|
7
|
+
import android.content.res.Configuration
|
|
6
8
|
import android.content.Intent
|
|
7
9
|
import android.graphics.BitmapFactory
|
|
8
10
|
import android.graphics.Color
|
|
9
11
|
import android.graphics.Paint
|
|
10
12
|
import android.graphics.Typeface
|
|
13
|
+
import android.graphics.drawable.Drawable
|
|
11
14
|
import android.graphics.drawable.GradientDrawable
|
|
12
15
|
import android.net.Uri
|
|
13
16
|
import android.os.Build
|
|
@@ -17,6 +20,8 @@ import android.os.Looper
|
|
|
17
20
|
import android.os.VibrationEffect
|
|
18
21
|
import android.os.Vibrator
|
|
19
22
|
import android.text.Editable
|
|
23
|
+
import android.text.InputFilter
|
|
24
|
+
import android.text.TextUtils
|
|
20
25
|
import android.text.TextWatcher
|
|
21
26
|
import android.util.Log
|
|
22
27
|
import android.util.Base64
|
|
@@ -25,8 +30,11 @@ import android.view.Gravity
|
|
|
25
30
|
import android.view.KeyEvent
|
|
26
31
|
import android.view.inputmethod.EditorInfo
|
|
27
32
|
import android.text.InputType
|
|
33
|
+
import android.text.Layout
|
|
28
34
|
import android.view.View
|
|
29
35
|
import android.view.ViewGroup
|
|
36
|
+
import android.widget.AdapterView
|
|
37
|
+
import android.widget.ArrayAdapter
|
|
30
38
|
import android.widget.Button
|
|
31
39
|
import android.widget.EditText
|
|
32
40
|
import android.widget.FrameLayout
|
|
@@ -34,10 +42,15 @@ import android.widget.HorizontalScrollView
|
|
|
34
42
|
import android.widget.ImageView
|
|
35
43
|
import android.widget.LinearLayout
|
|
36
44
|
import android.widget.ScrollView
|
|
45
|
+
import android.widget.SeekBar
|
|
46
|
+
import android.widget.Spinner
|
|
47
|
+
import android.widget.Switch
|
|
37
48
|
import android.widget.TextView
|
|
38
49
|
import androidx.appcompat.app.AppCompatActivity
|
|
50
|
+
import androidx.appcompat.app.AppCompatDelegate
|
|
39
51
|
import androidx.core.content.ContextCompat
|
|
40
52
|
import androidx.core.view.AccessibilityDelegateCompat
|
|
53
|
+
import androidx.core.view.WindowInsetsCompat
|
|
41
54
|
import androidx.core.view.ViewCompat
|
|
42
55
|
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
|
|
43
56
|
import androidx.javascriptengine.JavaScriptIsolate
|
|
@@ -55,6 +68,7 @@ import java.util.concurrent.Future
|
|
|
55
68
|
import java.util.UUID
|
|
56
69
|
import kotlin.math.max
|
|
57
70
|
import kotlin.math.min
|
|
71
|
+
import kotlin.math.roundToInt
|
|
58
72
|
|
|
59
73
|
private const val CRAFT_NATIVE_CAPABILITY_TIMEOUT_MS = 30_000L
|
|
60
74
|
|
|
@@ -71,30 +85,61 @@ private data class CraftNativeLayoutStyle(
|
|
|
71
85
|
val bottom: Int? = null,
|
|
72
86
|
val left: Int? = null,
|
|
73
87
|
val alignSelf: String? = null,
|
|
88
|
+
val flexGrow: Double = 0.0,
|
|
89
|
+
val flexShrink: Double = 0.0,
|
|
90
|
+
val flexBasis: Int? = null,
|
|
74
91
|
) {
|
|
75
92
|
companion object {
|
|
76
93
|
fun from(raw: JSONObject, density: Float): CraftNativeLayoutStyle {
|
|
77
|
-
fun number(name: String): Int?
|
|
94
|
+
fun number(name: String): Int? {
|
|
95
|
+
if (!raw.has(name) || raw.isNull(name)) return null
|
|
96
|
+
return raw.optDouble(name, Double.NaN).takeUnless { it.isNaN() }?.let { (it * density).toInt() }
|
|
97
|
+
}
|
|
78
98
|
return CraftNativeLayoutStyle(
|
|
79
99
|
width = number("width"), height = number("height"),
|
|
80
100
|
minWidth = number("minWidth"), maxWidth = number("maxWidth"),
|
|
81
101
|
minHeight = number("minHeight"), maxHeight = number("maxHeight"),
|
|
82
|
-
position = raw.
|
|
102
|
+
position = raw.opt("position")
|
|
103
|
+
?.takeUnless { it == JSONObject.NULL }
|
|
104
|
+
?.toString()
|
|
105
|
+
?.takeIf { it.isNotBlank() }
|
|
106
|
+
?: "relative",
|
|
83
107
|
top = number("top"), right = number("right"), bottom = number("bottom"), left = number("left"),
|
|
84
|
-
alignSelf = raw.
|
|
108
|
+
alignSelf = raw.opt("alignSelf")
|
|
109
|
+
?.takeUnless { it == JSONObject.NULL }
|
|
110
|
+
?.toString()
|
|
111
|
+
?.takeIf { it.isNotBlank() },
|
|
112
|
+
flexGrow = raw.optDouble("flexGrow", raw.optDouble("flex", 0.0)),
|
|
113
|
+
flexShrink = raw.optDouble("flexShrink", 0.0),
|
|
114
|
+
flexBasis = number("flexBasis"),
|
|
85
115
|
)
|
|
86
116
|
}
|
|
87
117
|
}
|
|
88
118
|
}
|
|
89
119
|
|
|
120
|
+
private fun craftNativeGridColumnCount(value: Any?): Int {
|
|
121
|
+
if (value is Number) return max(1, value.toInt())
|
|
122
|
+
val text = value as? String ?: return 1
|
|
123
|
+
val trimmed = text.trim()
|
|
124
|
+
if (trimmed.startsWith("repeat(")) {
|
|
125
|
+
val count = trimmed.removePrefix("repeat(").takeWhile { it.isDigit() }.toIntOrNull()
|
|
126
|
+
if (count != null) return max(1, count)
|
|
127
|
+
}
|
|
128
|
+
return max(1, trimmed.split(Regex("\\s+")).count { it.isNotBlank() })
|
|
129
|
+
}
|
|
130
|
+
|
|
90
131
|
/** A deterministic flex-shaped container shared by normal views and scroll content. */
|
|
91
132
|
private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
133
|
+
var onLayoutChanged: (() -> Unit)? = null
|
|
92
134
|
private val styles = mutableMapOf<View, CraftNativeLayoutStyle>()
|
|
93
135
|
private var wrap = false
|
|
94
136
|
private var alignItems = "stretch"
|
|
95
137
|
private var justifyContent = "flex-start"
|
|
96
138
|
private var rowGap = 0
|
|
97
139
|
private var columnGap = 0
|
|
140
|
+
private var grid = false
|
|
141
|
+
private var gridColumns = 1
|
|
142
|
+
private var gridAutoRows: Int? = null
|
|
98
143
|
|
|
99
144
|
fun configure(style: JSONObject, density: Float) {
|
|
100
145
|
orientation = if (style.optString("flexDirection") in setOf("row", "row-reverse")) HORIZONTAL else VERTICAL
|
|
@@ -104,6 +149,11 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
104
149
|
val gap = (style.optDouble("gap", 0.0) * density).toInt()
|
|
105
150
|
rowGap = (style.optDouble("rowGap", gap.toDouble() / density.toDouble()) * density).toInt()
|
|
106
151
|
columnGap = (style.optDouble("columnGap", gap.toDouble() / density.toDouble()) * density).toInt()
|
|
152
|
+
grid = style.optString("display") == "grid"
|
|
153
|
+
gridColumns = craftNativeGridColumnCount(
|
|
154
|
+
if (style.has("gridTemplateColumns")) style.opt("gridTemplateColumns") else style.opt("gridColumns")
|
|
155
|
+
)
|
|
156
|
+
gridAutoRows = style.optDouble("gridAutoRows", Double.NaN).takeUnless { it.isNaN() }?.let { (it * density).toInt() }
|
|
107
157
|
val padding = (style.optDouble("padding", 0.0) * density).toInt()
|
|
108
158
|
val horizontal = (style.optDouble("paddingHorizontal", padding.toDouble() / density.toDouble()) * density).toInt()
|
|
109
159
|
val vertical = (style.optDouble("paddingVertical", padding.toDouble() / density.toDouble()) * density).toInt()
|
|
@@ -128,6 +178,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
128
178
|
|
|
129
179
|
private data class Entry(val view: View, val style: CraftNativeLayoutStyle, val width: Int, val height: Int)
|
|
130
180
|
private data class Line(val entries: MutableList<Entry> = mutableListOf(), var main: Int = 0, var cross: Int = 0)
|
|
181
|
+
private data class GridLayout(val rows: List<List<Entry>>, val absolute: List<Entry>, val cellWidth: Int, val rowHeights: List<Int>)
|
|
131
182
|
|
|
132
183
|
private fun entries(width: Int, height: Int): Pair<List<Line>, List<Entry>> {
|
|
133
184
|
val availableMain = if (orientation == HORIZONTAL) max(0, width - paddingLeft - paddingRight)
|
|
@@ -143,6 +194,16 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
143
194
|
val lp = child.layoutParams as? MarginLayoutParams
|
|
144
195
|
val horizontalMargins = (lp?.leftMargin ?: 0) + (lp?.rightMargin ?: 0)
|
|
145
196
|
val verticalMargins = (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
|
|
197
|
+
if (child is CraftNativeModalView) {
|
|
198
|
+
val modalWidth = max(0, width - paddingLeft - paddingRight)
|
|
199
|
+
val modalHeight = max(0, height - paddingTop - paddingBottom)
|
|
200
|
+
child.measure(
|
|
201
|
+
View.MeasureSpec.makeMeasureSpec(modalWidth, View.MeasureSpec.EXACTLY),
|
|
202
|
+
View.MeasureSpec.makeMeasureSpec(modalHeight, View.MeasureSpec.EXACTLY),
|
|
203
|
+
)
|
|
204
|
+
absolute += Entry(child, style, modalWidth, modalHeight)
|
|
205
|
+
continue
|
|
206
|
+
}
|
|
146
207
|
val childWidth = measureChildWidth(child, style, width - paddingLeft - paddingRight, horizontalMargins)
|
|
147
208
|
val childHeight = measureChildHeight(child, style, height - paddingTop - paddingBottom, verticalMargins)
|
|
148
209
|
val entry = Entry(child, style, childWidth, childHeight)
|
|
@@ -162,8 +223,80 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
162
223
|
return lines to absolute
|
|
163
224
|
}
|
|
164
225
|
|
|
226
|
+
private fun gridLayout(width: Int, height: Int): GridLayout {
|
|
227
|
+
val availableWidth = max(0, width - paddingLeft - paddingRight)
|
|
228
|
+
val gap = columnGap
|
|
229
|
+
val cellWidth = max(0, (availableWidth - max(0, gridColumns - 1) * gap) / max(1, gridColumns))
|
|
230
|
+
val rows = mutableListOf<MutableList<Entry>>()
|
|
231
|
+
val absolute = mutableListOf<Entry>()
|
|
232
|
+
for (index in 0 until childCount) {
|
|
233
|
+
val child = getChildAt(index)
|
|
234
|
+
if (child.visibility == GONE) continue
|
|
235
|
+
val style = styles[child] ?: CraftNativeLayoutStyle()
|
|
236
|
+
val lp = child.layoutParams as? MarginLayoutParams
|
|
237
|
+
val horizontalMargins = (lp?.leftMargin ?: 0) + (lp?.rightMargin ?: 0)
|
|
238
|
+
val verticalMargins = (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
|
|
239
|
+
if (child is CraftNativeModalView) {
|
|
240
|
+
val modalWidth = max(0, width - paddingLeft - paddingRight)
|
|
241
|
+
val modalHeight = max(0, height - paddingTop - paddingBottom)
|
|
242
|
+
child.measure(
|
|
243
|
+
View.MeasureSpec.makeMeasureSpec(modalWidth, View.MeasureSpec.EXACTLY),
|
|
244
|
+
View.MeasureSpec.makeMeasureSpec(modalHeight, View.MeasureSpec.EXACTLY),
|
|
245
|
+
)
|
|
246
|
+
absolute += Entry(child, style, modalWidth, modalHeight)
|
|
247
|
+
continue
|
|
248
|
+
}
|
|
249
|
+
val childWidth = measureChildWidth(child, style, cellWidth, horizontalMargins)
|
|
250
|
+
val childHeight = measureChildHeight(child, style, height - paddingTop - paddingBottom, verticalMargins)
|
|
251
|
+
val entry = Entry(child, style, childWidth, childHeight)
|
|
252
|
+
if (style.position == "absolute") absolute += entry
|
|
253
|
+
else {
|
|
254
|
+
if (rows.isEmpty() || rows.last().size == gridColumns) rows.add(mutableListOf())
|
|
255
|
+
rows.last() += entry
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
val rowHeights = rows.map { row ->
|
|
259
|
+
max(gridAutoRows ?: 0, row.maxOfOrNull { entry ->
|
|
260
|
+
val lp = entry.view.layoutParams as? MarginLayoutParams
|
|
261
|
+
entry.height + (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
|
|
262
|
+
} ?: 0)
|
|
263
|
+
}
|
|
264
|
+
return GridLayout(rows, absolute, cellWidth, rowHeights)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private fun gridIntrinsicSize(height: Int): Pair<Int, Int> {
|
|
268
|
+
val columns = max(1, gridColumns)
|
|
269
|
+
val columnWidths = IntArray(columns)
|
|
270
|
+
val rowHeights = mutableListOf<Int>()
|
|
271
|
+
var flowIndex = 0
|
|
272
|
+
for (index in 0 until childCount) {
|
|
273
|
+
val child = getChildAt(index)
|
|
274
|
+
if (child.visibility == GONE) continue
|
|
275
|
+
val style = styles[child] ?: CraftNativeLayoutStyle()
|
|
276
|
+
if (style.position == "absolute" || child is CraftNativeModalView) continue
|
|
277
|
+
val lp = child.layoutParams as? MarginLayoutParams
|
|
278
|
+
val horizontalMargins = (lp?.leftMargin ?: 0) + (lp?.rightMargin ?: 0)
|
|
279
|
+
val verticalMargins = (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
|
|
280
|
+
val childWidth = measureChildWidth(child, style, 1_000_000, horizontalMargins)
|
|
281
|
+
val childHeight = measureChildHeight(child, style, height, verticalMargins)
|
|
282
|
+
val column = flowIndex % columns
|
|
283
|
+
val row = flowIndex / columns
|
|
284
|
+
while (rowHeights.size <= row) rowHeights += gridAutoRows ?: 0
|
|
285
|
+
columnWidths[column] = max(columnWidths[column], childWidth + horizontalMargins)
|
|
286
|
+
rowHeights[row] = max(rowHeights[row], childHeight + verticalMargins)
|
|
287
|
+
flowIndex += 1
|
|
288
|
+
}
|
|
289
|
+
val cellWidth = columnWidths.maxOrNull() ?: 0
|
|
290
|
+
val width = paddingLeft + paddingRight + cellWidth * columns + max(0, columns - 1) * columnGap
|
|
291
|
+
val rowGapTotal = max(0, rowHeights.size - 1) * rowGap
|
|
292
|
+
val measuredHeight = paddingTop + paddingBottom + rowHeights.sum() + rowGapTotal
|
|
293
|
+
return width to measuredHeight
|
|
294
|
+
}
|
|
295
|
+
|
|
165
296
|
private fun measureChildWidth(child: View, style: CraftNativeLayoutStyle, available: Int, margins: Int): Int {
|
|
166
|
-
val
|
|
297
|
+
val flexBasis = if (orientation == HORIZONTAL) style.flexBasis else null
|
|
298
|
+
val explicit = style.width ?: flexBasis
|
|
299
|
+
?: (child.layoutParams?.width ?: ViewGroup.LayoutParams.WRAP_CONTENT).takeIf { it >= 0 }
|
|
167
300
|
val target = explicit ?: max(0, available - margins)
|
|
168
301
|
val spec = if (explicit != null) View.MeasureSpec.makeMeasureSpec(target, View.MeasureSpec.EXACTLY)
|
|
169
302
|
else View.MeasureSpec.makeMeasureSpec(max(0, available - margins), View.MeasureSpec.AT_MOST)
|
|
@@ -172,7 +305,9 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
172
305
|
}
|
|
173
306
|
|
|
174
307
|
private fun measureChildHeight(child: View, style: CraftNativeLayoutStyle, available: Int, margins: Int): Int {
|
|
175
|
-
val
|
|
308
|
+
val flexBasis = if (orientation == VERTICAL) style.flexBasis else null
|
|
309
|
+
val explicit = style.height ?: flexBasis
|
|
310
|
+
?: (child.layoutParams?.height ?: ViewGroup.LayoutParams.WRAP_CONTENT).takeIf { it >= 0 }
|
|
176
311
|
val target = explicit ?: max(0, available - margins)
|
|
177
312
|
val spec = if (explicit != null) View.MeasureSpec.makeMeasureSpec(target, View.MeasureSpec.EXACTLY)
|
|
178
313
|
else View.MeasureSpec.makeMeasureSpec(max(0, available - margins), View.MeasureSpec.AT_MOST)
|
|
@@ -188,8 +323,24 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
188
323
|
}
|
|
189
324
|
|
|
190
325
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
191
|
-
val
|
|
326
|
+
val widthMode = View.MeasureSpec.getMode(widthMeasureSpec)
|
|
327
|
+
val width = View.MeasureSpec.getSize(widthMeasureSpec).takeUnless { widthMode == View.MeasureSpec.UNSPECIFIED } ?: 1_000_000
|
|
192
328
|
val height = View.MeasureSpec.getSize(heightMeasureSpec).takeUnless { View.MeasureSpec.getMode(heightMeasureSpec) == View.MeasureSpec.UNSPECIFIED } ?: 1_000_000
|
|
329
|
+
if (grid) {
|
|
330
|
+
if (widthMode == View.MeasureSpec.UNSPECIFIED) {
|
|
331
|
+
val (intrinsicWidth, intrinsicHeight) = gridIntrinsicSize(height)
|
|
332
|
+
setMeasuredDimension(
|
|
333
|
+
resolveSize(intrinsicWidth, widthMeasureSpec),
|
|
334
|
+
resolveSize(intrinsicHeight, heightMeasureSpec),
|
|
335
|
+
)
|
|
336
|
+
return
|
|
337
|
+
}
|
|
338
|
+
val layout = gridLayout(width, height)
|
|
339
|
+
val desiredWidth = paddingLeft + paddingRight + layout.cellWidth * gridColumns + max(0, gridColumns - 1) * columnGap
|
|
340
|
+
val desiredHeight = paddingTop + paddingBottom + layout.rowHeights.sum() + max(0, layout.rowHeights.size - 1) * rowGap
|
|
341
|
+
setMeasuredDimension(resolveSize(desiredWidth, widthMeasureSpec), resolveSize(desiredHeight, heightMeasureSpec))
|
|
342
|
+
return
|
|
343
|
+
}
|
|
193
344
|
val (lines, absolute) = entries(width, height)
|
|
194
345
|
val main = lines.maxOfOrNull { it.main } ?: 0
|
|
195
346
|
val cross = lines.sumOf { it.cross } + max(0, lines.size - 1) * (if (orientation == HORIZONTAL) rowGap else columnGap)
|
|
@@ -204,17 +355,78 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
204
355
|
}
|
|
205
356
|
|
|
206
357
|
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
358
|
+
if (grid) {
|
|
359
|
+
val layout = gridLayout(width, height)
|
|
360
|
+
var rowTop = paddingTop
|
|
361
|
+
for ((rowIndex, row) in layout.rows.withIndex()) {
|
|
362
|
+
val rowHeight = layout.rowHeights[rowIndex]
|
|
363
|
+
for ((column, entry) in row.withIndex()) {
|
|
364
|
+
val align = entry.style.alignSelf ?: alignItems
|
|
365
|
+
val lp = entry.view.layoutParams as? MarginLayoutParams
|
|
366
|
+
val marginLeft = lp?.leftMargin ?: 0
|
|
367
|
+
val marginRight = lp?.rightMargin ?: 0
|
|
368
|
+
val marginTop = lp?.topMargin ?: 0
|
|
369
|
+
val marginBottom = lp?.bottomMargin ?: 0
|
|
370
|
+
val availableWidth = max(0, layout.cellWidth - marginLeft - marginRight)
|
|
371
|
+
val availableHeight = max(0, rowHeight - marginTop - marginBottom)
|
|
372
|
+
val childWidth = if (align == "stretch" && entry.style.width == null) availableWidth else min(availableWidth, entry.width)
|
|
373
|
+
val childHeight = if (align == "stretch" && entry.style.height == null) availableHeight else min(availableHeight, entry.height)
|
|
374
|
+
val x = paddingLeft + column * (layout.cellWidth + columnGap) + marginLeft + when (align) {
|
|
375
|
+
"center" -> (availableWidth - childWidth) / 2
|
|
376
|
+
"flex-end" -> availableWidth - childWidth
|
|
377
|
+
else -> 0
|
|
378
|
+
}
|
|
379
|
+
val y = rowTop + marginTop + when (align) {
|
|
380
|
+
"center" -> (availableHeight - childHeight) / 2
|
|
381
|
+
"flex-end" -> availableHeight - childHeight
|
|
382
|
+
else -> 0
|
|
383
|
+
}
|
|
384
|
+
entry.view.layout(x, y, x + childWidth, y + childHeight)
|
|
385
|
+
}
|
|
386
|
+
rowTop += rowHeight + rowGap
|
|
387
|
+
}
|
|
388
|
+
val contentWidth = width - paddingLeft - paddingRight
|
|
389
|
+
val contentHeight = height - paddingTop - paddingBottom
|
|
390
|
+
for (entry in layout.absolute) {
|
|
391
|
+
if (entry.view is CraftNativeModalView && entry.view.visibility == View.VISIBLE) entry.view.bringToFront()
|
|
392
|
+
val childWidth = if (entry.style.left != null && entry.style.right != null) contentWidth - entry.style.left!! - entry.style.right!! else entry.width
|
|
393
|
+
val childHeight = if (entry.style.top != null && entry.style.bottom != null) contentHeight - entry.style.top!! - entry.style.bottom!! else entry.height
|
|
394
|
+
val x = paddingLeft + (entry.style.left ?: (entry.style.right?.let { contentWidth - it - childWidth } ?: 0))
|
|
395
|
+
val y = paddingTop + (entry.style.top ?: (entry.style.bottom?.let { contentHeight - it - childHeight } ?: 0))
|
|
396
|
+
entry.view.layout(x, y, x + max(0, childWidth), y + max(0, childHeight))
|
|
397
|
+
}
|
|
398
|
+
onLayoutChanged?.invoke()
|
|
399
|
+
return
|
|
400
|
+
}
|
|
207
401
|
val contentWidth = width - paddingLeft - paddingRight
|
|
208
402
|
val contentHeight = height - paddingTop - paddingBottom
|
|
209
403
|
val (lines, absolute) = entries(width, height)
|
|
210
404
|
val gap = if (orientation == HORIZONTAL) columnGap else rowGap
|
|
405
|
+
val crossGap = if (orientation == HORIZONTAL) rowGap else columnGap
|
|
406
|
+
val availableMain = if (orientation == HORIZONTAL) contentWidth else contentHeight
|
|
211
407
|
var crossOffset = if (orientation == HORIZONTAL) paddingTop else paddingLeft
|
|
212
408
|
for (line in lines) {
|
|
213
|
-
val
|
|
409
|
+
val rawFree = availableMain - line.main
|
|
410
|
+
val factors = line.entries.map { if (rawFree >= 0) it.style.flexGrow else it.style.flexShrink }
|
|
411
|
+
val totalFactor = factors.sum()
|
|
412
|
+
val distributedMain = line.entries.indices.sumOf { index ->
|
|
413
|
+
val base = if (orientation == HORIZONTAL) line.entries[index].width else line.entries[index].height
|
|
414
|
+
val primary = if (totalFactor > 0) max(0, (base + rawFree * factors[index] / totalFactor).toInt()) else base
|
|
415
|
+
val constrained = if (orientation == HORIZONTAL) {
|
|
416
|
+
clamp(primary, line.entries[index].style.minWidth, line.entries[index].style.maxWidth)
|
|
417
|
+
} else {
|
|
418
|
+
clamp(primary, line.entries[index].style.minHeight, line.entries[index].style.maxHeight)
|
|
419
|
+
}
|
|
420
|
+
val lp = line.entries[index].view.layoutParams as? MarginLayoutParams
|
|
421
|
+
val margins = if (orientation == HORIZONTAL) (lp?.leftMargin ?: 0) + (lp?.rightMargin ?: 0)
|
|
422
|
+
else (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
|
|
423
|
+
constrained + margins
|
|
424
|
+
} + max(0, line.entries.size - 1) * gap
|
|
425
|
+
val free = max(0, availableMain - distributedMain)
|
|
214
426
|
val (leading, between) = distribution(free, line.entries.size)
|
|
215
427
|
var mainOffset = if (orientation == HORIZONTAL) paddingLeft else paddingTop
|
|
216
428
|
mainOffset += leading
|
|
217
|
-
for (entry in line.entries) {
|
|
429
|
+
for ((index, entry) in line.entries.withIndex()) {
|
|
218
430
|
val lp = entry.view.layoutParams as? MarginLayoutParams
|
|
219
431
|
val marginStart = if (orientation == HORIZONTAL) lp?.leftMargin ?: 0 else lp?.topMargin ?: 0
|
|
220
432
|
val marginEnd = if (orientation == HORIZONTAL) lp?.rightMargin ?: 0 else lp?.bottomMargin ?: 0
|
|
@@ -222,27 +434,39 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
222
434
|
max(0, line.cross - (if (orientation == HORIZONTAL) (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0) else (lp?.leftMargin ?: 0) + (lp?.rightMargin ?: 0)))
|
|
223
435
|
else if (orientation == HORIZONTAL) entry.height else entry.width
|
|
224
436
|
val align = entry.style.alignSelf ?: alignItems
|
|
437
|
+
val crossLeading = if (orientation == HORIZONTAL) lp?.topMargin ?: 0 else lp?.leftMargin ?: 0
|
|
438
|
+
val crossTrailing = if (orientation == HORIZONTAL) lp?.bottomMargin ?: 0 else lp?.rightMargin ?: 0
|
|
439
|
+
val crossAvailable = max(0, line.cross - crossLeading - crossTrailing)
|
|
225
440
|
val crossPosition = when (align) {
|
|
226
|
-
"center" -> (
|
|
227
|
-
"flex-end" -> line.cross - crossSize
|
|
228
|
-
else ->
|
|
441
|
+
"center" -> crossLeading + (crossAvailable - crossSize) / 2
|
|
442
|
+
"flex-end" -> line.cross - crossTrailing - crossSize
|
|
443
|
+
else -> crossLeading
|
|
229
444
|
}
|
|
230
|
-
val x = if (orientation == HORIZONTAL) mainOffset + marginStart else crossOffset + crossPosition
|
|
231
|
-
val y = if (orientation == HORIZONTAL) crossOffset + crossPosition
|
|
232
|
-
val
|
|
233
|
-
val
|
|
445
|
+
val x = if (orientation == HORIZONTAL) mainOffset + marginStart else crossOffset + crossPosition
|
|
446
|
+
val y = if (orientation == HORIZONTAL) crossOffset + crossPosition else mainOffset + marginStart
|
|
447
|
+
val primary = if (orientation == HORIZONTAL) entry.width else entry.height
|
|
448
|
+
val distributed = if (totalFactor > 0) max(0, (primary + rawFree * factors[index] / totalFactor).toInt()) else primary
|
|
449
|
+
val distributedPrimary = if (orientation == HORIZONTAL) {
|
|
450
|
+
clamp(distributed, entry.style.minWidth, entry.style.maxWidth)
|
|
451
|
+
} else {
|
|
452
|
+
clamp(distributed, entry.style.minHeight, entry.style.maxHeight)
|
|
453
|
+
}
|
|
454
|
+
val w = if (orientation == HORIZONTAL) distributedPrimary else crossSize
|
|
455
|
+
val h = if (orientation == HORIZONTAL) crossSize else distributedPrimary
|
|
234
456
|
entry.view.layout(x, y, x + w, y + h)
|
|
235
|
-
mainOffset +=
|
|
457
|
+
mainOffset += distributedPrimary + marginStart + marginEnd + gap + between
|
|
236
458
|
}
|
|
237
|
-
crossOffset += line.cross +
|
|
459
|
+
crossOffset += line.cross + crossGap
|
|
238
460
|
}
|
|
239
461
|
for (entry in absolute) {
|
|
462
|
+
if (entry.view is CraftNativeModalView && entry.view.visibility == View.VISIBLE) entry.view.bringToFront()
|
|
240
463
|
val widthValue = if (entry.style.left != null && entry.style.right != null) contentWidth - entry.style.left!! - entry.style.right!! else entry.width
|
|
241
464
|
val heightValue = if (entry.style.top != null && entry.style.bottom != null) contentHeight - entry.style.top!! - entry.style.bottom!! else entry.height
|
|
242
465
|
val x = paddingLeft + (entry.style.left ?: (entry.style.right?.let { contentWidth - it - widthValue } ?: 0))
|
|
243
466
|
val y = paddingTop + (entry.style.top ?: (entry.style.bottom?.let { contentHeight - it - heightValue } ?: 0))
|
|
244
467
|
entry.view.layout(x, y, x + max(0, widthValue), y + max(0, heightValue))
|
|
245
468
|
}
|
|
469
|
+
onLayoutChanged?.invoke()
|
|
246
470
|
}
|
|
247
471
|
|
|
248
472
|
private fun distribution(free: Int, count: Int): Pair<Int, Int> = when (justifyContent) {
|
|
@@ -260,6 +484,11 @@ private class CraftNativeScrollView(context: Context) : FrameLayout(context) {
|
|
|
260
484
|
private val vertical = ScrollView(context)
|
|
261
485
|
private val horizontal = HorizontalScrollView(context)
|
|
262
486
|
private var horizontalMode: Boolean? = null
|
|
487
|
+
private var pagingEnabled = false
|
|
488
|
+
private var bounces = true
|
|
489
|
+
private var alwaysBounceVertical = true
|
|
490
|
+
private var alwaysBounceHorizontal = true
|
|
491
|
+
private var keyboardShouldPersistTaps = "never"
|
|
263
492
|
|
|
264
493
|
init {
|
|
265
494
|
vertical.isNestedScrollingEnabled = true
|
|
@@ -293,6 +522,62 @@ private class CraftNativeScrollView(context: Context) : FrameLayout(context) {
|
|
|
293
522
|
horizontal.isEnabled = value
|
|
294
523
|
}
|
|
295
524
|
|
|
525
|
+
fun setKeyboardShouldPersistTaps(value: String?) {
|
|
526
|
+
keyboardShouldPersistTaps = value?.takeIf { it in setOf("always", "handled", "never") } ?: "never"
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
fun setBounces(value: Boolean) {
|
|
530
|
+
bounces = value
|
|
531
|
+
applyBounceModes()
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
fun setAlwaysBounce(vertical: Boolean, horizontal: Boolean) {
|
|
535
|
+
alwaysBounceVertical = vertical
|
|
536
|
+
alwaysBounceHorizontal = horizontal
|
|
537
|
+
applyBounceModes()
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
private fun applyBounceModes() {
|
|
541
|
+
vertical.overScrollMode = if (bounces && alwaysBounceVertical) View.OVER_SCROLL_ALWAYS else View.OVER_SCROLL_NEVER
|
|
542
|
+
horizontal.overScrollMode = if (bounces && alwaysBounceHorizontal) View.OVER_SCROLL_ALWAYS else View.OVER_SCROLL_NEVER
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
private fun tapTarget(view: View, x: Float, y: Float): View? {
|
|
546
|
+
if (x < 0 || y < 0 || x >= view.width || y >= view.height || view.visibility != View.VISIBLE) return null
|
|
547
|
+
if (view is ViewGroup) {
|
|
548
|
+
for (index in view.childCount - 1 downTo 0) {
|
|
549
|
+
val child = view.getChildAt(index)
|
|
550
|
+
tapTarget(child, x - child.left, y - child.top)?.let { return it }
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return view
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
private fun shouldDismissKeyboard(scroller: View, event: android.view.MotionEvent): Boolean {
|
|
557
|
+
if (keyboardShouldPersistTaps == "always") return false
|
|
558
|
+
return when (keyboardShouldPersistTaps) {
|
|
559
|
+
"handled" -> (scroller as? ViewGroup)?.let { tapTarget(it, event.x, event.y)?.isClickable != true } ?: true
|
|
560
|
+
else -> true
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
fun setPagingEnabled(value: Boolean) {
|
|
565
|
+
pagingEnabled = value
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
private fun snapToPage(scroller: View) {
|
|
569
|
+
val horizontal = horizontalMode == true
|
|
570
|
+
val pageSize = if (horizontal) scroller.width else scroller.height
|
|
571
|
+
if (pageSize <= 0) return
|
|
572
|
+
val offset = if (horizontal) scroller.scrollX else scroller.scrollY
|
|
573
|
+
val target = (offset.toFloat() / pageSize).roundToInt() * pageSize
|
|
574
|
+
if (horizontal) {
|
|
575
|
+
(scroller as HorizontalScrollView).smoothScrollTo(target, 0)
|
|
576
|
+
} else {
|
|
577
|
+
(scroller as ScrollView).smoothScrollTo(0, target)
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
296
581
|
fun setScrollIndicators(verticalVisible: Boolean, horizontalVisible: Boolean) {
|
|
297
582
|
vertical.isVerticalScrollBarEnabled = verticalVisible
|
|
298
583
|
horizontal.isHorizontalScrollBarEnabled = horizontalVisible
|
|
@@ -302,13 +587,65 @@ private class CraftNativeScrollView(context: Context) : FrameLayout(context) {
|
|
|
302
587
|
onScroll: ((Int, Int) -> Unit)?,
|
|
303
588
|
onBeginDrag: (() -> Unit)?,
|
|
304
589
|
onEndDrag: (() -> Unit)?,
|
|
590
|
+
onMomentumBegin: (() -> Unit)?,
|
|
591
|
+
onMomentumEnd: (() -> Unit)?,
|
|
592
|
+
keyboardDismissMode: String,
|
|
593
|
+
dismissKeyboard: (() -> Unit)?,
|
|
305
594
|
) {
|
|
306
595
|
fun attach(scroller: View) {
|
|
596
|
+
var dismissed = false
|
|
597
|
+
var moved = false
|
|
598
|
+
var momentumActive = false
|
|
599
|
+
var lastX = 0
|
|
600
|
+
var lastY = 0
|
|
601
|
+
|
|
602
|
+
fun pollMomentum() {
|
|
603
|
+
if (!momentumActive) return
|
|
604
|
+
val x = scroller.scrollX
|
|
605
|
+
val y = scroller.scrollY
|
|
606
|
+
if (x == lastX && y == lastY) {
|
|
607
|
+
momentumActive = false
|
|
608
|
+
onMomentumEnd?.invoke()
|
|
609
|
+
} else {
|
|
610
|
+
lastX = x
|
|
611
|
+
lastY = y
|
|
612
|
+
scroller.postDelayed({ pollMomentum() }, 16L)
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
307
616
|
scroller.setOnScrollChangeListener { _, x, y, _, _ -> onScroll?.invoke(x, y) }
|
|
308
617
|
scroller.setOnTouchListener { _, event ->
|
|
309
618
|
when (event.actionMasked) {
|
|
310
|
-
android.view.MotionEvent.ACTION_DOWN ->
|
|
311
|
-
|
|
619
|
+
android.view.MotionEvent.ACTION_DOWN -> {
|
|
620
|
+
if (momentumActive) {
|
|
621
|
+
momentumActive = false
|
|
622
|
+
onMomentumEnd?.invoke()
|
|
623
|
+
}
|
|
624
|
+
dismissed = false
|
|
625
|
+
moved = false
|
|
626
|
+
onBeginDrag?.invoke()
|
|
627
|
+
}
|
|
628
|
+
android.view.MotionEvent.ACTION_MOVE -> {
|
|
629
|
+
moved = true
|
|
630
|
+
if (!dismissed && keyboardDismissMode in setOf("on-drag", "interactive")) {
|
|
631
|
+
dismissed = true
|
|
632
|
+
dismissKeyboard?.invoke()
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
android.view.MotionEvent.ACTION_UP, android.view.MotionEvent.ACTION_CANCEL -> {
|
|
636
|
+
if (event.actionMasked == android.view.MotionEvent.ACTION_UP && shouldDismissKeyboard(scroller, event)) {
|
|
637
|
+
dismissKeyboard?.invoke()
|
|
638
|
+
}
|
|
639
|
+
onEndDrag?.invoke()
|
|
640
|
+
if (pagingEnabled) snapToPage(scroller)
|
|
641
|
+
if (moved && !momentumActive) {
|
|
642
|
+
momentumActive = true
|
|
643
|
+
lastX = scroller.scrollX
|
|
644
|
+
lastY = scroller.scrollY
|
|
645
|
+
onMomentumBegin?.invoke()
|
|
646
|
+
scroller.postDelayed({ pollMomentum() }, 16L)
|
|
647
|
+
}
|
|
648
|
+
}
|
|
312
649
|
}
|
|
313
650
|
false
|
|
314
651
|
}
|
|
@@ -320,7 +657,54 @@ private class CraftNativeScrollView(context: Context) : FrameLayout(context) {
|
|
|
320
657
|
|
|
321
658
|
private class CraftNativeFlexSpacer(context: Context) : View(context)
|
|
322
659
|
|
|
323
|
-
|
|
660
|
+
private class CraftNativeModalView(context: Context) : FrameLayout(context) {
|
|
661
|
+
val content = CraftNativeFlexLayout(context)
|
|
662
|
+
private val blocker = View(context)
|
|
663
|
+
private var renderedVisible = false
|
|
664
|
+
var onRequestClose: (() -> Unit)? = null
|
|
665
|
+
|
|
666
|
+
var transparent: Boolean = false
|
|
667
|
+
set(value) {
|
|
668
|
+
field = value
|
|
669
|
+
blocker.setBackgroundColor(if (value) Color.TRANSPARENT else Color.argb(82, 0, 0, 0))
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
init {
|
|
673
|
+
isClickable = true
|
|
674
|
+
blocker.isClickable = true
|
|
675
|
+
blocker.setOnTouchListener { _, _ -> true }
|
|
676
|
+
blocker.setBackgroundColor(Color.argb(82, 0, 0, 0))
|
|
677
|
+
addView(blocker, LayoutParams(-1, -1))
|
|
678
|
+
addView(content, LayoutParams(-1, -1))
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
fun setVisible(visible: Boolean, opacity: Float, animationType: String?) {
|
|
682
|
+
if (renderedVisible == visible && visibility == if (visible) View.VISIBLE else View.GONE) return
|
|
683
|
+
renderedVisible = visible
|
|
684
|
+
if (!visible) {
|
|
685
|
+
animate().cancel()
|
|
686
|
+
alpha = 0f
|
|
687
|
+
visibility = View.GONE
|
|
688
|
+
return
|
|
689
|
+
}
|
|
690
|
+
visibility = View.VISIBLE
|
|
691
|
+
if (animationType == "none") {
|
|
692
|
+
alpha = opacity
|
|
693
|
+
} else {
|
|
694
|
+
alpha = 0f
|
|
695
|
+
animate().alpha(opacity).setDuration(if (animationType == "slide") 240L else 180L).start()
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
private data class LayoutBounds(
|
|
701
|
+
val x: Double,
|
|
702
|
+
val y: Double,
|
|
703
|
+
val width: Double,
|
|
704
|
+
val height: Double,
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
/** WebView-free host for a route bundle compiled by `stx native compile`. */
|
|
324
708
|
class MainActivity : AppCompatActivity() {
|
|
325
709
|
private class Screen(
|
|
326
710
|
val route: String?,
|
|
@@ -330,14 +714,34 @@ class MainActivity : AppCompatActivity() {
|
|
|
330
714
|
val port: MessagePort?
|
|
331
715
|
) {
|
|
332
716
|
val drafts = mutableMapOf<String, String>()
|
|
717
|
+
val inputIdentities = mutableMapOf<EditText, String>()
|
|
718
|
+
val suppressedTextChanges = mutableSetOf<EditText>()
|
|
333
719
|
val controls = mutableMapOf<String, View>()
|
|
334
720
|
val handlers = mutableMapOf<View, String>()
|
|
721
|
+
val pressInHandlers = mutableMapOf<View, String>()
|
|
722
|
+
val pressOutHandlers = mutableMapOf<View, String>()
|
|
723
|
+
val longPressHandlers = mutableMapOf<View, String>()
|
|
335
724
|
val scrollHandlers = mutableMapOf<View, String>()
|
|
336
725
|
val scrollBeginHandlers = mutableMapOf<View, String>()
|
|
337
726
|
val scrollEndHandlers = mutableMapOf<View, String>()
|
|
727
|
+
val scrollMomentumBeginHandlers = mutableMapOf<View, String>()
|
|
728
|
+
val scrollMomentumEndHandlers = mutableMapOf<View, String>()
|
|
729
|
+
val sliderCompleteHandlers = mutableMapOf<View, String>()
|
|
730
|
+
val sliderSteps = mutableMapOf<View, Double>()
|
|
338
731
|
val focusHandlers = mutableMapOf<View, String>()
|
|
339
732
|
val blurHandlers = mutableMapOf<View, String>()
|
|
733
|
+
val endEditingHandlers = mutableMapOf<View, String>()
|
|
340
734
|
val submitHandlers = mutableMapOf<View, String>()
|
|
735
|
+
val imageLoadStartHandlers = mutableMapOf<ImageView, String>()
|
|
736
|
+
val imageLoadHandlers = mutableMapOf<ImageView, String>()
|
|
737
|
+
val imageLoadEndHandlers = mutableMapOf<ImageView, String>()
|
|
738
|
+
val imageErrorHandlers = mutableMapOf<ImageView, String>()
|
|
739
|
+
val pressActiveOpacities = mutableMapOf<View, Float>()
|
|
740
|
+
val pressBaseOpacities = mutableMapOf<View, Float>()
|
|
741
|
+
val modalVisibility = mutableMapOf<View, Boolean>()
|
|
742
|
+
val modalCloseHandlers = mutableMapOf<View, String>()
|
|
743
|
+
val layoutHandlers = mutableMapOf<View, String>()
|
|
744
|
+
val lastLayoutBounds = mutableMapOf<View, LayoutBounds>()
|
|
341
745
|
val flatListRows = mutableMapOf<CraftNativeFlatList, MutableMap<String, MutableMap<String, View>>>()
|
|
342
746
|
val flatListOwners = mutableMapOf<String, String>()
|
|
343
747
|
val flatListOwnerByView = mutableMapOf<CraftNativeFlatList, String>()
|
|
@@ -374,14 +778,26 @@ class MainActivity : AppCompatActivity() {
|
|
|
374
778
|
private val imageSources = mutableMapOf<ImageView, String>()
|
|
375
779
|
private val imageJobs = mutableMapOf<ImageView, Future<*>>()
|
|
376
780
|
private val imageErrors = mutableMapOf<ImageView, String>()
|
|
781
|
+
private val inputHintDefaults = mutableMapOf<EditText, ColorStateList>()
|
|
782
|
+
private val inputHighlightDefaults = mutableMapOf<EditText, Int>()
|
|
783
|
+
private val defaultBackgrounds = mutableMapOf<View, Drawable?>()
|
|
377
784
|
|
|
378
785
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
786
|
+
applyConfiguredAppearance()
|
|
379
787
|
super.onCreate(savedInstanceState)
|
|
380
788
|
initialDeepLink = savedInstanceState?.getString("craft.initialDeepLink")?.let(Uri::parse) ?: intent?.data
|
|
381
789
|
initialDeepLinkClaimed = savedInstanceState?.getBoolean("craft.initialDeepLinkClaimed", false) ?: false
|
|
382
790
|
deliveredInitialDeepLink = savedInstanceState?.getBoolean("craft.deliveredInitialDeepLink", false) ?: false
|
|
383
791
|
setContentView(R.layout.activity_main)
|
|
384
792
|
root = findViewById(R.id.native_root)
|
|
793
|
+
ViewCompat.setOnApplyWindowInsetsListener(root) { view, insets ->
|
|
794
|
+
val safe = insets.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout())
|
|
795
|
+
view.setPadding(safe.left, safe.top, safe.right, safe.bottom)
|
|
796
|
+
view.requestLayout()
|
|
797
|
+
view.post { screens.lastOrNull()?.takeIf { it.active }?.let(::emitLayoutEvents) }
|
|
798
|
+
insets
|
|
799
|
+
}
|
|
800
|
+
ViewCompat.requestApplyInsets(root)
|
|
385
801
|
config = runCatching {
|
|
386
802
|
assets.open("craft.config.json").bufferedReader().use {
|
|
387
803
|
JSONObject(it.readText())
|
|
@@ -390,9 +806,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
390
806
|
hapticsEnabled = config.optBoolean("enableHaptics", false)
|
|
391
807
|
clipboardEnabled = config.optBoolean("enableClipboard", false)
|
|
392
808
|
capabilities = CraftNativeCapabilities(this, config)
|
|
393
|
-
|
|
394
|
-
root.setBackgroundColor(background)
|
|
395
|
-
defaultTextColor = if (Color.luminance(background) < 0.5) Color.WHITE else Color.BLACK
|
|
809
|
+
refreshThemeDefaults()
|
|
396
810
|
bundle = runCatching {
|
|
397
811
|
assets.open("native-screen.js").bufferedReader().use { it.readText() }
|
|
398
812
|
}.getOrElse {
|
|
@@ -433,6 +847,23 @@ class MainActivity : AppCompatActivity() {
|
|
|
433
847
|
}
|
|
434
848
|
}
|
|
435
849
|
|
|
850
|
+
private fun applyConfiguredAppearance() {
|
|
851
|
+
val configured = runCatching {
|
|
852
|
+
assets.open("craft.config.json").bufferedReader().use { JSONObject(it.readText()) }
|
|
853
|
+
}.getOrNull()
|
|
854
|
+
val mode = when (configured?.optString("appearance")) {
|
|
855
|
+
"light" -> AppCompatDelegate.MODE_NIGHT_NO
|
|
856
|
+
"dark" -> AppCompatDelegate.MODE_NIGHT_YES
|
|
857
|
+
"system" -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
|
858
|
+
else -> if (configured?.optBoolean("darkMode", true) == false) {
|
|
859
|
+
AppCompatDelegate.MODE_NIGHT_NO
|
|
860
|
+
} else {
|
|
861
|
+
AppCompatDelegate.MODE_NIGHT_YES
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
AppCompatDelegate.setDefaultNightMode(mode)
|
|
865
|
+
}
|
|
866
|
+
|
|
436
867
|
private fun attachSandbox(engine: JavaScriptSandbox) {
|
|
437
868
|
useMessagePorts = engine.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_MESSAGE_PORTS)
|
|
438
869
|
&& engine.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN)
|
|
@@ -470,6 +901,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
470
901
|
null
|
|
471
902
|
}
|
|
472
903
|
val activeScreen = Screen(route, params, view, isolate, port)
|
|
904
|
+
activeScreen.view.onLayoutChanged = {
|
|
905
|
+
if (activeScreen.active) emitLayoutEvents(activeScreen)
|
|
906
|
+
}
|
|
473
907
|
screen = activeScreen
|
|
474
908
|
screens.add(activeScreen)
|
|
475
909
|
show(activeScreen)
|
|
@@ -539,6 +973,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
539
973
|
root.removeAllViews()
|
|
540
974
|
(screen.view.parent as? ViewGroup)?.removeView(screen.view)
|
|
541
975
|
root.addView(screen.view)
|
|
976
|
+
refreshScreenTheme(screen)
|
|
542
977
|
}
|
|
543
978
|
|
|
544
979
|
override fun onSaveInstanceState(outState: Bundle) {
|
|
@@ -548,6 +983,43 @@ class MainActivity : AppCompatActivity() {
|
|
|
548
983
|
super.onSaveInstanceState(outState)
|
|
549
984
|
}
|
|
550
985
|
|
|
986
|
+
override fun onConfigurationChanged(newConfig: Configuration) {
|
|
987
|
+
super.onConfigurationChanged(newConfig)
|
|
988
|
+
if (!::root.isInitialized) return
|
|
989
|
+
refreshThemeDefaults()
|
|
990
|
+
ViewCompat.requestApplyInsets(root)
|
|
991
|
+
refreshInputDefaultsForTraitChange()
|
|
992
|
+
screens.lastOrNull()?.let { screen ->
|
|
993
|
+
refreshScreenTheme(screen)
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
private fun refreshScreenTheme(screen: Screen) {
|
|
998
|
+
screen.mutations.node("root")?.let { renderCommitted(screen, it) }
|
|
999
|
+
screen.controls.values.mapNotNull {
|
|
1000
|
+
when (it) {
|
|
1001
|
+
is CraftNativeRefreshableFlatList -> it.list
|
|
1002
|
+
is CraftNativeFlatList -> it
|
|
1003
|
+
else -> null
|
|
1004
|
+
}
|
|
1005
|
+
}.forEach { it.refreshThemeDefaults() }
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
private fun refreshThemeDefaults() {
|
|
1009
|
+
val background = ContextCompat.getColor(this, R.color.background)
|
|
1010
|
+
root.setBackgroundColor(background)
|
|
1011
|
+
defaultTextColor = if (Color.luminance(background) < 0.5) Color.WHITE else Color.BLACK
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
private fun refreshInputDefaultsForTraitChange() {
|
|
1015
|
+
if (inputHintDefaults.isEmpty() && inputHighlightDefaults.isEmpty()) return
|
|
1016
|
+
val probe = EditText(this)
|
|
1017
|
+
val hintColors = probe.hintTextColors
|
|
1018
|
+
val highlightColor = probe.highlightColor
|
|
1019
|
+
inputHintDefaults.keys.toList().forEach { inputHintDefaults[it] = hintColors }
|
|
1020
|
+
inputHighlightDefaults.keys.toList().forEach { inputHighlightDefaults[it] = highlightColor }
|
|
1021
|
+
}
|
|
1022
|
+
|
|
551
1023
|
private fun receive(screen: Screen, raw: String) {
|
|
552
1024
|
val message = runCatching { JSONObject(raw) }.getOrNull() ?: return
|
|
553
1025
|
val payload = message.optJSONObject("payload") ?: JSONObject()
|
|
@@ -679,12 +1151,38 @@ class MainActivity : AppCompatActivity() {
|
|
|
679
1151
|
?: explicitKey?.let { "${path.substringBeforeLast('.', "")}/$it" }
|
|
680
1152
|
?: path
|
|
681
1153
|
val previous = previousControls[identity]
|
|
1154
|
+
val reusableInput = previous as? EditText
|
|
682
1155
|
val control = when (type) {
|
|
683
1156
|
"Text" -> ((previous as? TextView)?.takeUnless { it is Button || it is EditText } ?: TextView(this)).apply {
|
|
1157
|
+
maxLines = props.optInt("numberOfLines", 0).takeIf { it > 0 } ?: Int.MAX_VALUE
|
|
1158
|
+
ellipsize = when (props.optString("ellipsizeMode")) {
|
|
1159
|
+
"head" -> TextUtils.TruncateAt.START
|
|
1160
|
+
"middle" -> TextUtils.TruncateAt.MIDDLE
|
|
1161
|
+
"tail" -> TextUtils.TruncateAt.END
|
|
1162
|
+
else -> null
|
|
1163
|
+
}
|
|
684
1164
|
configureText(this, childText(children), style)
|
|
685
1165
|
}
|
|
1166
|
+
"Link" -> ((previous as? TextView)?.takeUnless { it is Button } ?: TextView(this)).apply {
|
|
1167
|
+
configureText(this, props.optString("title").ifBlank { childText(children) }, style)
|
|
1168
|
+
val linkColor = props.optString("color").takeIf { it.isNotBlank() } ?: style.optString("color")
|
|
1169
|
+
setTextColor(color(linkColor, Color.rgb(33, 150, 243)))
|
|
1170
|
+
paintFlags = paintFlags or Paint.UNDERLINE_TEXT_FLAG
|
|
1171
|
+
isEnabled = !props.optBoolean("disabled", false)
|
|
1172
|
+
isClickable = true
|
|
1173
|
+
isFocusable = true
|
|
1174
|
+
val handler = events.optString("onPress").ifBlank { events.optString("onClick") }
|
|
1175
|
+
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
1176
|
+
setOnClickListener {
|
|
1177
|
+
val current = screen.handlers[this] ?: return@setOnClickListener
|
|
1178
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
686
1181
|
"Button" -> (previous as? Button ?: Button(this)).apply {
|
|
687
|
-
|
|
1182
|
+
val title = props.optString("title").ifBlank { childText(children) }
|
|
1183
|
+
configureText(this, title, style)
|
|
1184
|
+
props.optString("color").takeIf { it.isNotBlank() }?.let { setTextColor(color(it, defaultTextColor)) }
|
|
1185
|
+
isEnabled = !props.optBoolean("disabled", false)
|
|
688
1186
|
val handler = events.optString("onPress").ifBlank { events.optString("onClick") }
|
|
689
1187
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
690
1188
|
setOnClickListener {
|
|
@@ -692,26 +1190,62 @@ class MainActivity : AppCompatActivity() {
|
|
|
692
1190
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
693
1191
|
}
|
|
694
1192
|
}
|
|
695
|
-
"TextInput" -> (
|
|
1193
|
+
"TextInput" -> (reusableInput?.takeIf { it.isSingleLine == !props.optBoolean("multiline", false) }
|
|
1194
|
+
?: EditText(this)).apply {
|
|
1195
|
+
val defaultHintColors = inputHintDefaults.getOrPut(this) {
|
|
1196
|
+
hintTextColors ?: ColorStateList.valueOf(Color.GRAY)
|
|
1197
|
+
}
|
|
1198
|
+
val defaultHighlightColor = inputHighlightDefaults.getOrPut(this) { highlightColor }
|
|
696
1199
|
hint = props.optString("placeholder")
|
|
1200
|
+
setHintTextColor(
|
|
1201
|
+
props.optString("placeholderTextColor").takeIf { it.isNotBlank() }
|
|
1202
|
+
?.let { ColorStateList.valueOf(color(it, Color.GRAY)) }
|
|
1203
|
+
?: defaultHintColors,
|
|
1204
|
+
)
|
|
1205
|
+
highlightColor = props.optString("selectionColor").takeIf { it.isNotBlank() }
|
|
1206
|
+
?.let { color(it, defaultHighlightColor) }
|
|
1207
|
+
?: defaultHighlightColor
|
|
1208
|
+
val maxLength = props.optInt("maxLength", 0)
|
|
1209
|
+
filters = if (maxLength > 0) arrayOf(InputFilter.LengthFilter(maxLength)) else emptyArray()
|
|
697
1210
|
inputType = inputTypeFor(props)
|
|
698
1211
|
imeOptions = imeOptionsFor(props)
|
|
699
|
-
|
|
1212
|
+
val multiline = props.optBoolean("multiline", false)
|
|
1213
|
+
isSingleLine = !multiline
|
|
1214
|
+
maxLines = if (multiline) props.optInt("numberOfLines", Int.MAX_VALUE).coerceAtLeast(1) else 1
|
|
700
1215
|
isEnabled = props.optBoolean("editable", true)
|
|
701
|
-
|
|
1216
|
+
configureTextStyle(this, style)
|
|
1217
|
+
val controlledValue = props.opt("value")?.takeUnless { it == JSONObject.NULL }?.toString()
|
|
1218
|
+
if (controlledValue != null && text.toString() != controlledValue) {
|
|
1219
|
+
screen.suppressedTextChanges.add(this)
|
|
1220
|
+
setTextPreservingSelection(this, controlledValue)
|
|
1221
|
+
}
|
|
702
1222
|
else if (this !== previous && screen.drafts.containsKey(identity)) setText(screen.drafts[identity])
|
|
1223
|
+
else if (this !== previous && props.has("defaultValue") && !props.isNull("defaultValue")) setText(props.optString("defaultValue"))
|
|
1224
|
+
screen.inputIdentities[this] = identity
|
|
703
1225
|
val handler = events.optString("onChange").ifBlank { events.optString("onChangeText") }
|
|
704
1226
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
705
1227
|
val onFocus = events.optString("onFocus")
|
|
706
1228
|
if (onFocus.isNotBlank()) screen.focusHandlers[this] = onFocus else screen.focusHandlers.remove(this)
|
|
707
|
-
val onBlur = events.optString("onBlur")
|
|
1229
|
+
val onBlur = events.optString("onBlur")
|
|
708
1230
|
if (onBlur.isNotBlank()) screen.blurHandlers[this] = onBlur else screen.blurHandlers.remove(this)
|
|
1231
|
+
val onEndEditing = events.optString("onEndEditing")
|
|
1232
|
+
if (onEndEditing.isNotBlank()) screen.endEditingHandlers[this] = onEndEditing else screen.endEditingHandlers.remove(this)
|
|
709
1233
|
val onSubmit = events.optString("onSubmitEditing")
|
|
710
1234
|
if (onSubmit.isNotBlank()) screen.submitHandlers[this] = onSubmit else screen.submitHandlers.remove(this)
|
|
711
1235
|
setOnFocusChangeListener { view, focused ->
|
|
712
|
-
val
|
|
713
|
-
if (
|
|
714
|
-
|
|
1236
|
+
val nativeEvent = JSONObject().put("text", text.toString())
|
|
1237
|
+
if (focused) {
|
|
1238
|
+
screen.focusHandlers[view]?.let { current ->
|
|
1239
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", nativeEvent))
|
|
1240
|
+
}
|
|
1241
|
+
} else {
|
|
1242
|
+
screen.blurHandlers[view]?.let { current ->
|
|
1243
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", nativeEvent))
|
|
1244
|
+
}
|
|
1245
|
+
screen.endEditingHandlers[view]?.let { current ->
|
|
1246
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", nativeEvent))
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
715
1249
|
}
|
|
716
1250
|
setOnEditorActionListener { view, actionId, event ->
|
|
717
1251
|
val submitted = actionId != EditorInfo.IME_ACTION_NONE || event?.keyCode == KeyEvent.KEYCODE_ENTER
|
|
@@ -720,16 +1254,19 @@ class MainActivity : AppCompatActivity() {
|
|
|
720
1254
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject().put("text", text.toString())))
|
|
721
1255
|
true
|
|
722
1256
|
}
|
|
723
|
-
if (props.optBoolean("autoFocus", false)) post {
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
1257
|
+
if (props.optBoolean("autoFocus", false) && !isFocused) post {
|
|
1258
|
+
if (!isFocused) {
|
|
1259
|
+
requestFocus()
|
|
1260
|
+
(getSystemService(INPUT_METHOD_SERVICE) as? android.view.inputmethod.InputMethodManager)
|
|
1261
|
+
?.showSoftInput(this, android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT)
|
|
1262
|
+
}
|
|
727
1263
|
}
|
|
728
1264
|
if (this !== previous) addTextChangedListener(object : TextWatcher {
|
|
729
1265
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
|
730
1266
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
|
731
1267
|
val updated = s?.toString().orEmpty()
|
|
732
1268
|
screen.drafts[identity] = updated
|
|
1269
|
+
if (screen.suppressedTextChanges.remove(this@apply)) return
|
|
733
1270
|
val current = screen.handlers[this@apply] ?: return
|
|
734
1271
|
send(screen, "EVENT", JSONObject()
|
|
735
1272
|
.put("handlerName", current)
|
|
@@ -738,6 +1275,150 @@ class MainActivity : AppCompatActivity() {
|
|
|
738
1275
|
override fun afterTextChanged(s: Editable?) {}
|
|
739
1276
|
})
|
|
740
1277
|
}
|
|
1278
|
+
"Picker" -> (previous as? Spinner ?: Spinner(this)).apply {
|
|
1279
|
+
val options = (0 until children.length()).mapNotNull { index ->
|
|
1280
|
+
val option = children.optJSONObject(index) ?: return@mapNotNull null
|
|
1281
|
+
if (option.optString("type") != "Text") return@mapNotNull null
|
|
1282
|
+
val label = (option.optJSONArray("children") ?: JSONArray()).let { text ->
|
|
1283
|
+
(0 until text.length()).joinToString("") { text.optString(it) }
|
|
1284
|
+
}
|
|
1285
|
+
if (label.isBlank()) return@mapNotNull null
|
|
1286
|
+
val optionProps = option.optJSONObject("props")
|
|
1287
|
+
val value = optionProps?.optString("value").takeIf { !it.isNullOrBlank() } ?: label
|
|
1288
|
+
value to label
|
|
1289
|
+
}
|
|
1290
|
+
adapter = ArrayAdapter(this@MainActivity, android.R.layout.simple_spinner_item, options.map { it.second }).also {
|
|
1291
|
+
it.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
|
1292
|
+
}
|
|
1293
|
+
val requested = props.optString("selectedValue").ifBlank { props.optString("value") }
|
|
1294
|
+
val selected = options.indexOfFirst { it.first == requested }.takeIf { it >= 0 } ?: 0
|
|
1295
|
+
isEnabled = !props.optBoolean("disabled", false)
|
|
1296
|
+
val handler = events.optString("onValueChange").ifBlank { events.optString("onChange") }
|
|
1297
|
+
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
1298
|
+
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
|
1299
|
+
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
|
|
1300
|
+
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
|
1301
|
+
val current = screen.handlers[this@apply] ?: return
|
|
1302
|
+
val option = options.getOrNull(position) ?: return
|
|
1303
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()
|
|
1304
|
+
.put("value", option.first).put("index", position)))
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
setSelection(selected, false)
|
|
1308
|
+
}
|
|
1309
|
+
"Modal" -> (previous as? CraftNativeModalView ?: CraftNativeModalView(this)).apply {
|
|
1310
|
+
transparent = props.optBoolean("transparent", false)
|
|
1311
|
+
content.configure(style, resources.displayMetrics.density)
|
|
1312
|
+
reconcileChildren(screen, content, children, path, next, style, previousControls)
|
|
1313
|
+
val visible = props.optBoolean("visible", false)
|
|
1314
|
+
val wasVisible = screen.modalVisibility[this]
|
|
1315
|
+
screen.modalVisibility[this] = visible
|
|
1316
|
+
val requestClose = events.optString("onRequestClose")
|
|
1317
|
+
if (requestClose.isNotBlank()) screen.modalCloseHandlers[this] = requestClose else screen.modalCloseHandlers.remove(this)
|
|
1318
|
+
setVisible(visible, style.optDouble("opacity", 1.0).toFloat(), props.optString("animationType").ifBlank { null })
|
|
1319
|
+
if (wasVisible != visible && (visible || wasVisible == true)) {
|
|
1320
|
+
val lifecycleHandler = if (visible) events.optString("onShow") else events.optString("onDismiss")
|
|
1321
|
+
if (lifecycleHandler.isNotBlank()) {
|
|
1322
|
+
send(screen, "EVENT", JSONObject().put("handlerName", lifecycleHandler).put("nativeEvent", JSONObject()))
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
"TouchableOpacity", "TouchableHighlight", "Pressable" -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
|
|
1327
|
+
configureContainer(this, style)
|
|
1328
|
+
screen.pressActiveOpacities[this] = props.optDouble("activeOpacity", 0.5).toFloat()
|
|
1329
|
+
screen.pressBaseOpacities[this] = style.optDouble("opacity", 1.0).toFloat()
|
|
1330
|
+
reconcileChildren(screen, this, children, path, next, style, previousControls)
|
|
1331
|
+
}
|
|
1332
|
+
"Switch" -> (previous as? Switch ?: Switch(this)).apply {
|
|
1333
|
+
setOnCheckedChangeListener(null)
|
|
1334
|
+
val requestedValue = props.opt("value")
|
|
1335
|
+
isChecked = if (requestedValue != null && requestedValue != JSONObject.NULL) {
|
|
1336
|
+
props.optBoolean("value")
|
|
1337
|
+
} else {
|
|
1338
|
+
props.optBoolean("checked", false)
|
|
1339
|
+
}
|
|
1340
|
+
isEnabled = !props.optBoolean("disabled", false)
|
|
1341
|
+
props.optJSONObject("trackColor")?.let { track ->
|
|
1342
|
+
val falseColor = color(track.optString("false"), Color.GRAY)
|
|
1343
|
+
val trueColor = color(track.optString("true"), falseColor)
|
|
1344
|
+
trackTintList = ColorStateList(
|
|
1345
|
+
arrayOf(intArrayOf(android.R.attr.state_checked), intArrayOf()),
|
|
1346
|
+
intArrayOf(trueColor, falseColor),
|
|
1347
|
+
)
|
|
1348
|
+
} ?: run { trackTintList = null }
|
|
1349
|
+
thumbTintList = props.optString("thumbColor").takeIf { it.isNotBlank() }
|
|
1350
|
+
?.let { ColorStateList.valueOf(color(it, Color.WHITE)) }
|
|
1351
|
+
val handler = events.optString("onValueChange").ifBlank { events.optString("onChange") }
|
|
1352
|
+
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
1353
|
+
setOnCheckedChangeListener { _, checked ->
|
|
1354
|
+
val current = screen.handlers[this] ?: return@setOnCheckedChangeListener
|
|
1355
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject().put("value", checked)))
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
"Slider" -> (previous as? SeekBar ?: SeekBar(this)).apply {
|
|
1359
|
+
val minimum = props.optDouble("minimumValue", 0.0)
|
|
1360
|
+
val maximumCandidate = props.optDouble("maximumValue", 1.0)
|
|
1361
|
+
val maximum = if (maximumCandidate > minimum) maximumCandidate else minimum + Double.MIN_VALUE
|
|
1362
|
+
max = 1000
|
|
1363
|
+
setOnSeekBarChangeListener(null)
|
|
1364
|
+
val step = props.optDouble("step", 0.0).takeIf { it > 0 }
|
|
1365
|
+
val requested = props.optDouble("value", minimum)
|
|
1366
|
+
val value = if (step != null) minimum + ((requested - minimum) / step).roundToInt() * step else requested
|
|
1367
|
+
progress = (((value - minimum) / (maximum - minimum)) * max).roundToInt().coerceIn(0, max)
|
|
1368
|
+
if (step != null) screen.sliderSteps[this] = step else screen.sliderSteps.remove(this)
|
|
1369
|
+
progressTintList = props.optString("minimumTrackTintColor").takeIf { it.isNotBlank() }
|
|
1370
|
+
?.let { ColorStateList.valueOf(color(it, Color.WHITE)) }
|
|
1371
|
+
progressBackgroundTintList = props.optString("maximumTrackTintColor").takeIf { it.isNotBlank() }
|
|
1372
|
+
?.let { ColorStateList.valueOf(color(it, Color.GRAY)) }
|
|
1373
|
+
thumbTintList = props.optString("thumbTintColor").takeIf { it.isNotBlank() }
|
|
1374
|
+
?.let { ColorStateList.valueOf(color(it, Color.WHITE)) }
|
|
1375
|
+
isEnabled = !props.optBoolean("disabled", false)
|
|
1376
|
+
val handler = events.optString("onValueChange").ifBlank { events.optString("onChange") }
|
|
1377
|
+
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
1378
|
+
val complete = events.optString("onSlidingComplete")
|
|
1379
|
+
if (complete.isNotBlank()) screen.sliderCompleteHandlers[this] = complete else screen.sliderCompleteHandlers.remove(this)
|
|
1380
|
+
setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
|
1381
|
+
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
|
1382
|
+
val current = screen.handlers[seekBar] ?: return
|
|
1383
|
+
val raw = minimum + (maximum - minimum) * progress.toDouble() / max.toDouble()
|
|
1384
|
+
val step = screen.sliderSteps[seekBar]
|
|
1385
|
+
val value = if (step != null) minimum + ((raw - minimum) / step).roundToInt() * step else raw
|
|
1386
|
+
if (step != null) {
|
|
1387
|
+
val snappedProgress = (((value - minimum) / (maximum - minimum)) * max)
|
|
1388
|
+
.roundToInt().coerceIn(0, max)
|
|
1389
|
+
if (seekBar.progress != snappedProgress) {
|
|
1390
|
+
seekBar.progress = snappedProgress
|
|
1391
|
+
return
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject().put("value", value)))
|
|
1395
|
+
}
|
|
1396
|
+
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
|
1397
|
+
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
|
1398
|
+
val current = screen.sliderCompleteHandlers[seekBar] ?: return
|
|
1399
|
+
val raw = minimum + (maximum - minimum) * seekBar.progress.toDouble() / max.toDouble()
|
|
1400
|
+
val step = screen.sliderSteps[seekBar]
|
|
1401
|
+
val value = if (step != null) minimum + ((raw - minimum) / step).roundToInt() * step else raw
|
|
1402
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject().put("value", value)))
|
|
1403
|
+
}
|
|
1404
|
+
})
|
|
1405
|
+
}
|
|
1406
|
+
"ActivityIndicator" -> (previous as? android.widget.ProgressBar ?: android.widget.ProgressBar(this)).apply {
|
|
1407
|
+
val requestedSize = props.opt("size")
|
|
1408
|
+
val scale = when (requestedSize) {
|
|
1409
|
+
"large" -> 1.35f
|
|
1410
|
+
"small" -> 0.75f
|
|
1411
|
+
is Number -> (requestedSize.toDouble() / 24.0).toFloat().coerceAtLeast(0.5f)
|
|
1412
|
+
else -> 1f
|
|
1413
|
+
}
|
|
1414
|
+
scaleX = scale
|
|
1415
|
+
scaleY = scale
|
|
1416
|
+
val animating = props.optBoolean("animating", true)
|
|
1417
|
+
isIndeterminate = animating
|
|
1418
|
+
visibility = if (!animating && props.optBoolean("hidesWhenStopped", true)) View.GONE else View.VISIBLE
|
|
1419
|
+
indeterminateTintList = props.optString("color").takeIf { it.isNotBlank() }
|
|
1420
|
+
?.let { ColorStateList.valueOf(color(it, Color.WHITE)) }
|
|
1421
|
+
}
|
|
741
1422
|
"Image" -> (previous as? ImageView ?: ImageView(this)).apply {
|
|
742
1423
|
scaleType = when (style.optString("resizeMode").ifBlank { props.optString("resizeMode") }) {
|
|
743
1424
|
"cover" -> ImageView.ScaleType.CENTER_CROP
|
|
@@ -746,10 +1427,18 @@ class MainActivity : AppCompatActivity() {
|
|
|
746
1427
|
else -> ImageView.ScaleType.CENTER_INSIDE
|
|
747
1428
|
}
|
|
748
1429
|
adjustViewBounds = false
|
|
749
|
-
|
|
1430
|
+
val onLoadStart = events.optString("onLoadStart")
|
|
1431
|
+
if (onLoadStart.isNotBlank()) screen.imageLoadStartHandlers[this] = onLoadStart else screen.imageLoadStartHandlers.remove(this)
|
|
1432
|
+
val onLoad = events.optString("onLoad")
|
|
1433
|
+
if (onLoad.isNotBlank()) screen.imageLoadHandlers[this] = onLoad else screen.imageLoadHandlers.remove(this)
|
|
1434
|
+
val onLoadEnd = events.optString("onLoadEnd")
|
|
1435
|
+
if (onLoadEnd.isNotBlank()) screen.imageLoadEndHandlers[this] = onLoadEnd else screen.imageLoadEndHandlers.remove(this)
|
|
1436
|
+
val onError = events.optString("onError")
|
|
1437
|
+
if (onError.isNotBlank()) screen.imageErrorHandlers[this] = onError else screen.imageErrorHandlers.remove(this)
|
|
1438
|
+
updateImage(screen, this, props.opt("source"))
|
|
750
1439
|
}
|
|
751
1440
|
"ScrollView" -> (previous as? CraftNativeScrollView ?: CraftNativeScrollView(this)).apply {
|
|
752
|
-
val horizontal = props.optBoolean("horizontal", false) || style.optString("flexDirection")
|
|
1441
|
+
val horizontal = props.optBoolean("horizontal", false) || style.optString("flexDirection") in setOf("row", "row-reverse")
|
|
753
1442
|
setHorizontal(horizontal)
|
|
754
1443
|
setNativeNestedScrollingEnabled(true)
|
|
755
1444
|
setScrollIndicators(
|
|
@@ -757,12 +1446,23 @@ class MainActivity : AppCompatActivity() {
|
|
|
757
1446
|
props.optBoolean("showsHorizontalScrollIndicator", true),
|
|
758
1447
|
)
|
|
759
1448
|
setScrollEnabled(props.optBoolean("scrollEnabled", true))
|
|
1449
|
+
setKeyboardShouldPersistTaps(props.optString("keyboardShouldPersistTaps", "never"))
|
|
1450
|
+
setBounces(props.optBoolean("bounces", true))
|
|
1451
|
+
setAlwaysBounce(
|
|
1452
|
+
props.optBoolean("alwaysBounceVertical", !horizontal),
|
|
1453
|
+
props.optBoolean("alwaysBounceHorizontal", horizontal),
|
|
1454
|
+
)
|
|
1455
|
+
setPagingEnabled(props.optBoolean("pagingEnabled", false))
|
|
760
1456
|
val onScroll = events.optString("onScroll")
|
|
761
1457
|
if (onScroll.isNotBlank()) screen.scrollHandlers[this] = onScroll else screen.scrollHandlers.remove(this)
|
|
762
1458
|
val onBegin = events.optString("onScrollBeginDrag")
|
|
763
1459
|
if (onBegin.isNotBlank()) screen.scrollBeginHandlers[this] = onBegin else screen.scrollBeginHandlers.remove(this)
|
|
764
1460
|
val onEnd = events.optString("onScrollEndDrag")
|
|
765
1461
|
if (onEnd.isNotBlank()) screen.scrollEndHandlers[this] = onEnd else screen.scrollEndHandlers.remove(this)
|
|
1462
|
+
val onMomentumBegin = events.optString("onMomentumScrollBegin")
|
|
1463
|
+
if (onMomentumBegin.isNotBlank()) screen.scrollMomentumBeginHandlers[this] = onMomentumBegin else screen.scrollMomentumBeginHandlers.remove(this)
|
|
1464
|
+
val onMomentumEnd = events.optString("onMomentumScrollEnd")
|
|
1465
|
+
if (onMomentumEnd.isNotBlank()) screen.scrollMomentumEndHandlers[this] = onMomentumEnd else screen.scrollMomentumEndHandlers.remove(this)
|
|
766
1466
|
val nativeScroll = this
|
|
767
1467
|
setScrollListeners(
|
|
768
1468
|
onScroll = { scrollX, scrollY ->
|
|
@@ -784,40 +1484,141 @@ class MainActivity : AppCompatActivity() {
|
|
|
784
1484
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
785
1485
|
}
|
|
786
1486
|
},
|
|
1487
|
+
onMomentumBegin = {
|
|
1488
|
+
screen.scrollMomentumBeginHandlers[nativeScroll]?.let { current ->
|
|
1489
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
1490
|
+
}
|
|
1491
|
+
},
|
|
1492
|
+
onMomentumEnd = {
|
|
1493
|
+
screen.scrollMomentumEndHandlers[nativeScroll]?.let { current ->
|
|
1494
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
1495
|
+
}
|
|
1496
|
+
},
|
|
1497
|
+
keyboardDismissMode = props.optString("keyboardDismissMode"),
|
|
1498
|
+
dismissKeyboard = {
|
|
1499
|
+
(getSystemService(INPUT_METHOD_SERVICE) as? android.view.inputmethod.InputMethodManager)
|
|
1500
|
+
?.hideSoftInputFromWindow(nativeScroll.windowToken, 0)
|
|
1501
|
+
},
|
|
787
1502
|
)
|
|
788
|
-
|
|
789
|
-
|
|
1503
|
+
val contentStyle = JSONObject(style.toString()).apply {
|
|
1504
|
+
props.optJSONObject("contentContainerStyle")?.let { override ->
|
|
1505
|
+
override.keys().forEach { key -> put(key, override.get(key)) }
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
configureContainer(content, contentStyle)
|
|
1509
|
+
reconcileChildren(screen, content, children, path, next, contentStyle, previousControls)
|
|
790
1510
|
}
|
|
791
|
-
"FlatList" -> (previous as?
|
|
792
|
-
|
|
1511
|
+
"FlatList" -> (previous as? CraftNativeRefreshableFlatList ?: CraftNativeRefreshableFlatList(this)).apply {
|
|
1512
|
+
val nativeList = list
|
|
1513
|
+
configureRefresh(events.optString("onRefresh").takeIf { it.isNotBlank() }?.let { handler ->
|
|
1514
|
+
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject())) }
|
|
1515
|
+
}, props.optBoolean("refreshing", false))
|
|
1516
|
+
nativeList.setKeyboardShouldPersistTaps(props.optString("keyboardShouldPersistTaps", "never"))
|
|
1517
|
+
nativeList.setContentContainerStyle(props.optJSONObject("contentContainerStyle"), resources.displayMetrics.density)
|
|
1518
|
+
nativeList.setKeyboardDismissMode(props.optString("keyboardDismissMode")) {
|
|
1519
|
+
(getSystemService(INPUT_METHOD_SERVICE) as? android.view.inputmethod.InputMethodManager)
|
|
1520
|
+
?.hideSoftInputFromWindow(windowToken, 0)
|
|
1521
|
+
}
|
|
1522
|
+
nativeList.overScrollMode = if (props.optBoolean("bounces", true)) View.OVER_SCROLL_ALWAYS else View.OVER_SCROLL_NEVER
|
|
1523
|
+
reconcileFlatList(screen, nativeList, children, path, protocolId ?: path, props, events)
|
|
1524
|
+
}
|
|
1525
|
+
"View", "SafeAreaView" -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
|
|
1526
|
+
configureContainer(this, style)
|
|
1527
|
+
reconcileChildren(screen, this, children, path, next, style, previousControls)
|
|
793
1528
|
}
|
|
794
1529
|
else -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
|
|
795
1530
|
configureContainer(this, style)
|
|
796
1531
|
reconcileChildren(screen, this, children, path, next, style, previousControls)
|
|
797
1532
|
}
|
|
798
1533
|
}
|
|
799
|
-
|
|
1534
|
+
val pressInHandler = events.optString("onPressIn").takeIf { it.isNotBlank() }
|
|
1535
|
+
val pressOutHandler = events.optString("onPressOut").takeIf { it.isNotBlank() }
|
|
1536
|
+
if (pressInHandler != null) screen.pressInHandlers[control] = pressInHandler else screen.pressInHandlers.remove(control)
|
|
1537
|
+
if (pressOutHandler != null) screen.pressOutHandlers[control] = pressOutHandler else screen.pressOutHandlers.remove(control)
|
|
1538
|
+
if (type != "Button" && type != "Link" && type != "TextInput" && type != "Picker" && type != "Modal" && type != "Switch" && type != "Slider" && type != "ActivityIndicator") {
|
|
800
1539
|
val handler = events.optString("onPress").ifBlank { events.optString("onClick") }
|
|
801
|
-
|
|
802
|
-
|
|
1540
|
+
val disabled = props.optBoolean("disabled", false)
|
|
1541
|
+
val hasPressLifecycle = pressInHandler != null || pressOutHandler != null
|
|
1542
|
+
if ((handler.isNotBlank() || (type in setOf("TouchableOpacity", "TouchableHighlight", "Pressable") && hasPressLifecycle)) && !disabled) {
|
|
1543
|
+
if (handler.isNotBlank()) screen.handlers[control] = handler else screen.handlers.remove(control)
|
|
803
1544
|
control.isClickable = true
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
1545
|
+
if (type in setOf("TouchableOpacity", "TouchableHighlight", "Pressable")) {
|
|
1546
|
+
control.setOnClickListener(null)
|
|
1547
|
+
control.setOnTouchListener { view, event ->
|
|
1548
|
+
when (event.actionMasked) {
|
|
1549
|
+
android.view.MotionEvent.ACTION_DOWN -> {
|
|
1550
|
+
val active = screen.pressActiveOpacities[view] ?: 0.5f
|
|
1551
|
+
val base = screen.pressBaseOpacities[view] ?: 1f
|
|
1552
|
+
view.alpha = base * active
|
|
1553
|
+
screen.pressInHandlers[view]?.let { current ->
|
|
1554
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
1555
|
+
}
|
|
1556
|
+
true
|
|
1557
|
+
}
|
|
1558
|
+
android.view.MotionEvent.ACTION_UP, android.view.MotionEvent.ACTION_CANCEL -> {
|
|
1559
|
+
view.alpha = screen.pressBaseOpacities[view] ?: 1f
|
|
1560
|
+
screen.pressOutHandlers[view]?.let { current ->
|
|
1561
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
1562
|
+
}
|
|
1563
|
+
if (event.actionMasked == android.view.MotionEvent.ACTION_UP) {
|
|
1564
|
+
val current = screen.handlers[view] ?: return@setOnTouchListener true
|
|
1565
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
1566
|
+
}
|
|
1567
|
+
true
|
|
1568
|
+
}
|
|
1569
|
+
else -> true
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
} else {
|
|
1573
|
+
control.setOnTouchListener(null)
|
|
1574
|
+
control.setOnClickListener {
|
|
1575
|
+
val current = screen.handlers[control] ?: return@setOnClickListener
|
|
1576
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
1577
|
+
}
|
|
807
1578
|
}
|
|
808
1579
|
} else {
|
|
809
1580
|
screen.handlers.remove(control)
|
|
810
1581
|
control.isClickable = false
|
|
811
1582
|
control.setOnClickListener(null)
|
|
1583
|
+
control.setOnTouchListener(null)
|
|
812
1584
|
}
|
|
1585
|
+
control.isEnabled = !disabled
|
|
1586
|
+
}
|
|
1587
|
+
val longPressHandler = events.optString("onLongPress").takeUnless { props.optBoolean("disabled", false) } ?: ""
|
|
1588
|
+
if (longPressHandler.isNotBlank()) {
|
|
1589
|
+
screen.longPressHandlers[control] = longPressHandler
|
|
1590
|
+
control.setOnLongClickListener {
|
|
1591
|
+
val current = screen.longPressHandlers[control] ?: return@setOnLongClickListener false
|
|
1592
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
1593
|
+
true
|
|
1594
|
+
}
|
|
1595
|
+
} else {
|
|
1596
|
+
screen.longPressHandlers.remove(control)
|
|
1597
|
+
control.setOnLongClickListener(null)
|
|
813
1598
|
}
|
|
814
1599
|
applyAccessibility(control, props, type, explicitKey ?: protocolId ?: path)
|
|
815
|
-
|
|
1600
|
+
val layoutHandler = events.optString("onLayout")
|
|
1601
|
+
if (layoutHandler.isNotBlank()) {
|
|
1602
|
+
if (screen.layoutHandlers[control] != layoutHandler) screen.lastLayoutBounds.remove(control)
|
|
1603
|
+
screen.layoutHandlers[control] = layoutHandler
|
|
1604
|
+
} else {
|
|
1605
|
+
screen.layoutHandlers.remove(control)
|
|
1606
|
+
screen.lastLayoutBounds.remove(control)
|
|
1607
|
+
}
|
|
1608
|
+
if (control is ImageView) imageErrors[control]?.let { message ->
|
|
1609
|
+
val current = control.contentDescription?.toString().orEmpty()
|
|
1610
|
+
if (!current.contains(message)) {
|
|
1611
|
+
control.contentDescription = listOf(current, message).filter { it.isNotBlank() }.joinToString(", ")
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
816
1614
|
next[identity] = control
|
|
817
1615
|
applyViewStyle(control, style)
|
|
1616
|
+
if (type == "Modal" && !props.optBoolean("visible", false)) control.visibility = View.GONE
|
|
1617
|
+
val width = style.optDouble("width", Double.NaN).takeUnless { it.isNaN() }?.let(::dp)
|
|
1618
|
+
val height = style.optDouble("height", Double.NaN).takeUnless { it.isNaN() }?.let(::dp)
|
|
818
1619
|
control.layoutParams = LinearLayout.LayoutParams(
|
|
819
|
-
|
|
820
|
-
|
|
1620
|
+
width ?: ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
1621
|
+
height ?: ViewGroup.LayoutParams.WRAP_CONTENT
|
|
821
1622
|
).apply {
|
|
822
1623
|
val margin = style.optDouble("margin", 0.0)
|
|
823
1624
|
val horizontal = style.optDouble("marginHorizontal", margin)
|
|
@@ -843,6 +1644,17 @@ class MainActivity : AppCompatActivity() {
|
|
|
843
1644
|
layout.configure(style, resources.displayMetrics.density)
|
|
844
1645
|
}
|
|
845
1646
|
|
|
1647
|
+
private fun flatListScrollEvent(list: CraftNativeFlatList): JSONObject = JSONObject()
|
|
1648
|
+
.put("contentOffset", JSONObject()
|
|
1649
|
+
.put("x", list.computeHorizontalScrollOffset())
|
|
1650
|
+
.put("y", list.computeVerticalScrollOffset()))
|
|
1651
|
+
.put("contentSize", JSONObject()
|
|
1652
|
+
.put("width", list.computeHorizontalScrollRange())
|
|
1653
|
+
.put("height", list.computeVerticalScrollRange()))
|
|
1654
|
+
.put("layoutMeasurement", JSONObject()
|
|
1655
|
+
.put("width", list.width)
|
|
1656
|
+
.put("height", list.height))
|
|
1657
|
+
|
|
846
1658
|
private fun reconcileChildren(
|
|
847
1659
|
screen: Screen,
|
|
848
1660
|
layout: CraftNativeFlexLayout,
|
|
@@ -895,17 +1707,35 @@ class MainActivity : AppCompatActivity() {
|
|
|
895
1707
|
) {
|
|
896
1708
|
screen.flatListOwners.entries.removeAll { it.value == owner }
|
|
897
1709
|
screen.flatListOwnerByView[list] = owner
|
|
1710
|
+
list.onLayoutChanged = {
|
|
1711
|
+
if (screen.active) emitLayoutEvents(screen)
|
|
1712
|
+
}
|
|
898
1713
|
val nodes = (0 until children.length()).mapNotNull { children.optJSONObject(it) }
|
|
899
1714
|
nodes.forEach { registerFlatListOwnership(screen, it, owner) }
|
|
1715
|
+
list.onScrollEvent = events.optString("onScroll").takeIf { it.isNotBlank() }?.let { handler ->
|
|
1716
|
+
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", flatListScrollEvent(list))) }
|
|
1717
|
+
}
|
|
1718
|
+
list.onScrollBeginDrag = events.optString("onScrollBeginDrag").takeIf { it.isNotBlank() }?.let { handler ->
|
|
1719
|
+
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", flatListScrollEvent(list))) }
|
|
1720
|
+
}
|
|
1721
|
+
list.onScrollEndDrag = events.optString("onScrollEndDrag").takeIf { it.isNotBlank() }?.let { handler ->
|
|
1722
|
+
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", flatListScrollEvent(list))) }
|
|
1723
|
+
}
|
|
1724
|
+
list.onMomentumScrollBegin = events.optString("onMomentumScrollBegin").takeIf { it.isNotBlank() }?.let { handler ->
|
|
1725
|
+
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", flatListScrollEvent(list))) }
|
|
1726
|
+
}
|
|
1727
|
+
list.onMomentumScrollEnd = events.optString("onMomentumScrollEnd").takeIf { it.isNotBlank() }?.let { handler ->
|
|
1728
|
+
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", flatListScrollEvent(list))) }
|
|
1729
|
+
}
|
|
900
1730
|
list.apply(
|
|
901
1731
|
nodes = nodes,
|
|
902
1732
|
horizontal = props.optBoolean("horizontal", false),
|
|
903
1733
|
columns = props.optInt("numColumns", 1),
|
|
904
1734
|
inverted = props.optBoolean("inverted", false),
|
|
905
|
-
endReachedThreshold =
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1735
|
+
endReachedThreshold = props.opt("onEndReachedThreshold")
|
|
1736
|
+
?.takeUnless { it == JSONObject.NULL }
|
|
1737
|
+
?.let { props.optDouble("onEndReachedThreshold", 0.1) }
|
|
1738
|
+
?: props.optDouble("threshold", 0.1),
|
|
909
1739
|
renderItem = { node, itemIdentity, _ ->
|
|
910
1740
|
val rows = screen.flatListRows.getOrPut(list) { mutableMapOf() }
|
|
911
1741
|
val previous = rows[itemIdentity].orEmpty()
|
|
@@ -917,7 +1747,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
917
1747
|
},
|
|
918
1748
|
recycleItem = { itemIdentity ->
|
|
919
1749
|
val controls = screen.flatListRows[list]?.remove(itemIdentity).orEmpty()
|
|
920
|
-
releaseViewState(screen, controls.values.toList())
|
|
1750
|
+
releaseViewState(screen, controls.values.toList(), preservingInputDrafts = true)
|
|
921
1751
|
},
|
|
922
1752
|
endReached = events.optString("onEndReached").takeIf { it.isNotBlank() }?.let { handler ->
|
|
923
1753
|
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject())) }
|
|
@@ -942,21 +1772,26 @@ class MainActivity : AppCompatActivity() {
|
|
|
942
1772
|
view.alpha = style.optDouble("opacity", 1.0).toFloat()
|
|
943
1773
|
view.visibility = if (style.optString("display") == "none") View.GONE else View.VISIBLE
|
|
944
1774
|
view.elevation = dp(style.optDouble("elevation", 0.0)).toFloat()
|
|
945
|
-
val background = style.has("backgroundColor")
|
|
1775
|
+
val background = style.has("backgroundColor") && !style.isNull("backgroundColor")
|
|
946
1776
|
val border = style.optDouble("borderWidth", 0.0)
|
|
947
1777
|
val radius = style.optDouble("borderRadius", 0.0)
|
|
1778
|
+
val defaultBackground = if (defaultBackgrounds.containsKey(view)) {
|
|
1779
|
+
defaultBackgrounds[view]
|
|
1780
|
+
} else {
|
|
1781
|
+
view.background.also { defaultBackgrounds[view] = it }
|
|
1782
|
+
}
|
|
948
1783
|
if (background || border > 0 || radius > 0) {
|
|
949
1784
|
view.background = GradientDrawable().apply {
|
|
950
1785
|
setColor(color(style.optString("backgroundColor"), Color.TRANSPARENT))
|
|
951
1786
|
cornerRadius = dp(radius).toFloat()
|
|
952
1787
|
if (border > 0) setStroke(dp(border), color(style.optString("borderColor"), Color.TRANSPARENT))
|
|
953
1788
|
}
|
|
954
|
-
}
|
|
1789
|
+
} else view.background = defaultBackground
|
|
955
1790
|
view.clipToOutline = style.optString("overflow") == "hidden"
|
|
956
1791
|
if (view is ViewGroup) view.clipChildren = style.optString("overflow") == "hidden"
|
|
957
1792
|
if (view is ImageView) {
|
|
958
|
-
|
|
959
|
-
|
|
1793
|
+
style.optString("tintColor").takeIf { it.isNotBlank() }?.let { view.setColorFilter(color(it, Color.TRANSPARENT)) }
|
|
1794
|
+
?: view.clearColorFilter()
|
|
960
1795
|
}
|
|
961
1796
|
}
|
|
962
1797
|
|
|
@@ -967,8 +1802,12 @@ class MainActivity : AppCompatActivity() {
|
|
|
967
1802
|
"capitalize" -> value.split(' ').joinToString(" ") { word -> word.replaceFirstChar(Char::uppercase) }
|
|
968
1803
|
else -> value
|
|
969
1804
|
}
|
|
1805
|
+
configureTextStyle(view, style)
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
private fun configureTextStyle(view: TextView, style: JSONObject) {
|
|
970
1809
|
view.setTextColor(color(style.optString("color"), defaultTextColor))
|
|
971
|
-
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, style.optDouble("fontSize",
|
|
1810
|
+
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, style.optDouble("fontSize", 16.0).toFloat())
|
|
972
1811
|
val weight = when (style.optString("fontWeight")) {
|
|
973
1812
|
"bold" -> 700
|
|
974
1813
|
"100", "200", "300", "400", "500", "600", "700", "800", "900" -> style.optInt("fontWeight")
|
|
@@ -987,10 +1826,19 @@ class MainActivity : AppCompatActivity() {
|
|
|
987
1826
|
"justify" -> View.TEXT_ALIGNMENT_VIEW_START
|
|
988
1827
|
else -> View.TEXT_ALIGNMENT_INHERIT
|
|
989
1828
|
}
|
|
990
|
-
|
|
991
|
-
|
|
1829
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
1830
|
+
view.justificationMode = if (style.optString("textAlign") == "justify") {
|
|
1831
|
+
Layout.JUSTIFICATION_MODE_INTER_WORD
|
|
1832
|
+
} else {
|
|
1833
|
+
Layout.JUSTIFICATION_MODE_NONE
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
view.letterSpacing = style.optDouble("letterSpacing", Double.NaN)
|
|
1837
|
+
.takeUnless { it.isNaN() }
|
|
1838
|
+
?.let { if (view.textSize > 0) dp(it).toFloat() / view.textSize else 0f }
|
|
1839
|
+
?: 0f
|
|
992
1840
|
view.setLineSpacing(
|
|
993
|
-
|
|
1841
|
+
style.optDouble("lineHeight", Double.NaN).takeUnless { it.isNaN() }?.let { dp(it) - view.textSize } ?: 0f,
|
|
994
1842
|
1f
|
|
995
1843
|
)
|
|
996
1844
|
view.paintFlags = view.paintFlags and Paint.UNDERLINE_TEXT_FLAG.inv() and Paint.STRIKE_THRU_TEXT_FLAG.inv()
|
|
@@ -1002,7 +1850,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
1002
1850
|
}
|
|
1003
1851
|
}
|
|
1004
1852
|
|
|
1005
|
-
private fun updateImage(view: ImageView, source: Any?) {
|
|
1853
|
+
private fun updateImage(screen: Screen, view: ImageView, source: Any?) {
|
|
1006
1854
|
val uri = when (source) {
|
|
1007
1855
|
is String -> source
|
|
1008
1856
|
is JSONObject -> source.optString("uri")
|
|
@@ -1013,6 +1861,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
1013
1861
|
imageSources.remove(view)
|
|
1014
1862
|
imageErrors[view] = "Image source is missing"
|
|
1015
1863
|
view.setImageDrawable(null)
|
|
1864
|
+
imageFailure(screen, view, uri, "Image source is missing")
|
|
1016
1865
|
return
|
|
1017
1866
|
}
|
|
1018
1867
|
if (imageSources[view] == uri) return
|
|
@@ -1020,20 +1869,29 @@ class MainActivity : AppCompatActivity() {
|
|
|
1020
1869
|
imageSources[view] = uri
|
|
1021
1870
|
imageErrors.remove(view)
|
|
1022
1871
|
view.setImageDrawable(null)
|
|
1872
|
+
emitImageEvent(screen, view, screen.imageLoadStartHandlers[view])
|
|
1023
1873
|
|
|
1024
1874
|
if (uri.startsWith("data:image/") && uri.contains(',')) {
|
|
1025
1875
|
val bytes = runCatching { Base64.decode(uri.substringAfter(','), Base64.DEFAULT) }.getOrNull()
|
|
1026
1876
|
val bitmap = bytes?.let { BitmapFactory.decodeByteArray(it, 0, it.size) }
|
|
1027
|
-
if (bitmap != null)
|
|
1877
|
+
if (bitmap != null) {
|
|
1878
|
+
view.setImageBitmap(bitmap)
|
|
1879
|
+
emitImageEvent(screen, view, screen.imageLoadHandlers[view])
|
|
1880
|
+
emitImageEvent(screen, view, screen.imageLoadEndHandlers[view])
|
|
1881
|
+
} else imageFailure(screen, view, uri, "Image data is invalid")
|
|
1028
1882
|
return
|
|
1029
1883
|
}
|
|
1030
1884
|
if (!uri.contains(':')) {
|
|
1031
1885
|
val bitmap = runCatching { assets.open(uri).use { BitmapFactory.decodeStream(it) } }.getOrNull()
|
|
1032
|
-
if (bitmap != null)
|
|
1886
|
+
if (bitmap != null) {
|
|
1887
|
+
view.setImageBitmap(bitmap)
|
|
1888
|
+
emitImageEvent(screen, view, screen.imageLoadHandlers[view])
|
|
1889
|
+
emitImageEvent(screen, view, screen.imageLoadEndHandlers[view])
|
|
1890
|
+
} else imageFailure(screen, view, uri, "Bundled image was not found")
|
|
1033
1891
|
return
|
|
1034
1892
|
}
|
|
1035
1893
|
if (!uri.startsWith("https://")) {
|
|
1036
|
-
imageFailure(view, uri, "Unsupported image source")
|
|
1894
|
+
imageFailure(screen, view, uri, "Unsupported image source")
|
|
1037
1895
|
return
|
|
1038
1896
|
}
|
|
1039
1897
|
imageJobs[view] = imageExecutor.submit {
|
|
@@ -1047,27 +1905,68 @@ class MainActivity : AppCompatActivity() {
|
|
|
1047
1905
|
runOnUiThread {
|
|
1048
1906
|
if (imageSources[view] != uri) return@runOnUiThread
|
|
1049
1907
|
imageJobs.remove(view)
|
|
1050
|
-
if (bitmap != null)
|
|
1908
|
+
if (bitmap != null) {
|
|
1909
|
+
view.setImageBitmap(bitmap)
|
|
1910
|
+
emitImageEvent(screen, view, screen.imageLoadHandlers[view])
|
|
1911
|
+
emitImageEvent(screen, view, screen.imageLoadEndHandlers[view])
|
|
1912
|
+
} else imageFailure(screen, view, uri, "Image download failed")
|
|
1051
1913
|
}
|
|
1052
1914
|
}
|
|
1053
1915
|
}
|
|
1054
1916
|
|
|
1055
|
-
private fun imageFailure(view: ImageView, uri: String, message: String) {
|
|
1917
|
+
private fun imageFailure(screen: Screen, view: ImageView, uri: String, message: String) {
|
|
1056
1918
|
imageErrors[view] = message
|
|
1057
1919
|
view.setImageDrawable(null)
|
|
1058
|
-
|
|
1920
|
+
val current = view.contentDescription?.toString().orEmpty()
|
|
1921
|
+
if (!current.contains(message)) {
|
|
1922
|
+
view.contentDescription = listOf(
|
|
1923
|
+
current.ifBlank { view.tag?.toString() ?: "Image" },
|
|
1924
|
+
message,
|
|
1925
|
+
).joinToString(", ")
|
|
1926
|
+
}
|
|
1927
|
+
emitImageEvent(screen, view, screen.imageErrorHandlers[view], JSONObject().put("error", JSONObject().put("message", message)))
|
|
1928
|
+
emitImageEvent(screen, view, screen.imageLoadEndHandlers[view])
|
|
1059
1929
|
Log.e(TAG, "$message: $uri")
|
|
1060
1930
|
}
|
|
1061
1931
|
|
|
1932
|
+
private fun emitImageEvent(screen: Screen, view: ImageView, handler: String?, nativeEvent: JSONObject = JSONObject()) {
|
|
1933
|
+
if (handler.isNullOrBlank()) return
|
|
1934
|
+
send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", nativeEvent))
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
private fun setTextPreservingSelection(field: EditText, value: String) {
|
|
1938
|
+
val start = field.selectionStart
|
|
1939
|
+
val end = field.selectionEnd
|
|
1940
|
+
field.setText(value)
|
|
1941
|
+
if (start < 0 || end < 0) {
|
|
1942
|
+
field.setSelection(value.length)
|
|
1943
|
+
return
|
|
1944
|
+
}
|
|
1945
|
+
val boundedStart = start.coerceIn(0, value.length)
|
|
1946
|
+
val boundedEnd = end.coerceIn(0, value.length)
|
|
1947
|
+
field.setSelection(minOf(boundedStart, boundedEnd), maxOf(boundedStart, boundedEnd))
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1062
1950
|
private fun applyAccessibility(view: View, props: JSONObject, type: String, fallbackLabel: String) {
|
|
1063
1951
|
view.tag = fallbackLabel
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
props.
|
|
1952
|
+
val label = props.optString("accessibilityLabel").takeIf { it.isNotBlank() }
|
|
1953
|
+
?: props.optString("alt").takeIf { type == "Image" && it.isNotBlank() }
|
|
1954
|
+
val value = props.opt("accessibilityValue")?.takeUnless { it == JSONObject.NULL }?.toString()
|
|
1955
|
+
view.contentDescription = when {
|
|
1956
|
+
label != null && value != null -> "$label, $value"
|
|
1957
|
+
label != null -> label
|
|
1958
|
+
value != null -> value
|
|
1959
|
+
else -> null
|
|
1960
|
+
}
|
|
1961
|
+
view.tooltipText = props.optString("accessibilityHint").takeIf { it.isNotBlank() }
|
|
1067
1962
|
val role = props.optString("accessibilityRole").ifBlank { type.lowercase() }
|
|
1068
1963
|
val state = props.optJSONObject("accessibilityState")
|
|
1069
1964
|
val disabled = state?.optBoolean("disabled", false) == true
|
|
1070
|
-
if (
|
|
1965
|
+
if (type !in setOf("Button", "Link", "TextInput", "Picker", "Modal", "Switch", "Slider", "ActivityIndicator")) {
|
|
1966
|
+
view.isEnabled = !disabled
|
|
1967
|
+
} else if (disabled) {
|
|
1968
|
+
view.isEnabled = false
|
|
1969
|
+
}
|
|
1071
1970
|
ViewCompat.setAccessibilityDelegate(view, object : AccessibilityDelegateCompat() {
|
|
1072
1971
|
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
|
|
1073
1972
|
super.onInitializeAccessibilityNodeInfo(host, info)
|
|
@@ -1080,7 +1979,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
1080
1979
|
}
|
|
1081
1980
|
info.isHeading = role == "header"
|
|
1082
1981
|
info.roleDescription = role.takeIf { it !in setOf("view", "text", "textinput", "scrollview") }
|
|
1083
|
-
info.
|
|
1982
|
+
info.hintText = host.tooltipText
|
|
1983
|
+
info.isEnabled = host.isEnabled && !disabled
|
|
1084
1984
|
info.isSelected = state?.optBoolean("selected", false) == true
|
|
1085
1985
|
info.isChecked = state?.optBoolean("checked", false) == true
|
|
1086
1986
|
}
|
|
@@ -1100,9 +2000,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
1100
2000
|
val keyboard = when (props.optString("keyboardType")) {
|
|
1101
2001
|
"email-address" -> InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
|
|
1102
2002
|
"url" -> InputType.TYPE_TEXT_VARIATION_URI
|
|
1103
|
-
"numeric" -> InputType.TYPE_CLASS_NUMBER
|
|
2003
|
+
"numeric", "number-pad" -> InputType.TYPE_CLASS_NUMBER
|
|
1104
2004
|
"decimal-pad" -> InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
|
|
1105
2005
|
"phone-pad" -> InputType.TYPE_CLASS_PHONE
|
|
2006
|
+
"web-search" -> InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT
|
|
2007
|
+
"visible-password" -> InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
|
|
1106
2008
|
else -> 0
|
|
1107
2009
|
}
|
|
1108
2010
|
val capitalization = when (props.optString("autoCapitalize")) {
|
|
@@ -1243,6 +2145,36 @@ class MainActivity : AppCompatActivity() {
|
|
|
1243
2145
|
return initialDeepLink
|
|
1244
2146
|
}
|
|
1245
2147
|
|
|
2148
|
+
private fun emitLayoutEvents(screen: Screen) {
|
|
2149
|
+
emitLayoutEvents(screen, screen.view)
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
private fun emitLayoutEvents(screen: Screen, view: View) {
|
|
2153
|
+
val handler = screen.layoutHandlers[view]
|
|
2154
|
+
if (handler != null && view.isShown) {
|
|
2155
|
+
val density = resources.displayMetrics.density
|
|
2156
|
+
val bounds = LayoutBounds(
|
|
2157
|
+
x = view.left.toDouble() / density.toDouble(),
|
|
2158
|
+
y = view.top.toDouble() / density.toDouble(),
|
|
2159
|
+
width = view.width.toDouble() / density.toDouble(),
|
|
2160
|
+
height = view.height.toDouble() / density.toDouble(),
|
|
2161
|
+
)
|
|
2162
|
+
if (screen.lastLayoutBounds[view] != bounds) {
|
|
2163
|
+
screen.lastLayoutBounds[view] = bounds
|
|
2164
|
+
send(screen, "EVENT", JSONObject()
|
|
2165
|
+
.put("handlerName", handler)
|
|
2166
|
+
.put("nativeEvent", JSONObject().put("layout", JSONObject()
|
|
2167
|
+
.put("x", bounds.x)
|
|
2168
|
+
.put("y", bounds.y)
|
|
2169
|
+
.put("width", bounds.width)
|
|
2170
|
+
.put("height", bounds.height))))
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
if (view is ViewGroup) {
|
|
2174
|
+
for (index in 0 until view.childCount) emitLayoutEvents(screen, view.getChildAt(index))
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
|
|
1246
2178
|
private fun send(screen: Screen, type: String, payload: JSONObject, correlationId: String? = null) {
|
|
1247
2179
|
if (!screen.active) return
|
|
1248
2180
|
val message = JSONObject().put("type", type).put("payload", payload)
|
|
@@ -1276,22 +2208,68 @@ class MainActivity : AppCompatActivity() {
|
|
|
1276
2208
|
screen.flatListRows.values.forEach { rows -> rows.values.forEach { addAll(it.values) } }
|
|
1277
2209
|
}
|
|
1278
2210
|
|
|
1279
|
-
private fun
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
2211
|
+
private fun releaseFlatList(
|
|
2212
|
+
screen: Screen,
|
|
2213
|
+
list: CraftNativeFlatList,
|
|
2214
|
+
preservingInputDrafts: Boolean,
|
|
2215
|
+
) {
|
|
2216
|
+
val owner = screen.flatListOwnerByView.remove(list)
|
|
2217
|
+
list.discardAll()
|
|
2218
|
+
screen.flatListRows.remove(list)?.values?.forEach {
|
|
2219
|
+
releaseViewState(screen, it.values, preservingInputDrafts)
|
|
2220
|
+
}
|
|
2221
|
+
if (owner != null) screen.flatListOwners.entries.removeAll { it.value == owner }
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
private fun releaseViewState(
|
|
2225
|
+
screen: Screen,
|
|
2226
|
+
views: Collection<View>,
|
|
2227
|
+
preservingInputDrafts: Boolean = false,
|
|
2228
|
+
) {
|
|
2229
|
+
val released = views.toMutableSet()
|
|
2230
|
+
val refreshableLists = released.filterIsInstance<CraftNativeRefreshableFlatList>()
|
|
2231
|
+
val nestedLists = refreshableLists.map { it.list }.toSet()
|
|
2232
|
+
released.addAll(nestedLists)
|
|
2233
|
+
refreshableLists.forEach {
|
|
2234
|
+
releaseFlatList(screen, it.list, preservingInputDrafts)
|
|
2235
|
+
}
|
|
2236
|
+
released.filterIsInstance<CraftNativeFlatList>().filterNot { nestedLists.contains(it) }.forEach {
|
|
2237
|
+
releaseFlatList(screen, it, preservingInputDrafts)
|
|
1286
2238
|
}
|
|
1287
2239
|
releaseImages(released)
|
|
2240
|
+
released.filterIsInstance<EditText>().forEach {
|
|
2241
|
+
inputHintDefaults.remove(it)
|
|
2242
|
+
inputHighlightDefaults.remove(it)
|
|
2243
|
+
screen.suppressedTextChanges.remove(it)
|
|
2244
|
+
val identity = screen.inputIdentities.remove(it)
|
|
2245
|
+
if (!preservingInputDrafts) identity?.let(screen.drafts::remove)
|
|
2246
|
+
}
|
|
2247
|
+
released.forEach(defaultBackgrounds::remove)
|
|
1288
2248
|
screen.handlers.keys.removeAll(released)
|
|
2249
|
+
screen.pressInHandlers.keys.removeAll(released)
|
|
2250
|
+
screen.pressOutHandlers.keys.removeAll(released)
|
|
2251
|
+
screen.longPressHandlers.keys.removeAll(released)
|
|
2252
|
+
screen.layoutHandlers.keys.removeAll(released)
|
|
2253
|
+
screen.lastLayoutBounds.keys.removeAll(released)
|
|
1289
2254
|
screen.scrollHandlers.keys.removeAll(released)
|
|
1290
2255
|
screen.scrollBeginHandlers.keys.removeAll(released)
|
|
1291
2256
|
screen.scrollEndHandlers.keys.removeAll(released)
|
|
2257
|
+
screen.scrollMomentumBeginHandlers.keys.removeAll(released)
|
|
2258
|
+
screen.scrollMomentumEndHandlers.keys.removeAll(released)
|
|
2259
|
+
screen.sliderCompleteHandlers.keys.removeAll(released)
|
|
2260
|
+
screen.sliderSteps.keys.removeAll(released)
|
|
1292
2261
|
screen.focusHandlers.keys.removeAll(released)
|
|
1293
2262
|
screen.blurHandlers.keys.removeAll(released)
|
|
2263
|
+
screen.endEditingHandlers.keys.removeAll(released)
|
|
1294
2264
|
screen.submitHandlers.keys.removeAll(released)
|
|
2265
|
+
screen.imageLoadStartHandlers.keys.removeAll(released)
|
|
2266
|
+
screen.imageLoadHandlers.keys.removeAll(released)
|
|
2267
|
+
screen.imageLoadEndHandlers.keys.removeAll(released)
|
|
2268
|
+
screen.imageErrorHandlers.keys.removeAll(released)
|
|
2269
|
+
screen.pressActiveOpacities.keys.removeAll(released)
|
|
2270
|
+
screen.pressBaseOpacities.keys.removeAll(released)
|
|
2271
|
+
screen.modalVisibility.keys.removeAll(released)
|
|
2272
|
+
screen.modalCloseHandlers.keys.removeAll(released)
|
|
1295
2273
|
}
|
|
1296
2274
|
|
|
1297
2275
|
private fun closeScreen(screen: Screen) {
|
|
@@ -1302,12 +2280,25 @@ class MainActivity : AppCompatActivity() {
|
|
|
1302
2280
|
releaseViewState(screen, retainedViews(screen).toList())
|
|
1303
2281
|
screen.controls.clear()
|
|
1304
2282
|
screen.handlers.clear()
|
|
2283
|
+
screen.pressInHandlers.clear()
|
|
2284
|
+
screen.pressOutHandlers.clear()
|
|
2285
|
+
screen.layoutHandlers.clear()
|
|
2286
|
+
screen.lastLayoutBounds.clear()
|
|
1305
2287
|
screen.scrollHandlers.clear()
|
|
1306
2288
|
screen.scrollBeginHandlers.clear()
|
|
1307
2289
|
screen.scrollEndHandlers.clear()
|
|
2290
|
+
screen.scrollMomentumBeginHandlers.clear()
|
|
2291
|
+
screen.scrollMomentumEndHandlers.clear()
|
|
2292
|
+
screen.sliderCompleteHandlers.clear()
|
|
2293
|
+
screen.sliderSteps.clear()
|
|
1308
2294
|
screen.focusHandlers.clear()
|
|
1309
2295
|
screen.blurHandlers.clear()
|
|
2296
|
+
screen.endEditingHandlers.clear()
|
|
1310
2297
|
screen.submitHandlers.clear()
|
|
2298
|
+
screen.imageLoadStartHandlers.clear()
|
|
2299
|
+
screen.imageLoadHandlers.clear()
|
|
2300
|
+
screen.imageLoadEndHandlers.clear()
|
|
2301
|
+
screen.imageErrorHandlers.clear()
|
|
1311
2302
|
screen.flatListRows.clear()
|
|
1312
2303
|
screen.flatListOwners.clear()
|
|
1313
2304
|
screen.flatListOwnerByView.clear()
|
|
@@ -1315,7 +2306,17 @@ class MainActivity : AppCompatActivity() {
|
|
|
1315
2306
|
}
|
|
1316
2307
|
|
|
1317
2308
|
@Deprecated("Uses the activity back callback for native route navigation")
|
|
1318
|
-
override fun onBackPressed()
|
|
2309
|
+
override fun onBackPressed() {
|
|
2310
|
+
val screen = screens.lastOrNull() ?: return back()
|
|
2311
|
+
val modal = screen.modalVisibility.entries.lastOrNull { (view, visible) -> visible && view.visibility == View.VISIBLE }
|
|
2312
|
+
if (modal != null) {
|
|
2313
|
+
screen.modalCloseHandlers[modal.key]?.let { handler ->
|
|
2314
|
+
send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject()))
|
|
2315
|
+
}
|
|
2316
|
+
return
|
|
2317
|
+
}
|
|
2318
|
+
back()
|
|
2319
|
+
}
|
|
1319
2320
|
|
|
1320
2321
|
private fun showError(message: String) {
|
|
1321
2322
|
root.removeAllViews()
|