craft-native 0.0.123 → 0.0.124
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 +17 -0
- package/dist/android/templates/AndroidManifest.xml.template +1 -1
- package/dist/android/templates/CraftNativeFlatList.kt.template +113 -6
- package/dist/android/templates/MainActivity.kt.template +17 -0
- package/dist/android/templates/MainActivityNative.kt.template +826 -68
- package/dist/api/mobile.d.ts +207 -2
- package/dist/cli.js +1 -1
- package/dist/index.cjs +22 -1
- package/dist/index.js +22 -1
- package/dist/ios/src/index.d.ts +76 -2
- package/dist/ios/src/index.js +134 -43
- package/dist/ios/templates/CraftApp.swift +1086 -67
- package/dist/ios/templates/CraftNativeActions.swift +97 -9
- package/dist/ios/templates/CraftNativeFlatList.swift +92 -12
- package/dist/ios/templates/CraftNativeScreen.swift +876 -64
- package/dist/ios/templates/CraftNativeUI.swift +562 -0
- package/dist/ios/templates/Info.plist.template +2 -1
- package/dist/mobile.js +138 -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,6 +30,7 @@ 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
|
|
30
36
|
import android.widget.Button
|
|
@@ -34,10 +40,14 @@ import android.widget.HorizontalScrollView
|
|
|
34
40
|
import android.widget.ImageView
|
|
35
41
|
import android.widget.LinearLayout
|
|
36
42
|
import android.widget.ScrollView
|
|
43
|
+
import android.widget.SeekBar
|
|
44
|
+
import android.widget.Switch
|
|
37
45
|
import android.widget.TextView
|
|
38
46
|
import androidx.appcompat.app.AppCompatActivity
|
|
47
|
+
import androidx.appcompat.app.AppCompatDelegate
|
|
39
48
|
import androidx.core.content.ContextCompat
|
|
40
49
|
import androidx.core.view.AccessibilityDelegateCompat
|
|
50
|
+
import androidx.core.view.WindowInsetsCompat
|
|
41
51
|
import androidx.core.view.ViewCompat
|
|
42
52
|
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
|
|
43
53
|
import androidx.javascriptengine.JavaScriptIsolate
|
|
@@ -55,6 +65,7 @@ import java.util.concurrent.Future
|
|
|
55
65
|
import java.util.UUID
|
|
56
66
|
import kotlin.math.max
|
|
57
67
|
import kotlin.math.min
|
|
68
|
+
import kotlin.math.roundToInt
|
|
58
69
|
|
|
59
70
|
private const val CRAFT_NATIVE_CAPABILITY_TIMEOUT_MS = 30_000L
|
|
60
71
|
|
|
@@ -71,30 +82,61 @@ private data class CraftNativeLayoutStyle(
|
|
|
71
82
|
val bottom: Int? = null,
|
|
72
83
|
val left: Int? = null,
|
|
73
84
|
val alignSelf: String? = null,
|
|
85
|
+
val flexGrow: Double = 0.0,
|
|
86
|
+
val flexShrink: Double = 0.0,
|
|
87
|
+
val flexBasis: Int? = null,
|
|
74
88
|
) {
|
|
75
89
|
companion object {
|
|
76
90
|
fun from(raw: JSONObject, density: Float): CraftNativeLayoutStyle {
|
|
77
|
-
fun number(name: String): Int?
|
|
91
|
+
fun number(name: String): Int? {
|
|
92
|
+
if (!raw.has(name) || raw.isNull(name)) return null
|
|
93
|
+
return raw.optDouble(name, Double.NaN).takeUnless { it.isNaN() }?.let { (it * density).toInt() }
|
|
94
|
+
}
|
|
78
95
|
return CraftNativeLayoutStyle(
|
|
79
96
|
width = number("width"), height = number("height"),
|
|
80
97
|
minWidth = number("minWidth"), maxWidth = number("maxWidth"),
|
|
81
98
|
minHeight = number("minHeight"), maxHeight = number("maxHeight"),
|
|
82
|
-
position = raw.
|
|
99
|
+
position = raw.opt("position")
|
|
100
|
+
?.takeUnless { it == JSONObject.NULL }
|
|
101
|
+
?.toString()
|
|
102
|
+
?.takeIf { it.isNotBlank() }
|
|
103
|
+
?: "relative",
|
|
83
104
|
top = number("top"), right = number("right"), bottom = number("bottom"), left = number("left"),
|
|
84
|
-
alignSelf = raw.
|
|
105
|
+
alignSelf = raw.opt("alignSelf")
|
|
106
|
+
?.takeUnless { it == JSONObject.NULL }
|
|
107
|
+
?.toString()
|
|
108
|
+
?.takeIf { it.isNotBlank() },
|
|
109
|
+
flexGrow = raw.optDouble("flexGrow", raw.optDouble("flex", 0.0)),
|
|
110
|
+
flexShrink = raw.optDouble("flexShrink", 0.0),
|
|
111
|
+
flexBasis = number("flexBasis"),
|
|
85
112
|
)
|
|
86
113
|
}
|
|
87
114
|
}
|
|
88
115
|
}
|
|
89
116
|
|
|
117
|
+
private fun craftNativeGridColumnCount(value: Any?): Int {
|
|
118
|
+
if (value is Number) return max(1, value.toInt())
|
|
119
|
+
val text = value as? String ?: return 1
|
|
120
|
+
val trimmed = text.trim()
|
|
121
|
+
if (trimmed.startsWith("repeat(")) {
|
|
122
|
+
val count = trimmed.removePrefix("repeat(").takeWhile { it.isDigit() }.toIntOrNull()
|
|
123
|
+
if (count != null) return max(1, count)
|
|
124
|
+
}
|
|
125
|
+
return max(1, trimmed.split(Regex("\\s+")).count { it.isNotBlank() })
|
|
126
|
+
}
|
|
127
|
+
|
|
90
128
|
/** A deterministic flex-shaped container shared by normal views and scroll content. */
|
|
91
129
|
private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
130
|
+
var onLayoutChanged: (() -> Unit)? = null
|
|
92
131
|
private val styles = mutableMapOf<View, CraftNativeLayoutStyle>()
|
|
93
132
|
private var wrap = false
|
|
94
133
|
private var alignItems = "stretch"
|
|
95
134
|
private var justifyContent = "flex-start"
|
|
96
135
|
private var rowGap = 0
|
|
97
136
|
private var columnGap = 0
|
|
137
|
+
private var grid = false
|
|
138
|
+
private var gridColumns = 1
|
|
139
|
+
private var gridAutoRows: Int? = null
|
|
98
140
|
|
|
99
141
|
fun configure(style: JSONObject, density: Float) {
|
|
100
142
|
orientation = if (style.optString("flexDirection") in setOf("row", "row-reverse")) HORIZONTAL else VERTICAL
|
|
@@ -104,6 +146,11 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
104
146
|
val gap = (style.optDouble("gap", 0.0) * density).toInt()
|
|
105
147
|
rowGap = (style.optDouble("rowGap", gap.toDouble() / density.toDouble()) * density).toInt()
|
|
106
148
|
columnGap = (style.optDouble("columnGap", gap.toDouble() / density.toDouble()) * density).toInt()
|
|
149
|
+
grid = style.optString("display") == "grid"
|
|
150
|
+
gridColumns = craftNativeGridColumnCount(
|
|
151
|
+
if (style.has("gridTemplateColumns")) style.opt("gridTemplateColumns") else style.opt("gridColumns")
|
|
152
|
+
)
|
|
153
|
+
gridAutoRows = style.optDouble("gridAutoRows", Double.NaN).takeUnless { it.isNaN() }?.let { (it * density).toInt() }
|
|
107
154
|
val padding = (style.optDouble("padding", 0.0) * density).toInt()
|
|
108
155
|
val horizontal = (style.optDouble("paddingHorizontal", padding.toDouble() / density.toDouble()) * density).toInt()
|
|
109
156
|
val vertical = (style.optDouble("paddingVertical", padding.toDouble() / density.toDouble()) * density).toInt()
|
|
@@ -128,6 +175,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
128
175
|
|
|
129
176
|
private data class Entry(val view: View, val style: CraftNativeLayoutStyle, val width: Int, val height: Int)
|
|
130
177
|
private data class Line(val entries: MutableList<Entry> = mutableListOf(), var main: Int = 0, var cross: Int = 0)
|
|
178
|
+
private data class GridLayout(val rows: List<List<Entry>>, val absolute: List<Entry>, val cellWidth: Int, val rowHeights: List<Int>)
|
|
131
179
|
|
|
132
180
|
private fun entries(width: Int, height: Int): Pair<List<Line>, List<Entry>> {
|
|
133
181
|
val availableMain = if (orientation == HORIZONTAL) max(0, width - paddingLeft - paddingRight)
|
|
@@ -162,8 +210,70 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
162
210
|
return lines to absolute
|
|
163
211
|
}
|
|
164
212
|
|
|
213
|
+
private fun gridLayout(width: Int, height: Int): GridLayout {
|
|
214
|
+
val availableWidth = max(0, width - paddingLeft - paddingRight)
|
|
215
|
+
val gap = columnGap
|
|
216
|
+
val cellWidth = max(0, (availableWidth - max(0, gridColumns - 1) * gap) / max(1, gridColumns))
|
|
217
|
+
val rows = mutableListOf<MutableList<Entry>>()
|
|
218
|
+
val absolute = mutableListOf<Entry>()
|
|
219
|
+
for (index in 0 until childCount) {
|
|
220
|
+
val child = getChildAt(index)
|
|
221
|
+
if (child.visibility == GONE) continue
|
|
222
|
+
val style = styles[child] ?: CraftNativeLayoutStyle()
|
|
223
|
+
val lp = child.layoutParams as? MarginLayoutParams
|
|
224
|
+
val horizontalMargins = (lp?.leftMargin ?: 0) + (lp?.rightMargin ?: 0)
|
|
225
|
+
val verticalMargins = (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
|
|
226
|
+
val childWidth = measureChildWidth(child, style, cellWidth, horizontalMargins)
|
|
227
|
+
val childHeight = measureChildHeight(child, style, height - paddingTop - paddingBottom, verticalMargins)
|
|
228
|
+
val entry = Entry(child, style, childWidth, childHeight)
|
|
229
|
+
if (style.position == "absolute") absolute += entry
|
|
230
|
+
else {
|
|
231
|
+
if (rows.isEmpty() || rows.last().size == gridColumns) rows.add(mutableListOf())
|
|
232
|
+
rows.last() += entry
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
val rowHeights = rows.map { row ->
|
|
236
|
+
max(gridAutoRows ?: 0, row.maxOfOrNull { entry ->
|
|
237
|
+
val lp = entry.view.layoutParams as? MarginLayoutParams
|
|
238
|
+
entry.height + (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
|
|
239
|
+
} ?: 0)
|
|
240
|
+
}
|
|
241
|
+
return GridLayout(rows, absolute, cellWidth, rowHeights)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private fun gridIntrinsicSize(height: Int): Pair<Int, Int> {
|
|
245
|
+
val columns = max(1, gridColumns)
|
|
246
|
+
val columnWidths = IntArray(columns)
|
|
247
|
+
val rowHeights = mutableListOf<Int>()
|
|
248
|
+
var flowIndex = 0
|
|
249
|
+
for (index in 0 until childCount) {
|
|
250
|
+
val child = getChildAt(index)
|
|
251
|
+
if (child.visibility == GONE) continue
|
|
252
|
+
val style = styles[child] ?: CraftNativeLayoutStyle()
|
|
253
|
+
if (style.position == "absolute") continue
|
|
254
|
+
val lp = child.layoutParams as? MarginLayoutParams
|
|
255
|
+
val horizontalMargins = (lp?.leftMargin ?: 0) + (lp?.rightMargin ?: 0)
|
|
256
|
+
val verticalMargins = (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
|
|
257
|
+
val childWidth = measureChildWidth(child, style, 1_000_000, horizontalMargins)
|
|
258
|
+
val childHeight = measureChildHeight(child, style, height, verticalMargins)
|
|
259
|
+
val column = flowIndex % columns
|
|
260
|
+
val row = flowIndex / columns
|
|
261
|
+
while (rowHeights.size <= row) rowHeights += gridAutoRows ?: 0
|
|
262
|
+
columnWidths[column] = max(columnWidths[column], childWidth + horizontalMargins)
|
|
263
|
+
rowHeights[row] = max(rowHeights[row], childHeight + verticalMargins)
|
|
264
|
+
flowIndex += 1
|
|
265
|
+
}
|
|
266
|
+
val cellWidth = columnWidths.maxOrNull() ?: 0
|
|
267
|
+
val width = paddingLeft + paddingRight + cellWidth * columns + max(0, columns - 1) * columnGap
|
|
268
|
+
val rowGapTotal = max(0, rowHeights.size - 1) * rowGap
|
|
269
|
+
val measuredHeight = paddingTop + paddingBottom + rowHeights.sum() + rowGapTotal
|
|
270
|
+
return width to measuredHeight
|
|
271
|
+
}
|
|
272
|
+
|
|
165
273
|
private fun measureChildWidth(child: View, style: CraftNativeLayoutStyle, available: Int, margins: Int): Int {
|
|
166
|
-
val
|
|
274
|
+
val flexBasis = if (orientation == HORIZONTAL) style.flexBasis else null
|
|
275
|
+
val explicit = style.width ?: flexBasis
|
|
276
|
+
?: (child.layoutParams?.width ?: ViewGroup.LayoutParams.WRAP_CONTENT).takeIf { it >= 0 }
|
|
167
277
|
val target = explicit ?: max(0, available - margins)
|
|
168
278
|
val spec = if (explicit != null) View.MeasureSpec.makeMeasureSpec(target, View.MeasureSpec.EXACTLY)
|
|
169
279
|
else View.MeasureSpec.makeMeasureSpec(max(0, available - margins), View.MeasureSpec.AT_MOST)
|
|
@@ -172,7 +282,9 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
172
282
|
}
|
|
173
283
|
|
|
174
284
|
private fun measureChildHeight(child: View, style: CraftNativeLayoutStyle, available: Int, margins: Int): Int {
|
|
175
|
-
val
|
|
285
|
+
val flexBasis = if (orientation == VERTICAL) style.flexBasis else null
|
|
286
|
+
val explicit = style.height ?: flexBasis
|
|
287
|
+
?: (child.layoutParams?.height ?: ViewGroup.LayoutParams.WRAP_CONTENT).takeIf { it >= 0 }
|
|
176
288
|
val target = explicit ?: max(0, available - margins)
|
|
177
289
|
val spec = if (explicit != null) View.MeasureSpec.makeMeasureSpec(target, View.MeasureSpec.EXACTLY)
|
|
178
290
|
else View.MeasureSpec.makeMeasureSpec(max(0, available - margins), View.MeasureSpec.AT_MOST)
|
|
@@ -188,8 +300,24 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
188
300
|
}
|
|
189
301
|
|
|
190
302
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
191
|
-
val
|
|
303
|
+
val widthMode = View.MeasureSpec.getMode(widthMeasureSpec)
|
|
304
|
+
val width = View.MeasureSpec.getSize(widthMeasureSpec).takeUnless { widthMode == View.MeasureSpec.UNSPECIFIED } ?: 1_000_000
|
|
192
305
|
val height = View.MeasureSpec.getSize(heightMeasureSpec).takeUnless { View.MeasureSpec.getMode(heightMeasureSpec) == View.MeasureSpec.UNSPECIFIED } ?: 1_000_000
|
|
306
|
+
if (grid) {
|
|
307
|
+
if (widthMode == View.MeasureSpec.UNSPECIFIED) {
|
|
308
|
+
val (intrinsicWidth, intrinsicHeight) = gridIntrinsicSize(height)
|
|
309
|
+
setMeasuredDimension(
|
|
310
|
+
resolveSize(intrinsicWidth, widthMeasureSpec),
|
|
311
|
+
resolveSize(intrinsicHeight, heightMeasureSpec),
|
|
312
|
+
)
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
val layout = gridLayout(width, height)
|
|
316
|
+
val desiredWidth = paddingLeft + paddingRight + layout.cellWidth * gridColumns + max(0, gridColumns - 1) * columnGap
|
|
317
|
+
val desiredHeight = paddingTop + paddingBottom + layout.rowHeights.sum() + max(0, layout.rowHeights.size - 1) * rowGap
|
|
318
|
+
setMeasuredDimension(resolveSize(desiredWidth, widthMeasureSpec), resolveSize(desiredHeight, heightMeasureSpec))
|
|
319
|
+
return
|
|
320
|
+
}
|
|
193
321
|
val (lines, absolute) = entries(width, height)
|
|
194
322
|
val main = lines.maxOfOrNull { it.main } ?: 0
|
|
195
323
|
val cross = lines.sumOf { it.cross } + max(0, lines.size - 1) * (if (orientation == HORIZONTAL) rowGap else columnGap)
|
|
@@ -204,17 +332,77 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
204
332
|
}
|
|
205
333
|
|
|
206
334
|
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
335
|
+
if (grid) {
|
|
336
|
+
val layout = gridLayout(width, height)
|
|
337
|
+
var rowTop = paddingTop
|
|
338
|
+
for ((rowIndex, row) in layout.rows.withIndex()) {
|
|
339
|
+
val rowHeight = layout.rowHeights[rowIndex]
|
|
340
|
+
for ((column, entry) in row.withIndex()) {
|
|
341
|
+
val align = entry.style.alignSelf ?: alignItems
|
|
342
|
+
val lp = entry.view.layoutParams as? MarginLayoutParams
|
|
343
|
+
val marginLeft = lp?.leftMargin ?: 0
|
|
344
|
+
val marginRight = lp?.rightMargin ?: 0
|
|
345
|
+
val marginTop = lp?.topMargin ?: 0
|
|
346
|
+
val marginBottom = lp?.bottomMargin ?: 0
|
|
347
|
+
val availableWidth = max(0, layout.cellWidth - marginLeft - marginRight)
|
|
348
|
+
val availableHeight = max(0, rowHeight - marginTop - marginBottom)
|
|
349
|
+
val childWidth = if (align == "stretch" && entry.style.width == null) availableWidth else min(availableWidth, entry.width)
|
|
350
|
+
val childHeight = if (align == "stretch" && entry.style.height == null) availableHeight else min(availableHeight, entry.height)
|
|
351
|
+
val x = paddingLeft + column * (layout.cellWidth + columnGap) + marginLeft + when (align) {
|
|
352
|
+
"center" -> (availableWidth - childWidth) / 2
|
|
353
|
+
"flex-end" -> availableWidth - childWidth
|
|
354
|
+
else -> 0
|
|
355
|
+
}
|
|
356
|
+
val y = rowTop + marginTop + when (align) {
|
|
357
|
+
"center" -> (availableHeight - childHeight) / 2
|
|
358
|
+
"flex-end" -> availableHeight - childHeight
|
|
359
|
+
else -> 0
|
|
360
|
+
}
|
|
361
|
+
entry.view.layout(x, y, x + childWidth, y + childHeight)
|
|
362
|
+
}
|
|
363
|
+
rowTop += rowHeight + rowGap
|
|
364
|
+
}
|
|
365
|
+
val contentWidth = width - paddingLeft - paddingRight
|
|
366
|
+
val contentHeight = height - paddingTop - paddingBottom
|
|
367
|
+
for (entry in layout.absolute) {
|
|
368
|
+
val childWidth = if (entry.style.left != null && entry.style.right != null) contentWidth - entry.style.left!! - entry.style.right!! else entry.width
|
|
369
|
+
val childHeight = if (entry.style.top != null && entry.style.bottom != null) contentHeight - entry.style.top!! - entry.style.bottom!! else entry.height
|
|
370
|
+
val x = paddingLeft + (entry.style.left ?: (entry.style.right?.let { contentWidth - it - childWidth } ?: 0))
|
|
371
|
+
val y = paddingTop + (entry.style.top ?: (entry.style.bottom?.let { contentHeight - it - childHeight } ?: 0))
|
|
372
|
+
entry.view.layout(x, y, x + max(0, childWidth), y + max(0, childHeight))
|
|
373
|
+
}
|
|
374
|
+
onLayoutChanged?.invoke()
|
|
375
|
+
return
|
|
376
|
+
}
|
|
207
377
|
val contentWidth = width - paddingLeft - paddingRight
|
|
208
378
|
val contentHeight = height - paddingTop - paddingBottom
|
|
209
379
|
val (lines, absolute) = entries(width, height)
|
|
210
380
|
val gap = if (orientation == HORIZONTAL) columnGap else rowGap
|
|
381
|
+
val crossGap = if (orientation == HORIZONTAL) rowGap else columnGap
|
|
382
|
+
val availableMain = if (orientation == HORIZONTAL) contentWidth else contentHeight
|
|
211
383
|
var crossOffset = if (orientation == HORIZONTAL) paddingTop else paddingLeft
|
|
212
384
|
for (line in lines) {
|
|
213
|
-
val
|
|
385
|
+
val rawFree = availableMain - line.main
|
|
386
|
+
val factors = line.entries.map { if (rawFree >= 0) it.style.flexGrow else it.style.flexShrink }
|
|
387
|
+
val totalFactor = factors.sum()
|
|
388
|
+
val distributedMain = line.entries.indices.sumOf { index ->
|
|
389
|
+
val base = if (orientation == HORIZONTAL) line.entries[index].width else line.entries[index].height
|
|
390
|
+
val primary = if (totalFactor > 0) max(0, (base + rawFree * factors[index] / totalFactor).toInt()) else base
|
|
391
|
+
val constrained = if (orientation == HORIZONTAL) {
|
|
392
|
+
clamp(primary, line.entries[index].style.minWidth, line.entries[index].style.maxWidth)
|
|
393
|
+
} else {
|
|
394
|
+
clamp(primary, line.entries[index].style.minHeight, line.entries[index].style.maxHeight)
|
|
395
|
+
}
|
|
396
|
+
val lp = line.entries[index].view.layoutParams as? MarginLayoutParams
|
|
397
|
+
val margins = if (orientation == HORIZONTAL) (lp?.leftMargin ?: 0) + (lp?.rightMargin ?: 0)
|
|
398
|
+
else (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
|
|
399
|
+
constrained + margins
|
|
400
|
+
} + max(0, line.entries.size - 1) * gap
|
|
401
|
+
val free = max(0, availableMain - distributedMain)
|
|
214
402
|
val (leading, between) = distribution(free, line.entries.size)
|
|
215
403
|
var mainOffset = if (orientation == HORIZONTAL) paddingLeft else paddingTop
|
|
216
404
|
mainOffset += leading
|
|
217
|
-
for (entry in line.entries) {
|
|
405
|
+
for ((index, entry) in line.entries.withIndex()) {
|
|
218
406
|
val lp = entry.view.layoutParams as? MarginLayoutParams
|
|
219
407
|
val marginStart = if (orientation == HORIZONTAL) lp?.leftMargin ?: 0 else lp?.topMargin ?: 0
|
|
220
408
|
val marginEnd = if (orientation == HORIZONTAL) lp?.rightMargin ?: 0 else lp?.bottomMargin ?: 0
|
|
@@ -222,19 +410,29 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
222
410
|
max(0, line.cross - (if (orientation == HORIZONTAL) (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0) else (lp?.leftMargin ?: 0) + (lp?.rightMargin ?: 0)))
|
|
223
411
|
else if (orientation == HORIZONTAL) entry.height else entry.width
|
|
224
412
|
val align = entry.style.alignSelf ?: alignItems
|
|
413
|
+
val crossLeading = if (orientation == HORIZONTAL) lp?.topMargin ?: 0 else lp?.leftMargin ?: 0
|
|
414
|
+
val crossTrailing = if (orientation == HORIZONTAL) lp?.bottomMargin ?: 0 else lp?.rightMargin ?: 0
|
|
415
|
+
val crossAvailable = max(0, line.cross - crossLeading - crossTrailing)
|
|
225
416
|
val crossPosition = when (align) {
|
|
226
|
-
"center" -> (
|
|
227
|
-
"flex-end" -> line.cross - crossSize
|
|
228
|
-
else ->
|
|
417
|
+
"center" -> crossLeading + (crossAvailable - crossSize) / 2
|
|
418
|
+
"flex-end" -> line.cross - crossTrailing - crossSize
|
|
419
|
+
else -> crossLeading
|
|
420
|
+
}
|
|
421
|
+
val x = if (orientation == HORIZONTAL) mainOffset + marginStart else crossOffset + crossPosition
|
|
422
|
+
val y = if (orientation == HORIZONTAL) crossOffset + crossPosition else mainOffset + marginStart
|
|
423
|
+
val primary = if (orientation == HORIZONTAL) entry.width else entry.height
|
|
424
|
+
val distributed = if (totalFactor > 0) max(0, (primary + rawFree * factors[index] / totalFactor).toInt()) else primary
|
|
425
|
+
val distributedPrimary = if (orientation == HORIZONTAL) {
|
|
426
|
+
clamp(distributed, entry.style.minWidth, entry.style.maxWidth)
|
|
427
|
+
} else {
|
|
428
|
+
clamp(distributed, entry.style.minHeight, entry.style.maxHeight)
|
|
229
429
|
}
|
|
230
|
-
val
|
|
231
|
-
val
|
|
232
|
-
val w = if (orientation == HORIZONTAL) entry.width else crossSize
|
|
233
|
-
val h = if (orientation == HORIZONTAL) crossSize else entry.height
|
|
430
|
+
val w = if (orientation == HORIZONTAL) distributedPrimary else crossSize
|
|
431
|
+
val h = if (orientation == HORIZONTAL) crossSize else distributedPrimary
|
|
234
432
|
entry.view.layout(x, y, x + w, y + h)
|
|
235
|
-
mainOffset +=
|
|
433
|
+
mainOffset += distributedPrimary + marginStart + marginEnd + gap + between
|
|
236
434
|
}
|
|
237
|
-
crossOffset += line.cross +
|
|
435
|
+
crossOffset += line.cross + crossGap
|
|
238
436
|
}
|
|
239
437
|
for (entry in absolute) {
|
|
240
438
|
val widthValue = if (entry.style.left != null && entry.style.right != null) contentWidth - entry.style.left!! - entry.style.right!! else entry.width
|
|
@@ -243,6 +441,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
243
441
|
val y = paddingTop + (entry.style.top ?: (entry.style.bottom?.let { contentHeight - it - heightValue } ?: 0))
|
|
244
442
|
entry.view.layout(x, y, x + max(0, widthValue), y + max(0, heightValue))
|
|
245
443
|
}
|
|
444
|
+
onLayoutChanged?.invoke()
|
|
246
445
|
}
|
|
247
446
|
|
|
248
447
|
private fun distribution(free: Int, count: Int): Pair<Int, Int> = when (justifyContent) {
|
|
@@ -260,6 +459,10 @@ private class CraftNativeScrollView(context: Context) : FrameLayout(context) {
|
|
|
260
459
|
private val vertical = ScrollView(context)
|
|
261
460
|
private val horizontal = HorizontalScrollView(context)
|
|
262
461
|
private var horizontalMode: Boolean? = null
|
|
462
|
+
private var pagingEnabled = false
|
|
463
|
+
private var bounces = true
|
|
464
|
+
private var alwaysBounceVertical = true
|
|
465
|
+
private var alwaysBounceHorizontal = true
|
|
263
466
|
|
|
264
467
|
init {
|
|
265
468
|
vertical.isNestedScrollingEnabled = true
|
|
@@ -293,6 +496,39 @@ private class CraftNativeScrollView(context: Context) : FrameLayout(context) {
|
|
|
293
496
|
horizontal.isEnabled = value
|
|
294
497
|
}
|
|
295
498
|
|
|
499
|
+
fun setBounces(value: Boolean) {
|
|
500
|
+
bounces = value
|
|
501
|
+
applyBounceModes()
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
fun setAlwaysBounce(vertical: Boolean, horizontal: Boolean) {
|
|
505
|
+
alwaysBounceVertical = vertical
|
|
506
|
+
alwaysBounceHorizontal = horizontal
|
|
507
|
+
applyBounceModes()
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
private fun applyBounceModes() {
|
|
511
|
+
vertical.overScrollMode = if (bounces && alwaysBounceVertical) View.OVER_SCROLL_ALWAYS else View.OVER_SCROLL_NEVER
|
|
512
|
+
horizontal.overScrollMode = if (bounces && alwaysBounceHorizontal) View.OVER_SCROLL_ALWAYS else View.OVER_SCROLL_NEVER
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
fun setPagingEnabled(value: Boolean) {
|
|
516
|
+
pagingEnabled = value
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
private fun snapToPage(scroller: View) {
|
|
520
|
+
val horizontal = horizontalMode == true
|
|
521
|
+
val pageSize = if (horizontal) scroller.width else scroller.height
|
|
522
|
+
if (pageSize <= 0) return
|
|
523
|
+
val offset = if (horizontal) scroller.scrollX else scroller.scrollY
|
|
524
|
+
val target = (offset.toFloat() / pageSize).roundToInt() * pageSize
|
|
525
|
+
if (horizontal) {
|
|
526
|
+
(scroller as HorizontalScrollView).smoothScrollTo(target, 0)
|
|
527
|
+
} else {
|
|
528
|
+
(scroller as ScrollView).smoothScrollTo(0, target)
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
296
532
|
fun setScrollIndicators(verticalVisible: Boolean, horizontalVisible: Boolean) {
|
|
297
533
|
vertical.isVerticalScrollBarEnabled = verticalVisible
|
|
298
534
|
horizontal.isHorizontalScrollBarEnabled = horizontalVisible
|
|
@@ -302,13 +538,62 @@ private class CraftNativeScrollView(context: Context) : FrameLayout(context) {
|
|
|
302
538
|
onScroll: ((Int, Int) -> Unit)?,
|
|
303
539
|
onBeginDrag: (() -> Unit)?,
|
|
304
540
|
onEndDrag: (() -> Unit)?,
|
|
541
|
+
onMomentumBegin: (() -> Unit)?,
|
|
542
|
+
onMomentumEnd: (() -> Unit)?,
|
|
543
|
+
keyboardDismissMode: String,
|
|
544
|
+
dismissKeyboard: (() -> Unit)?,
|
|
305
545
|
) {
|
|
306
546
|
fun attach(scroller: View) {
|
|
547
|
+
var dismissed = false
|
|
548
|
+
var moved = false
|
|
549
|
+
var momentumActive = false
|
|
550
|
+
var lastX = 0
|
|
551
|
+
var lastY = 0
|
|
552
|
+
|
|
553
|
+
fun pollMomentum() {
|
|
554
|
+
if (!momentumActive) return
|
|
555
|
+
val x = scroller.scrollX
|
|
556
|
+
val y = scroller.scrollY
|
|
557
|
+
if (x == lastX && y == lastY) {
|
|
558
|
+
momentumActive = false
|
|
559
|
+
onMomentumEnd?.invoke()
|
|
560
|
+
} else {
|
|
561
|
+
lastX = x
|
|
562
|
+
lastY = y
|
|
563
|
+
scroller.postDelayed({ pollMomentum() }, 16L)
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
307
567
|
scroller.setOnScrollChangeListener { _, x, y, _, _ -> onScroll?.invoke(x, y) }
|
|
308
568
|
scroller.setOnTouchListener { _, event ->
|
|
309
569
|
when (event.actionMasked) {
|
|
310
|
-
android.view.MotionEvent.ACTION_DOWN ->
|
|
311
|
-
|
|
570
|
+
android.view.MotionEvent.ACTION_DOWN -> {
|
|
571
|
+
if (momentumActive) {
|
|
572
|
+
momentumActive = false
|
|
573
|
+
onMomentumEnd?.invoke()
|
|
574
|
+
}
|
|
575
|
+
dismissed = false
|
|
576
|
+
moved = false
|
|
577
|
+
onBeginDrag?.invoke()
|
|
578
|
+
}
|
|
579
|
+
android.view.MotionEvent.ACTION_MOVE -> {
|
|
580
|
+
moved = true
|
|
581
|
+
if (!dismissed && keyboardDismissMode in setOf("on-drag", "interactive")) {
|
|
582
|
+
dismissed = true
|
|
583
|
+
dismissKeyboard?.invoke()
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
android.view.MotionEvent.ACTION_UP, android.view.MotionEvent.ACTION_CANCEL -> {
|
|
587
|
+
onEndDrag?.invoke()
|
|
588
|
+
if (pagingEnabled) snapToPage(scroller)
|
|
589
|
+
if (moved && !momentumActive) {
|
|
590
|
+
momentumActive = true
|
|
591
|
+
lastX = scroller.scrollX
|
|
592
|
+
lastY = scroller.scrollY
|
|
593
|
+
onMomentumBegin?.invoke()
|
|
594
|
+
scroller.postDelayed({ pollMomentum() }, 16L)
|
|
595
|
+
}
|
|
596
|
+
}
|
|
312
597
|
}
|
|
313
598
|
false
|
|
314
599
|
}
|
|
@@ -320,6 +605,13 @@ private class CraftNativeScrollView(context: Context) : FrameLayout(context) {
|
|
|
320
605
|
|
|
321
606
|
private class CraftNativeFlexSpacer(context: Context) : View(context)
|
|
322
607
|
|
|
608
|
+
private data class LayoutBounds(
|
|
609
|
+
val x: Double,
|
|
610
|
+
val y: Double,
|
|
611
|
+
val width: Double,
|
|
612
|
+
val height: Double,
|
|
613
|
+
)
|
|
614
|
+
|
|
323
615
|
/** WebView-free host for a compiled stx-native route bundle. */
|
|
324
616
|
class MainActivity : AppCompatActivity() {
|
|
325
617
|
private class Screen(
|
|
@@ -330,14 +622,28 @@ class MainActivity : AppCompatActivity() {
|
|
|
330
622
|
val port: MessagePort?
|
|
331
623
|
) {
|
|
332
624
|
val drafts = mutableMapOf<String, String>()
|
|
625
|
+
val inputIdentities = mutableMapOf<EditText, String>()
|
|
626
|
+
val suppressedTextChanges = mutableSetOf<EditText>()
|
|
333
627
|
val controls = mutableMapOf<String, View>()
|
|
334
628
|
val handlers = mutableMapOf<View, String>()
|
|
629
|
+
val longPressHandlers = mutableMapOf<View, String>()
|
|
335
630
|
val scrollHandlers = mutableMapOf<View, String>()
|
|
336
631
|
val scrollBeginHandlers = mutableMapOf<View, String>()
|
|
337
632
|
val scrollEndHandlers = mutableMapOf<View, String>()
|
|
633
|
+
val scrollMomentumBeginHandlers = mutableMapOf<View, String>()
|
|
634
|
+
val scrollMomentumEndHandlers = mutableMapOf<View, String>()
|
|
635
|
+
val sliderCompleteHandlers = mutableMapOf<View, String>()
|
|
636
|
+
val sliderSteps = mutableMapOf<View, Double>()
|
|
338
637
|
val focusHandlers = mutableMapOf<View, String>()
|
|
339
638
|
val blurHandlers = mutableMapOf<View, String>()
|
|
639
|
+
val endEditingHandlers = mutableMapOf<View, String>()
|
|
340
640
|
val submitHandlers = mutableMapOf<View, String>()
|
|
641
|
+
val imageLoadStartHandlers = mutableMapOf<ImageView, String>()
|
|
642
|
+
val imageLoadHandlers = mutableMapOf<ImageView, String>()
|
|
643
|
+
val imageLoadEndHandlers = mutableMapOf<ImageView, String>()
|
|
644
|
+
val imageErrorHandlers = mutableMapOf<ImageView, String>()
|
|
645
|
+
val layoutHandlers = mutableMapOf<View, String>()
|
|
646
|
+
val lastLayoutBounds = mutableMapOf<View, LayoutBounds>()
|
|
341
647
|
val flatListRows = mutableMapOf<CraftNativeFlatList, MutableMap<String, MutableMap<String, View>>>()
|
|
342
648
|
val flatListOwners = mutableMapOf<String, String>()
|
|
343
649
|
val flatListOwnerByView = mutableMapOf<CraftNativeFlatList, String>()
|
|
@@ -374,14 +680,24 @@ class MainActivity : AppCompatActivity() {
|
|
|
374
680
|
private val imageSources = mutableMapOf<ImageView, String>()
|
|
375
681
|
private val imageJobs = mutableMapOf<ImageView, Future<*>>()
|
|
376
682
|
private val imageErrors = mutableMapOf<ImageView, String>()
|
|
683
|
+
private val inputHintDefaults = mutableMapOf<EditText, ColorStateList>()
|
|
684
|
+
private val inputHighlightDefaults = mutableMapOf<EditText, Int>()
|
|
685
|
+
private val defaultBackgrounds = mutableMapOf<View, Drawable?>()
|
|
377
686
|
|
|
378
687
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
688
|
+
applyConfiguredAppearance()
|
|
379
689
|
super.onCreate(savedInstanceState)
|
|
380
690
|
initialDeepLink = savedInstanceState?.getString("craft.initialDeepLink")?.let(Uri::parse) ?: intent?.data
|
|
381
691
|
initialDeepLinkClaimed = savedInstanceState?.getBoolean("craft.initialDeepLinkClaimed", false) ?: false
|
|
382
692
|
deliveredInitialDeepLink = savedInstanceState?.getBoolean("craft.deliveredInitialDeepLink", false) ?: false
|
|
383
693
|
setContentView(R.layout.activity_main)
|
|
384
694
|
root = findViewById(R.id.native_root)
|
|
695
|
+
ViewCompat.setOnApplyWindowInsetsListener(root) { view, insets ->
|
|
696
|
+
val safe = insets.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout())
|
|
697
|
+
view.setPadding(safe.left, safe.top, safe.right, safe.bottom)
|
|
698
|
+
insets
|
|
699
|
+
}
|
|
700
|
+
ViewCompat.requestApplyInsets(root)
|
|
385
701
|
config = runCatching {
|
|
386
702
|
assets.open("craft.config.json").bufferedReader().use {
|
|
387
703
|
JSONObject(it.readText())
|
|
@@ -390,9 +706,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
390
706
|
hapticsEnabled = config.optBoolean("enableHaptics", false)
|
|
391
707
|
clipboardEnabled = config.optBoolean("enableClipboard", false)
|
|
392
708
|
capabilities = CraftNativeCapabilities(this, config)
|
|
393
|
-
|
|
394
|
-
root.setBackgroundColor(background)
|
|
395
|
-
defaultTextColor = if (Color.luminance(background) < 0.5) Color.WHITE else Color.BLACK
|
|
709
|
+
refreshThemeDefaults()
|
|
396
710
|
bundle = runCatching {
|
|
397
711
|
assets.open("native-screen.js").bufferedReader().use { it.readText() }
|
|
398
712
|
}.getOrElse {
|
|
@@ -433,6 +747,23 @@ class MainActivity : AppCompatActivity() {
|
|
|
433
747
|
}
|
|
434
748
|
}
|
|
435
749
|
|
|
750
|
+
private fun applyConfiguredAppearance() {
|
|
751
|
+
val configured = runCatching {
|
|
752
|
+
assets.open("craft.config.json").bufferedReader().use { JSONObject(it.readText()) }
|
|
753
|
+
}.getOrNull()
|
|
754
|
+
val mode = when (configured?.optString("appearance")) {
|
|
755
|
+
"light" -> AppCompatDelegate.MODE_NIGHT_NO
|
|
756
|
+
"dark" -> AppCompatDelegate.MODE_NIGHT_YES
|
|
757
|
+
"system" -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
|
758
|
+
else -> if (configured?.optBoolean("darkMode", true) == false) {
|
|
759
|
+
AppCompatDelegate.MODE_NIGHT_NO
|
|
760
|
+
} else {
|
|
761
|
+
AppCompatDelegate.MODE_NIGHT_YES
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
AppCompatDelegate.setDefaultNightMode(mode)
|
|
765
|
+
}
|
|
766
|
+
|
|
436
767
|
private fun attachSandbox(engine: JavaScriptSandbox) {
|
|
437
768
|
useMessagePorts = engine.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_MESSAGE_PORTS)
|
|
438
769
|
&& engine.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN)
|
|
@@ -470,6 +801,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
470
801
|
null
|
|
471
802
|
}
|
|
472
803
|
val activeScreen = Screen(route, params, view, isolate, port)
|
|
804
|
+
activeScreen.view.onLayoutChanged = {
|
|
805
|
+
if (activeScreen.active) emitLayoutEvents(activeScreen)
|
|
806
|
+
}
|
|
473
807
|
screen = activeScreen
|
|
474
808
|
screens.add(activeScreen)
|
|
475
809
|
show(activeScreen)
|
|
@@ -539,6 +873,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
539
873
|
root.removeAllViews()
|
|
540
874
|
(screen.view.parent as? ViewGroup)?.removeView(screen.view)
|
|
541
875
|
root.addView(screen.view)
|
|
876
|
+
refreshScreenTheme(screen)
|
|
542
877
|
}
|
|
543
878
|
|
|
544
879
|
override fun onSaveInstanceState(outState: Bundle) {
|
|
@@ -548,6 +883,37 @@ class MainActivity : AppCompatActivity() {
|
|
|
548
883
|
super.onSaveInstanceState(outState)
|
|
549
884
|
}
|
|
550
885
|
|
|
886
|
+
override fun onConfigurationChanged(newConfig: Configuration) {
|
|
887
|
+
super.onConfigurationChanged(newConfig)
|
|
888
|
+
if (!::root.isInitialized) return
|
|
889
|
+
refreshThemeDefaults()
|
|
890
|
+
ViewCompat.requestApplyInsets(root)
|
|
891
|
+
refreshInputDefaultsForTraitChange()
|
|
892
|
+
screens.lastOrNull()?.let { screen ->
|
|
893
|
+
refreshScreenTheme(screen)
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
private fun refreshScreenTheme(screen: Screen) {
|
|
898
|
+
screen.mutations.node("root")?.let { renderCommitted(screen, it) }
|
|
899
|
+
screen.controls.values.filterIsInstance<CraftNativeFlatList>().forEach { it.refreshThemeDefaults() }
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
private fun refreshThemeDefaults() {
|
|
903
|
+
val background = ContextCompat.getColor(this, R.color.background)
|
|
904
|
+
root.setBackgroundColor(background)
|
|
905
|
+
defaultTextColor = if (Color.luminance(background) < 0.5) Color.WHITE else Color.BLACK
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
private fun refreshInputDefaultsForTraitChange() {
|
|
909
|
+
if (inputHintDefaults.isEmpty() && inputHighlightDefaults.isEmpty()) return
|
|
910
|
+
val probe = EditText(this)
|
|
911
|
+
val hintColors = probe.hintTextColors
|
|
912
|
+
val highlightColor = probe.highlightColor
|
|
913
|
+
inputHintDefaults.keys.toList().forEach { inputHintDefaults[it] = hintColors }
|
|
914
|
+
inputHighlightDefaults.keys.toList().forEach { inputHighlightDefaults[it] = highlightColor }
|
|
915
|
+
}
|
|
916
|
+
|
|
551
917
|
private fun receive(screen: Screen, raw: String) {
|
|
552
918
|
val message = runCatching { JSONObject(raw) }.getOrNull() ?: return
|
|
553
919
|
val payload = message.optJSONObject("payload") ?: JSONObject()
|
|
@@ -679,12 +1045,38 @@ class MainActivity : AppCompatActivity() {
|
|
|
679
1045
|
?: explicitKey?.let { "${path.substringBeforeLast('.', "")}/$it" }
|
|
680
1046
|
?: path
|
|
681
1047
|
val previous = previousControls[identity]
|
|
1048
|
+
val reusableInput = previous as? EditText
|
|
682
1049
|
val control = when (type) {
|
|
683
1050
|
"Text" -> ((previous as? TextView)?.takeUnless { it is Button || it is EditText } ?: TextView(this)).apply {
|
|
1051
|
+
maxLines = props.optInt("numberOfLines", 0).takeIf { it > 0 } ?: Int.MAX_VALUE
|
|
1052
|
+
ellipsize = when (props.optString("ellipsizeMode")) {
|
|
1053
|
+
"head" -> TextUtils.TruncateAt.START
|
|
1054
|
+
"middle" -> TextUtils.TruncateAt.MIDDLE
|
|
1055
|
+
"tail" -> TextUtils.TruncateAt.END
|
|
1056
|
+
else -> null
|
|
1057
|
+
}
|
|
684
1058
|
configureText(this, childText(children), style)
|
|
685
1059
|
}
|
|
1060
|
+
"Link" -> ((previous as? TextView)?.takeUnless { it is Button } ?: TextView(this)).apply {
|
|
1061
|
+
configureText(this, props.optString("title").ifBlank { childText(children) }, style)
|
|
1062
|
+
val linkColor = props.optString("color").takeIf { it.isNotBlank() } ?: style.optString("color")
|
|
1063
|
+
setTextColor(color(linkColor, Color.rgb(33, 150, 243)))
|
|
1064
|
+
paintFlags = paintFlags or Paint.UNDERLINE_TEXT_FLAG
|
|
1065
|
+
isEnabled = !props.optBoolean("disabled", false)
|
|
1066
|
+
isClickable = true
|
|
1067
|
+
isFocusable = true
|
|
1068
|
+
val handler = events.optString("onPress").ifBlank { events.optString("onClick") }
|
|
1069
|
+
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
1070
|
+
setOnClickListener {
|
|
1071
|
+
val current = screen.handlers[this] ?: return@setOnClickListener
|
|
1072
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
686
1075
|
"Button" -> (previous as? Button ?: Button(this)).apply {
|
|
687
|
-
|
|
1076
|
+
val title = props.optString("title").ifBlank { childText(children) }
|
|
1077
|
+
configureText(this, title, style)
|
|
1078
|
+
props.optString("color").takeIf { it.isNotBlank() }?.let { setTextColor(color(it, defaultTextColor)) }
|
|
1079
|
+
isEnabled = !props.optBoolean("disabled", false)
|
|
688
1080
|
val handler = events.optString("onPress").ifBlank { events.optString("onClick") }
|
|
689
1081
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
690
1082
|
setOnClickListener {
|
|
@@ -692,26 +1084,62 @@ class MainActivity : AppCompatActivity() {
|
|
|
692
1084
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
693
1085
|
}
|
|
694
1086
|
}
|
|
695
|
-
"TextInput" -> (
|
|
1087
|
+
"TextInput" -> (reusableInput?.takeIf { it.isSingleLine == !props.optBoolean("multiline", false) }
|
|
1088
|
+
?: EditText(this)).apply {
|
|
1089
|
+
val defaultHintColors = inputHintDefaults.getOrPut(this) {
|
|
1090
|
+
hintTextColors ?: ColorStateList.valueOf(Color.GRAY)
|
|
1091
|
+
}
|
|
1092
|
+
val defaultHighlightColor = inputHighlightDefaults.getOrPut(this) { highlightColor }
|
|
696
1093
|
hint = props.optString("placeholder")
|
|
1094
|
+
setHintTextColor(
|
|
1095
|
+
props.optString("placeholderTextColor").takeIf { it.isNotBlank() }
|
|
1096
|
+
?.let { ColorStateList.valueOf(color(it, Color.GRAY)) }
|
|
1097
|
+
?: defaultHintColors,
|
|
1098
|
+
)
|
|
1099
|
+
highlightColor = props.optString("selectionColor").takeIf { it.isNotBlank() }
|
|
1100
|
+
?.let { color(it, defaultHighlightColor) }
|
|
1101
|
+
?: defaultHighlightColor
|
|
1102
|
+
val maxLength = props.optInt("maxLength", 0)
|
|
1103
|
+
filters = if (maxLength > 0) arrayOf(InputFilter.LengthFilter(maxLength)) else emptyArray()
|
|
697
1104
|
inputType = inputTypeFor(props)
|
|
698
1105
|
imeOptions = imeOptionsFor(props)
|
|
699
|
-
|
|
1106
|
+
val multiline = props.optBoolean("multiline", false)
|
|
1107
|
+
isSingleLine = !multiline
|
|
1108
|
+
maxLines = if (multiline) props.optInt("numberOfLines", Int.MAX_VALUE).coerceAtLeast(1) else 1
|
|
700
1109
|
isEnabled = props.optBoolean("editable", true)
|
|
701
|
-
|
|
1110
|
+
configureTextStyle(this, style)
|
|
1111
|
+
val controlledValue = props.opt("value")?.takeUnless { it == JSONObject.NULL }?.toString()
|
|
1112
|
+
if (controlledValue != null && text.toString() != controlledValue) {
|
|
1113
|
+
screen.suppressedTextChanges.add(this)
|
|
1114
|
+
setTextPreservingSelection(this, controlledValue)
|
|
1115
|
+
}
|
|
702
1116
|
else if (this !== previous && screen.drafts.containsKey(identity)) setText(screen.drafts[identity])
|
|
1117
|
+
else if (this !== previous && props.has("defaultValue") && !props.isNull("defaultValue")) setText(props.optString("defaultValue"))
|
|
1118
|
+
screen.inputIdentities[this] = identity
|
|
703
1119
|
val handler = events.optString("onChange").ifBlank { events.optString("onChangeText") }
|
|
704
1120
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
705
1121
|
val onFocus = events.optString("onFocus")
|
|
706
1122
|
if (onFocus.isNotBlank()) screen.focusHandlers[this] = onFocus else screen.focusHandlers.remove(this)
|
|
707
|
-
val onBlur = events.optString("onBlur")
|
|
1123
|
+
val onBlur = events.optString("onBlur")
|
|
708
1124
|
if (onBlur.isNotBlank()) screen.blurHandlers[this] = onBlur else screen.blurHandlers.remove(this)
|
|
1125
|
+
val onEndEditing = events.optString("onEndEditing")
|
|
1126
|
+
if (onEndEditing.isNotBlank()) screen.endEditingHandlers[this] = onEndEditing else screen.endEditingHandlers.remove(this)
|
|
709
1127
|
val onSubmit = events.optString("onSubmitEditing")
|
|
710
1128
|
if (onSubmit.isNotBlank()) screen.submitHandlers[this] = onSubmit else screen.submitHandlers.remove(this)
|
|
711
1129
|
setOnFocusChangeListener { view, focused ->
|
|
712
|
-
val
|
|
713
|
-
if (
|
|
714
|
-
|
|
1130
|
+
val nativeEvent = JSONObject().put("text", text.toString())
|
|
1131
|
+
if (focused) {
|
|
1132
|
+
screen.focusHandlers[view]?.let { current ->
|
|
1133
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", nativeEvent))
|
|
1134
|
+
}
|
|
1135
|
+
} else {
|
|
1136
|
+
screen.blurHandlers[view]?.let { current ->
|
|
1137
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", nativeEvent))
|
|
1138
|
+
}
|
|
1139
|
+
screen.endEditingHandlers[view]?.let { current ->
|
|
1140
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", nativeEvent))
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
715
1143
|
}
|
|
716
1144
|
setOnEditorActionListener { view, actionId, event ->
|
|
717
1145
|
val submitted = actionId != EditorInfo.IME_ACTION_NONE || event?.keyCode == KeyEvent.KEYCODE_ENTER
|
|
@@ -720,16 +1148,19 @@ class MainActivity : AppCompatActivity() {
|
|
|
720
1148
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject().put("text", text.toString())))
|
|
721
1149
|
true
|
|
722
1150
|
}
|
|
723
|
-
if (props.optBoolean("autoFocus", false)) post {
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
1151
|
+
if (props.optBoolean("autoFocus", false) && !isFocused) post {
|
|
1152
|
+
if (!isFocused) {
|
|
1153
|
+
requestFocus()
|
|
1154
|
+
(getSystemService(INPUT_METHOD_SERVICE) as? android.view.inputmethod.InputMethodManager)
|
|
1155
|
+
?.showSoftInput(this, android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT)
|
|
1156
|
+
}
|
|
727
1157
|
}
|
|
728
1158
|
if (this !== previous) addTextChangedListener(object : TextWatcher {
|
|
729
1159
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
|
730
1160
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
|
731
1161
|
val updated = s?.toString().orEmpty()
|
|
732
1162
|
screen.drafts[identity] = updated
|
|
1163
|
+
if (screen.suppressedTextChanges.remove(this@apply)) return
|
|
733
1164
|
val current = screen.handlers[this@apply] ?: return
|
|
734
1165
|
send(screen, "EVENT", JSONObject()
|
|
735
1166
|
.put("handlerName", current)
|
|
@@ -738,6 +1169,96 @@ class MainActivity : AppCompatActivity() {
|
|
|
738
1169
|
override fun afterTextChanged(s: Editable?) {}
|
|
739
1170
|
})
|
|
740
1171
|
}
|
|
1172
|
+
"Switch" -> (previous as? Switch ?: Switch(this)).apply {
|
|
1173
|
+
setOnCheckedChangeListener(null)
|
|
1174
|
+
val requestedValue = props.opt("value")
|
|
1175
|
+
isChecked = if (requestedValue != null && requestedValue != JSONObject.NULL) {
|
|
1176
|
+
props.optBoolean("value")
|
|
1177
|
+
} else {
|
|
1178
|
+
props.optBoolean("checked", false)
|
|
1179
|
+
}
|
|
1180
|
+
isEnabled = !props.optBoolean("disabled", false)
|
|
1181
|
+
props.optJSONObject("trackColor")?.let { track ->
|
|
1182
|
+
val falseColor = color(track.optString("false"), Color.GRAY)
|
|
1183
|
+
val trueColor = color(track.optString("true"), falseColor)
|
|
1184
|
+
trackTintList = ColorStateList(
|
|
1185
|
+
arrayOf(intArrayOf(android.R.attr.state_checked), intArrayOf()),
|
|
1186
|
+
intArrayOf(trueColor, falseColor),
|
|
1187
|
+
)
|
|
1188
|
+
} ?: run { trackTintList = null }
|
|
1189
|
+
thumbTintList = props.optString("thumbColor").takeIf { it.isNotBlank() }
|
|
1190
|
+
?.let { ColorStateList.valueOf(color(it, Color.WHITE)) }
|
|
1191
|
+
val handler = events.optString("onValueChange").ifBlank { events.optString("onChange") }
|
|
1192
|
+
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
1193
|
+
setOnCheckedChangeListener { _, checked ->
|
|
1194
|
+
val current = screen.handlers[this] ?: return@setOnCheckedChangeListener
|
|
1195
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject().put("value", checked)))
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
"Slider" -> (previous as? SeekBar ?: SeekBar(this)).apply {
|
|
1199
|
+
val minimum = props.optDouble("minimumValue", 0.0)
|
|
1200
|
+
val maximumCandidate = props.optDouble("maximumValue", 1.0)
|
|
1201
|
+
val maximum = if (maximumCandidate > minimum) maximumCandidate else minimum + Double.MIN_VALUE
|
|
1202
|
+
max = 1000
|
|
1203
|
+
setOnSeekBarChangeListener(null)
|
|
1204
|
+
val step = props.optDouble("step", 0.0).takeIf { it > 0 }
|
|
1205
|
+
val requested = props.optDouble("value", minimum)
|
|
1206
|
+
val value = if (step != null) minimum + ((requested - minimum) / step).roundToInt() * step else requested
|
|
1207
|
+
progress = (((value - minimum) / (maximum - minimum)) * max).roundToInt().coerceIn(0, max)
|
|
1208
|
+
if (step != null) screen.sliderSteps[this] = step else screen.sliderSteps.remove(this)
|
|
1209
|
+
progressTintList = props.optString("minimumTrackTintColor").takeIf { it.isNotBlank() }
|
|
1210
|
+
?.let { ColorStateList.valueOf(color(it, Color.WHITE)) }
|
|
1211
|
+
progressBackgroundTintList = props.optString("maximumTrackTintColor").takeIf { it.isNotBlank() }
|
|
1212
|
+
?.let { ColorStateList.valueOf(color(it, Color.GRAY)) }
|
|
1213
|
+
thumbTintList = props.optString("thumbTintColor").takeIf { it.isNotBlank() }
|
|
1214
|
+
?.let { ColorStateList.valueOf(color(it, Color.WHITE)) }
|
|
1215
|
+
isEnabled = !props.optBoolean("disabled", false)
|
|
1216
|
+
val handler = events.optString("onValueChange").ifBlank { events.optString("onChange") }
|
|
1217
|
+
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
1218
|
+
val complete = events.optString("onSlidingComplete")
|
|
1219
|
+
if (complete.isNotBlank()) screen.sliderCompleteHandlers[this] = complete else screen.sliderCompleteHandlers.remove(this)
|
|
1220
|
+
setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
|
1221
|
+
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
|
1222
|
+
val current = screen.handlers[seekBar] ?: return
|
|
1223
|
+
val raw = minimum + (maximum - minimum) * progress.toDouble() / max.toDouble()
|
|
1224
|
+
val step = screen.sliderSteps[seekBar]
|
|
1225
|
+
val value = if (step != null) minimum + ((raw - minimum) / step).roundToInt() * step else raw
|
|
1226
|
+
if (step != null) {
|
|
1227
|
+
val snappedProgress = (((value - minimum) / (maximum - minimum)) * max)
|
|
1228
|
+
.roundToInt().coerceIn(0, max)
|
|
1229
|
+
if (seekBar.progress != snappedProgress) {
|
|
1230
|
+
seekBar.progress = snappedProgress
|
|
1231
|
+
return
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject().put("value", value)))
|
|
1235
|
+
}
|
|
1236
|
+
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
|
1237
|
+
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
|
1238
|
+
val current = screen.sliderCompleteHandlers[seekBar] ?: return
|
|
1239
|
+
val raw = minimum + (maximum - minimum) * seekBar.progress.toDouble() / max.toDouble()
|
|
1240
|
+
val step = screen.sliderSteps[seekBar]
|
|
1241
|
+
val value = if (step != null) minimum + ((raw - minimum) / step).roundToInt() * step else raw
|
|
1242
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject().put("value", value)))
|
|
1243
|
+
}
|
|
1244
|
+
})
|
|
1245
|
+
}
|
|
1246
|
+
"ActivityIndicator" -> (previous as? android.widget.ProgressBar ?: android.widget.ProgressBar(this)).apply {
|
|
1247
|
+
val requestedSize = props.opt("size")
|
|
1248
|
+
val scale = when (requestedSize) {
|
|
1249
|
+
"large" -> 1.35f
|
|
1250
|
+
"small" -> 0.75f
|
|
1251
|
+
is Number -> (requestedSize.toDouble() / 24.0).toFloat().coerceAtLeast(0.5f)
|
|
1252
|
+
else -> 1f
|
|
1253
|
+
}
|
|
1254
|
+
scaleX = scale
|
|
1255
|
+
scaleY = scale
|
|
1256
|
+
val animating = props.optBoolean("animating", true)
|
|
1257
|
+
isIndeterminate = animating
|
|
1258
|
+
visibility = if (!animating && props.optBoolean("hidesWhenStopped", true)) View.GONE else View.VISIBLE
|
|
1259
|
+
indeterminateTintList = props.optString("color").takeIf { it.isNotBlank() }
|
|
1260
|
+
?.let { ColorStateList.valueOf(color(it, Color.WHITE)) }
|
|
1261
|
+
}
|
|
741
1262
|
"Image" -> (previous as? ImageView ?: ImageView(this)).apply {
|
|
742
1263
|
scaleType = when (style.optString("resizeMode").ifBlank { props.optString("resizeMode") }) {
|
|
743
1264
|
"cover" -> ImageView.ScaleType.CENTER_CROP
|
|
@@ -746,10 +1267,18 @@ class MainActivity : AppCompatActivity() {
|
|
|
746
1267
|
else -> ImageView.ScaleType.CENTER_INSIDE
|
|
747
1268
|
}
|
|
748
1269
|
adjustViewBounds = false
|
|
749
|
-
|
|
1270
|
+
val onLoadStart = events.optString("onLoadStart")
|
|
1271
|
+
if (onLoadStart.isNotBlank()) screen.imageLoadStartHandlers[this] = onLoadStart else screen.imageLoadStartHandlers.remove(this)
|
|
1272
|
+
val onLoad = events.optString("onLoad")
|
|
1273
|
+
if (onLoad.isNotBlank()) screen.imageLoadHandlers[this] = onLoad else screen.imageLoadHandlers.remove(this)
|
|
1274
|
+
val onLoadEnd = events.optString("onLoadEnd")
|
|
1275
|
+
if (onLoadEnd.isNotBlank()) screen.imageLoadEndHandlers[this] = onLoadEnd else screen.imageLoadEndHandlers.remove(this)
|
|
1276
|
+
val onError = events.optString("onError")
|
|
1277
|
+
if (onError.isNotBlank()) screen.imageErrorHandlers[this] = onError else screen.imageErrorHandlers.remove(this)
|
|
1278
|
+
updateImage(screen, this, props.opt("source"))
|
|
750
1279
|
}
|
|
751
1280
|
"ScrollView" -> (previous as? CraftNativeScrollView ?: CraftNativeScrollView(this)).apply {
|
|
752
|
-
val horizontal = props.optBoolean("horizontal", false) || style.optString("flexDirection")
|
|
1281
|
+
val horizontal = props.optBoolean("horizontal", false) || style.optString("flexDirection") in setOf("row", "row-reverse")
|
|
753
1282
|
setHorizontal(horizontal)
|
|
754
1283
|
setNativeNestedScrollingEnabled(true)
|
|
755
1284
|
setScrollIndicators(
|
|
@@ -757,12 +1286,22 @@ class MainActivity : AppCompatActivity() {
|
|
|
757
1286
|
props.optBoolean("showsHorizontalScrollIndicator", true),
|
|
758
1287
|
)
|
|
759
1288
|
setScrollEnabled(props.optBoolean("scrollEnabled", true))
|
|
1289
|
+
setBounces(props.optBoolean("bounces", true))
|
|
1290
|
+
setAlwaysBounce(
|
|
1291
|
+
props.optBoolean("alwaysBounceVertical", !horizontal),
|
|
1292
|
+
props.optBoolean("alwaysBounceHorizontal", horizontal),
|
|
1293
|
+
)
|
|
1294
|
+
setPagingEnabled(props.optBoolean("pagingEnabled", false))
|
|
760
1295
|
val onScroll = events.optString("onScroll")
|
|
761
1296
|
if (onScroll.isNotBlank()) screen.scrollHandlers[this] = onScroll else screen.scrollHandlers.remove(this)
|
|
762
1297
|
val onBegin = events.optString("onScrollBeginDrag")
|
|
763
1298
|
if (onBegin.isNotBlank()) screen.scrollBeginHandlers[this] = onBegin else screen.scrollBeginHandlers.remove(this)
|
|
764
1299
|
val onEnd = events.optString("onScrollEndDrag")
|
|
765
1300
|
if (onEnd.isNotBlank()) screen.scrollEndHandlers[this] = onEnd else screen.scrollEndHandlers.remove(this)
|
|
1301
|
+
val onMomentumBegin = events.optString("onMomentumScrollBegin")
|
|
1302
|
+
if (onMomentumBegin.isNotBlank()) screen.scrollMomentumBeginHandlers[this] = onMomentumBegin else screen.scrollMomentumBeginHandlers.remove(this)
|
|
1303
|
+
val onMomentumEnd = events.optString("onMomentumScrollEnd")
|
|
1304
|
+
if (onMomentumEnd.isNotBlank()) screen.scrollMomentumEndHandlers[this] = onMomentumEnd else screen.scrollMomentumEndHandlers.remove(this)
|
|
766
1305
|
val nativeScroll = this
|
|
767
1306
|
setScrollListeners(
|
|
768
1307
|
onScroll = { scrollX, scrollY ->
|
|
@@ -784,19 +1323,43 @@ class MainActivity : AppCompatActivity() {
|
|
|
784
1323
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
785
1324
|
}
|
|
786
1325
|
},
|
|
1326
|
+
onMomentumBegin = {
|
|
1327
|
+
screen.scrollMomentumBeginHandlers[nativeScroll]?.let { current ->
|
|
1328
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
1329
|
+
}
|
|
1330
|
+
},
|
|
1331
|
+
onMomentumEnd = {
|
|
1332
|
+
screen.scrollMomentumEndHandlers[nativeScroll]?.let { current ->
|
|
1333
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
1334
|
+
}
|
|
1335
|
+
},
|
|
1336
|
+
keyboardDismissMode = props.optString("keyboardDismissMode"),
|
|
1337
|
+
dismissKeyboard = {
|
|
1338
|
+
(getSystemService(INPUT_METHOD_SERVICE) as? android.view.inputmethod.InputMethodManager)
|
|
1339
|
+
?.hideSoftInputFromWindow(nativeScroll.windowToken, 0)
|
|
1340
|
+
},
|
|
787
1341
|
)
|
|
788
1342
|
configureContainer(content, style)
|
|
789
1343
|
reconcileChildren(screen, content, children, path, next, style, previousControls)
|
|
790
1344
|
}
|
|
791
1345
|
"FlatList" -> (previous as? CraftNativeFlatList ?: CraftNativeFlatList(this)).apply {
|
|
1346
|
+
setKeyboardDismissMode(props.optString("keyboardDismissMode")) {
|
|
1347
|
+
(getSystemService(INPUT_METHOD_SERVICE) as? android.view.inputmethod.InputMethodManager)
|
|
1348
|
+
?.hideSoftInputFromWindow(windowToken, 0)
|
|
1349
|
+
}
|
|
1350
|
+
overScrollMode = if (props.optBoolean("bounces", true)) View.OVER_SCROLL_ALWAYS else View.OVER_SCROLL_NEVER
|
|
792
1351
|
reconcileFlatList(screen, this, children, path, protocolId ?: path, props, events)
|
|
793
1352
|
}
|
|
1353
|
+
"View", "SafeAreaView" -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
|
|
1354
|
+
configureContainer(this, style)
|
|
1355
|
+
reconcileChildren(screen, this, children, path, next, style, previousControls)
|
|
1356
|
+
}
|
|
794
1357
|
else -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
|
|
795
1358
|
configureContainer(this, style)
|
|
796
1359
|
reconcileChildren(screen, this, children, path, next, style, previousControls)
|
|
797
1360
|
}
|
|
798
1361
|
}
|
|
799
|
-
if (type != "Button" && type != "TextInput") {
|
|
1362
|
+
if (type != "Button" && type != "Link" && type != "TextInput" && type != "Switch" && type != "Slider" && type != "ActivityIndicator") {
|
|
800
1363
|
val handler = events.optString("onPress").ifBlank { events.optString("onClick") }
|
|
801
1364
|
if (handler.isNotBlank()) {
|
|
802
1365
|
screen.handlers[control] = handler
|
|
@@ -811,13 +1374,40 @@ class MainActivity : AppCompatActivity() {
|
|
|
811
1374
|
control.setOnClickListener(null)
|
|
812
1375
|
}
|
|
813
1376
|
}
|
|
1377
|
+
val longPressHandler = events.optString("onLongPress")
|
|
1378
|
+
if (longPressHandler.isNotBlank()) {
|
|
1379
|
+
screen.longPressHandlers[control] = longPressHandler
|
|
1380
|
+
control.setOnLongClickListener {
|
|
1381
|
+
val current = screen.longPressHandlers[control] ?: return@setOnLongClickListener false
|
|
1382
|
+
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
1383
|
+
true
|
|
1384
|
+
}
|
|
1385
|
+
} else {
|
|
1386
|
+
screen.longPressHandlers.remove(control)
|
|
1387
|
+
control.setOnLongClickListener(null)
|
|
1388
|
+
}
|
|
814
1389
|
applyAccessibility(control, props, type, explicitKey ?: protocolId ?: path)
|
|
815
|
-
|
|
1390
|
+
val layoutHandler = events.optString("onLayout")
|
|
1391
|
+
if (layoutHandler.isNotBlank()) {
|
|
1392
|
+
if (screen.layoutHandlers[control] != layoutHandler) screen.lastLayoutBounds.remove(control)
|
|
1393
|
+
screen.layoutHandlers[control] = layoutHandler
|
|
1394
|
+
} else {
|
|
1395
|
+
screen.layoutHandlers.remove(control)
|
|
1396
|
+
screen.lastLayoutBounds.remove(control)
|
|
1397
|
+
}
|
|
1398
|
+
if (control is ImageView) imageErrors[control]?.let { message ->
|
|
1399
|
+
val current = control.contentDescription?.toString().orEmpty()
|
|
1400
|
+
if (!current.contains(message)) {
|
|
1401
|
+
control.contentDescription = listOf(current, message).filter { it.isNotBlank() }.joinToString(", ")
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
816
1404
|
next[identity] = control
|
|
817
1405
|
applyViewStyle(control, style)
|
|
1406
|
+
val width = style.optDouble("width", Double.NaN).takeUnless { it.isNaN() }?.let(::dp)
|
|
1407
|
+
val height = style.optDouble("height", Double.NaN).takeUnless { it.isNaN() }?.let(::dp)
|
|
818
1408
|
control.layoutParams = LinearLayout.LayoutParams(
|
|
819
|
-
|
|
820
|
-
|
|
1409
|
+
width ?: ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
1410
|
+
height ?: ViewGroup.LayoutParams.WRAP_CONTENT
|
|
821
1411
|
).apply {
|
|
822
1412
|
val margin = style.optDouble("margin", 0.0)
|
|
823
1413
|
val horizontal = style.optDouble("marginHorizontal", margin)
|
|
@@ -843,6 +1433,17 @@ class MainActivity : AppCompatActivity() {
|
|
|
843
1433
|
layout.configure(style, resources.displayMetrics.density)
|
|
844
1434
|
}
|
|
845
1435
|
|
|
1436
|
+
private fun flatListScrollEvent(list: CraftNativeFlatList): JSONObject = JSONObject()
|
|
1437
|
+
.put("contentOffset", JSONObject()
|
|
1438
|
+
.put("x", list.computeHorizontalScrollOffset())
|
|
1439
|
+
.put("y", list.computeVerticalScrollOffset()))
|
|
1440
|
+
.put("contentSize", JSONObject()
|
|
1441
|
+
.put("width", list.computeHorizontalScrollRange())
|
|
1442
|
+
.put("height", list.computeVerticalScrollRange()))
|
|
1443
|
+
.put("layoutMeasurement", JSONObject()
|
|
1444
|
+
.put("width", list.width)
|
|
1445
|
+
.put("height", list.height))
|
|
1446
|
+
|
|
846
1447
|
private fun reconcileChildren(
|
|
847
1448
|
screen: Screen,
|
|
848
1449
|
layout: CraftNativeFlexLayout,
|
|
@@ -895,17 +1496,35 @@ class MainActivity : AppCompatActivity() {
|
|
|
895
1496
|
) {
|
|
896
1497
|
screen.flatListOwners.entries.removeAll { it.value == owner }
|
|
897
1498
|
screen.flatListOwnerByView[list] = owner
|
|
1499
|
+
list.onLayoutChanged = {
|
|
1500
|
+
if (screen.active) emitLayoutEvents(screen)
|
|
1501
|
+
}
|
|
898
1502
|
val nodes = (0 until children.length()).mapNotNull { children.optJSONObject(it) }
|
|
899
1503
|
nodes.forEach { registerFlatListOwnership(screen, it, owner) }
|
|
1504
|
+
list.onScrollEvent = events.optString("onScroll").takeIf { it.isNotBlank() }?.let { handler ->
|
|
1505
|
+
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", flatListScrollEvent(list))) }
|
|
1506
|
+
}
|
|
1507
|
+
list.onScrollBeginDrag = events.optString("onScrollBeginDrag").takeIf { it.isNotBlank() }?.let { handler ->
|
|
1508
|
+
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", flatListScrollEvent(list))) }
|
|
1509
|
+
}
|
|
1510
|
+
list.onScrollEndDrag = events.optString("onScrollEndDrag").takeIf { it.isNotBlank() }?.let { handler ->
|
|
1511
|
+
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", flatListScrollEvent(list))) }
|
|
1512
|
+
}
|
|
1513
|
+
list.onMomentumScrollBegin = events.optString("onMomentumScrollBegin").takeIf { it.isNotBlank() }?.let { handler ->
|
|
1514
|
+
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", flatListScrollEvent(list))) }
|
|
1515
|
+
}
|
|
1516
|
+
list.onMomentumScrollEnd = events.optString("onMomentumScrollEnd").takeIf { it.isNotBlank() }?.let { handler ->
|
|
1517
|
+
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", flatListScrollEvent(list))) }
|
|
1518
|
+
}
|
|
900
1519
|
list.apply(
|
|
901
1520
|
nodes = nodes,
|
|
902
1521
|
horizontal = props.optBoolean("horizontal", false),
|
|
903
1522
|
columns = props.optInt("numColumns", 1),
|
|
904
1523
|
inverted = props.optBoolean("inverted", false),
|
|
905
|
-
endReachedThreshold =
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1524
|
+
endReachedThreshold = props.opt("onEndReachedThreshold")
|
|
1525
|
+
?.takeUnless { it == JSONObject.NULL }
|
|
1526
|
+
?.let { props.optDouble("onEndReachedThreshold", 0.1) }
|
|
1527
|
+
?: props.optDouble("threshold", 0.1),
|
|
909
1528
|
renderItem = { node, itemIdentity, _ ->
|
|
910
1529
|
val rows = screen.flatListRows.getOrPut(list) { mutableMapOf() }
|
|
911
1530
|
val previous = rows[itemIdentity].orEmpty()
|
|
@@ -917,7 +1536,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
917
1536
|
},
|
|
918
1537
|
recycleItem = { itemIdentity ->
|
|
919
1538
|
val controls = screen.flatListRows[list]?.remove(itemIdentity).orEmpty()
|
|
920
|
-
releaseViewState(screen, controls.values.toList())
|
|
1539
|
+
releaseViewState(screen, controls.values.toList(), preservingInputDrafts = true)
|
|
921
1540
|
},
|
|
922
1541
|
endReached = events.optString("onEndReached").takeIf { it.isNotBlank() }?.let { handler ->
|
|
923
1542
|
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject())) }
|
|
@@ -942,21 +1561,26 @@ class MainActivity : AppCompatActivity() {
|
|
|
942
1561
|
view.alpha = style.optDouble("opacity", 1.0).toFloat()
|
|
943
1562
|
view.visibility = if (style.optString("display") == "none") View.GONE else View.VISIBLE
|
|
944
1563
|
view.elevation = dp(style.optDouble("elevation", 0.0)).toFloat()
|
|
945
|
-
val background = style.has("backgroundColor")
|
|
1564
|
+
val background = style.has("backgroundColor") && !style.isNull("backgroundColor")
|
|
946
1565
|
val border = style.optDouble("borderWidth", 0.0)
|
|
947
1566
|
val radius = style.optDouble("borderRadius", 0.0)
|
|
1567
|
+
val defaultBackground = if (defaultBackgrounds.containsKey(view)) {
|
|
1568
|
+
defaultBackgrounds[view]
|
|
1569
|
+
} else {
|
|
1570
|
+
view.background.also { defaultBackgrounds[view] = it }
|
|
1571
|
+
}
|
|
948
1572
|
if (background || border > 0 || radius > 0) {
|
|
949
1573
|
view.background = GradientDrawable().apply {
|
|
950
1574
|
setColor(color(style.optString("backgroundColor"), Color.TRANSPARENT))
|
|
951
1575
|
cornerRadius = dp(radius).toFloat()
|
|
952
1576
|
if (border > 0) setStroke(dp(border), color(style.optString("borderColor"), Color.TRANSPARENT))
|
|
953
1577
|
}
|
|
954
|
-
}
|
|
1578
|
+
} else view.background = defaultBackground
|
|
955
1579
|
view.clipToOutline = style.optString("overflow") == "hidden"
|
|
956
1580
|
if (view is ViewGroup) view.clipChildren = style.optString("overflow") == "hidden"
|
|
957
1581
|
if (view is ImageView) {
|
|
958
|
-
|
|
959
|
-
|
|
1582
|
+
style.optString("tintColor").takeIf { it.isNotBlank() }?.let { view.setColorFilter(color(it, Color.TRANSPARENT)) }
|
|
1583
|
+
?: view.clearColorFilter()
|
|
960
1584
|
}
|
|
961
1585
|
}
|
|
962
1586
|
|
|
@@ -967,8 +1591,12 @@ class MainActivity : AppCompatActivity() {
|
|
|
967
1591
|
"capitalize" -> value.split(' ').joinToString(" ") { word -> word.replaceFirstChar(Char::uppercase) }
|
|
968
1592
|
else -> value
|
|
969
1593
|
}
|
|
1594
|
+
configureTextStyle(view, style)
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
private fun configureTextStyle(view: TextView, style: JSONObject) {
|
|
970
1598
|
view.setTextColor(color(style.optString("color"), defaultTextColor))
|
|
971
|
-
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, style.optDouble("fontSize",
|
|
1599
|
+
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, style.optDouble("fontSize", 16.0).toFloat())
|
|
972
1600
|
val weight = when (style.optString("fontWeight")) {
|
|
973
1601
|
"bold" -> 700
|
|
974
1602
|
"100", "200", "300", "400", "500", "600", "700", "800", "900" -> style.optInt("fontWeight")
|
|
@@ -987,10 +1615,19 @@ class MainActivity : AppCompatActivity() {
|
|
|
987
1615
|
"justify" -> View.TEXT_ALIGNMENT_VIEW_START
|
|
988
1616
|
else -> View.TEXT_ALIGNMENT_INHERIT
|
|
989
1617
|
}
|
|
990
|
-
|
|
991
|
-
|
|
1618
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
1619
|
+
view.justificationMode = if (style.optString("textAlign") == "justify") {
|
|
1620
|
+
Layout.JUSTIFICATION_MODE_INTER_WORD
|
|
1621
|
+
} else {
|
|
1622
|
+
Layout.JUSTIFICATION_MODE_NONE
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
view.letterSpacing = style.optDouble("letterSpacing", Double.NaN)
|
|
1626
|
+
.takeUnless { it.isNaN() }
|
|
1627
|
+
?.let { if (view.textSize > 0) dp(it).toFloat() / view.textSize else 0f }
|
|
1628
|
+
?: 0f
|
|
992
1629
|
view.setLineSpacing(
|
|
993
|
-
|
|
1630
|
+
style.optDouble("lineHeight", Double.NaN).takeUnless { it.isNaN() }?.let { dp(it) - view.textSize } ?: 0f,
|
|
994
1631
|
1f
|
|
995
1632
|
)
|
|
996
1633
|
view.paintFlags = view.paintFlags and Paint.UNDERLINE_TEXT_FLAG.inv() and Paint.STRIKE_THRU_TEXT_FLAG.inv()
|
|
@@ -1002,7 +1639,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
1002
1639
|
}
|
|
1003
1640
|
}
|
|
1004
1641
|
|
|
1005
|
-
private fun updateImage(view: ImageView, source: Any?) {
|
|
1642
|
+
private fun updateImage(screen: Screen, view: ImageView, source: Any?) {
|
|
1006
1643
|
val uri = when (source) {
|
|
1007
1644
|
is String -> source
|
|
1008
1645
|
is JSONObject -> source.optString("uri")
|
|
@@ -1013,6 +1650,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
1013
1650
|
imageSources.remove(view)
|
|
1014
1651
|
imageErrors[view] = "Image source is missing"
|
|
1015
1652
|
view.setImageDrawable(null)
|
|
1653
|
+
imageFailure(screen, view, uri, "Image source is missing")
|
|
1016
1654
|
return
|
|
1017
1655
|
}
|
|
1018
1656
|
if (imageSources[view] == uri) return
|
|
@@ -1020,20 +1658,29 @@ class MainActivity : AppCompatActivity() {
|
|
|
1020
1658
|
imageSources[view] = uri
|
|
1021
1659
|
imageErrors.remove(view)
|
|
1022
1660
|
view.setImageDrawable(null)
|
|
1661
|
+
emitImageEvent(screen, view, screen.imageLoadStartHandlers[view])
|
|
1023
1662
|
|
|
1024
1663
|
if (uri.startsWith("data:image/") && uri.contains(',')) {
|
|
1025
1664
|
val bytes = runCatching { Base64.decode(uri.substringAfter(','), Base64.DEFAULT) }.getOrNull()
|
|
1026
1665
|
val bitmap = bytes?.let { BitmapFactory.decodeByteArray(it, 0, it.size) }
|
|
1027
|
-
if (bitmap != null)
|
|
1666
|
+
if (bitmap != null) {
|
|
1667
|
+
view.setImageBitmap(bitmap)
|
|
1668
|
+
emitImageEvent(screen, view, screen.imageLoadHandlers[view])
|
|
1669
|
+
emitImageEvent(screen, view, screen.imageLoadEndHandlers[view])
|
|
1670
|
+
} else imageFailure(screen, view, uri, "Image data is invalid")
|
|
1028
1671
|
return
|
|
1029
1672
|
}
|
|
1030
1673
|
if (!uri.contains(':')) {
|
|
1031
1674
|
val bitmap = runCatching { assets.open(uri).use { BitmapFactory.decodeStream(it) } }.getOrNull()
|
|
1032
|
-
if (bitmap != null)
|
|
1675
|
+
if (bitmap != null) {
|
|
1676
|
+
view.setImageBitmap(bitmap)
|
|
1677
|
+
emitImageEvent(screen, view, screen.imageLoadHandlers[view])
|
|
1678
|
+
emitImageEvent(screen, view, screen.imageLoadEndHandlers[view])
|
|
1679
|
+
} else imageFailure(screen, view, uri, "Bundled image was not found")
|
|
1033
1680
|
return
|
|
1034
1681
|
}
|
|
1035
1682
|
if (!uri.startsWith("https://")) {
|
|
1036
|
-
imageFailure(view, uri, "Unsupported image source")
|
|
1683
|
+
imageFailure(screen, view, uri, "Unsupported image source")
|
|
1037
1684
|
return
|
|
1038
1685
|
}
|
|
1039
1686
|
imageJobs[view] = imageExecutor.submit {
|
|
@@ -1047,27 +1694,68 @@ class MainActivity : AppCompatActivity() {
|
|
|
1047
1694
|
runOnUiThread {
|
|
1048
1695
|
if (imageSources[view] != uri) return@runOnUiThread
|
|
1049
1696
|
imageJobs.remove(view)
|
|
1050
|
-
if (bitmap != null)
|
|
1697
|
+
if (bitmap != null) {
|
|
1698
|
+
view.setImageBitmap(bitmap)
|
|
1699
|
+
emitImageEvent(screen, view, screen.imageLoadHandlers[view])
|
|
1700
|
+
emitImageEvent(screen, view, screen.imageLoadEndHandlers[view])
|
|
1701
|
+
} else imageFailure(screen, view, uri, "Image download failed")
|
|
1051
1702
|
}
|
|
1052
1703
|
}
|
|
1053
1704
|
}
|
|
1054
1705
|
|
|
1055
|
-
private fun imageFailure(view: ImageView, uri: String, message: String) {
|
|
1706
|
+
private fun imageFailure(screen: Screen, view: ImageView, uri: String, message: String) {
|
|
1056
1707
|
imageErrors[view] = message
|
|
1057
1708
|
view.setImageDrawable(null)
|
|
1058
|
-
|
|
1709
|
+
val current = view.contentDescription?.toString().orEmpty()
|
|
1710
|
+
if (!current.contains(message)) {
|
|
1711
|
+
view.contentDescription = listOf(
|
|
1712
|
+
current.ifBlank { view.tag?.toString() ?: "Image" },
|
|
1713
|
+
message,
|
|
1714
|
+
).joinToString(", ")
|
|
1715
|
+
}
|
|
1716
|
+
emitImageEvent(screen, view, screen.imageErrorHandlers[view], JSONObject().put("error", JSONObject().put("message", message)))
|
|
1717
|
+
emitImageEvent(screen, view, screen.imageLoadEndHandlers[view])
|
|
1059
1718
|
Log.e(TAG, "$message: $uri")
|
|
1060
1719
|
}
|
|
1061
1720
|
|
|
1721
|
+
private fun emitImageEvent(screen: Screen, view: ImageView, handler: String?, nativeEvent: JSONObject = JSONObject()) {
|
|
1722
|
+
if (handler.isNullOrBlank()) return
|
|
1723
|
+
send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", nativeEvent))
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
private fun setTextPreservingSelection(field: EditText, value: String) {
|
|
1727
|
+
val start = field.selectionStart
|
|
1728
|
+
val end = field.selectionEnd
|
|
1729
|
+
field.setText(value)
|
|
1730
|
+
if (start < 0 || end < 0) {
|
|
1731
|
+
field.setSelection(value.length)
|
|
1732
|
+
return
|
|
1733
|
+
}
|
|
1734
|
+
val boundedStart = start.coerceIn(0, value.length)
|
|
1735
|
+
val boundedEnd = end.coerceIn(0, value.length)
|
|
1736
|
+
field.setSelection(minOf(boundedStart, boundedEnd), maxOf(boundedStart, boundedEnd))
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1062
1739
|
private fun applyAccessibility(view: View, props: JSONObject, type: String, fallbackLabel: String) {
|
|
1063
1740
|
view.tag = fallbackLabel
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
props.
|
|
1741
|
+
val label = props.optString("accessibilityLabel").takeIf { it.isNotBlank() }
|
|
1742
|
+
?: props.optString("alt").takeIf { type == "Image" && it.isNotBlank() }
|
|
1743
|
+
val value = props.opt("accessibilityValue")?.takeUnless { it == JSONObject.NULL }?.toString()
|
|
1744
|
+
view.contentDescription = when {
|
|
1745
|
+
label != null && value != null -> "$label, $value"
|
|
1746
|
+
label != null -> label
|
|
1747
|
+
value != null -> value
|
|
1748
|
+
else -> null
|
|
1749
|
+
}
|
|
1750
|
+
view.tooltipText = props.optString("accessibilityHint").takeIf { it.isNotBlank() }
|
|
1067
1751
|
val role = props.optString("accessibilityRole").ifBlank { type.lowercase() }
|
|
1068
1752
|
val state = props.optJSONObject("accessibilityState")
|
|
1069
1753
|
val disabled = state?.optBoolean("disabled", false) == true
|
|
1070
|
-
if (
|
|
1754
|
+
if (type !in setOf("Button", "Link", "TextInput", "Switch", "Slider", "ActivityIndicator")) {
|
|
1755
|
+
view.isEnabled = !disabled
|
|
1756
|
+
} else if (disabled) {
|
|
1757
|
+
view.isEnabled = false
|
|
1758
|
+
}
|
|
1071
1759
|
ViewCompat.setAccessibilityDelegate(view, object : AccessibilityDelegateCompat() {
|
|
1072
1760
|
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
|
|
1073
1761
|
super.onInitializeAccessibilityNodeInfo(host, info)
|
|
@@ -1080,7 +1768,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
1080
1768
|
}
|
|
1081
1769
|
info.isHeading = role == "header"
|
|
1082
1770
|
info.roleDescription = role.takeIf { it !in setOf("view", "text", "textinput", "scrollview") }
|
|
1083
|
-
info.
|
|
1771
|
+
info.hintText = host.tooltipText
|
|
1772
|
+
info.isEnabled = host.isEnabled && !disabled
|
|
1084
1773
|
info.isSelected = state?.optBoolean("selected", false) == true
|
|
1085
1774
|
info.isChecked = state?.optBoolean("checked", false) == true
|
|
1086
1775
|
}
|
|
@@ -1100,9 +1789,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
1100
1789
|
val keyboard = when (props.optString("keyboardType")) {
|
|
1101
1790
|
"email-address" -> InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
|
|
1102
1791
|
"url" -> InputType.TYPE_TEXT_VARIATION_URI
|
|
1103
|
-
"numeric" -> InputType.TYPE_CLASS_NUMBER
|
|
1792
|
+
"numeric", "number-pad" -> InputType.TYPE_CLASS_NUMBER
|
|
1104
1793
|
"decimal-pad" -> InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
|
|
1105
1794
|
"phone-pad" -> InputType.TYPE_CLASS_PHONE
|
|
1795
|
+
"web-search" -> InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT
|
|
1796
|
+
"visible-password" -> InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
|
|
1106
1797
|
else -> 0
|
|
1107
1798
|
}
|
|
1108
1799
|
val capitalization = when (props.optString("autoCapitalize")) {
|
|
@@ -1243,6 +1934,36 @@ class MainActivity : AppCompatActivity() {
|
|
|
1243
1934
|
return initialDeepLink
|
|
1244
1935
|
}
|
|
1245
1936
|
|
|
1937
|
+
private fun emitLayoutEvents(screen: Screen) {
|
|
1938
|
+
emitLayoutEvents(screen, screen.view)
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
private fun emitLayoutEvents(screen: Screen, view: View) {
|
|
1942
|
+
val handler = screen.layoutHandlers[view]
|
|
1943
|
+
if (handler != null && view.isShown) {
|
|
1944
|
+
val density = resources.displayMetrics.density
|
|
1945
|
+
val bounds = LayoutBounds(
|
|
1946
|
+
x = view.left.toDouble() / density.toDouble(),
|
|
1947
|
+
y = view.top.toDouble() / density.toDouble(),
|
|
1948
|
+
width = view.width.toDouble() / density.toDouble(),
|
|
1949
|
+
height = view.height.toDouble() / density.toDouble(),
|
|
1950
|
+
)
|
|
1951
|
+
if (screen.lastLayoutBounds[view] != bounds) {
|
|
1952
|
+
screen.lastLayoutBounds[view] = bounds
|
|
1953
|
+
send(screen, "EVENT", JSONObject()
|
|
1954
|
+
.put("handlerName", handler)
|
|
1955
|
+
.put("nativeEvent", JSONObject().put("layout", JSONObject()
|
|
1956
|
+
.put("x", bounds.x)
|
|
1957
|
+
.put("y", bounds.y)
|
|
1958
|
+
.put("width", bounds.width)
|
|
1959
|
+
.put("height", bounds.height))))
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
if (view is ViewGroup) {
|
|
1963
|
+
for (index in 0 until view.childCount) emitLayoutEvents(screen, view.getChildAt(index))
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1246
1967
|
private fun send(screen: Screen, type: String, payload: JSONObject, correlationId: String? = null) {
|
|
1247
1968
|
if (!screen.active) return
|
|
1248
1969
|
val message = JSONObject().put("type", type).put("payload", payload)
|
|
@@ -1276,22 +1997,48 @@ class MainActivity : AppCompatActivity() {
|
|
|
1276
1997
|
screen.flatListRows.values.forEach { rows -> rows.values.forEach { addAll(it.values) } }
|
|
1277
1998
|
}
|
|
1278
1999
|
|
|
1279
|
-
private fun releaseViewState(
|
|
2000
|
+
private fun releaseViewState(
|
|
2001
|
+
screen: Screen,
|
|
2002
|
+
views: Collection<View>,
|
|
2003
|
+
preservingInputDrafts: Boolean = false,
|
|
2004
|
+
) {
|
|
1280
2005
|
val released = views.toSet()
|
|
1281
2006
|
released.filterIsInstance<CraftNativeFlatList>().forEach { list ->
|
|
1282
2007
|
val owner = screen.flatListOwnerByView.remove(list)
|
|
1283
2008
|
list.discardAll()
|
|
1284
|
-
screen.flatListRows.remove(list)?.values?.forEach {
|
|
2009
|
+
screen.flatListRows.remove(list)?.values?.forEach {
|
|
2010
|
+
releaseViewState(screen, it.values, preservingInputDrafts)
|
|
2011
|
+
}
|
|
1285
2012
|
if (owner != null) screen.flatListOwners.entries.removeAll { it.value == owner }
|
|
1286
2013
|
}
|
|
1287
2014
|
releaseImages(released)
|
|
2015
|
+
released.filterIsInstance<EditText>().forEach {
|
|
2016
|
+
inputHintDefaults.remove(it)
|
|
2017
|
+
inputHighlightDefaults.remove(it)
|
|
2018
|
+
screen.suppressedTextChanges.remove(it)
|
|
2019
|
+
val identity = screen.inputIdentities.remove(it)
|
|
2020
|
+
if (!preservingInputDrafts) identity?.let(screen.drafts::remove)
|
|
2021
|
+
}
|
|
2022
|
+
released.forEach(defaultBackgrounds::remove)
|
|
1288
2023
|
screen.handlers.keys.removeAll(released)
|
|
2024
|
+
screen.longPressHandlers.keys.removeAll(released)
|
|
2025
|
+
screen.layoutHandlers.keys.removeAll(released)
|
|
2026
|
+
screen.lastLayoutBounds.keys.removeAll(released)
|
|
1289
2027
|
screen.scrollHandlers.keys.removeAll(released)
|
|
1290
2028
|
screen.scrollBeginHandlers.keys.removeAll(released)
|
|
1291
2029
|
screen.scrollEndHandlers.keys.removeAll(released)
|
|
2030
|
+
screen.scrollMomentumBeginHandlers.keys.removeAll(released)
|
|
2031
|
+
screen.scrollMomentumEndHandlers.keys.removeAll(released)
|
|
2032
|
+
screen.sliderCompleteHandlers.keys.removeAll(released)
|
|
2033
|
+
screen.sliderSteps.keys.removeAll(released)
|
|
1292
2034
|
screen.focusHandlers.keys.removeAll(released)
|
|
1293
2035
|
screen.blurHandlers.keys.removeAll(released)
|
|
2036
|
+
screen.endEditingHandlers.keys.removeAll(released)
|
|
1294
2037
|
screen.submitHandlers.keys.removeAll(released)
|
|
2038
|
+
screen.imageLoadStartHandlers.keys.removeAll(released)
|
|
2039
|
+
screen.imageLoadHandlers.keys.removeAll(released)
|
|
2040
|
+
screen.imageLoadEndHandlers.keys.removeAll(released)
|
|
2041
|
+
screen.imageErrorHandlers.keys.removeAll(released)
|
|
1295
2042
|
}
|
|
1296
2043
|
|
|
1297
2044
|
private fun closeScreen(screen: Screen) {
|
|
@@ -1302,12 +2049,23 @@ class MainActivity : AppCompatActivity() {
|
|
|
1302
2049
|
releaseViewState(screen, retainedViews(screen).toList())
|
|
1303
2050
|
screen.controls.clear()
|
|
1304
2051
|
screen.handlers.clear()
|
|
2052
|
+
screen.layoutHandlers.clear()
|
|
2053
|
+
screen.lastLayoutBounds.clear()
|
|
1305
2054
|
screen.scrollHandlers.clear()
|
|
1306
2055
|
screen.scrollBeginHandlers.clear()
|
|
1307
2056
|
screen.scrollEndHandlers.clear()
|
|
2057
|
+
screen.scrollMomentumBeginHandlers.clear()
|
|
2058
|
+
screen.scrollMomentumEndHandlers.clear()
|
|
2059
|
+
screen.sliderCompleteHandlers.clear()
|
|
2060
|
+
screen.sliderSteps.clear()
|
|
1308
2061
|
screen.focusHandlers.clear()
|
|
1309
2062
|
screen.blurHandlers.clear()
|
|
2063
|
+
screen.endEditingHandlers.clear()
|
|
1310
2064
|
screen.submitHandlers.clear()
|
|
2065
|
+
screen.imageLoadStartHandlers.clear()
|
|
2066
|
+
screen.imageLoadHandlers.clear()
|
|
2067
|
+
screen.imageLoadEndHandlers.clear()
|
|
2068
|
+
screen.imageErrorHandlers.clear()
|
|
1311
2069
|
screen.flatListRows.clear()
|
|
1312
2070
|
screen.flatListOwners.clear()
|
|
1313
2071
|
screen.flatListOwnerByView.clear()
|