craft-native 0.0.140 → 0.0.142
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 +854 -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 +798 -62
- package/package.json +1 -1
|
@@ -6,9 +6,16 @@ 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
|
|
16
|
+
import android.graphics.Matrix
|
|
11
17
|
import android.graphics.Paint
|
|
18
|
+
import android.graphics.Path
|
|
12
19
|
import android.graphics.Typeface
|
|
13
20
|
import android.graphics.drawable.Drawable
|
|
14
21
|
import android.graphics.drawable.GradientDrawable
|
|
@@ -28,11 +35,13 @@ import android.util.Base64
|
|
|
28
35
|
import android.util.TypedValue
|
|
29
36
|
import android.view.Gravity
|
|
30
37
|
import android.view.KeyEvent
|
|
38
|
+
import android.view.MotionEvent
|
|
31
39
|
import android.view.inputmethod.EditorInfo
|
|
32
40
|
import android.text.InputType
|
|
33
41
|
import android.text.Layout
|
|
34
42
|
import android.view.View
|
|
35
43
|
import android.view.ViewGroup
|
|
44
|
+
import android.view.ViewTreeObserver
|
|
36
45
|
import android.widget.AdapterView
|
|
37
46
|
import android.widget.ArrayAdapter
|
|
38
47
|
import android.widget.Button
|
|
@@ -46,6 +55,8 @@ import android.widget.SeekBar
|
|
|
46
55
|
import android.widget.Spinner
|
|
47
56
|
import android.widget.Switch
|
|
48
57
|
import android.widget.TextView
|
|
58
|
+
import android.widget.MediaController
|
|
59
|
+
import android.widget.VideoView
|
|
49
60
|
import androidx.appcompat.app.AppCompatActivity
|
|
50
61
|
import androidx.appcompat.app.AppCompatDelegate
|
|
51
62
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|
@@ -61,6 +72,7 @@ import androidx.javascriptengine.Message
|
|
|
61
72
|
import androidx.javascriptengine.MessagePort
|
|
62
73
|
import androidx.javascriptengine.MessagePortClient
|
|
63
74
|
import com.google.common.util.concurrent.ListenableFuture
|
|
75
|
+
import com.craft.runtime.CraftNative
|
|
64
76
|
import org.json.JSONArray
|
|
65
77
|
import org.json.JSONObject
|
|
66
78
|
import java.net.URL
|
|
@@ -69,7 +81,10 @@ import java.util.concurrent.Future
|
|
|
69
81
|
import java.util.UUID
|
|
70
82
|
import kotlin.math.max
|
|
71
83
|
import kotlin.math.min
|
|
84
|
+
import kotlin.math.PI
|
|
72
85
|
import kotlin.math.roundToInt
|
|
86
|
+
import kotlin.math.roundToLong
|
|
87
|
+
import kotlin.math.sin
|
|
73
88
|
|
|
74
89
|
private const val CRAFT_NATIVE_CAPABILITY_TIMEOUT_MS = 30_000L
|
|
75
90
|
|
|
@@ -154,6 +169,214 @@ private fun craftNativeGridColumnCount(value: Any?): Int {
|
|
|
154
169
|
return max(1, trimmed.split(Regex("\\s+")).count { it.isNotBlank() })
|
|
155
170
|
}
|
|
156
171
|
|
|
172
|
+
/** A DOM-free SVG surface for the geometry emitted by the shared STX native protocol. */
|
|
173
|
+
private class CraftNativeSvgView(context: Context) : View(context) {
|
|
174
|
+
var drawing: JSONObject = JSONObject()
|
|
175
|
+
set(value) {
|
|
176
|
+
field = value
|
|
177
|
+
invalidate()
|
|
178
|
+
requestLayout()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private data class SvgPaint(
|
|
182
|
+
val fill: String? = "#000000",
|
|
183
|
+
val stroke: String? = null,
|
|
184
|
+
val strokeWidth: Float = 1f,
|
|
185
|
+
val color: String = "#000000",
|
|
186
|
+
val opacity: Float = 1f,
|
|
187
|
+
val typeface: Typeface = Typeface.DEFAULT,
|
|
188
|
+
) {
|
|
189
|
+
fun merge(node: JSONObject): SvgPaint {
|
|
190
|
+
val props = node.optJSONObject("props") ?: JSONObject()
|
|
191
|
+
val style = node.optJSONObject("style") ?: JSONObject()
|
|
192
|
+
fun value(name: String): String? = when {
|
|
193
|
+
style.has(name) && !style.isNull(name) -> style.optString(name)
|
|
194
|
+
props.has(name) && !props.isNull(name) -> props.optString(name)
|
|
195
|
+
else -> null
|
|
196
|
+
}
|
|
197
|
+
val weight = value("fontWeight")
|
|
198
|
+
return copy(
|
|
199
|
+
fill = value("fill") ?: fill,
|
|
200
|
+
stroke = value("stroke") ?: stroke,
|
|
201
|
+
strokeWidth = value("strokeWidth")?.toFloatOrNull() ?: strokeWidth,
|
|
202
|
+
color = value("color") ?: color,
|
|
203
|
+
opacity = (value("opacity")?.toFloatOrNull() ?: 1f) * opacity,
|
|
204
|
+
typeface = if (weight in setOf("bold", "700")) Typeface.DEFAULT_BOLD else typeface,
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
210
|
+
val width = MeasureSpec.getSize(widthMeasureSpec)
|
|
211
|
+
val box = viewBox()
|
|
212
|
+
val naturalHeight = if (box != null && box.second > 0) (width * box.second / box.first).roundToInt() else 0
|
|
213
|
+
val height = when (MeasureSpec.getMode(heightMeasureSpec)) {
|
|
214
|
+
MeasureSpec.EXACTLY -> MeasureSpec.getSize(heightMeasureSpec)
|
|
215
|
+
MeasureSpec.AT_MOST -> min(naturalHeight, MeasureSpec.getSize(heightMeasureSpec))
|
|
216
|
+
else -> naturalHeight
|
|
217
|
+
}
|
|
218
|
+
setMeasuredDimension(resolveSize(width, widthMeasureSpec), resolveSize(height, heightMeasureSpec))
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** viewBox units to the view's pixels, set by each draw. */
|
|
222
|
+
private val viewTransform = Matrix()
|
|
223
|
+
private val inverseTransform = Matrix()
|
|
224
|
+
|
|
225
|
+
override fun onDraw(canvas: Canvas) {
|
|
226
|
+
super.onDraw(canvas)
|
|
227
|
+
val box = viewBox() ?: return
|
|
228
|
+
if (box.first <= 0 || box.second <= 0) return
|
|
229
|
+
// Centred at one scale (SVG's default, `xMidYMid meet`), or each axis
|
|
230
|
+
// stretched to fill (`preserveAspectRatio="none"`).
|
|
231
|
+
val stretch = drawing.optJSONObject("props")?.optString("preserveAspectRatio")?.trim() == "none"
|
|
232
|
+
var sx = width.toFloat() / box.first
|
|
233
|
+
var sy = height.toFloat() / box.second
|
|
234
|
+
var dx = 0f
|
|
235
|
+
var dy = 0f
|
|
236
|
+
if (!stretch) {
|
|
237
|
+
val scale = min(sx, sy)
|
|
238
|
+
sx = scale
|
|
239
|
+
sy = scale
|
|
240
|
+
dx = (width - box.first * scale) / 2f
|
|
241
|
+
dy = (height - box.second * scale) / 2f
|
|
242
|
+
}
|
|
243
|
+
if (!sx.isFinite() || !sy.isFinite() || sx <= 0f || sy <= 0f) return
|
|
244
|
+
viewTransform.reset()
|
|
245
|
+
viewTransform.postTranslate(-box.third, -box.fourth)
|
|
246
|
+
viewTransform.postScale(sx, sy)
|
|
247
|
+
viewTransform.postTranslate(dx, dy)
|
|
248
|
+
viewTransform.invert(inverseTransform)
|
|
249
|
+
canvas.save()
|
|
250
|
+
canvas.concat(viewTransform)
|
|
251
|
+
drawNodes(canvas, drawing.optJSONArray("children") ?: JSONArray(), SvgPaint().merge(drawing))
|
|
252
|
+
canvas.restore()
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private fun viewBox(): Quadruple<Float, Float, Float, Float>? {
|
|
256
|
+
val values = drawing.optJSONObject("props")?.optJSONArray("viewBox") ?: return null
|
|
257
|
+
if (values.length() != 4) return null
|
|
258
|
+
val x = values.optDouble(0, Double.NaN).toFloat()
|
|
259
|
+
val y = values.optDouble(1, Double.NaN).toFloat()
|
|
260
|
+
val width = values.optDouble(2, Double.NaN).toFloat()
|
|
261
|
+
val height = values.optDouble(3, Double.NaN).toFloat()
|
|
262
|
+
if (!x.isFinite() || !y.isFinite() || !width.isFinite() || !height.isFinite()) return null
|
|
263
|
+
return Quadruple(width, height, x, y)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private fun drawNodes(canvas: Canvas, children: JSONArray, inherited: SvgPaint) {
|
|
267
|
+
for (index in 0 until children.length()) {
|
|
268
|
+
val node = children.optJSONObject(index) ?: continue
|
|
269
|
+
val style = node.optJSONObject("style") ?: JSONObject()
|
|
270
|
+
if (style.optString("display") == "none") continue
|
|
271
|
+
val paint = inherited.merge(node)
|
|
272
|
+
when (node.optString("type")) {
|
|
273
|
+
"G" -> drawNodes(canvas, node.optJSONArray("children") ?: JSONArray(), paint)
|
|
274
|
+
"SvgText" -> drawText(canvas, node, paint)
|
|
275
|
+
else -> drawPath(canvas, node, paint)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private fun drawPath(canvas: Canvas, node: JSONObject, style: SvgPaint) {
|
|
281
|
+
val props = node.optJSONObject("props") ?: JSONObject()
|
|
282
|
+
val geometry = props.optJSONArray("geometry") ?: return
|
|
283
|
+
val path = Path()
|
|
284
|
+
var index = 0
|
|
285
|
+
while (index < geometry.length()) {
|
|
286
|
+
val command = geometry.optDouble(index, Double.NaN).toInt()
|
|
287
|
+
fun number(offset: Int): Float = geometry.optDouble(index + offset, Double.NaN).toFloat()
|
|
288
|
+
when (command) {
|
|
289
|
+
0 -> if (index + 2 < geometry.length()) { path.moveTo(number(1), number(2)); index += 3 } else break
|
|
290
|
+
1 -> if (index + 2 < geometry.length()) { path.lineTo(number(1), number(2)); index += 3 } else break
|
|
291
|
+
2 -> if (index + 6 < geometry.length()) { path.cubicTo(number(1), number(2), number(3), number(4), number(5), number(6)); index += 7 } else break
|
|
292
|
+
3 -> if (index + 4 < geometry.length()) { path.quadTo(number(1), number(2), number(3), number(4)); index += 5 } else break
|
|
293
|
+
4 -> { path.close(); index += 1 }
|
|
294
|
+
else -> break
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
val fill = android.graphics.Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
298
|
+
color = resolveColor(style.fill, style.color, Color.TRANSPARENT)
|
|
299
|
+
alpha = (style.opacity * 255f).roundToInt().coerceIn(0, 255)
|
|
300
|
+
this.style = Paint.Style.FILL
|
|
301
|
+
}
|
|
302
|
+
if (style.fill != "none" && style.fill != "transparent") canvas.drawPath(path, fill)
|
|
303
|
+
if (!style.stroke.isNullOrBlank() && style.stroke != "none" && style.stroke != "transparent" && style.strokeWidth > 0) {
|
|
304
|
+
val stroke = android.graphics.Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
305
|
+
color = resolveColor(style.stroke, style.color, Color.TRANSPARENT)
|
|
306
|
+
alpha = (style.opacity * 255f).roundToInt().coerceIn(0, 255)
|
|
307
|
+
this.style = Paint.Style.STROKE
|
|
308
|
+
strokeWidth = style.strokeWidth
|
|
309
|
+
strokeCap = when (props.optString("strokeLinecap")) {
|
|
310
|
+
"round" -> Paint.Cap.ROUND
|
|
311
|
+
"square" -> Paint.Cap.SQUARE
|
|
312
|
+
else -> Paint.Cap.BUTT
|
|
313
|
+
}
|
|
314
|
+
strokeJoin = when (props.optString("strokeLinejoin")) {
|
|
315
|
+
"round" -> Paint.Join.ROUND
|
|
316
|
+
"bevel" -> Paint.Join.BEVEL
|
|
317
|
+
else -> Paint.Join.MITER
|
|
318
|
+
}
|
|
319
|
+
props.optJSONArray("strokeDasharray")?.let { values ->
|
|
320
|
+
val dashes: List<Float> = (0 until values.length()).mapNotNull {
|
|
321
|
+
values.optDouble(it, Double.NaN).takeUnless { value -> value.isNaN() }?.toFloat()
|
|
322
|
+
}
|
|
323
|
+
if (dashes.isNotEmpty()) {
|
|
324
|
+
val dashPattern = dashes.toFloatArray()
|
|
325
|
+
val pattern: FloatArray = if (dashPattern.size % 2 == 1) {
|
|
326
|
+
FloatArray(dashPattern.size * 2) { index -> dashPattern[index % dashPattern.size] }
|
|
327
|
+
} else {
|
|
328
|
+
dashPattern
|
|
329
|
+
}
|
|
330
|
+
pathEffect = DashPathEffect(pattern, 0f)
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// `vector-effect="non-scaling-stroke"`: the stroke is in the view's
|
|
335
|
+
// pixels, so a stretched chart's line keeps its width.
|
|
336
|
+
if (props.optString("vectorEffect") == "non-scaling-stroke") {
|
|
337
|
+
val line = Path(path).apply { transform(viewTransform) }
|
|
338
|
+
canvas.save()
|
|
339
|
+
canvas.concat(inverseTransform)
|
|
340
|
+
canvas.drawPath(line, stroke)
|
|
341
|
+
canvas.restore()
|
|
342
|
+
} else {
|
|
343
|
+
canvas.drawPath(path, stroke)
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private fun drawText(canvas: Canvas, node: JSONObject, style: SvgPaint) {
|
|
349
|
+
val props = node.optJSONObject("props") ?: JSONObject()
|
|
350
|
+
val nodeStyle = node.optJSONObject("style") ?: JSONObject()
|
|
351
|
+
val text = buildString {
|
|
352
|
+
val children = node.optJSONArray("children") ?: JSONArray()
|
|
353
|
+
for (index in 0 until children.length()) if (children.opt(index) is String) append(children.optString(index))
|
|
354
|
+
}
|
|
355
|
+
if (text.isEmpty()) return
|
|
356
|
+
val size = (if (props.has("fontSize")) props.optDouble("fontSize", 12.0) else nodeStyle.optDouble("fontSize", 12.0))
|
|
357
|
+
.toFloat()
|
|
358
|
+
val paint = android.graphics.Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
359
|
+
color = resolveColor(style.fill, style.color, Color.BLACK)
|
|
360
|
+
alpha = (style.opacity * 255f).roundToInt().coerceIn(0, 255)
|
|
361
|
+
textSize = size
|
|
362
|
+
typeface = style.typeface
|
|
363
|
+
}
|
|
364
|
+
var x = props.optDouble("x", 0.0).toFloat()
|
|
365
|
+
val y = props.optDouble("y", 0.0).toFloat()
|
|
366
|
+
when (props.optString("textAnchor")) {
|
|
367
|
+
"middle" -> x -= paint.measureText(text) / 2f
|
|
368
|
+
"end" -> x -= paint.measureText(text)
|
|
369
|
+
}
|
|
370
|
+
canvas.drawText(text, x, y, paint)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private fun resolveColor(value: String?, current: String, fallback: Int): Int = runCatching {
|
|
374
|
+
Color.parseColor(if (value == "currentColor") current else value ?: current)
|
|
375
|
+
}.getOrDefault(fallback)
|
|
376
|
+
|
|
377
|
+
private data class Quadruple<T1, T2, T3, T4>(val first: T1, val second: T2, val third: T3, val fourth: T4)
|
|
378
|
+
}
|
|
379
|
+
|
|
157
380
|
/** A deterministic flex-shaped container shared by normal views and scroll content. */
|
|
158
381
|
private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
159
382
|
var onLayoutChanged: (() -> Unit)? = null
|
|
@@ -167,9 +390,11 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
167
390
|
private var grid = false
|
|
168
391
|
private var gridColumns = 1
|
|
169
392
|
private var gridAutoRows: Int? = null
|
|
393
|
+
private var reverse = false
|
|
170
394
|
|
|
171
395
|
fun configure(style: JSONObject, density: Float) {
|
|
172
396
|
orientation = if (style.optString("flexDirection") in setOf("row", "row-reverse")) HORIZONTAL else VERTICAL
|
|
397
|
+
reverse = style.optString("flexDirection").endsWith("-reverse")
|
|
173
398
|
wrap = style.optString("flexWrap") == "wrap" || style.optBoolean("flexWrap", false)
|
|
174
399
|
alignItems = style.optString("alignItems", "stretch")
|
|
175
400
|
alignContent = style.optString("alignContent", "flex-start")
|
|
@@ -214,7 +439,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
214
439
|
val gap = if (orientation == HORIZONTAL) columnGap else rowGap
|
|
215
440
|
val lines = mutableListOf<Line>()
|
|
216
441
|
val absolute = mutableListOf<Entry>()
|
|
217
|
-
|
|
442
|
+
val pending = mutableListOf<Entry>()
|
|
218
443
|
for (index in 0 until childCount) {
|
|
219
444
|
val child = getChildAt(index)
|
|
220
445
|
if (child.visibility == GONE) continue
|
|
@@ -236,18 +461,43 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
236
461
|
val childHeight = measureChildHeight(child, style, height - paddingTop - paddingBottom, verticalMargins)
|
|
237
462
|
val entry = Entry(child, style, childWidth, childHeight)
|
|
238
463
|
if (style.position == "absolute") { absolute += entry; continue }
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
464
|
+
pending += entry
|
|
465
|
+
}
|
|
466
|
+
if (pending.isNotEmpty()) {
|
|
467
|
+
val basis = pending.map { entry ->
|
|
468
|
+
if (orientation == HORIZONTAL) entry.width.toFloat() else entry.height.toFloat()
|
|
469
|
+
}.toFloatArray()
|
|
470
|
+
val margins = pending.flatMap { entry ->
|
|
471
|
+
val lp = entry.view.layoutParams as? MarginLayoutParams
|
|
472
|
+
if (orientation == HORIZONTAL) listOf((lp?.leftMargin ?: 0).toFloat(), (lp?.rightMargin ?: 0).toFloat())
|
|
473
|
+
else listOf((lp?.topMargin ?: 0).toFloat(), (lp?.bottomMargin ?: 0).toFloat())
|
|
474
|
+
}.toFloatArray()
|
|
475
|
+
val sharedLineIds = CraftNative.flexLines(availableMain.toFloat(), gap.toFloat(), wrap, basis, margins)
|
|
476
|
+
val lineIds = sharedLineIds?.takeIf { ids ->
|
|
477
|
+
ids.size >= pending.size && ids.firstOrNull() == 0 &&
|
|
478
|
+
ids.indices.drop(1).all { index -> ids[index] >= ids[index - 1] }
|
|
479
|
+
}
|
|
480
|
+
var line = Line()
|
|
481
|
+
pending.forEachIndexed { index, entry ->
|
|
482
|
+
val childMain = if (orientation == HORIZONTAL) entry.width + (margins[index * 2] + margins[index * 2 + 1]).roundToInt()
|
|
483
|
+
else entry.height + (margins[index * 2] + margins[index * 2 + 1]).roundToInt()
|
|
484
|
+
val lp = entry.view.layoutParams as? MarginLayoutParams
|
|
485
|
+
val crossMargins = if (orientation == HORIZONTAL) (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
|
|
486
|
+
else (lp?.leftMargin ?: 0) + (lp?.rightMargin ?: 0)
|
|
487
|
+
val childCross = if (orientation == HORIZONTAL) entry.height + crossMargins else entry.width + crossMargins
|
|
488
|
+
if (index > 0 && lineIds != null && lineIds[index] != lineIds[index - 1]) {
|
|
489
|
+
lines += line
|
|
490
|
+
line = Line()
|
|
491
|
+
} else if (index > 0 && lineIds == null && wrap && line.entries.isNotEmpty() && line.main + gap + childMain > availableMain) {
|
|
492
|
+
lines += line
|
|
493
|
+
line = Line()
|
|
494
|
+
}
|
|
495
|
+
line.entries += entry
|
|
496
|
+
line.main = if (line.entries.size == 1) childMain else line.main + gap + childMain
|
|
497
|
+
line.cross = max(line.cross, childCross)
|
|
498
|
+
}
|
|
499
|
+
if (line.entries.isNotEmpty()) lines += line
|
|
500
|
+
}
|
|
251
501
|
return lines to absolute
|
|
252
502
|
}
|
|
253
503
|
|
|
@@ -361,6 +611,24 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
361
611
|
else -> 0.0
|
|
362
612
|
}
|
|
363
613
|
|
|
614
|
+
private fun justifyCode(): Int = when (justifyContent) {
|
|
615
|
+
"center" -> 1
|
|
616
|
+
"flex-end" -> 2
|
|
617
|
+
"space-between" -> 3
|
|
618
|
+
"space-around" -> 4
|
|
619
|
+
"space-evenly" -> 5
|
|
620
|
+
else -> 0
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
private fun alignContentCode(): Int = when (alignContent) {
|
|
624
|
+
"center" -> 1
|
|
625
|
+
"flex-end" -> 2
|
|
626
|
+
"space-between" -> 3
|
|
627
|
+
"space-around" -> 4
|
|
628
|
+
"stretch" -> 5
|
|
629
|
+
else -> 0
|
|
630
|
+
}
|
|
631
|
+
|
|
364
632
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
365
633
|
val widthMode = View.MeasureSpec.getMode(widthMeasureSpec)
|
|
366
634
|
val width = View.MeasureSpec.getSize(widthMeasureSpec).takeUnless { widthMode == View.MeasureSpec.UNSPECIFIED } ?: 1_000_000
|
|
@@ -396,36 +664,66 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
396
664
|
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
397
665
|
if (grid) {
|
|
398
666
|
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
|
-
|
|
667
|
+
val contentWidth = width - paddingLeft - paddingRight
|
|
668
|
+
val contentHeight = height - paddingTop - paddingBottom
|
|
669
|
+
val flow = layout.rows.flatten()
|
|
670
|
+
val sharedFrames = CraftNative.grid(
|
|
671
|
+
contentWidth.toFloat(), contentHeight.toFloat(), gridColumns, columnGap.toFloat(), rowGap.toFloat(),
|
|
672
|
+
flow.map { it.width.toFloat() }.toFloatArray(), flow.map { it.height.toFloat() }.toFloatArray(),
|
|
673
|
+
flow.map { it.style.width?.resolve(layout.cellWidth)?.toFloat() ?: Float.NaN }.toFloatArray(),
|
|
674
|
+
flow.map { it.style.height?.resolve(null)?.toFloat() ?: Float.NaN }.toFloatArray(),
|
|
675
|
+
flow.map { it.style.minWidth?.resolve(layout.cellWidth)?.toFloat() ?: Float.NaN }.toFloatArray(),
|
|
676
|
+
flow.map { it.style.maxWidth?.resolve(layout.cellWidth)?.toFloat() ?: Float.NaN }.toFloatArray(),
|
|
677
|
+
flow.map { it.style.minHeight?.resolve(null)?.toFloat() ?: Float.NaN }.toFloatArray(),
|
|
678
|
+
flow.map { it.style.maxHeight?.resolve(null)?.toFloat() ?: Float.NaN }.toFloatArray(),
|
|
679
|
+
flow.flatMap {
|
|
680
|
+
val lp = it.view.layoutParams as? MarginLayoutParams
|
|
681
|
+
listOf((lp?.leftMargin ?: 0).toFloat(), (lp?.rightMargin ?: 0).toFloat(), (lp?.topMargin ?: 0).toFloat(), (lp?.bottomMargin ?: 0).toFloat())
|
|
682
|
+
}.toFloatArray(),
|
|
683
|
+
flow.flatMap {
|
|
684
|
+
val align = it.style.alignSelf ?: alignItems
|
|
685
|
+
listOf(if (align == "stretch") 1f else 0f, if (align == "center") 1f else if (align == "flex-end") 2f else 0f)
|
|
686
|
+
}.toFloatArray(),
|
|
687
|
+
)
|
|
688
|
+
if (sharedFrames != null) {
|
|
689
|
+
for ((index, entry) in flow.withIndex()) {
|
|
690
|
+
val offset = index * 4
|
|
691
|
+
val x = paddingLeft + sharedFrames[offset].roundToInt()
|
|
692
|
+
val y = paddingTop + sharedFrames[offset + 1].roundToInt()
|
|
693
|
+
val childWidth = sharedFrames[offset + 2].roundToInt()
|
|
694
|
+
val childHeight = sharedFrames[offset + 3].roundToInt()
|
|
423
695
|
entry.view.layout(x, y, x + childWidth, y + childHeight)
|
|
424
696
|
}
|
|
425
|
-
|
|
697
|
+
} else {
|
|
698
|
+
var rowTop = paddingTop
|
|
699
|
+
for ((rowIndex, row) in layout.rows.withIndex()) {
|
|
700
|
+
val rowHeight = layout.rowHeights[rowIndex]
|
|
701
|
+
for ((column, entry) in row.withIndex()) {
|
|
702
|
+
val align = entry.style.alignSelf ?: alignItems
|
|
703
|
+
val lp = entry.view.layoutParams as? MarginLayoutParams
|
|
704
|
+
val marginLeft = lp?.leftMargin ?: 0
|
|
705
|
+
val marginRight = lp?.rightMargin ?: 0
|
|
706
|
+
val marginTop = lp?.topMargin ?: 0
|
|
707
|
+
val marginBottom = lp?.bottomMargin ?: 0
|
|
708
|
+
val availableWidth = max(0, layout.cellWidth - marginLeft - marginRight)
|
|
709
|
+
val availableHeight = max(0, rowHeight - marginTop - marginBottom)
|
|
710
|
+
val childWidth = if (align == "stretch" && entry.style.width == null) availableWidth else min(availableWidth, entry.width)
|
|
711
|
+
val childHeight = if (align == "stretch" && entry.style.height == null) availableHeight else min(availableHeight, entry.height)
|
|
712
|
+
val x = paddingLeft + column * (layout.cellWidth + columnGap) + marginLeft + when (align) {
|
|
713
|
+
"center" -> (availableWidth - childWidth) / 2
|
|
714
|
+
"flex-end" -> availableWidth - childWidth
|
|
715
|
+
else -> 0
|
|
716
|
+
}
|
|
717
|
+
val y = rowTop + marginTop + when (align) {
|
|
718
|
+
"center" -> (availableHeight - childHeight) / 2
|
|
719
|
+
"flex-end" -> availableHeight - childHeight
|
|
720
|
+
else -> 0
|
|
721
|
+
}
|
|
722
|
+
entry.view.layout(x, y, x + childWidth, y + childHeight)
|
|
723
|
+
}
|
|
724
|
+
rowTop += rowHeight + rowGap
|
|
725
|
+
}
|
|
426
726
|
}
|
|
427
|
-
val contentWidth = width - paddingLeft - paddingRight
|
|
428
|
-
val contentHeight = height - paddingTop - paddingBottom
|
|
429
727
|
for (entry in layout.absolute) {
|
|
430
728
|
if (entry.view is CraftNativeModalView && entry.view.visibility == View.VISIBLE) entry.view.bringToFront()
|
|
431
729
|
val childWidth = if (entry.style.left != null && entry.style.right != null) contentWidth - entry.style.left!! - entry.style.right!! else entry.width
|
|
@@ -448,8 +746,14 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
448
746
|
val crossFree = max(0, availableCross - totalCross)
|
|
449
747
|
var crossLeading = 0
|
|
450
748
|
var crossBetween = crossGap
|
|
749
|
+
val sharedCross = if (wrap && lines.size > 1) {
|
|
750
|
+
CraftNative.flexLineDistribution(
|
|
751
|
+
availableCross.toFloat(), crossGap.toFloat(), alignContentCode(), lines.map { it.cross.toFloat() }.toFloatArray()
|
|
752
|
+
)
|
|
753
|
+
} else null
|
|
754
|
+
sharedCross?.second?.forEachIndexed { index, size -> lines[index].cross = size.roundToInt() }
|
|
451
755
|
if (wrap && lines.size > 1) {
|
|
452
|
-
when (alignContent) {
|
|
756
|
+
if (sharedCross == null) when (alignContent) {
|
|
453
757
|
"center" -> crossLeading = crossFree / 2
|
|
454
758
|
"flex-end" -> crossLeading = crossFree
|
|
455
759
|
"space-between" -> crossBetween = crossFree / (lines.size - 1) + crossGap
|
|
@@ -464,23 +768,55 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
464
768
|
}
|
|
465
769
|
}
|
|
466
770
|
var crossOffset = (if (orientation == HORIZONTAL) paddingTop else paddingLeft) + crossLeading
|
|
467
|
-
for (line in lines) {
|
|
771
|
+
for ((lineIndex, line) in lines.withIndex()) {
|
|
772
|
+
val lineOffset = sharedCross?.first?.get(lineIndex)?.roundToInt()?.let { (if (orientation == HORIZONTAL) paddingTop else paddingLeft) + it } ?: crossOffset
|
|
468
773
|
val rawFree = availableMain - line.main
|
|
469
|
-
val
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
774
|
+
val sharedPrimary = CraftNative.flexRow(
|
|
775
|
+
availableMain.toFloat(),
|
|
776
|
+
gap.toFloat(),
|
|
777
|
+
justifyCode(),
|
|
778
|
+
line.entries.map { entry -> if (orientation == HORIZONTAL) entry.width.toFloat() else entry.height.toFloat() }.toFloatArray(),
|
|
779
|
+
line.entries.map { it.style.flexGrow.toFloat() }.toFloatArray(),
|
|
780
|
+
line.entries.map { (it.style.flexShrink ?: defaultFlexShrink(it.view)).toFloat() }.toFloatArray(),
|
|
781
|
+
line.entries.map {
|
|
782
|
+
(if (orientation == HORIZONTAL) it.style.minWidth?.resolve(contentWidth) else it.style.minHeight?.resolve(contentHeight))?.toFloat()
|
|
783
|
+
?: Float.NaN
|
|
784
|
+
}.toFloatArray(),
|
|
785
|
+
line.entries.map {
|
|
786
|
+
(if (orientation == HORIZONTAL) it.style.maxWidth?.resolve(contentWidth) else it.style.maxHeight?.resolve(contentHeight))?.toFloat()
|
|
787
|
+
?: Float.NaN
|
|
788
|
+
}.toFloatArray(),
|
|
789
|
+
line.entries.flatMap { entry ->
|
|
790
|
+
val lp = entry.view.layoutParams as? MarginLayoutParams
|
|
791
|
+
if (orientation == HORIZONTAL) listOf((lp?.leftMargin ?: 0).toFloat(), (lp?.rightMargin ?: 0).toFloat())
|
|
792
|
+
else listOf((lp?.topMargin ?: 0).toFloat(), (lp?.bottomMargin ?: 0).toFloat())
|
|
793
|
+
}.toFloatArray(),
|
|
794
|
+
)
|
|
795
|
+
val sharedPositions = sharedPrimary?.takeIf { it.size == line.entries.size * 2 }
|
|
796
|
+
val distributedMain = if (sharedPrimary != null) {
|
|
797
|
+
line.entries.indices.sumOf { index ->
|
|
798
|
+
val lp = line.entries[index].view.layoutParams as? MarginLayoutParams
|
|
799
|
+
val margins = if (orientation == HORIZONTAL) (lp?.leftMargin ?: 0) + (lp?.rightMargin ?: 0)
|
|
481
800
|
else (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
|
|
482
|
-
|
|
483
|
-
|
|
801
|
+
sharedPrimary[index].roundToInt() + margins
|
|
802
|
+
} + max(0, line.entries.size - 1) * gap
|
|
803
|
+
} else {
|
|
804
|
+
val factors = line.entries.map { if (rawFree >= 0) it.style.flexGrow else it.style.flexShrink ?: defaultFlexShrink(it.view) }
|
|
805
|
+
val totalFactor = factors.sum()
|
|
806
|
+
line.entries.indices.sumOf { index ->
|
|
807
|
+
val base = if (orientation == HORIZONTAL) line.entries[index].width else line.entries[index].height
|
|
808
|
+
val primary = if (totalFactor > 0) max(0, (base + rawFree * factors[index] / totalFactor).toInt()) else base
|
|
809
|
+
val constrained = if (orientation == HORIZONTAL) {
|
|
810
|
+
clamp(primary, line.entries[index].style.minWidth?.resolve(contentWidth), line.entries[index].style.maxWidth?.resolve(contentWidth))
|
|
811
|
+
} else {
|
|
812
|
+
clamp(primary, line.entries[index].style.minHeight?.resolve(contentHeight), line.entries[index].style.maxHeight?.resolve(contentHeight))
|
|
813
|
+
}
|
|
814
|
+
val lp = line.entries[index].view.layoutParams as? MarginLayoutParams
|
|
815
|
+
val margins = if (orientation == HORIZONTAL) (lp?.leftMargin ?: 0) + (lp?.rightMargin ?: 0)
|
|
816
|
+
else (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
|
|
817
|
+
constrained + margins
|
|
818
|
+
} + max(0, line.entries.size - 1) * gap
|
|
819
|
+
}
|
|
484
820
|
val free = max(0, availableMain - distributedMain)
|
|
485
821
|
val (leading, between) = distribution(free, line.entries.size)
|
|
486
822
|
var mainOffset = if (orientation == HORIZONTAL) paddingLeft else paddingTop
|
|
@@ -495,27 +831,43 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
495
831
|
val align = entry.style.alignSelf ?: alignItems
|
|
496
832
|
val crossLeading = if (orientation == HORIZONTAL) lp?.topMargin ?: 0 else lp?.leftMargin ?: 0
|
|
497
833
|
val crossTrailing = if (orientation == HORIZONTAL) lp?.bottomMargin ?: 0 else lp?.rightMargin ?: 0
|
|
498
|
-
val
|
|
499
|
-
|
|
500
|
-
"
|
|
501
|
-
|
|
834
|
+
val alignment = when (align) {
|
|
835
|
+
"center" -> 1
|
|
836
|
+
"flex-end" -> 2
|
|
837
|
+
else -> 0
|
|
838
|
+
}
|
|
839
|
+
val crossPosition = CraftNative.crossAxisPosition(
|
|
840
|
+
line.cross.toFloat(), crossSize.toFloat(), crossLeading.toFloat(), crossTrailing.toFloat(), alignment
|
|
841
|
+
)?.roundToInt() ?: when (alignment) {
|
|
842
|
+
1 -> crossLeading + (line.cross - crossLeading - crossTrailing - crossSize) / 2
|
|
843
|
+
2 -> line.cross - crossTrailing - crossSize
|
|
502
844
|
else -> crossLeading
|
|
503
845
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
val
|
|
508
|
-
val
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
846
|
+
// Shared positions cover every non-reverse justification; the
|
|
847
|
+
// local main-axis rule remains the fallback for old runtimes
|
|
848
|
+
// that return widths only.
|
|
849
|
+
val sharedPosition = if (!reverse) sharedPositions?.get(index)?.roundToInt() else null
|
|
850
|
+
val positionedMain = sharedPosition?.let { (if (orientation == HORIZONTAL) paddingLeft else paddingTop) + it }
|
|
851
|
+
?: mainOffset + marginStart
|
|
852
|
+
val x = if (orientation == HORIZONTAL) positionedMain else lineOffset + crossPosition
|
|
853
|
+
val y = if (orientation == HORIZONTAL) lineOffset + crossPosition else positionedMain
|
|
854
|
+
val distributedPrimary = sharedPrimary?.get(index)?.roundToInt() ?: run {
|
|
855
|
+
val primary = if (orientation == HORIZONTAL) entry.width else entry.height
|
|
856
|
+
val factor = if (rawFree >= 0) entry.style.flexGrow else entry.style.flexShrink ?: defaultFlexShrink(entry.view)
|
|
857
|
+
val totalFactor = line.entries.sumOf { if (rawFree >= 0) it.style.flexGrow else it.style.flexShrink ?: defaultFlexShrink(it.view) }
|
|
858
|
+
val distributed = if (totalFactor > 0) max(0, (primary + rawFree * factor / totalFactor).toInt()) else primary
|
|
859
|
+
if (orientation == HORIZONTAL) {
|
|
860
|
+
clamp(distributed, entry.style.minWidth?.resolve(contentWidth), entry.style.maxWidth?.resolve(contentWidth))
|
|
861
|
+
} else {
|
|
862
|
+
clamp(distributed, entry.style.minHeight?.resolve(contentHeight), entry.style.maxHeight?.resolve(contentHeight))
|
|
863
|
+
}
|
|
512
864
|
}
|
|
513
865
|
val w = if (orientation == HORIZONTAL) distributedPrimary else crossSize
|
|
514
866
|
val h = if (orientation == HORIZONTAL) crossSize else distributedPrimary
|
|
515
867
|
entry.view.layout(x, y, x + w, y + h)
|
|
516
|
-
mainOffset += distributedPrimary + marginStart + marginEnd + gap + between
|
|
868
|
+
if (sharedPosition == null) mainOffset += distributedPrimary + marginStart + marginEnd + gap + between
|
|
517
869
|
}
|
|
518
|
-
crossOffset += line.cross + crossBetween
|
|
870
|
+
if (sharedCross == null) crossOffset += line.cross + crossBetween
|
|
519
871
|
}
|
|
520
872
|
for (entry in absolute) {
|
|
521
873
|
if (entry.view is CraftNativeModalView && entry.view.visibility == View.VISIBLE) entry.view.bringToFront()
|
|
@@ -736,6 +1088,7 @@ private class CraftNativeModalView(context: Context) : FrameLayout(context) {
|
|
|
736
1088
|
val content = CraftNativeFlexLayout(context)
|
|
737
1089
|
private val blocker = View(context)
|
|
738
1090
|
private var renderedVisible = false
|
|
1091
|
+
private var dismissSuppressedByRequest = false
|
|
739
1092
|
private var animationGeneration = 0
|
|
740
1093
|
var onRequestClose: (() -> Unit)? = null
|
|
741
1094
|
|
|
@@ -748,13 +1101,38 @@ private class CraftNativeModalView(context: Context) : FrameLayout(context) {
|
|
|
748
1101
|
init {
|
|
749
1102
|
isClickable = true
|
|
750
1103
|
blocker.isClickable = true
|
|
751
|
-
blocker.setOnTouchListener { _,
|
|
1104
|
+
blocker.setOnTouchListener { _, event ->
|
|
1105
|
+
if (event.action == MotionEvent.ACTION_UP) requestClose()
|
|
1106
|
+
true
|
|
1107
|
+
}
|
|
752
1108
|
blocker.setBackgroundColor(Color.argb(82, 0, 0, 0))
|
|
753
1109
|
addView(blocker, LayoutParams(-1, -1))
|
|
754
1110
|
addView(content, LayoutParams(-1, -1))
|
|
755
1111
|
}
|
|
756
1112
|
|
|
1113
|
+
fun requestClose() {
|
|
1114
|
+
val handler = onRequestClose ?: return
|
|
1115
|
+
dismissSuppressedByRequest = true
|
|
1116
|
+
isClickable = false
|
|
1117
|
+
blocker.isEnabled = false
|
|
1118
|
+
handler()
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
fun consumeDismissSuppression(): Boolean {
|
|
1122
|
+
val suppressed = dismissSuppressedByRequest
|
|
1123
|
+
dismissSuppressedByRequest = false
|
|
1124
|
+
return suppressed
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
fun clearDismissSuppression() {
|
|
1128
|
+
dismissSuppressedByRequest = false
|
|
1129
|
+
}
|
|
1130
|
+
|
|
757
1131
|
fun setVisible(visible: Boolean, opacity: Float, animationType: String?) {
|
|
1132
|
+
if (visible) {
|
|
1133
|
+
isClickable = true
|
|
1134
|
+
blocker.isEnabled = true
|
|
1135
|
+
}
|
|
758
1136
|
if (renderedVisible == visible && visibility == if (visible) View.VISIBLE else View.GONE) {
|
|
759
1137
|
if (visible && alpha != opacity) alpha = opacity
|
|
760
1138
|
return
|
|
@@ -806,6 +1184,9 @@ private class CraftNativeModalView(context: Context) : FrameLayout(context) {
|
|
|
806
1184
|
visibility = View.GONE
|
|
807
1185
|
translationY = 0f
|
|
808
1186
|
alpha = 0f
|
|
1187
|
+
isClickable = true
|
|
1188
|
+
blocker.isEnabled = true
|
|
1189
|
+
dismissSuppressedByRequest = false
|
|
809
1190
|
onRequestClose = null
|
|
810
1191
|
}
|
|
811
1192
|
}
|
|
@@ -817,6 +1198,232 @@ private data class LayoutBounds(
|
|
|
817
1198
|
val height: Double,
|
|
818
1199
|
)
|
|
819
1200
|
|
|
1201
|
+
/**
|
|
1202
|
+
* Android's default spinner style is indeterminate-only, so assigning
|
|
1203
|
+
* `isIndeterminate = false` to it is silently ignored. Use a determinate
|
|
1204
|
+
* ProgressBar style and carry over the platform spinner drawable instead;
|
|
1205
|
+
* that preserves the native look while allowing `animating={false}` to be a
|
|
1206
|
+
* genuinely stopped control.
|
|
1207
|
+
*/
|
|
1208
|
+
private class CraftNativeActivityIndicator(context: Context) :
|
|
1209
|
+
android.widget.ProgressBar(context, null, android.R.attr.progressBarStyleHorizontal) {
|
|
1210
|
+
init {
|
|
1211
|
+
val spinner = android.widget.ProgressBar(context).indeterminateDrawable
|
|
1212
|
+
indeterminateDrawable = spinner
|
|
1213
|
+
progressDrawable = spinner?.constantState?.newDrawable()?.mutate() ?: spinner
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
private class CraftNativeVideoView(context: Context) : VideoView(context) {
|
|
1218
|
+
private var sourceUri = ""
|
|
1219
|
+
private var shouldLoop = false
|
|
1220
|
+
private var shouldPlay = false
|
|
1221
|
+
private var muted = false
|
|
1222
|
+
|
|
1223
|
+
fun configure(source: Any?, autoplay: Boolean, paused: Boolean, loop: Boolean, muted: Boolean, controls: Boolean) {
|
|
1224
|
+
val uri = when (source) {
|
|
1225
|
+
is String -> source
|
|
1226
|
+
is JSONObject -> source.optString("uri")
|
|
1227
|
+
else -> ""
|
|
1228
|
+
}
|
|
1229
|
+
shouldLoop = loop
|
|
1230
|
+
shouldPlay = autoplay && !paused
|
|
1231
|
+
this.muted = muted
|
|
1232
|
+
setMediaController(if (controls) MediaController(context).also { it.setAnchorView(this) } else null)
|
|
1233
|
+
if (uri != sourceUri) {
|
|
1234
|
+
sourceUri = uri
|
|
1235
|
+
stopPlayback()
|
|
1236
|
+
if (uri.isBlank()) return
|
|
1237
|
+
setVideoURI(Uri.parse(uri))
|
|
1238
|
+
setOnPreparedListener { mediaPlayer ->
|
|
1239
|
+
mediaPlayer.isLooping = shouldLoop
|
|
1240
|
+
val volume = if (this.muted) 0f else 1f
|
|
1241
|
+
mediaPlayer.setVolume(volume, volume)
|
|
1242
|
+
if (shouldPlay) start()
|
|
1243
|
+
}
|
|
1244
|
+
setOnCompletionListener { if (shouldLoop && !isPlaying) start() }
|
|
1245
|
+
}
|
|
1246
|
+
if (uri.isBlank()) return
|
|
1247
|
+
runCatching {
|
|
1248
|
+
if (shouldPlay && !isPlaying) start()
|
|
1249
|
+
else if (!shouldPlay && isPlaying) pause()
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
fun stop() {
|
|
1254
|
+
stopPlayback()
|
|
1255
|
+
sourceUri = ""
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/** The oscillator/gain subset of Web Audio used by native STX screens. */
|
|
1260
|
+
private class CraftNativeAudio {
|
|
1261
|
+
private data class ParameterEvent(val time: Double, val value: Double, val ramp: Boolean)
|
|
1262
|
+
|
|
1263
|
+
private class Parameter(initial: Double) {
|
|
1264
|
+
private var current = initial
|
|
1265
|
+
private val events = mutableListOf<ParameterEvent>()
|
|
1266
|
+
|
|
1267
|
+
@Synchronized fun set(value: Double) {
|
|
1268
|
+
current = value
|
|
1269
|
+
events.clear()
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
@Synchronized fun schedule(value: Double, time: Double, ramp: Boolean) {
|
|
1273
|
+
events += ParameterEvent(time.coerceAtLeast(0.0), value, ramp)
|
|
1274
|
+
events.sortBy { it.time }
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
@Synchronized fun valueAt(time: Double): Double {
|
|
1278
|
+
var previousTime = 0.0
|
|
1279
|
+
var previousValue = current
|
|
1280
|
+
for (event in events) {
|
|
1281
|
+
if (time < event.time) {
|
|
1282
|
+
if (!event.ramp || event.time <= previousTime) return previousValue
|
|
1283
|
+
val fraction = (time - previousTime) / (event.time - previousTime)
|
|
1284
|
+
return previousValue + (event.value - previousValue) * fraction
|
|
1285
|
+
}
|
|
1286
|
+
previousTime = event.time
|
|
1287
|
+
previousValue = event.value
|
|
1288
|
+
}
|
|
1289
|
+
return previousValue
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
private class OscillatorState {
|
|
1294
|
+
val frequency = Parameter(440.0)
|
|
1295
|
+
@Volatile var gain = Parameter(1.0)
|
|
1296
|
+
@Volatile var phase = 0.0
|
|
1297
|
+
@Volatile var active = false
|
|
1298
|
+
var contextStart = 0.0
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
private val handler = Handler(Looper.getMainLooper())
|
|
1302
|
+
private val oscillators = mutableMapOf<String, OscillatorState>()
|
|
1303
|
+
private val gains = mutableMapOf<String, Parameter>()
|
|
1304
|
+
private val contextStarts = mutableMapOf<String, Double>()
|
|
1305
|
+
private var track: AudioTrack? = null
|
|
1306
|
+
@Volatile private var running = false
|
|
1307
|
+
private var renderThread: Thread? = null
|
|
1308
|
+
|
|
1309
|
+
@Synchronized fun handle(payload: JSONObject) {
|
|
1310
|
+
when (payload.optString("action")) {
|
|
1311
|
+
"createContext" -> {
|
|
1312
|
+
payload.optString("contextId").takeIf { it.isNotBlank() }?.let {
|
|
1313
|
+
contextStarts[it] = System.nanoTime() / 1_000_000_000.0
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
"createOscillator" -> {
|
|
1317
|
+
val id = payload.optString("nodeId")
|
|
1318
|
+
if (id.isNotBlank()) oscillators.putIfAbsent(id, OscillatorState()).also {
|
|
1319
|
+
oscillators[id]?.contextStart = contextStarts[payload.optString("contextId")]
|
|
1320
|
+
?: (System.nanoTime() / 1_000_000_000.0)
|
|
1321
|
+
oscillators[id]?.frequency?.set(payload.optDouble("frequency", 440.0).coerceAtLeast(0.0))
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
"createGain" -> {
|
|
1325
|
+
val id = payload.optString("nodeId")
|
|
1326
|
+
if (id.isNotBlank()) gains[id] = Parameter(payload.optDouble("gain", 1.0).coerceAtLeast(0.0))
|
|
1327
|
+
}
|
|
1328
|
+
"connect" -> {
|
|
1329
|
+
val source = payload.optString("sourceId")
|
|
1330
|
+
oscillators[source]?.gain = gains[payload.optString("destinationId")] ?: Parameter(1.0)
|
|
1331
|
+
}
|
|
1332
|
+
"frequency" -> oscillators[payload.optString("nodeId")]?.frequency?.set(payload.optDouble("value", 0.0).coerceAtLeast(0.0))
|
|
1333
|
+
"frequencyAtTime", "frequencyRampToTime" -> oscillators[payload.optString("nodeId")]?.frequency?.schedule(
|
|
1334
|
+
payload.optDouble("value", 0.0).coerceAtLeast(0.0),
|
|
1335
|
+
payload.optDouble("time", 0.0),
|
|
1336
|
+
payload.optString("action") == "frequencyRampToTime",
|
|
1337
|
+
)
|
|
1338
|
+
"gain" -> gains[payload.optString("nodeId")]?.set(payload.optDouble("value", 0.0).coerceAtLeast(0.0))
|
|
1339
|
+
"gainAtTime", "gainRampToTime" -> gains[payload.optString("nodeId")]?.schedule(
|
|
1340
|
+
payload.optDouble("value", 0.0).coerceAtLeast(0.0),
|
|
1341
|
+
payload.optDouble("time", 0.0),
|
|
1342
|
+
payload.optString("action") == "gainRampToTime",
|
|
1343
|
+
)
|
|
1344
|
+
"start", "stop" -> {
|
|
1345
|
+
val state = oscillators[payload.optString("nodeId")] ?: return
|
|
1346
|
+
ensureTrack()
|
|
1347
|
+
val active = payload.optString("action") == "start"
|
|
1348
|
+
val delay = (payload.optDouble("delay", 0.0).coerceAtLeast(0.0) * 1000).roundToLong()
|
|
1349
|
+
handler.postDelayed({ state.active = active }, delay)
|
|
1350
|
+
}
|
|
1351
|
+
"close" -> close()
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
@Synchronized fun close() {
|
|
1356
|
+
oscillators.values.forEach { it.active = false }
|
|
1357
|
+
oscillators.clear()
|
|
1358
|
+
gains.clear()
|
|
1359
|
+
contextStarts.clear()
|
|
1360
|
+
running = false
|
|
1361
|
+
renderThread?.interrupt()
|
|
1362
|
+
renderThread = null
|
|
1363
|
+
track?.let { output ->
|
|
1364
|
+
runCatching {
|
|
1365
|
+
output.pause()
|
|
1366
|
+
output.flush()
|
|
1367
|
+
output.release()
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
track = null
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
@Synchronized private fun ensureTrack() {
|
|
1374
|
+
if (track != null) return
|
|
1375
|
+
val sampleRate = 44_100
|
|
1376
|
+
val minBuffer = AudioTrack.getMinBufferSize(sampleRate, AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT)
|
|
1377
|
+
val format = AudioFormat.Builder()
|
|
1378
|
+
.setSampleRate(sampleRate)
|
|
1379
|
+
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
|
|
1380
|
+
.setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
|
|
1381
|
+
.build()
|
|
1382
|
+
val attributes = AudioAttributes.Builder()
|
|
1383
|
+
.setUsage(AudioAttributes.USAGE_MEDIA)
|
|
1384
|
+
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
|
1385
|
+
.build()
|
|
1386
|
+
val created = AudioTrack.Builder()
|
|
1387
|
+
.setAudioAttributes(attributes)
|
|
1388
|
+
.setAudioFormat(format)
|
|
1389
|
+
.setBufferSizeInBytes(max(minBuffer, sampleRate / 4))
|
|
1390
|
+
.setTransferMode(AudioTrack.MODE_STREAM)
|
|
1391
|
+
.build()
|
|
1392
|
+
track = created
|
|
1393
|
+
running = true
|
|
1394
|
+
created.play()
|
|
1395
|
+
renderThread = Thread({ render(created, sampleRate) }, "craft-native-audio").also { it.start() }
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
private fun render(output: AudioTrack, sampleRate: Int) {
|
|
1399
|
+
val buffer = ShortArray(1024 * 2)
|
|
1400
|
+
while (running && !Thread.currentThread().isInterrupted) {
|
|
1401
|
+
val snapshot = synchronized(this) {
|
|
1402
|
+
oscillators.mapValues { (id, state) ->
|
|
1403
|
+
Triple(state, state.gain, state.active)
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
val now = System.nanoTime() / 1_000_000_000.0
|
|
1407
|
+
for (frame in 0 until 1024) {
|
|
1408
|
+
var sample = 0.0
|
|
1409
|
+
snapshot.forEach { (_, value) ->
|
|
1410
|
+
val (state, gain, active) = value
|
|
1411
|
+
if (!active) return@forEach
|
|
1412
|
+
val time = (now - state.contextStart).coerceAtLeast(0.0) + frame.toDouble() / sampleRate
|
|
1413
|
+
val frequency = state.frequency.valueAt(time)
|
|
1414
|
+
sample += sin(state.phase) * gain.valueAt(time)
|
|
1415
|
+
state.phase += 2 * PI * frequency / sampleRate
|
|
1416
|
+
if (state.phase >= 2 * PI) state.phase -= 2 * PI
|
|
1417
|
+
}
|
|
1418
|
+
val value = (sample.coerceIn(-1.0, 1.0) * Short.MAX_VALUE).toInt().toShort()
|
|
1419
|
+
buffer[frame * 2] = value
|
|
1420
|
+
buffer[frame * 2 + 1] = value
|
|
1421
|
+
}
|
|
1422
|
+
if (output.write(buffer, 0, buffer.size, AudioTrack.WRITE_BLOCKING) < 0) break
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
|
|
820
1427
|
/** WebView-free host for a route bundle compiled by `stx native compile`. */
|
|
821
1428
|
class MainActivity : AppCompatActivity() {
|
|
822
1429
|
private class Screen(
|
|
@@ -854,6 +1461,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
854
1461
|
val modalVisibility = mutableMapOf<View, Boolean>()
|
|
855
1462
|
val modalCloseHandlers = mutableMapOf<View, String>()
|
|
856
1463
|
val layoutHandlers = mutableMapOf<View, String>()
|
|
1464
|
+
val layoutListeners = mutableMapOf<View, View.OnLayoutChangeListener>()
|
|
857
1465
|
val lastLayoutBounds = mutableMapOf<View, LayoutBounds>()
|
|
858
1466
|
val flatListRows = mutableMapOf<CraftNativeFlatList, MutableMap<String, MutableMap<String, View>>>()
|
|
859
1467
|
val flatListOwners = mutableMapOf<String, String>()
|
|
@@ -862,11 +1470,13 @@ class MainActivity : AppCompatActivity() {
|
|
|
862
1470
|
val capabilityScope = UUID.randomUUID().toString()
|
|
863
1471
|
val pendingCapabilities = mutableSetOf<String>()
|
|
864
1472
|
val capabilityTimeouts = mutableMapOf<String, Runnable>()
|
|
1473
|
+
val audio = CraftNativeAudio()
|
|
865
1474
|
var active = true
|
|
866
1475
|
|
|
867
1476
|
fun close() {
|
|
868
1477
|
if (!active) return
|
|
869
1478
|
active = false
|
|
1479
|
+
audio.close()
|
|
870
1480
|
val finish = {
|
|
871
1481
|
port?.close()
|
|
872
1482
|
isolate.close()
|
|
@@ -1040,6 +1650,80 @@ class MainActivity : AppCompatActivity() {
|
|
|
1040
1650
|
globalThis.__stxNativeRoute = ${route?.let { JSONObject.quote(it) } ?: "undefined"};
|
|
1041
1651
|
globalThis.__stxNativeParams = ${params};
|
|
1042
1652
|
""".trimIndent()
|
|
1653
|
+
val audioPrelude = """
|
|
1654
|
+
(function() {
|
|
1655
|
+
if (typeof globalThis.AudioContext !== 'function') {
|
|
1656
|
+
var nextAudioId = 0;
|
|
1657
|
+
function audioMessage(payload) {
|
|
1658
|
+
globalThis.__stxNativeBridge.postMessage({ type: 'AUDIO', payload: payload });
|
|
1659
|
+
}
|
|
1660
|
+
function audioParam(contextId, nodeId, action, initial) {
|
|
1661
|
+
var current = Number(initial) || 0;
|
|
1662
|
+
var param = {};
|
|
1663
|
+
Object.defineProperty(param, 'value', {
|
|
1664
|
+
configurable: true,
|
|
1665
|
+
enumerable: true,
|
|
1666
|
+
get: function() { return current; },
|
|
1667
|
+
set: function(value) {
|
|
1668
|
+
current = Number(value) || 0;
|
|
1669
|
+
audioMessage({ action: action, contextId: contextId, nodeId: nodeId, value: current });
|
|
1670
|
+
}
|
|
1671
|
+
});
|
|
1672
|
+
function schedule(value, when, ramp) {
|
|
1673
|
+
current = Number(value) || 0;
|
|
1674
|
+
var time = Number(when);
|
|
1675
|
+
audioMessage({ action: action + (ramp ? 'RampToTime' : 'AtTime'), contextId: contextId, nodeId: nodeId, value: current, time: isFinite(time) ? Math.max(0, time) : 0 });
|
|
1676
|
+
}
|
|
1677
|
+
param.setValueAtTime = function(value, when) { schedule(value, when, false); return param; };
|
|
1678
|
+
param.linearRampToValueAtTime = function(value, when) { schedule(value, when, true); return param; };
|
|
1679
|
+
return param;
|
|
1680
|
+
}
|
|
1681
|
+
function AudioContext() {
|
|
1682
|
+
this._id = 'audio-' + (++nextAudioId);
|
|
1683
|
+
this._started = Date.now() / 1000;
|
|
1684
|
+
this.state = 'running';
|
|
1685
|
+
this.destination = { _contextId: this._id, _nodeId: 'destination' };
|
|
1686
|
+
audioMessage({ action: 'createContext', contextId: this._id });
|
|
1687
|
+
}
|
|
1688
|
+
Object.defineProperty(AudioContext.prototype, 'currentTime', {
|
|
1689
|
+
get: function() { return Date.now() / 1000 - this._started; }
|
|
1690
|
+
});
|
|
1691
|
+
AudioContext.prototype.createOscillator = function() {
|
|
1692
|
+
var context = this;
|
|
1693
|
+
var node = { _contextId: context._id, _nodeId: context._id + '-osc-' + (++nextAudioId) };
|
|
1694
|
+
node.frequency = audioParam(context._id, node._nodeId, 'frequency', 440);
|
|
1695
|
+
node.connect = function(destination) {
|
|
1696
|
+
audioMessage({ action: 'connect', contextId: context._id, sourceId: node._nodeId, destinationId: destination && destination._nodeId || 'destination' });
|
|
1697
|
+
return destination;
|
|
1698
|
+
};
|
|
1699
|
+
node.start = function(when) {
|
|
1700
|
+
audioMessage({ action: 'start', contextId: context._id, nodeId: node._nodeId, delay: Math.max(0, (Number(when) || 0) - context.currentTime) });
|
|
1701
|
+
};
|
|
1702
|
+
node.stop = function(when) {
|
|
1703
|
+
audioMessage({ action: 'stop', contextId: context._id, nodeId: node._nodeId, delay: Math.max(0, (Number(when) || 0) - context.currentTime) });
|
|
1704
|
+
};
|
|
1705
|
+
audioMessage({ action: 'createOscillator', contextId: context._id, nodeId: node._nodeId, frequency: node.frequency.value });
|
|
1706
|
+
return node;
|
|
1707
|
+
};
|
|
1708
|
+
AudioContext.prototype.createGain = function() {
|
|
1709
|
+
var context = this;
|
|
1710
|
+
var node = { _contextId: context._id, _nodeId: context._id + '-gain-' + (++nextAudioId) };
|
|
1711
|
+
node.gain = audioParam(context._id, node._nodeId, 'gain', 1);
|
|
1712
|
+
node.connect = function(destination) {
|
|
1713
|
+
audioMessage({ action: 'connect', contextId: context._id, sourceId: node._nodeId, destinationId: destination && destination._nodeId || 'destination' });
|
|
1714
|
+
return destination;
|
|
1715
|
+
};
|
|
1716
|
+
audioMessage({ action: 'createGain', contextId: context._id, nodeId: node._nodeId, gain: node.gain.value });
|
|
1717
|
+
return node;
|
|
1718
|
+
};
|
|
1719
|
+
AudioContext.prototype.resume = function() { this.state = 'running'; return Promise.resolve(); };
|
|
1720
|
+
AudioContext.prototype.suspend = function() { this.state = 'suspended'; return Promise.resolve(); };
|
|
1721
|
+
AudioContext.prototype.close = function() { this.state = 'closed'; audioMessage({ action: 'close', contextId: this._id }); return Promise.resolve(); };
|
|
1722
|
+
globalThis.AudioContext = AudioContext;
|
|
1723
|
+
globalThis.webkitAudioContext = AudioContext;
|
|
1724
|
+
}
|
|
1725
|
+
})()
|
|
1726
|
+
""".trimIndent()
|
|
1043
1727
|
val prelude = if (useMessagePorts) """
|
|
1044
1728
|
(async function() {
|
|
1045
1729
|
const port = await android.getNamedPort('craft');
|
|
@@ -1055,6 +1739,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
1055
1739
|
postMessage: function(value) { port.postMessage(value); },
|
|
1056
1740
|
onMessage: function(callback) { port.onmessage = function(event) { callback(event.data); }; }
|
|
1057
1741
|
};
|
|
1742
|
+
$audioPrelude
|
|
1058
1743
|
})()
|
|
1059
1744
|
""".trimIndent() else """
|
|
1060
1745
|
$routePrelude
|
|
@@ -1069,6 +1754,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
1069
1754
|
postMessage: function(value) { console.log(${JSONObject.quote(prefix)} + value); },
|
|
1070
1755
|
onMessage: function(callback) { globalThis.__stxNativeCallback = callback; }
|
|
1071
1756
|
};
|
|
1757
|
+
$audioPrelude
|
|
1072
1758
|
""".trimIndent()
|
|
1073
1759
|
val setup = isolate.evaluateJavaScriptAsync(prelude)
|
|
1074
1760
|
setup.addListener({
|
|
@@ -1206,6 +1892,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
1206
1892
|
capabilities.cancel(token)
|
|
1207
1893
|
}
|
|
1208
1894
|
}
|
|
1895
|
+
"AUDIO" -> screen.audio.handle(payload)
|
|
1209
1896
|
}
|
|
1210
1897
|
}
|
|
1211
1898
|
|
|
@@ -1243,9 +1930,30 @@ class MainActivity : AppCompatActivity() {
|
|
|
1243
1930
|
val updated = screen.controls["id:$target"] ?: return false
|
|
1244
1931
|
if (!updateParentLayoutStyle(updated, document.optJSONObject("style") ?: JSONObject())) return false
|
|
1245
1932
|
}
|
|
1933
|
+
if (applied.isNotEmpty()) {
|
|
1934
|
+
scheduleLayoutEvents(screen)
|
|
1935
|
+
}
|
|
1246
1936
|
return true
|
|
1247
1937
|
}
|
|
1248
1938
|
|
|
1939
|
+
private fun scheduleLayoutEvents(screen: Screen) {
|
|
1940
|
+
if (!screen.active) return
|
|
1941
|
+
val observer = screen.view.viewTreeObserver
|
|
1942
|
+
if (observer.isAlive) {
|
|
1943
|
+
val listener = object : ViewTreeObserver.OnGlobalLayoutListener {
|
|
1944
|
+
override fun onGlobalLayout() {
|
|
1945
|
+
if (observer.isAlive) observer.removeOnGlobalLayoutListener(this)
|
|
1946
|
+
if (screen.active) emitLayoutEvents(screen)
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
observer.addOnGlobalLayoutListener(listener)
|
|
1950
|
+
}
|
|
1951
|
+
screen.view.requestLayout()
|
|
1952
|
+
// A tree that is already settled may not produce a global-layout
|
|
1953
|
+
// callback; keep the event path live for that case as well.
|
|
1954
|
+
screen.view.postOnAnimation { if (screen.active) emitLayoutEvents(screen) }
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1249
1957
|
/**
|
|
1250
1958
|
* Targeted mutations bypass the parent's child reconciliation. Rebind the
|
|
1251
1959
|
* updated node's layout style explicitly so removing a width/height (or
|
|
@@ -1482,16 +2190,39 @@ class MainActivity : AppCompatActivity() {
|
|
|
1482
2190
|
transparent = props.optBoolean("transparent", false)
|
|
1483
2191
|
content.configure(style, resources.displayMetrics.density)
|
|
1484
2192
|
reconcileChildren(screen, content, children, path, next, style, previousControls)
|
|
1485
|
-
|
|
2193
|
+
// Dynamic :visible bindings arrive as a display style mutation;
|
|
2194
|
+
// static visible props remain authoritative when present.
|
|
2195
|
+
val visible = if (props.has("visible")) {
|
|
2196
|
+
// An explicit visible prop owns modal visibility. The
|
|
2197
|
+
// binding runtime may leave a stale display:none style
|
|
2198
|
+
// from the previous hidden state when it patches props.
|
|
2199
|
+
props.optBoolean("visible", false)
|
|
2200
|
+
} else {
|
|
2201
|
+
style.has("display") && style.optString("display") != "none"
|
|
2202
|
+
}
|
|
1486
2203
|
val wasVisible = screen.modalVisibility[this]
|
|
1487
2204
|
screen.modalVisibility[this] = visible
|
|
1488
2205
|
val requestClose = events.optString("onRequestClose")
|
|
1489
2206
|
if (requestClose.isNotBlank()) screen.modalCloseHandlers[this] = requestClose else screen.modalCloseHandlers.remove(this)
|
|
2207
|
+
onRequestClose = if (requestClose.isNotBlank()) {
|
|
2208
|
+
{ send(screen, "EVENT", JSONObject().put("handlerName", requestClose).put("nativeEvent", JSONObject())) }
|
|
2209
|
+
} else {
|
|
2210
|
+
null
|
|
2211
|
+
}
|
|
1490
2212
|
setVisible(visible, style.optDouble("opacity", 1.0).toFloat(), props.optString("animationType").ifBlank { null })
|
|
2213
|
+
if (visible) clearDismissSuppression()
|
|
2214
|
+
val suppressDismiss = !visible && consumeDismissSuppression()
|
|
1491
2215
|
if (wasVisible != visible && (visible || wasVisible == true)) {
|
|
1492
2216
|
val lifecycleHandler = if (visible) events.optString("onShow") else events.optString("onDismiss")
|
|
1493
|
-
if (lifecycleHandler.isNotBlank()) {
|
|
1494
|
-
|
|
2217
|
+
if (lifecycleHandler.isNotBlank() && !suppressDismiss) {
|
|
2218
|
+
// Deliver lifecycle callbacks after reconciliation returns to
|
|
2219
|
+
// the queue; the handler may immediately schedule another
|
|
2220
|
+
// render against this native view.
|
|
2221
|
+
post {
|
|
2222
|
+
if (screen.active && screen.modalVisibility[this] == visible) {
|
|
2223
|
+
send(screen, "EVENT", JSONObject().put("handlerName", lifecycleHandler).put("nativeEvent", JSONObject()))
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
1495
2226
|
}
|
|
1496
2227
|
}
|
|
1497
2228
|
}
|
|
@@ -1579,7 +2310,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
1579
2310
|
}
|
|
1580
2311
|
})
|
|
1581
2312
|
}
|
|
1582
|
-
"ActivityIndicator" -> (previous as?
|
|
2313
|
+
"ActivityIndicator" -> (previous as? CraftNativeActivityIndicator ?: CraftNativeActivityIndicator(this)).apply {
|
|
1583
2314
|
val requestedSize = props.opt("size")
|
|
1584
2315
|
val scale = when (requestedSize) {
|
|
1585
2316
|
"large" -> 1.35f
|
|
@@ -1613,6 +2344,19 @@ class MainActivity : AppCompatActivity() {
|
|
|
1613
2344
|
if (onError.isNotBlank()) screen.imageErrorHandlers[this] = onError else screen.imageErrorHandlers.remove(this)
|
|
1614
2345
|
updateImage(screen, this, props.opt("source"))
|
|
1615
2346
|
}
|
|
2347
|
+
"Video" -> (previous as? CraftNativeVideoView ?: CraftNativeVideoView(this)).apply {
|
|
2348
|
+
configure(
|
|
2349
|
+
source = props.opt("source"),
|
|
2350
|
+
autoplay = props.optBoolean("autoplay", props.optBoolean("autoPlay", false)),
|
|
2351
|
+
paused = props.optBoolean("paused", false),
|
|
2352
|
+
loop = props.optBoolean("loop", false),
|
|
2353
|
+
muted = props.optBoolean("muted", false),
|
|
2354
|
+
controls = props.optBoolean("controls", false),
|
|
2355
|
+
)
|
|
2356
|
+
}
|
|
2357
|
+
"Svg" -> (previous as? CraftNativeSvgView ?: CraftNativeSvgView(this)).apply {
|
|
2358
|
+
drawing = node
|
|
2359
|
+
}
|
|
1616
2360
|
"ScrollView" -> (previous as? CraftNativeScrollView ?: CraftNativeScrollView(this)).apply {
|
|
1617
2361
|
val horizontal = props.optBoolean("horizontal", false) || style.optString("flexDirection") in setOf("row", "row-reverse")
|
|
1618
2362
|
setHorizontal(horizontal)
|
|
@@ -1711,6 +2455,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
1711
2455
|
reconcileChildren(screen, this, children, path, next, style, previousControls)
|
|
1712
2456
|
}
|
|
1713
2457
|
}
|
|
2458
|
+
if (control is CraftNativeFlexLayout) {
|
|
2459
|
+
control.onLayoutChanged = { if (screen.active) emitLayoutEvents(screen) }
|
|
2460
|
+
}
|
|
1714
2461
|
val pressInHandler = events.optString("onPressIn").takeIf { it.isNotBlank() }
|
|
1715
2462
|
val pressOutHandler = events.optString("onPressOut").takeIf { it.isNotBlank() }
|
|
1716
2463
|
if (pressInHandler != null) screen.pressInHandlers[control] = pressInHandler else screen.pressInHandlers.remove(control)
|
|
@@ -1786,9 +2533,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
1786
2533
|
if (layoutHandler.isNotBlank()) {
|
|
1787
2534
|
if (screen.layoutHandlers[control] != layoutHandler) screen.lastLayoutBounds.remove(control)
|
|
1788
2535
|
screen.layoutHandlers[control] = layoutHandler
|
|
2536
|
+
ensureLayoutListener(screen, control)
|
|
1789
2537
|
} else {
|
|
1790
2538
|
screen.layoutHandlers.remove(control)
|
|
1791
2539
|
screen.lastLayoutBounds.remove(control)
|
|
2540
|
+
screen.layoutListeners.remove(control)?.let(control::removeOnLayoutChangeListener)
|
|
1792
2541
|
}
|
|
1793
2542
|
if (control is ImageView) imageErrors[control]?.let { message ->
|
|
1794
2543
|
val current = control.contentDescription?.toString().orEmpty()
|
|
@@ -1807,7 +2556,6 @@ class MainActivity : AppCompatActivity() {
|
|
|
1807
2556
|
View.VISIBLE
|
|
1808
2557
|
}
|
|
1809
2558
|
}
|
|
1810
|
-
if (type == "Modal" && !props.optBoolean("visible", false)) control.visibility = View.GONE
|
|
1811
2559
|
val width = style.optDouble("width", Double.NaN).takeUnless { it.isNaN() }?.let(::dp)
|
|
1812
2560
|
val height = style.optDouble("height", Double.NaN).takeUnless { it.isNaN() }?.let(::dp)
|
|
1813
2561
|
control.layoutParams = LinearLayout.LayoutParams(
|
|
@@ -1968,7 +2716,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
1968
2716
|
|
|
1969
2717
|
private fun applyViewStyle(view: View, style: JSONObject) {
|
|
1970
2718
|
view.alpha = style.optDouble("opacity", 1.0).toFloat()
|
|
1971
|
-
|
|
2719
|
+
if (view !is CraftNativeModalView) {
|
|
2720
|
+
view.visibility = if (style.optString("display") == "none") View.GONE else View.VISIBLE
|
|
2721
|
+
}
|
|
1972
2722
|
view.elevation = dp(style.optDouble("elevation", 0.0)).toFloat()
|
|
1973
2723
|
val background = style.has("backgroundColor") && !style.isNull("backgroundColor")
|
|
1974
2724
|
val border = style.optDouble("borderWidth", 0.0)
|
|
@@ -1997,6 +2747,15 @@ class MainActivity : AppCompatActivity() {
|
|
|
1997
2747
|
}
|
|
1998
2748
|
}
|
|
1999
2749
|
|
|
2750
|
+
private fun ensureLayoutListener(screen: Screen, view: View) {
|
|
2751
|
+
if (screen.layoutListeners.containsKey(view)) return
|
|
2752
|
+
val listener = View.OnLayoutChangeListener { changed, left, top, right, bottom, _, _, _, _ ->
|
|
2753
|
+
if (screen.active) emitLayoutEvent(screen, changed, left, top, right, bottom)
|
|
2754
|
+
}
|
|
2755
|
+
screen.layoutListeners[view] = listener
|
|
2756
|
+
view.addOnLayoutChangeListener(listener)
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2000
2759
|
private fun configureText(view: TextView, value: String, style: JSONObject) {
|
|
2001
2760
|
view.text = when (style.optString("textTransform")) {
|
|
2002
2761
|
"uppercase" -> value.uppercase()
|
|
@@ -2353,14 +3112,21 @@ class MainActivity : AppCompatActivity() {
|
|
|
2353
3112
|
}
|
|
2354
3113
|
|
|
2355
3114
|
private fun emitLayoutEvents(screen: Screen, view: View) {
|
|
3115
|
+
emitLayoutEvent(screen, view, view.left, view.top, view.right, view.bottom)
|
|
3116
|
+
if (view is ViewGroup) {
|
|
3117
|
+
for (index in 0 until view.childCount) emitLayoutEvents(screen, view.getChildAt(index))
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
private fun emitLayoutEvent(screen: Screen, view: View, left: Int, top: Int, right: Int, bottom: Int) {
|
|
2356
3122
|
val handler = screen.layoutHandlers[view]
|
|
2357
3123
|
if (handler != null && view.isShown) {
|
|
2358
3124
|
val density = resources.displayMetrics.density
|
|
2359
3125
|
val bounds = LayoutBounds(
|
|
2360
|
-
x =
|
|
2361
|
-
y =
|
|
2362
|
-
width =
|
|
2363
|
-
height =
|
|
3126
|
+
x = left.toDouble() / density.toDouble(),
|
|
3127
|
+
y = top.toDouble() / density.toDouble(),
|
|
3128
|
+
width = (right - left).toDouble() / density.toDouble(),
|
|
3129
|
+
height = (bottom - top).toDouble() / density.toDouble(),
|
|
2364
3130
|
)
|
|
2365
3131
|
if (screen.lastLayoutBounds[view] != bounds) {
|
|
2366
3132
|
screen.lastLayoutBounds[view] = bounds
|
|
@@ -2373,9 +3139,6 @@ class MainActivity : AppCompatActivity() {
|
|
|
2373
3139
|
.put("height", bounds.height))))
|
|
2374
3140
|
}
|
|
2375
3141
|
}
|
|
2376
|
-
if (view is ViewGroup) {
|
|
2377
|
-
for (index in 0 until view.childCount) emitLayoutEvents(screen, view.getChildAt(index))
|
|
2378
|
-
}
|
|
2379
3142
|
}
|
|
2380
3143
|
|
|
2381
3144
|
private fun send(screen: Screen, type: String, payload: JSONObject, correlationId: String? = null) {
|
|
@@ -2440,6 +3203,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
2440
3203
|
releaseFlatList(screen, it, preservingInputDrafts)
|
|
2441
3204
|
}
|
|
2442
3205
|
released.filterIsInstance<CraftNativeModalView>().forEach { it.resetForTeardown() }
|
|
3206
|
+
released.filterIsInstance<CraftNativeVideoView>().forEach { it.stop() }
|
|
3207
|
+
released.filterIsInstance<CraftNativeFlexLayout>().forEach { it.onLayoutChanged = null }
|
|
2443
3208
|
released.filterIsInstance<CraftNativeScrollView>().forEach { it.setRefreshHandler(null, false) }
|
|
2444
3209
|
releaseImages(released)
|
|
2445
3210
|
released.filterIsInstance<EditText>().forEach {
|
|
@@ -2455,6 +3220,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
2455
3220
|
screen.pressOutHandlers.keys.removeAll(released)
|
|
2456
3221
|
screen.longPressHandlers.keys.removeAll(released)
|
|
2457
3222
|
screen.layoutHandlers.keys.removeAll(released)
|
|
3223
|
+
screen.layoutListeners.forEach { (view, listener) ->
|
|
3224
|
+
if (view in released) view.removeOnLayoutChangeListener(listener)
|
|
3225
|
+
}
|
|
3226
|
+
screen.layoutListeners.keys.removeAll(released)
|
|
2458
3227
|
screen.lastLayoutBounds.keys.removeAll(released)
|
|
2459
3228
|
screen.scrollHandlers.keys.removeAll(released)
|
|
2460
3229
|
screen.scrollBeginHandlers.keys.removeAll(released)
|
|
@@ -2515,9 +3284,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
2515
3284
|
val screen = screens.lastOrNull() ?: return back()
|
|
2516
3285
|
val modal = screen.modalVisibility.entries.lastOrNull { (view, visible) -> visible && view.visibility == View.VISIBLE }
|
|
2517
3286
|
if (modal != null) {
|
|
2518
|
-
|
|
2519
|
-
send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject()))
|
|
2520
|
-
}
|
|
3287
|
+
(modal.key as? CraftNativeModalView)?.requestClose()
|
|
2521
3288
|
return
|
|
2522
3289
|
}
|
|
2523
3290
|
back()
|