craft-native 0.0.140 → 0.0.141
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/android/templates/CraftNative.kt.template +137 -0
- package/dist/android/templates/MainActivityNative.kt.template +822 -87
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.js +1 -0
- package/dist/ios/templates/CraftApp.swift +14 -482
- package/dist/ios/templates/CraftHealth.swift +551 -0
- package/dist/ios/templates/CraftNativeActions.swift +11 -0
- package/dist/ios/templates/CraftNativeScreen.swift +753 -52
- package/package.json +1 -1
|
@@ -6,9 +6,15 @@ import android.content.Context
|
|
|
6
6
|
import android.content.res.ColorStateList
|
|
7
7
|
import android.content.res.Configuration
|
|
8
8
|
import android.content.Intent
|
|
9
|
+
import android.media.AudioAttributes
|
|
10
|
+
import android.media.AudioFormat
|
|
11
|
+
import android.media.AudioTrack
|
|
9
12
|
import android.graphics.BitmapFactory
|
|
13
|
+
import android.graphics.Canvas
|
|
10
14
|
import android.graphics.Color
|
|
15
|
+
import android.graphics.DashPathEffect
|
|
11
16
|
import android.graphics.Paint
|
|
17
|
+
import android.graphics.Path
|
|
12
18
|
import android.graphics.Typeface
|
|
13
19
|
import android.graphics.drawable.Drawable
|
|
14
20
|
import android.graphics.drawable.GradientDrawable
|
|
@@ -28,11 +34,13 @@ import android.util.Base64
|
|
|
28
34
|
import android.util.TypedValue
|
|
29
35
|
import android.view.Gravity
|
|
30
36
|
import android.view.KeyEvent
|
|
37
|
+
import android.view.MotionEvent
|
|
31
38
|
import android.view.inputmethod.EditorInfo
|
|
32
39
|
import android.text.InputType
|
|
33
40
|
import android.text.Layout
|
|
34
41
|
import android.view.View
|
|
35
42
|
import android.view.ViewGroup
|
|
43
|
+
import android.view.ViewTreeObserver
|
|
36
44
|
import android.widget.AdapterView
|
|
37
45
|
import android.widget.ArrayAdapter
|
|
38
46
|
import android.widget.Button
|
|
@@ -46,6 +54,8 @@ import android.widget.SeekBar
|
|
|
46
54
|
import android.widget.Spinner
|
|
47
55
|
import android.widget.Switch
|
|
48
56
|
import android.widget.TextView
|
|
57
|
+
import android.widget.MediaController
|
|
58
|
+
import android.widget.VideoView
|
|
49
59
|
import androidx.appcompat.app.AppCompatActivity
|
|
50
60
|
import androidx.appcompat.app.AppCompatDelegate
|
|
51
61
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|
@@ -61,6 +71,7 @@ import androidx.javascriptengine.Message
|
|
|
61
71
|
import androidx.javascriptengine.MessagePort
|
|
62
72
|
import androidx.javascriptengine.MessagePortClient
|
|
63
73
|
import com.google.common.util.concurrent.ListenableFuture
|
|
74
|
+
import com.craft.runtime.CraftNative
|
|
64
75
|
import org.json.JSONArray
|
|
65
76
|
import org.json.JSONObject
|
|
66
77
|
import java.net.URL
|
|
@@ -69,7 +80,10 @@ import java.util.concurrent.Future
|
|
|
69
80
|
import java.util.UUID
|
|
70
81
|
import kotlin.math.max
|
|
71
82
|
import kotlin.math.min
|
|
83
|
+
import kotlin.math.PI
|
|
72
84
|
import kotlin.math.roundToInt
|
|
85
|
+
import kotlin.math.roundToLong
|
|
86
|
+
import kotlin.math.sin
|
|
73
87
|
|
|
74
88
|
private const val CRAFT_NATIVE_CAPABILITY_TIMEOUT_MS = 30_000L
|
|
75
89
|
|
|
@@ -154,6 +168,184 @@ private fun craftNativeGridColumnCount(value: Any?): Int {
|
|
|
154
168
|
return max(1, trimmed.split(Regex("\\s+")).count { it.isNotBlank() })
|
|
155
169
|
}
|
|
156
170
|
|
|
171
|
+
/** A DOM-free SVG surface for the geometry emitted by the shared STX native protocol. */
|
|
172
|
+
private class CraftNativeSvgView(context: Context) : View(context) {
|
|
173
|
+
var drawing: JSONObject = JSONObject()
|
|
174
|
+
set(value) {
|
|
175
|
+
field = value
|
|
176
|
+
invalidate()
|
|
177
|
+
requestLayout()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private data class SvgPaint(
|
|
181
|
+
val fill: String? = "#000000",
|
|
182
|
+
val stroke: String? = null,
|
|
183
|
+
val strokeWidth: Float = 1f,
|
|
184
|
+
val color: String = "#000000",
|
|
185
|
+
val opacity: Float = 1f,
|
|
186
|
+
val typeface: Typeface = Typeface.DEFAULT,
|
|
187
|
+
) {
|
|
188
|
+
fun merge(node: JSONObject): SvgPaint {
|
|
189
|
+
val props = node.optJSONObject("props") ?: JSONObject()
|
|
190
|
+
val style = node.optJSONObject("style") ?: JSONObject()
|
|
191
|
+
fun value(name: String): String? = when {
|
|
192
|
+
style.has(name) && !style.isNull(name) -> style.optString(name)
|
|
193
|
+
props.has(name) && !props.isNull(name) -> props.optString(name)
|
|
194
|
+
else -> null
|
|
195
|
+
}
|
|
196
|
+
val weight = value("fontWeight")
|
|
197
|
+
return copy(
|
|
198
|
+
fill = value("fill") ?: fill,
|
|
199
|
+
stroke = value("stroke") ?: stroke,
|
|
200
|
+
strokeWidth = value("strokeWidth")?.toFloatOrNull() ?: strokeWidth,
|
|
201
|
+
color = value("color") ?: color,
|
|
202
|
+
opacity = (value("opacity")?.toFloatOrNull() ?: 1f) * opacity,
|
|
203
|
+
typeface = if (weight in setOf("bold", "700")) Typeface.DEFAULT_BOLD else typeface,
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
209
|
+
val width = MeasureSpec.getSize(widthMeasureSpec)
|
|
210
|
+
val box = viewBox()
|
|
211
|
+
val naturalHeight = if (box != null && box.second > 0) (width * box.second / box.first).roundToInt() else 0
|
|
212
|
+
val height = when (MeasureSpec.getMode(heightMeasureSpec)) {
|
|
213
|
+
MeasureSpec.EXACTLY -> MeasureSpec.getSize(heightMeasureSpec)
|
|
214
|
+
MeasureSpec.AT_MOST -> min(naturalHeight, MeasureSpec.getSize(heightMeasureSpec))
|
|
215
|
+
else -> naturalHeight
|
|
216
|
+
}
|
|
217
|
+
setMeasuredDimension(resolveSize(width, widthMeasureSpec), resolveSize(height, heightMeasureSpec))
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
override fun onDraw(canvas: Canvas) {
|
|
221
|
+
super.onDraw(canvas)
|
|
222
|
+
val box = viewBox() ?: return
|
|
223
|
+
if (box.first <= 0 || box.second <= 0) return
|
|
224
|
+
val scale = min(width.toFloat() / box.first, height.toFloat() / box.second)
|
|
225
|
+
if (!scale.isFinite() || scale <= 0f) return
|
|
226
|
+
canvas.save()
|
|
227
|
+
canvas.translate((width - box.first * scale) / 2f, (height - box.second * scale) / 2f)
|
|
228
|
+
canvas.scale(scale, scale)
|
|
229
|
+
canvas.translate(-box.third, -box.fourth)
|
|
230
|
+
drawNodes(canvas, drawing.optJSONArray("children") ?: JSONArray(), SvgPaint().merge(drawing))
|
|
231
|
+
canvas.restore()
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private fun viewBox(): Quadruple<Float, Float, Float, Float>? {
|
|
235
|
+
val values = drawing.optJSONObject("props")?.optJSONArray("viewBox") ?: return null
|
|
236
|
+
if (values.length() != 4) return null
|
|
237
|
+
val x = values.optDouble(0, Double.NaN).toFloat()
|
|
238
|
+
val y = values.optDouble(1, Double.NaN).toFloat()
|
|
239
|
+
val width = values.optDouble(2, Double.NaN).toFloat()
|
|
240
|
+
val height = values.optDouble(3, Double.NaN).toFloat()
|
|
241
|
+
if (!x.isFinite() || !y.isFinite() || !width.isFinite() || !height.isFinite()) return null
|
|
242
|
+
return Quadruple(width, height, x, y)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private fun drawNodes(canvas: Canvas, children: JSONArray, inherited: SvgPaint) {
|
|
246
|
+
for (index in 0 until children.length()) {
|
|
247
|
+
val node = children.optJSONObject(index) ?: continue
|
|
248
|
+
val style = node.optJSONObject("style") ?: JSONObject()
|
|
249
|
+
if (style.optString("display") == "none") continue
|
|
250
|
+
val paint = inherited.merge(node)
|
|
251
|
+
when (node.optString("type")) {
|
|
252
|
+
"G" -> drawNodes(canvas, node.optJSONArray("children") ?: JSONArray(), paint)
|
|
253
|
+
"SvgText" -> drawText(canvas, node, paint)
|
|
254
|
+
else -> drawPath(canvas, node, paint)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private fun drawPath(canvas: Canvas, node: JSONObject, style: SvgPaint) {
|
|
260
|
+
val props = node.optJSONObject("props") ?: JSONObject()
|
|
261
|
+
val geometry = props.optJSONArray("geometry") ?: return
|
|
262
|
+
val path = Path()
|
|
263
|
+
var index = 0
|
|
264
|
+
while (index < geometry.length()) {
|
|
265
|
+
val command = geometry.optDouble(index, Double.NaN).toInt()
|
|
266
|
+
fun number(offset: Int): Float = geometry.optDouble(index + offset, Double.NaN).toFloat()
|
|
267
|
+
when (command) {
|
|
268
|
+
0 -> if (index + 2 < geometry.length()) { path.moveTo(number(1), number(2)); index += 3 } else break
|
|
269
|
+
1 -> if (index + 2 < geometry.length()) { path.lineTo(number(1), number(2)); index += 3 } else break
|
|
270
|
+
2 -> if (index + 6 < geometry.length()) { path.cubicTo(number(1), number(2), number(3), number(4), number(5), number(6)); index += 7 } else break
|
|
271
|
+
3 -> if (index + 4 < geometry.length()) { path.quadTo(number(1), number(2), number(3), number(4)); index += 5 } else break
|
|
272
|
+
4 -> { path.close(); index += 1 }
|
|
273
|
+
else -> break
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
val fill = android.graphics.Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
277
|
+
color = resolveColor(style.fill, style.color, Color.TRANSPARENT)
|
|
278
|
+
alpha = (style.opacity * 255f).roundToInt().coerceIn(0, 255)
|
|
279
|
+
this.style = Paint.Style.FILL
|
|
280
|
+
}
|
|
281
|
+
if (style.fill != "none" && style.fill != "transparent") canvas.drawPath(path, fill)
|
|
282
|
+
if (!style.stroke.isNullOrBlank() && style.stroke != "none" && style.stroke != "transparent" && style.strokeWidth > 0) {
|
|
283
|
+
val stroke = android.graphics.Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
284
|
+
color = resolveColor(style.stroke, style.color, Color.TRANSPARENT)
|
|
285
|
+
alpha = (style.opacity * 255f).roundToInt().coerceIn(0, 255)
|
|
286
|
+
this.style = Paint.Style.STROKE
|
|
287
|
+
strokeWidth = style.strokeWidth
|
|
288
|
+
strokeCap = when (props.optString("strokeLinecap")) {
|
|
289
|
+
"round" -> Paint.Cap.ROUND
|
|
290
|
+
"square" -> Paint.Cap.SQUARE
|
|
291
|
+
else -> Paint.Cap.BUTT
|
|
292
|
+
}
|
|
293
|
+
strokeJoin = when (props.optString("strokeLinejoin")) {
|
|
294
|
+
"round" -> Paint.Join.ROUND
|
|
295
|
+
"bevel" -> Paint.Join.BEVEL
|
|
296
|
+
else -> Paint.Join.MITER
|
|
297
|
+
}
|
|
298
|
+
props.optJSONArray("strokeDasharray")?.let { values ->
|
|
299
|
+
val dashes: List<Float> = (0 until values.length()).mapNotNull {
|
|
300
|
+
values.optDouble(it, Double.NaN).takeUnless { value -> value.isNaN() }?.toFloat()
|
|
301
|
+
}
|
|
302
|
+
if (dashes.isNotEmpty()) {
|
|
303
|
+
val dashPattern = dashes.toFloatArray()
|
|
304
|
+
val pattern: FloatArray = if (dashPattern.size % 2 == 1) {
|
|
305
|
+
FloatArray(dashPattern.size * 2) { index -> dashPattern[index % dashPattern.size] }
|
|
306
|
+
} else {
|
|
307
|
+
dashPattern
|
|
308
|
+
}
|
|
309
|
+
pathEffect = DashPathEffect(pattern, 0f)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
canvas.drawPath(path, stroke)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private fun drawText(canvas: Canvas, node: JSONObject, style: SvgPaint) {
|
|
318
|
+
val props = node.optJSONObject("props") ?: JSONObject()
|
|
319
|
+
val nodeStyle = node.optJSONObject("style") ?: JSONObject()
|
|
320
|
+
val text = buildString {
|
|
321
|
+
val children = node.optJSONArray("children") ?: JSONArray()
|
|
322
|
+
for (index in 0 until children.length()) if (children.opt(index) is String) append(children.optString(index))
|
|
323
|
+
}
|
|
324
|
+
if (text.isEmpty()) return
|
|
325
|
+
val size = (if (props.has("fontSize")) props.optDouble("fontSize", 12.0) else nodeStyle.optDouble("fontSize", 12.0))
|
|
326
|
+
.toFloat()
|
|
327
|
+
val paint = android.graphics.Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
328
|
+
color = resolveColor(style.fill, style.color, Color.BLACK)
|
|
329
|
+
alpha = (style.opacity * 255f).roundToInt().coerceIn(0, 255)
|
|
330
|
+
textSize = size
|
|
331
|
+
typeface = style.typeface
|
|
332
|
+
}
|
|
333
|
+
var x = props.optDouble("x", 0.0).toFloat()
|
|
334
|
+
val y = props.optDouble("y", 0.0).toFloat()
|
|
335
|
+
when (props.optString("textAnchor")) {
|
|
336
|
+
"middle" -> x -= paint.measureText(text) / 2f
|
|
337
|
+
"end" -> x -= paint.measureText(text)
|
|
338
|
+
}
|
|
339
|
+
canvas.drawText(text, x, y, paint)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private fun resolveColor(value: String?, current: String, fallback: Int): Int = runCatching {
|
|
343
|
+
Color.parseColor(if (value == "currentColor") current else value ?: current)
|
|
344
|
+
}.getOrDefault(fallback)
|
|
345
|
+
|
|
346
|
+
private data class Quadruple<T1, T2, T3, T4>(val first: T1, val second: T2, val third: T3, val fourth: T4)
|
|
347
|
+
}
|
|
348
|
+
|
|
157
349
|
/** A deterministic flex-shaped container shared by normal views and scroll content. */
|
|
158
350
|
private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
159
351
|
var onLayoutChanged: (() -> Unit)? = null
|
|
@@ -167,9 +359,11 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
167
359
|
private var grid = false
|
|
168
360
|
private var gridColumns = 1
|
|
169
361
|
private var gridAutoRows: Int? = null
|
|
362
|
+
private var reverse = false
|
|
170
363
|
|
|
171
364
|
fun configure(style: JSONObject, density: Float) {
|
|
172
365
|
orientation = if (style.optString("flexDirection") in setOf("row", "row-reverse")) HORIZONTAL else VERTICAL
|
|
366
|
+
reverse = style.optString("flexDirection").endsWith("-reverse")
|
|
173
367
|
wrap = style.optString("flexWrap") == "wrap" || style.optBoolean("flexWrap", false)
|
|
174
368
|
alignItems = style.optString("alignItems", "stretch")
|
|
175
369
|
alignContent = style.optString("alignContent", "flex-start")
|
|
@@ -214,7 +408,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
214
408
|
val gap = if (orientation == HORIZONTAL) columnGap else rowGap
|
|
215
409
|
val lines = mutableListOf<Line>()
|
|
216
410
|
val absolute = mutableListOf<Entry>()
|
|
217
|
-
|
|
411
|
+
val pending = mutableListOf<Entry>()
|
|
218
412
|
for (index in 0 until childCount) {
|
|
219
413
|
val child = getChildAt(index)
|
|
220
414
|
if (child.visibility == GONE) continue
|
|
@@ -236,18 +430,42 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
236
430
|
val childHeight = measureChildHeight(child, style, height - paddingTop - paddingBottom, verticalMargins)
|
|
237
431
|
val entry = Entry(child, style, childWidth, childHeight)
|
|
238
432
|
if (style.position == "absolute") { absolute += entry; continue }
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
433
|
+
pending += entry
|
|
434
|
+
}
|
|
435
|
+
if (pending.isNotEmpty()) {
|
|
436
|
+
val basis = pending.map { entry ->
|
|
437
|
+
if (orientation == HORIZONTAL) entry.width.toFloat() else entry.height.toFloat()
|
|
438
|
+
}.toFloatArray()
|
|
439
|
+
val margins = pending.flatMap { entry ->
|
|
440
|
+
val lp = entry.view.layoutParams as? MarginLayoutParams
|
|
441
|
+
if (orientation == HORIZONTAL) listOf((lp?.leftMargin ?: 0).toFloat(), (lp?.rightMargin ?: 0).toFloat())
|
|
442
|
+
else listOf((lp?.topMargin ?: 0).toFloat(), (lp?.bottomMargin ?: 0).toFloat())
|
|
443
|
+
}.toFloatArray()
|
|
444
|
+
val sharedLineIds = CraftNative.flexLines(availableMain.toFloat(), gap.toFloat(), wrap, basis, margins)
|
|
445
|
+
val lineIds = sharedLineIds?.takeIf { ids ->
|
|
446
|
+
ids.size >= pending.size && ids.firstOrNull() == 0 && ids.zipWithNext().all { (left, right) -> right >= left }
|
|
447
|
+
}
|
|
448
|
+
var line = Line()
|
|
449
|
+
pending.forEachIndexed { index, entry ->
|
|
450
|
+
val childMain = if (orientation == HORIZONTAL) entry.width + (margins[index * 2] + margins[index * 2 + 1]).roundToInt()
|
|
451
|
+
else entry.height + (margins[index * 2] + margins[index * 2 + 1]).roundToInt()
|
|
452
|
+
val lp = entry.view.layoutParams as? MarginLayoutParams
|
|
453
|
+
val crossMargins = if (orientation == HORIZONTAL) (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
|
|
454
|
+
else (lp?.leftMargin ?: 0) + (lp?.rightMargin ?: 0)
|
|
455
|
+
val childCross = if (orientation == HORIZONTAL) entry.height + crossMargins else entry.width + crossMargins
|
|
456
|
+
if (index > 0 && lineIds != null && lineIds[index] != lineIds[index - 1]) {
|
|
457
|
+
lines += line
|
|
458
|
+
line = Line()
|
|
459
|
+
} else if (index > 0 && lineIds == null && wrap && line.entries.isNotEmpty() && line.main + gap + childMain > availableMain) {
|
|
460
|
+
lines += line
|
|
461
|
+
line = Line()
|
|
462
|
+
}
|
|
463
|
+
line.entries += entry
|
|
464
|
+
line.main = if (line.entries.size == 1) childMain else line.main + gap + childMain
|
|
465
|
+
line.cross = max(line.cross, childCross)
|
|
466
|
+
}
|
|
467
|
+
if (line.entries.isNotEmpty()) lines += line
|
|
468
|
+
}
|
|
251
469
|
return lines to absolute
|
|
252
470
|
}
|
|
253
471
|
|
|
@@ -361,6 +579,24 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
361
579
|
else -> 0.0
|
|
362
580
|
}
|
|
363
581
|
|
|
582
|
+
private fun justifyCode(): Int = when (justifyContent) {
|
|
583
|
+
"center" -> 1
|
|
584
|
+
"flex-end" -> 2
|
|
585
|
+
"space-between" -> 3
|
|
586
|
+
"space-around" -> 4
|
|
587
|
+
"space-evenly" -> 5
|
|
588
|
+
else -> 0
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
private fun alignContentCode(): Int = when (alignContent) {
|
|
592
|
+
"center" -> 1
|
|
593
|
+
"flex-end" -> 2
|
|
594
|
+
"space-between" -> 3
|
|
595
|
+
"space-around" -> 4
|
|
596
|
+
"stretch" -> 5
|
|
597
|
+
else -> 0
|
|
598
|
+
}
|
|
599
|
+
|
|
364
600
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
365
601
|
val widthMode = View.MeasureSpec.getMode(widthMeasureSpec)
|
|
366
602
|
val width = View.MeasureSpec.getSize(widthMeasureSpec).takeUnless { widthMode == View.MeasureSpec.UNSPECIFIED } ?: 1_000_000
|
|
@@ -396,36 +632,66 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
396
632
|
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
397
633
|
if (grid) {
|
|
398
634
|
val layout = gridLayout(width, height)
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
val
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
635
|
+
val contentWidth = width - paddingLeft - paddingRight
|
|
636
|
+
val contentHeight = height - paddingTop - paddingBottom
|
|
637
|
+
val flow = layout.rows.flatten()
|
|
638
|
+
val sharedFrames = CraftNative.grid(
|
|
639
|
+
contentWidth.toFloat(), contentHeight.toFloat(), gridColumns, columnGap.toFloat(), rowGap.toFloat(),
|
|
640
|
+
flow.map { it.width.toFloat() }.toFloatArray(), flow.map { it.height.toFloat() }.toFloatArray(),
|
|
641
|
+
flow.map { it.style.width?.resolve(layout.cellWidth)?.toFloat() ?: Float.NaN }.toFloatArray(),
|
|
642
|
+
flow.map { it.style.height?.resolve(null)?.toFloat() ?: Float.NaN }.toFloatArray(),
|
|
643
|
+
flow.map { it.style.minWidth?.resolve(layout.cellWidth)?.toFloat() ?: Float.NaN }.toFloatArray(),
|
|
644
|
+
flow.map { it.style.maxWidth?.resolve(layout.cellWidth)?.toFloat() ?: Float.NaN }.toFloatArray(),
|
|
645
|
+
flow.map { it.style.minHeight?.resolve(null)?.toFloat() ?: Float.NaN }.toFloatArray(),
|
|
646
|
+
flow.map { it.style.maxHeight?.resolve(null)?.toFloat() ?: Float.NaN }.toFloatArray(),
|
|
647
|
+
flow.flatMap {
|
|
648
|
+
val lp = it.view.layoutParams as? MarginLayoutParams
|
|
649
|
+
listOf((lp?.leftMargin ?: 0).toFloat(), (lp?.rightMargin ?: 0).toFloat(), (lp?.topMargin ?: 0).toFloat(), (lp?.bottomMargin ?: 0).toFloat())
|
|
650
|
+
}.toFloatArray(),
|
|
651
|
+
flow.flatMap {
|
|
652
|
+
val align = it.style.alignSelf ?: alignItems
|
|
653
|
+
listOf(if (align == "stretch") 1f else 0f, if (align == "center") 1f else if (align == "flex-end") 2f else 0f)
|
|
654
|
+
}.toFloatArray(),
|
|
655
|
+
)
|
|
656
|
+
if (sharedFrames != null) {
|
|
657
|
+
for ((index, entry) in flow.withIndex()) {
|
|
658
|
+
val offset = index * 4
|
|
659
|
+
val x = paddingLeft + sharedFrames[offset].roundToInt()
|
|
660
|
+
val y = paddingTop + sharedFrames[offset + 1].roundToInt()
|
|
661
|
+
val childWidth = sharedFrames[offset + 2].roundToInt()
|
|
662
|
+
val childHeight = sharedFrames[offset + 3].roundToInt()
|
|
423
663
|
entry.view.layout(x, y, x + childWidth, y + childHeight)
|
|
424
664
|
}
|
|
425
|
-
|
|
665
|
+
} else {
|
|
666
|
+
var rowTop = paddingTop
|
|
667
|
+
for ((rowIndex, row) in layout.rows.withIndex()) {
|
|
668
|
+
val rowHeight = layout.rowHeights[rowIndex]
|
|
669
|
+
for ((column, entry) in row.withIndex()) {
|
|
670
|
+
val align = entry.style.alignSelf ?: alignItems
|
|
671
|
+
val lp = entry.view.layoutParams as? MarginLayoutParams
|
|
672
|
+
val marginLeft = lp?.leftMargin ?: 0
|
|
673
|
+
val marginRight = lp?.rightMargin ?: 0
|
|
674
|
+
val marginTop = lp?.topMargin ?: 0
|
|
675
|
+
val marginBottom = lp?.bottomMargin ?: 0
|
|
676
|
+
val availableWidth = max(0, layout.cellWidth - marginLeft - marginRight)
|
|
677
|
+
val availableHeight = max(0, rowHeight - marginTop - marginBottom)
|
|
678
|
+
val childWidth = if (align == "stretch" && entry.style.width == null) availableWidth else min(availableWidth, entry.width)
|
|
679
|
+
val childHeight = if (align == "stretch" && entry.style.height == null) availableHeight else min(availableHeight, entry.height)
|
|
680
|
+
val x = paddingLeft + column * (layout.cellWidth + columnGap) + marginLeft + when (align) {
|
|
681
|
+
"center" -> (availableWidth - childWidth) / 2
|
|
682
|
+
"flex-end" -> availableWidth - childWidth
|
|
683
|
+
else -> 0
|
|
684
|
+
}
|
|
685
|
+
val y = rowTop + marginTop + when (align) {
|
|
686
|
+
"center" -> (availableHeight - childHeight) / 2
|
|
687
|
+
"flex-end" -> availableHeight - childHeight
|
|
688
|
+
else -> 0
|
|
689
|
+
}
|
|
690
|
+
entry.view.layout(x, y, x + childWidth, y + childHeight)
|
|
691
|
+
}
|
|
692
|
+
rowTop += rowHeight + rowGap
|
|
693
|
+
}
|
|
426
694
|
}
|
|
427
|
-
val contentWidth = width - paddingLeft - paddingRight
|
|
428
|
-
val contentHeight = height - paddingTop - paddingBottom
|
|
429
695
|
for (entry in layout.absolute) {
|
|
430
696
|
if (entry.view is CraftNativeModalView && entry.view.visibility == View.VISIBLE) entry.view.bringToFront()
|
|
431
697
|
val childWidth = if (entry.style.left != null && entry.style.right != null) contentWidth - entry.style.left!! - entry.style.right!! else entry.width
|
|
@@ -448,8 +714,14 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
448
714
|
val crossFree = max(0, availableCross - totalCross)
|
|
449
715
|
var crossLeading = 0
|
|
450
716
|
var crossBetween = crossGap
|
|
717
|
+
val sharedCross = if (wrap && lines.size > 1) {
|
|
718
|
+
CraftNative.flexLineDistribution(
|
|
719
|
+
availableCross.toFloat(), crossGap.toFloat(), alignContentCode(), lines.map { it.cross.toFloat() }.toFloatArray()
|
|
720
|
+
)
|
|
721
|
+
} else null
|
|
722
|
+
sharedCross?.second?.forEachIndexed { index, size -> lines[index].cross = size.roundToInt() }
|
|
451
723
|
if (wrap && lines.size > 1) {
|
|
452
|
-
when (alignContent) {
|
|
724
|
+
if (sharedCross == null) when (alignContent) {
|
|
453
725
|
"center" -> crossLeading = crossFree / 2
|
|
454
726
|
"flex-end" -> crossLeading = crossFree
|
|
455
727
|
"space-between" -> crossBetween = crossFree / (lines.size - 1) + crossGap
|
|
@@ -464,23 +736,55 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
464
736
|
}
|
|
465
737
|
}
|
|
466
738
|
var crossOffset = (if (orientation == HORIZONTAL) paddingTop else paddingLeft) + crossLeading
|
|
467
|
-
for (line in lines) {
|
|
739
|
+
for ((lineIndex, line) in lines.withIndex()) {
|
|
740
|
+
val lineOffset = sharedCross?.first?.get(lineIndex)?.roundToInt()?.let { (if (orientation == HORIZONTAL) paddingTop else paddingLeft) + it } ?: crossOffset
|
|
468
741
|
val rawFree = availableMain - line.main
|
|
469
|
-
val
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
742
|
+
val sharedPrimary = CraftNative.flexRow(
|
|
743
|
+
availableMain.toFloat(),
|
|
744
|
+
gap.toFloat(),
|
|
745
|
+
justifyCode(),
|
|
746
|
+
line.entries.map { entry -> if (orientation == HORIZONTAL) entry.width.toFloat() else entry.height.toFloat() }.toFloatArray(),
|
|
747
|
+
line.entries.map { it.style.flexGrow.toFloat() }.toFloatArray(),
|
|
748
|
+
line.entries.map { (it.style.flexShrink ?: defaultFlexShrink(it.view)).toFloat() }.toFloatArray(),
|
|
749
|
+
line.entries.map {
|
|
750
|
+
(if (orientation == HORIZONTAL) it.style.minWidth?.resolve(contentWidth) else it.style.minHeight?.resolve(contentHeight))?.toFloat()
|
|
751
|
+
?: Float.NaN
|
|
752
|
+
}.toFloatArray(),
|
|
753
|
+
line.entries.map {
|
|
754
|
+
(if (orientation == HORIZONTAL) it.style.maxWidth?.resolve(contentWidth) else it.style.maxHeight?.resolve(contentHeight))?.toFloat()
|
|
755
|
+
?: Float.NaN
|
|
756
|
+
}.toFloatArray(),
|
|
757
|
+
line.entries.flatMap { entry ->
|
|
758
|
+
val lp = entry.view.layoutParams as? MarginLayoutParams
|
|
759
|
+
if (orientation == HORIZONTAL) listOf((lp?.leftMargin ?: 0).toFloat(), (lp?.rightMargin ?: 0).toFloat())
|
|
760
|
+
else listOf((lp?.topMargin ?: 0).toFloat(), (lp?.bottomMargin ?: 0).toFloat())
|
|
761
|
+
}.toFloatArray(),
|
|
762
|
+
)
|
|
763
|
+
val sharedPositions = sharedPrimary?.takeIf { it.size == line.entries.size * 2 }
|
|
764
|
+
val distributedMain = if (sharedPrimary != null) {
|
|
765
|
+
line.entries.indices.sumOf { index ->
|
|
766
|
+
val lp = line.entries[index].view.layoutParams as? MarginLayoutParams
|
|
767
|
+
val margins = if (orientation == HORIZONTAL) (lp?.leftMargin ?: 0) + (lp?.rightMargin ?: 0)
|
|
481
768
|
else (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
|
|
482
|
-
|
|
483
|
-
|
|
769
|
+
sharedPrimary[index].roundToInt() + margins
|
|
770
|
+
} + max(0, line.entries.size - 1) * gap
|
|
771
|
+
} else {
|
|
772
|
+
val factors = line.entries.map { if (rawFree >= 0) it.style.flexGrow else it.style.flexShrink ?: defaultFlexShrink(it.view) }
|
|
773
|
+
val totalFactor = factors.sum()
|
|
774
|
+
line.entries.indices.sumOf { index ->
|
|
775
|
+
val base = if (orientation == HORIZONTAL) line.entries[index].width else line.entries[index].height
|
|
776
|
+
val primary = if (totalFactor > 0) max(0, (base + rawFree * factors[index] / totalFactor).toInt()) else base
|
|
777
|
+
val constrained = if (orientation == HORIZONTAL) {
|
|
778
|
+
clamp(primary, line.entries[index].style.minWidth?.resolve(contentWidth), line.entries[index].style.maxWidth?.resolve(contentWidth))
|
|
779
|
+
} else {
|
|
780
|
+
clamp(primary, line.entries[index].style.minHeight?.resolve(contentHeight), line.entries[index].style.maxHeight?.resolve(contentHeight))
|
|
781
|
+
}
|
|
782
|
+
val lp = line.entries[index].view.layoutParams as? MarginLayoutParams
|
|
783
|
+
val margins = if (orientation == HORIZONTAL) (lp?.leftMargin ?: 0) + (lp?.rightMargin ?: 0)
|
|
784
|
+
else (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
|
|
785
|
+
constrained + margins
|
|
786
|
+
} + max(0, line.entries.size - 1) * gap
|
|
787
|
+
}
|
|
484
788
|
val free = max(0, availableMain - distributedMain)
|
|
485
789
|
val (leading, between) = distribution(free, line.entries.size)
|
|
486
790
|
var mainOffset = if (orientation == HORIZONTAL) paddingLeft else paddingTop
|
|
@@ -495,27 +799,43 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
495
799
|
val align = entry.style.alignSelf ?: alignItems
|
|
496
800
|
val crossLeading = if (orientation == HORIZONTAL) lp?.topMargin ?: 0 else lp?.leftMargin ?: 0
|
|
497
801
|
val crossTrailing = if (orientation == HORIZONTAL) lp?.bottomMargin ?: 0 else lp?.rightMargin ?: 0
|
|
498
|
-
val
|
|
499
|
-
|
|
500
|
-
"
|
|
501
|
-
|
|
802
|
+
val alignment = when (align) {
|
|
803
|
+
"center" -> 1
|
|
804
|
+
"flex-end" -> 2
|
|
805
|
+
else -> 0
|
|
806
|
+
}
|
|
807
|
+
val crossPosition = CraftNative.crossAxisPosition(
|
|
808
|
+
line.cross.toFloat(), crossSize.toFloat(), crossLeading.toFloat(), crossTrailing.toFloat(), alignment
|
|
809
|
+
)?.roundToInt() ?: when (alignment) {
|
|
810
|
+
1 -> crossLeading + (line.cross - crossLeading - crossTrailing - crossSize) / 2
|
|
811
|
+
2 -> line.cross - crossTrailing - crossSize
|
|
502
812
|
else -> crossLeading
|
|
503
813
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
val
|
|
508
|
-
val
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
814
|
+
// Shared positions cover every non-reverse justification; the
|
|
815
|
+
// local main-axis rule remains the fallback for old runtimes
|
|
816
|
+
// that return widths only.
|
|
817
|
+
val sharedPosition = if (!reverse) sharedPositions?.get(index)?.roundToInt() else null
|
|
818
|
+
val positionedMain = sharedPosition?.let { (if (orientation == HORIZONTAL) paddingLeft else paddingTop) + it }
|
|
819
|
+
?: mainOffset + marginStart
|
|
820
|
+
val x = if (orientation == HORIZONTAL) positionedMain else lineOffset + crossPosition
|
|
821
|
+
val y = if (orientation == HORIZONTAL) lineOffset + crossPosition else positionedMain
|
|
822
|
+
val distributedPrimary = sharedPrimary?.get(index)?.roundToInt() ?: run {
|
|
823
|
+
val primary = if (orientation == HORIZONTAL) entry.width else entry.height
|
|
824
|
+
val factor = if (rawFree >= 0) entry.style.flexGrow else entry.style.flexShrink ?: defaultFlexShrink(entry.view)
|
|
825
|
+
val totalFactor = line.entries.sumOf { if (rawFree >= 0) it.style.flexGrow else it.style.flexShrink ?: defaultFlexShrink(it.view) }
|
|
826
|
+
val distributed = if (totalFactor > 0) max(0, (primary + rawFree * factor / totalFactor).toInt()) else primary
|
|
827
|
+
if (orientation == HORIZONTAL) {
|
|
828
|
+
clamp(distributed, entry.style.minWidth?.resolve(contentWidth), entry.style.maxWidth?.resolve(contentWidth))
|
|
829
|
+
} else {
|
|
830
|
+
clamp(distributed, entry.style.minHeight?.resolve(contentHeight), entry.style.maxHeight?.resolve(contentHeight))
|
|
831
|
+
}
|
|
512
832
|
}
|
|
513
833
|
val w = if (orientation == HORIZONTAL) distributedPrimary else crossSize
|
|
514
834
|
val h = if (orientation == HORIZONTAL) crossSize else distributedPrimary
|
|
515
835
|
entry.view.layout(x, y, x + w, y + h)
|
|
516
|
-
mainOffset += distributedPrimary + marginStart + marginEnd + gap + between
|
|
836
|
+
if (sharedPosition == null) mainOffset += distributedPrimary + marginStart + marginEnd + gap + between
|
|
517
837
|
}
|
|
518
|
-
crossOffset += line.cross + crossBetween
|
|
838
|
+
if (sharedCross == null) crossOffset += line.cross + crossBetween
|
|
519
839
|
}
|
|
520
840
|
for (entry in absolute) {
|
|
521
841
|
if (entry.view is CraftNativeModalView && entry.view.visibility == View.VISIBLE) entry.view.bringToFront()
|
|
@@ -736,6 +1056,7 @@ private class CraftNativeModalView(context: Context) : FrameLayout(context) {
|
|
|
736
1056
|
val content = CraftNativeFlexLayout(context)
|
|
737
1057
|
private val blocker = View(context)
|
|
738
1058
|
private var renderedVisible = false
|
|
1059
|
+
private var dismissSuppressedByRequest = false
|
|
739
1060
|
private var animationGeneration = 0
|
|
740
1061
|
var onRequestClose: (() -> Unit)? = null
|
|
741
1062
|
|
|
@@ -748,13 +1069,38 @@ private class CraftNativeModalView(context: Context) : FrameLayout(context) {
|
|
|
748
1069
|
init {
|
|
749
1070
|
isClickable = true
|
|
750
1071
|
blocker.isClickable = true
|
|
751
|
-
blocker.setOnTouchListener { _,
|
|
1072
|
+
blocker.setOnTouchListener { _, event ->
|
|
1073
|
+
if (event.action == MotionEvent.ACTION_UP) requestClose()
|
|
1074
|
+
true
|
|
1075
|
+
}
|
|
752
1076
|
blocker.setBackgroundColor(Color.argb(82, 0, 0, 0))
|
|
753
1077
|
addView(blocker, LayoutParams(-1, -1))
|
|
754
1078
|
addView(content, LayoutParams(-1, -1))
|
|
755
1079
|
}
|
|
756
1080
|
|
|
1081
|
+
fun requestClose() {
|
|
1082
|
+
val handler = onRequestClose ?: return
|
|
1083
|
+
dismissSuppressedByRequest = true
|
|
1084
|
+
isClickable = false
|
|
1085
|
+
blocker.isEnabled = false
|
|
1086
|
+
handler()
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
fun consumeDismissSuppression(): Boolean {
|
|
1090
|
+
val suppressed = dismissSuppressedByRequest
|
|
1091
|
+
dismissSuppressedByRequest = false
|
|
1092
|
+
return suppressed
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
fun clearDismissSuppression() {
|
|
1096
|
+
dismissSuppressedByRequest = false
|
|
1097
|
+
}
|
|
1098
|
+
|
|
757
1099
|
fun setVisible(visible: Boolean, opacity: Float, animationType: String?) {
|
|
1100
|
+
if (visible) {
|
|
1101
|
+
isClickable = true
|
|
1102
|
+
blocker.isEnabled = true
|
|
1103
|
+
}
|
|
758
1104
|
if (renderedVisible == visible && visibility == if (visible) View.VISIBLE else View.GONE) {
|
|
759
1105
|
if (visible && alpha != opacity) alpha = opacity
|
|
760
1106
|
return
|
|
@@ -806,6 +1152,9 @@ private class CraftNativeModalView(context: Context) : FrameLayout(context) {
|
|
|
806
1152
|
visibility = View.GONE
|
|
807
1153
|
translationY = 0f
|
|
808
1154
|
alpha = 0f
|
|
1155
|
+
isClickable = true
|
|
1156
|
+
blocker.isEnabled = true
|
|
1157
|
+
dismissSuppressedByRequest = false
|
|
809
1158
|
onRequestClose = null
|
|
810
1159
|
}
|
|
811
1160
|
}
|
|
@@ -817,6 +1166,232 @@ private data class LayoutBounds(
|
|
|
817
1166
|
val height: Double,
|
|
818
1167
|
)
|
|
819
1168
|
|
|
1169
|
+
/**
|
|
1170
|
+
* Android's default spinner style is indeterminate-only, so assigning
|
|
1171
|
+
* `isIndeterminate = false` to it is silently ignored. Use a determinate
|
|
1172
|
+
* ProgressBar style and carry over the platform spinner drawable instead;
|
|
1173
|
+
* that preserves the native look while allowing `animating={false}` to be a
|
|
1174
|
+
* genuinely stopped control.
|
|
1175
|
+
*/
|
|
1176
|
+
private class CraftNativeActivityIndicator(context: Context) :
|
|
1177
|
+
android.widget.ProgressBar(context, null, android.R.attr.progressBarStyleHorizontal) {
|
|
1178
|
+
init {
|
|
1179
|
+
val spinner = android.widget.ProgressBar(context).indeterminateDrawable
|
|
1180
|
+
indeterminateDrawable = spinner
|
|
1181
|
+
progressDrawable = spinner?.constantState?.newDrawable()?.mutate() ?: spinner
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
private class CraftNativeVideoView(context: Context) : VideoView(context) {
|
|
1186
|
+
private var sourceUri = ""
|
|
1187
|
+
private var shouldLoop = false
|
|
1188
|
+
private var shouldPlay = false
|
|
1189
|
+
private var muted = false
|
|
1190
|
+
|
|
1191
|
+
fun configure(source: Any?, autoplay: Boolean, paused: Boolean, loop: Boolean, muted: Boolean, controls: Boolean) {
|
|
1192
|
+
val uri = when (source) {
|
|
1193
|
+
is String -> source
|
|
1194
|
+
is JSONObject -> source.optString("uri")
|
|
1195
|
+
else -> ""
|
|
1196
|
+
}
|
|
1197
|
+
shouldLoop = loop
|
|
1198
|
+
shouldPlay = autoplay && !paused
|
|
1199
|
+
this.muted = muted
|
|
1200
|
+
setMediaController(if (controls) MediaController(context).also { it.setAnchorView(this) } else null)
|
|
1201
|
+
if (uri != sourceUri) {
|
|
1202
|
+
sourceUri = uri
|
|
1203
|
+
stopPlayback()
|
|
1204
|
+
if (uri.isBlank()) return
|
|
1205
|
+
setVideoURI(Uri.parse(uri))
|
|
1206
|
+
setOnPreparedListener { mediaPlayer ->
|
|
1207
|
+
mediaPlayer.isLooping = shouldLoop
|
|
1208
|
+
val volume = if (this.muted) 0f else 1f
|
|
1209
|
+
mediaPlayer.setVolume(volume, volume)
|
|
1210
|
+
if (shouldPlay) start()
|
|
1211
|
+
}
|
|
1212
|
+
setOnCompletionListener { if (shouldLoop && !isPlaying) start() }
|
|
1213
|
+
}
|
|
1214
|
+
if (uri.isBlank()) return
|
|
1215
|
+
runCatching {
|
|
1216
|
+
if (shouldPlay && !isPlaying) start()
|
|
1217
|
+
else if (!shouldPlay && isPlaying) pause()
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
fun stop() {
|
|
1222
|
+
stopPlayback()
|
|
1223
|
+
sourceUri = ""
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
/** The oscillator/gain subset of Web Audio used by native STX screens. */
|
|
1228
|
+
private class CraftNativeAudio {
|
|
1229
|
+
private data class ParameterEvent(val time: Double, val value: Double, val ramp: Boolean)
|
|
1230
|
+
|
|
1231
|
+
private class Parameter(initial: Double) {
|
|
1232
|
+
private var current = initial
|
|
1233
|
+
private val events = mutableListOf<ParameterEvent>()
|
|
1234
|
+
|
|
1235
|
+
@Synchronized fun set(value: Double) {
|
|
1236
|
+
current = value
|
|
1237
|
+
events.clear()
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
@Synchronized fun schedule(value: Double, time: Double, ramp: Boolean) {
|
|
1241
|
+
events += ParameterEvent(time.coerceAtLeast(0.0), value, ramp)
|
|
1242
|
+
events.sortBy { it.time }
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
@Synchronized fun valueAt(time: Double): Double {
|
|
1246
|
+
var previousTime = 0.0
|
|
1247
|
+
var previousValue = current
|
|
1248
|
+
for (event in events) {
|
|
1249
|
+
if (time < event.time) {
|
|
1250
|
+
if (!event.ramp || event.time <= previousTime) return previousValue
|
|
1251
|
+
val fraction = (time - previousTime) / (event.time - previousTime)
|
|
1252
|
+
return previousValue + (event.value - previousValue) * fraction
|
|
1253
|
+
}
|
|
1254
|
+
previousTime = event.time
|
|
1255
|
+
previousValue = event.value
|
|
1256
|
+
}
|
|
1257
|
+
return previousValue
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
private class OscillatorState {
|
|
1262
|
+
val frequency = Parameter(440.0)
|
|
1263
|
+
@Volatile var gain = Parameter(1.0)
|
|
1264
|
+
@Volatile var phase = 0.0
|
|
1265
|
+
@Volatile var active = false
|
|
1266
|
+
var contextStart = 0.0
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
private val handler = Handler(Looper.getMainLooper())
|
|
1270
|
+
private val oscillators = mutableMapOf<String, OscillatorState>()
|
|
1271
|
+
private val gains = mutableMapOf<String, Parameter>()
|
|
1272
|
+
private val contextStarts = mutableMapOf<String, Double>()
|
|
1273
|
+
private var track: AudioTrack? = null
|
|
1274
|
+
@Volatile private var running = false
|
|
1275
|
+
private var renderThread: Thread? = null
|
|
1276
|
+
|
|
1277
|
+
@Synchronized fun handle(payload: JSONObject) {
|
|
1278
|
+
when (payload.optString("action")) {
|
|
1279
|
+
"createContext" -> {
|
|
1280
|
+
payload.optString("contextId").takeIf { it.isNotBlank() }?.let {
|
|
1281
|
+
contextStarts[it] = System.nanoTime() / 1_000_000_000.0
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
"createOscillator" -> {
|
|
1285
|
+
val id = payload.optString("nodeId")
|
|
1286
|
+
if (id.isNotBlank()) oscillators.putIfAbsent(id, OscillatorState()).also {
|
|
1287
|
+
oscillators[id]?.contextStart = contextStarts[payload.optString("contextId")]
|
|
1288
|
+
?: (System.nanoTime() / 1_000_000_000.0)
|
|
1289
|
+
oscillators[id]?.frequency?.set(payload.optDouble("frequency", 440.0).coerceAtLeast(0.0))
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
"createGain" -> {
|
|
1293
|
+
val id = payload.optString("nodeId")
|
|
1294
|
+
if (id.isNotBlank()) gains[id] = Parameter(payload.optDouble("gain", 1.0).coerceAtLeast(0.0))
|
|
1295
|
+
}
|
|
1296
|
+
"connect" -> {
|
|
1297
|
+
val source = payload.optString("sourceId")
|
|
1298
|
+
oscillators[source]?.gain = gains[payload.optString("destinationId")] ?: Parameter(1.0)
|
|
1299
|
+
}
|
|
1300
|
+
"frequency" -> oscillators[payload.optString("nodeId")]?.frequency?.set(payload.optDouble("value", 0.0).coerceAtLeast(0.0))
|
|
1301
|
+
"frequencyAtTime", "frequencyRampToTime" -> oscillators[payload.optString("nodeId")]?.frequency?.schedule(
|
|
1302
|
+
payload.optDouble("value", 0.0).coerceAtLeast(0.0),
|
|
1303
|
+
payload.optDouble("time", 0.0),
|
|
1304
|
+
payload.optString("action") == "frequencyRampToTime",
|
|
1305
|
+
)
|
|
1306
|
+
"gain" -> gains[payload.optString("nodeId")]?.set(payload.optDouble("value", 0.0).coerceAtLeast(0.0))
|
|
1307
|
+
"gainAtTime", "gainRampToTime" -> gains[payload.optString("nodeId")]?.schedule(
|
|
1308
|
+
payload.optDouble("value", 0.0).coerceAtLeast(0.0),
|
|
1309
|
+
payload.optDouble("time", 0.0),
|
|
1310
|
+
payload.optString("action") == "gainRampToTime",
|
|
1311
|
+
)
|
|
1312
|
+
"start", "stop" -> {
|
|
1313
|
+
val state = oscillators[payload.optString("nodeId")] ?: return
|
|
1314
|
+
ensureTrack()
|
|
1315
|
+
val active = payload.optString("action") == "start"
|
|
1316
|
+
val delay = (payload.optDouble("delay", 0.0).coerceAtLeast(0.0) * 1000).roundToLong()
|
|
1317
|
+
handler.postDelayed({ state.active = active }, delay)
|
|
1318
|
+
}
|
|
1319
|
+
"close" -> close()
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
@Synchronized fun close() {
|
|
1324
|
+
oscillators.values.forEach { it.active = false }
|
|
1325
|
+
oscillators.clear()
|
|
1326
|
+
gains.clear()
|
|
1327
|
+
contextStarts.clear()
|
|
1328
|
+
running = false
|
|
1329
|
+
renderThread?.interrupt()
|
|
1330
|
+
renderThread = null
|
|
1331
|
+
track?.let { output ->
|
|
1332
|
+
runCatching {
|
|
1333
|
+
output.pause()
|
|
1334
|
+
output.flush()
|
|
1335
|
+
output.release()
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
track = null
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
@Synchronized private fun ensureTrack() {
|
|
1342
|
+
if (track != null) return
|
|
1343
|
+
val sampleRate = 44_100
|
|
1344
|
+
val minBuffer = AudioTrack.getMinBufferSize(sampleRate, AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT)
|
|
1345
|
+
val format = AudioFormat.Builder()
|
|
1346
|
+
.setSampleRate(sampleRate)
|
|
1347
|
+
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
|
|
1348
|
+
.setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
|
|
1349
|
+
.build()
|
|
1350
|
+
val attributes = AudioAttributes.Builder()
|
|
1351
|
+
.setUsage(AudioAttributes.USAGE_MEDIA)
|
|
1352
|
+
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
|
1353
|
+
.build()
|
|
1354
|
+
val created = AudioTrack.Builder()
|
|
1355
|
+
.setAudioAttributes(attributes)
|
|
1356
|
+
.setAudioFormat(format)
|
|
1357
|
+
.setBufferSizeInBytes(max(minBuffer, sampleRate / 4))
|
|
1358
|
+
.setTransferMode(AudioTrack.MODE_STREAM)
|
|
1359
|
+
.build()
|
|
1360
|
+
track = created
|
|
1361
|
+
running = true
|
|
1362
|
+
created.play()
|
|
1363
|
+
renderThread = Thread({ render(created, sampleRate) }, "craft-native-audio").also { it.start() }
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
private fun render(output: AudioTrack, sampleRate: Int) {
|
|
1367
|
+
val buffer = ShortArray(1024 * 2)
|
|
1368
|
+
while (running && !Thread.currentThread().isInterrupted) {
|
|
1369
|
+
val snapshot = synchronized(this) {
|
|
1370
|
+
oscillators.mapValues { (id, state) ->
|
|
1371
|
+
Triple(state, state.gain, state.active)
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
val now = System.nanoTime() / 1_000_000_000.0
|
|
1375
|
+
for (frame in 0 until 1024) {
|
|
1376
|
+
var sample = 0.0
|
|
1377
|
+
snapshot.forEach { (_, value) ->
|
|
1378
|
+
val (state, gain, active) = value
|
|
1379
|
+
if (!active) return@forEach
|
|
1380
|
+
val time = (now - state.contextStart).coerceAtLeast(0.0) + frame.toDouble() / sampleRate
|
|
1381
|
+
val frequency = state.frequency.valueAt(time)
|
|
1382
|
+
sample += sin(state.phase) * gain.valueAt(time)
|
|
1383
|
+
state.phase += 2 * PI * frequency / sampleRate
|
|
1384
|
+
if (state.phase >= 2 * PI) state.phase -= 2 * PI
|
|
1385
|
+
}
|
|
1386
|
+
val value = (sample.coerceIn(-1.0, 1.0) * Short.MAX_VALUE).toInt().toShort()
|
|
1387
|
+
buffer[frame * 2] = value
|
|
1388
|
+
buffer[frame * 2 + 1] = value
|
|
1389
|
+
}
|
|
1390
|
+
if (output.write(buffer, 0, buffer.size, AudioTrack.WRITE_BLOCKING) < 0) break
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
|
|
820
1395
|
/** WebView-free host for a route bundle compiled by `stx native compile`. */
|
|
821
1396
|
class MainActivity : AppCompatActivity() {
|
|
822
1397
|
private class Screen(
|
|
@@ -854,6 +1429,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
854
1429
|
val modalVisibility = mutableMapOf<View, Boolean>()
|
|
855
1430
|
val modalCloseHandlers = mutableMapOf<View, String>()
|
|
856
1431
|
val layoutHandlers = mutableMapOf<View, String>()
|
|
1432
|
+
val layoutListeners = mutableMapOf<View, View.OnLayoutChangeListener>()
|
|
857
1433
|
val lastLayoutBounds = mutableMapOf<View, LayoutBounds>()
|
|
858
1434
|
val flatListRows = mutableMapOf<CraftNativeFlatList, MutableMap<String, MutableMap<String, View>>>()
|
|
859
1435
|
val flatListOwners = mutableMapOf<String, String>()
|
|
@@ -862,11 +1438,13 @@ class MainActivity : AppCompatActivity() {
|
|
|
862
1438
|
val capabilityScope = UUID.randomUUID().toString()
|
|
863
1439
|
val pendingCapabilities = mutableSetOf<String>()
|
|
864
1440
|
val capabilityTimeouts = mutableMapOf<String, Runnable>()
|
|
1441
|
+
val audio = CraftNativeAudio()
|
|
865
1442
|
var active = true
|
|
866
1443
|
|
|
867
1444
|
fun close() {
|
|
868
1445
|
if (!active) return
|
|
869
1446
|
active = false
|
|
1447
|
+
audio.close()
|
|
870
1448
|
val finish = {
|
|
871
1449
|
port?.close()
|
|
872
1450
|
isolate.close()
|
|
@@ -1040,6 +1618,80 @@ class MainActivity : AppCompatActivity() {
|
|
|
1040
1618
|
globalThis.__stxNativeRoute = ${route?.let { JSONObject.quote(it) } ?: "undefined"};
|
|
1041
1619
|
globalThis.__stxNativeParams = ${params};
|
|
1042
1620
|
""".trimIndent()
|
|
1621
|
+
val audioPrelude = """
|
|
1622
|
+
(function() {
|
|
1623
|
+
if (typeof globalThis.AudioContext !== 'function') {
|
|
1624
|
+
var nextAudioId = 0;
|
|
1625
|
+
function audioMessage(payload) {
|
|
1626
|
+
globalThis.__stxNativeBridge.postMessage({ type: 'AUDIO', payload: payload });
|
|
1627
|
+
}
|
|
1628
|
+
function audioParam(contextId, nodeId, action, initial) {
|
|
1629
|
+
var current = Number(initial) || 0;
|
|
1630
|
+
var param = {};
|
|
1631
|
+
Object.defineProperty(param, 'value', {
|
|
1632
|
+
configurable: true,
|
|
1633
|
+
enumerable: true,
|
|
1634
|
+
get: function() { return current; },
|
|
1635
|
+
set: function(value) {
|
|
1636
|
+
current = Number(value) || 0;
|
|
1637
|
+
audioMessage({ action: action, contextId: contextId, nodeId: nodeId, value: current });
|
|
1638
|
+
}
|
|
1639
|
+
});
|
|
1640
|
+
function schedule(value, when, ramp) {
|
|
1641
|
+
current = Number(value) || 0;
|
|
1642
|
+
var time = Number(when);
|
|
1643
|
+
audioMessage({ action: action + (ramp ? 'RampToTime' : 'AtTime'), contextId: contextId, nodeId: nodeId, value: current, time: isFinite(time) ? Math.max(0, time) : 0 });
|
|
1644
|
+
}
|
|
1645
|
+
param.setValueAtTime = function(value, when) { schedule(value, when, false); return param; };
|
|
1646
|
+
param.linearRampToValueAtTime = function(value, when) { schedule(value, when, true); return param; };
|
|
1647
|
+
return param;
|
|
1648
|
+
}
|
|
1649
|
+
function AudioContext() {
|
|
1650
|
+
this._id = 'audio-' + (++nextAudioId);
|
|
1651
|
+
this._started = Date.now() / 1000;
|
|
1652
|
+
this.state = 'running';
|
|
1653
|
+
this.destination = { _contextId: this._id, _nodeId: 'destination' };
|
|
1654
|
+
audioMessage({ action: 'createContext', contextId: this._id });
|
|
1655
|
+
}
|
|
1656
|
+
Object.defineProperty(AudioContext.prototype, 'currentTime', {
|
|
1657
|
+
get: function() { return Date.now() / 1000 - this._started; }
|
|
1658
|
+
});
|
|
1659
|
+
AudioContext.prototype.createOscillator = function() {
|
|
1660
|
+
var context = this;
|
|
1661
|
+
var node = { _contextId: context._id, _nodeId: context._id + '-osc-' + (++nextAudioId) };
|
|
1662
|
+
node.frequency = audioParam(context._id, node._nodeId, 'frequency', 440);
|
|
1663
|
+
node.connect = function(destination) {
|
|
1664
|
+
audioMessage({ action: 'connect', contextId: context._id, sourceId: node._nodeId, destinationId: destination && destination._nodeId || 'destination' });
|
|
1665
|
+
return destination;
|
|
1666
|
+
};
|
|
1667
|
+
node.start = function(when) {
|
|
1668
|
+
audioMessage({ action: 'start', contextId: context._id, nodeId: node._nodeId, delay: Math.max(0, (Number(when) || 0) - context.currentTime) });
|
|
1669
|
+
};
|
|
1670
|
+
node.stop = function(when) {
|
|
1671
|
+
audioMessage({ action: 'stop', contextId: context._id, nodeId: node._nodeId, delay: Math.max(0, (Number(when) || 0) - context.currentTime) });
|
|
1672
|
+
};
|
|
1673
|
+
audioMessage({ action: 'createOscillator', contextId: context._id, nodeId: node._nodeId, frequency: node.frequency.value });
|
|
1674
|
+
return node;
|
|
1675
|
+
};
|
|
1676
|
+
AudioContext.prototype.createGain = function() {
|
|
1677
|
+
var context = this;
|
|
1678
|
+
var node = { _contextId: context._id, _nodeId: context._id + '-gain-' + (++nextAudioId) };
|
|
1679
|
+
node.gain = audioParam(context._id, node._nodeId, 'gain', 1);
|
|
1680
|
+
node.connect = function(destination) {
|
|
1681
|
+
audioMessage({ action: 'connect', contextId: context._id, sourceId: node._nodeId, destinationId: destination && destination._nodeId || 'destination' });
|
|
1682
|
+
return destination;
|
|
1683
|
+
};
|
|
1684
|
+
audioMessage({ action: 'createGain', contextId: context._id, nodeId: node._nodeId, gain: node.gain.value });
|
|
1685
|
+
return node;
|
|
1686
|
+
};
|
|
1687
|
+
AudioContext.prototype.resume = function() { this.state = 'running'; return Promise.resolve(); };
|
|
1688
|
+
AudioContext.prototype.suspend = function() { this.state = 'suspended'; return Promise.resolve(); };
|
|
1689
|
+
AudioContext.prototype.close = function() { this.state = 'closed'; audioMessage({ action: 'close', contextId: this._id }); return Promise.resolve(); };
|
|
1690
|
+
globalThis.AudioContext = AudioContext;
|
|
1691
|
+
globalThis.webkitAudioContext = AudioContext;
|
|
1692
|
+
}
|
|
1693
|
+
})()
|
|
1694
|
+
""".trimIndent()
|
|
1043
1695
|
val prelude = if (useMessagePorts) """
|
|
1044
1696
|
(async function() {
|
|
1045
1697
|
const port = await android.getNamedPort('craft');
|
|
@@ -1055,6 +1707,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
1055
1707
|
postMessage: function(value) { port.postMessage(value); },
|
|
1056
1708
|
onMessage: function(callback) { port.onmessage = function(event) { callback(event.data); }; }
|
|
1057
1709
|
};
|
|
1710
|
+
$audioPrelude
|
|
1058
1711
|
})()
|
|
1059
1712
|
""".trimIndent() else """
|
|
1060
1713
|
$routePrelude
|
|
@@ -1069,6 +1722,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
1069
1722
|
postMessage: function(value) { console.log(${JSONObject.quote(prefix)} + value); },
|
|
1070
1723
|
onMessage: function(callback) { globalThis.__stxNativeCallback = callback; }
|
|
1071
1724
|
};
|
|
1725
|
+
$audioPrelude
|
|
1072
1726
|
""".trimIndent()
|
|
1073
1727
|
val setup = isolate.evaluateJavaScriptAsync(prelude)
|
|
1074
1728
|
setup.addListener({
|
|
@@ -1206,6 +1860,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
1206
1860
|
capabilities.cancel(token)
|
|
1207
1861
|
}
|
|
1208
1862
|
}
|
|
1863
|
+
"AUDIO" -> screen.audio.handle(payload)
|
|
1209
1864
|
}
|
|
1210
1865
|
}
|
|
1211
1866
|
|
|
@@ -1243,9 +1898,30 @@ class MainActivity : AppCompatActivity() {
|
|
|
1243
1898
|
val updated = screen.controls["id:$target"] ?: return false
|
|
1244
1899
|
if (!updateParentLayoutStyle(updated, document.optJSONObject("style") ?: JSONObject())) return false
|
|
1245
1900
|
}
|
|
1901
|
+
if (applied.isNotEmpty()) {
|
|
1902
|
+
scheduleLayoutEvents(screen)
|
|
1903
|
+
}
|
|
1246
1904
|
return true
|
|
1247
1905
|
}
|
|
1248
1906
|
|
|
1907
|
+
private fun scheduleLayoutEvents(screen: Screen) {
|
|
1908
|
+
if (!screen.active) return
|
|
1909
|
+
val observer = screen.view.viewTreeObserver
|
|
1910
|
+
if (observer.isAlive) {
|
|
1911
|
+
val listener = object : ViewTreeObserver.OnGlobalLayoutListener {
|
|
1912
|
+
override fun onGlobalLayout() {
|
|
1913
|
+
if (observer.isAlive) observer.removeOnGlobalLayoutListener(this)
|
|
1914
|
+
if (screen.active) emitLayoutEvents(screen)
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
observer.addOnGlobalLayoutListener(listener)
|
|
1918
|
+
}
|
|
1919
|
+
screen.view.requestLayout()
|
|
1920
|
+
// A tree that is already settled may not produce a global-layout
|
|
1921
|
+
// callback; keep the event path live for that case as well.
|
|
1922
|
+
screen.view.postOnAnimation { if (screen.active) emitLayoutEvents(screen) }
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1249
1925
|
/**
|
|
1250
1926
|
* Targeted mutations bypass the parent's child reconciliation. Rebind the
|
|
1251
1927
|
* updated node's layout style explicitly so removing a width/height (or
|
|
@@ -1482,16 +2158,39 @@ class MainActivity : AppCompatActivity() {
|
|
|
1482
2158
|
transparent = props.optBoolean("transparent", false)
|
|
1483
2159
|
content.configure(style, resources.displayMetrics.density)
|
|
1484
2160
|
reconcileChildren(screen, content, children, path, next, style, previousControls)
|
|
1485
|
-
|
|
2161
|
+
// Dynamic :visible bindings arrive as a display style mutation;
|
|
2162
|
+
// static visible props remain authoritative when present.
|
|
2163
|
+
val visible = if (props.has("visible")) {
|
|
2164
|
+
// An explicit visible prop owns modal visibility. The
|
|
2165
|
+
// binding runtime may leave a stale display:none style
|
|
2166
|
+
// from the previous hidden state when it patches props.
|
|
2167
|
+
props.optBoolean("visible", false)
|
|
2168
|
+
} else {
|
|
2169
|
+
style.has("display") && style.optString("display") != "none"
|
|
2170
|
+
}
|
|
1486
2171
|
val wasVisible = screen.modalVisibility[this]
|
|
1487
2172
|
screen.modalVisibility[this] = visible
|
|
1488
2173
|
val requestClose = events.optString("onRequestClose")
|
|
1489
2174
|
if (requestClose.isNotBlank()) screen.modalCloseHandlers[this] = requestClose else screen.modalCloseHandlers.remove(this)
|
|
2175
|
+
onRequestClose = if (requestClose.isNotBlank()) {
|
|
2176
|
+
{ send(screen, "EVENT", JSONObject().put("handlerName", requestClose).put("nativeEvent", JSONObject())) }
|
|
2177
|
+
} else {
|
|
2178
|
+
null
|
|
2179
|
+
}
|
|
1490
2180
|
setVisible(visible, style.optDouble("opacity", 1.0).toFloat(), props.optString("animationType").ifBlank { null })
|
|
2181
|
+
if (visible) clearDismissSuppression()
|
|
2182
|
+
val suppressDismiss = !visible && consumeDismissSuppression()
|
|
1491
2183
|
if (wasVisible != visible && (visible || wasVisible == true)) {
|
|
1492
2184
|
val lifecycleHandler = if (visible) events.optString("onShow") else events.optString("onDismiss")
|
|
1493
|
-
if (lifecycleHandler.isNotBlank()) {
|
|
1494
|
-
|
|
2185
|
+
if (lifecycleHandler.isNotBlank() && !suppressDismiss) {
|
|
2186
|
+
// Deliver lifecycle callbacks after reconciliation returns to
|
|
2187
|
+
// the queue; the handler may immediately schedule another
|
|
2188
|
+
// render against this native view.
|
|
2189
|
+
post {
|
|
2190
|
+
if (screen.active && screen.modalVisibility[this] == visible) {
|
|
2191
|
+
send(screen, "EVENT", JSONObject().put("handlerName", lifecycleHandler).put("nativeEvent", JSONObject()))
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
1495
2194
|
}
|
|
1496
2195
|
}
|
|
1497
2196
|
}
|
|
@@ -1579,7 +2278,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
1579
2278
|
}
|
|
1580
2279
|
})
|
|
1581
2280
|
}
|
|
1582
|
-
"ActivityIndicator" -> (previous as?
|
|
2281
|
+
"ActivityIndicator" -> (previous as? CraftNativeActivityIndicator ?: CraftNativeActivityIndicator(this)).apply {
|
|
1583
2282
|
val requestedSize = props.opt("size")
|
|
1584
2283
|
val scale = when (requestedSize) {
|
|
1585
2284
|
"large" -> 1.35f
|
|
@@ -1613,6 +2312,19 @@ class MainActivity : AppCompatActivity() {
|
|
|
1613
2312
|
if (onError.isNotBlank()) screen.imageErrorHandlers[this] = onError else screen.imageErrorHandlers.remove(this)
|
|
1614
2313
|
updateImage(screen, this, props.opt("source"))
|
|
1615
2314
|
}
|
|
2315
|
+
"Video" -> (previous as? CraftNativeVideoView ?: CraftNativeVideoView(this)).apply {
|
|
2316
|
+
configure(
|
|
2317
|
+
source = props.opt("source"),
|
|
2318
|
+
autoplay = props.optBoolean("autoplay", props.optBoolean("autoPlay", false)),
|
|
2319
|
+
paused = props.optBoolean("paused", false),
|
|
2320
|
+
loop = props.optBoolean("loop", false),
|
|
2321
|
+
muted = props.optBoolean("muted", false),
|
|
2322
|
+
controls = props.optBoolean("controls", false),
|
|
2323
|
+
)
|
|
2324
|
+
}
|
|
2325
|
+
"Svg" -> (previous as? CraftNativeSvgView ?: CraftNativeSvgView(this)).apply {
|
|
2326
|
+
drawing = node
|
|
2327
|
+
}
|
|
1616
2328
|
"ScrollView" -> (previous as? CraftNativeScrollView ?: CraftNativeScrollView(this)).apply {
|
|
1617
2329
|
val horizontal = props.optBoolean("horizontal", false) || style.optString("flexDirection") in setOf("row", "row-reverse")
|
|
1618
2330
|
setHorizontal(horizontal)
|
|
@@ -1711,6 +2423,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
1711
2423
|
reconcileChildren(screen, this, children, path, next, style, previousControls)
|
|
1712
2424
|
}
|
|
1713
2425
|
}
|
|
2426
|
+
if (control is CraftNativeFlexLayout) {
|
|
2427
|
+
control.onLayoutChanged = { if (screen.active) emitLayoutEvents(screen) }
|
|
2428
|
+
}
|
|
1714
2429
|
val pressInHandler = events.optString("onPressIn").takeIf { it.isNotBlank() }
|
|
1715
2430
|
val pressOutHandler = events.optString("onPressOut").takeIf { it.isNotBlank() }
|
|
1716
2431
|
if (pressInHandler != null) screen.pressInHandlers[control] = pressInHandler else screen.pressInHandlers.remove(control)
|
|
@@ -1786,9 +2501,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
1786
2501
|
if (layoutHandler.isNotBlank()) {
|
|
1787
2502
|
if (screen.layoutHandlers[control] != layoutHandler) screen.lastLayoutBounds.remove(control)
|
|
1788
2503
|
screen.layoutHandlers[control] = layoutHandler
|
|
2504
|
+
ensureLayoutListener(screen, control)
|
|
1789
2505
|
} else {
|
|
1790
2506
|
screen.layoutHandlers.remove(control)
|
|
1791
2507
|
screen.lastLayoutBounds.remove(control)
|
|
2508
|
+
screen.layoutListeners.remove(control)?.let(control::removeOnLayoutChangeListener)
|
|
1792
2509
|
}
|
|
1793
2510
|
if (control is ImageView) imageErrors[control]?.let { message ->
|
|
1794
2511
|
val current = control.contentDescription?.toString().orEmpty()
|
|
@@ -1807,7 +2524,6 @@ class MainActivity : AppCompatActivity() {
|
|
|
1807
2524
|
View.VISIBLE
|
|
1808
2525
|
}
|
|
1809
2526
|
}
|
|
1810
|
-
if (type == "Modal" && !props.optBoolean("visible", false)) control.visibility = View.GONE
|
|
1811
2527
|
val width = style.optDouble("width", Double.NaN).takeUnless { it.isNaN() }?.let(::dp)
|
|
1812
2528
|
val height = style.optDouble("height", Double.NaN).takeUnless { it.isNaN() }?.let(::dp)
|
|
1813
2529
|
control.layoutParams = LinearLayout.LayoutParams(
|
|
@@ -1968,7 +2684,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
1968
2684
|
|
|
1969
2685
|
private fun applyViewStyle(view: View, style: JSONObject) {
|
|
1970
2686
|
view.alpha = style.optDouble("opacity", 1.0).toFloat()
|
|
1971
|
-
|
|
2687
|
+
if (view !is CraftNativeModalView) {
|
|
2688
|
+
view.visibility = if (style.optString("display") == "none") View.GONE else View.VISIBLE
|
|
2689
|
+
}
|
|
1972
2690
|
view.elevation = dp(style.optDouble("elevation", 0.0)).toFloat()
|
|
1973
2691
|
val background = style.has("backgroundColor") && !style.isNull("backgroundColor")
|
|
1974
2692
|
val border = style.optDouble("borderWidth", 0.0)
|
|
@@ -1997,6 +2715,15 @@ class MainActivity : AppCompatActivity() {
|
|
|
1997
2715
|
}
|
|
1998
2716
|
}
|
|
1999
2717
|
|
|
2718
|
+
private fun ensureLayoutListener(screen: Screen, view: View) {
|
|
2719
|
+
if (screen.layoutListeners.containsKey(view)) return
|
|
2720
|
+
val listener = View.OnLayoutChangeListener { changed, left, top, right, bottom, _, _, _, _ ->
|
|
2721
|
+
if (screen.active) emitLayoutEvent(screen, changed, left, top, right, bottom)
|
|
2722
|
+
}
|
|
2723
|
+
screen.layoutListeners[view] = listener
|
|
2724
|
+
view.addOnLayoutChangeListener(listener)
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2000
2727
|
private fun configureText(view: TextView, value: String, style: JSONObject) {
|
|
2001
2728
|
view.text = when (style.optString("textTransform")) {
|
|
2002
2729
|
"uppercase" -> value.uppercase()
|
|
@@ -2353,14 +3080,21 @@ class MainActivity : AppCompatActivity() {
|
|
|
2353
3080
|
}
|
|
2354
3081
|
|
|
2355
3082
|
private fun emitLayoutEvents(screen: Screen, view: View) {
|
|
3083
|
+
emitLayoutEvent(screen, view, view.left, view.top, view.right, view.bottom)
|
|
3084
|
+
if (view is ViewGroup) {
|
|
3085
|
+
for (index in 0 until view.childCount) emitLayoutEvents(screen, view.getChildAt(index))
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
|
|
3089
|
+
private fun emitLayoutEvent(screen: Screen, view: View, left: Int, top: Int, right: Int, bottom: Int) {
|
|
2356
3090
|
val handler = screen.layoutHandlers[view]
|
|
2357
3091
|
if (handler != null && view.isShown) {
|
|
2358
3092
|
val density = resources.displayMetrics.density
|
|
2359
3093
|
val bounds = LayoutBounds(
|
|
2360
|
-
x =
|
|
2361
|
-
y =
|
|
2362
|
-
width =
|
|
2363
|
-
height =
|
|
3094
|
+
x = left.toDouble() / density.toDouble(),
|
|
3095
|
+
y = top.toDouble() / density.toDouble(),
|
|
3096
|
+
width = (right - left).toDouble() / density.toDouble(),
|
|
3097
|
+
height = (bottom - top).toDouble() / density.toDouble(),
|
|
2364
3098
|
)
|
|
2365
3099
|
if (screen.lastLayoutBounds[view] != bounds) {
|
|
2366
3100
|
screen.lastLayoutBounds[view] = bounds
|
|
@@ -2373,9 +3107,6 @@ class MainActivity : AppCompatActivity() {
|
|
|
2373
3107
|
.put("height", bounds.height))))
|
|
2374
3108
|
}
|
|
2375
3109
|
}
|
|
2376
|
-
if (view is ViewGroup) {
|
|
2377
|
-
for (index in 0 until view.childCount) emitLayoutEvents(screen, view.getChildAt(index))
|
|
2378
|
-
}
|
|
2379
3110
|
}
|
|
2380
3111
|
|
|
2381
3112
|
private fun send(screen: Screen, type: String, payload: JSONObject, correlationId: String? = null) {
|
|
@@ -2440,6 +3171,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
2440
3171
|
releaseFlatList(screen, it, preservingInputDrafts)
|
|
2441
3172
|
}
|
|
2442
3173
|
released.filterIsInstance<CraftNativeModalView>().forEach { it.resetForTeardown() }
|
|
3174
|
+
released.filterIsInstance<CraftNativeVideoView>().forEach { it.stop() }
|
|
3175
|
+
released.filterIsInstance<CraftNativeFlexLayout>().forEach { it.onLayoutChanged = null }
|
|
2443
3176
|
released.filterIsInstance<CraftNativeScrollView>().forEach { it.setRefreshHandler(null, false) }
|
|
2444
3177
|
releaseImages(released)
|
|
2445
3178
|
released.filterIsInstance<EditText>().forEach {
|
|
@@ -2455,6 +3188,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
2455
3188
|
screen.pressOutHandlers.keys.removeAll(released)
|
|
2456
3189
|
screen.longPressHandlers.keys.removeAll(released)
|
|
2457
3190
|
screen.layoutHandlers.keys.removeAll(released)
|
|
3191
|
+
screen.layoutListeners.forEach { (view, listener) ->
|
|
3192
|
+
if (view in released) view.removeOnLayoutChangeListener(listener)
|
|
3193
|
+
}
|
|
3194
|
+
screen.layoutListeners.keys.removeAll(released)
|
|
2458
3195
|
screen.lastLayoutBounds.keys.removeAll(released)
|
|
2459
3196
|
screen.scrollHandlers.keys.removeAll(released)
|
|
2460
3197
|
screen.scrollBeginHandlers.keys.removeAll(released)
|
|
@@ -2515,9 +3252,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
2515
3252
|
val screen = screens.lastOrNull() ?: return back()
|
|
2516
3253
|
val modal = screen.modalVisibility.entries.lastOrNull { (view, visible) -> visible && view.visibility == View.VISIBLE }
|
|
2517
3254
|
if (modal != null) {
|
|
2518
|
-
|
|
2519
|
-
send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject()))
|
|
2520
|
-
}
|
|
3255
|
+
(modal.key as? CraftNativeModalView)?.requestClose()
|
|
2521
3256
|
return
|
|
2522
3257
|
}
|
|
2523
3258
|
back()
|