@craft-native/android 0.0.187 → 0.0.189
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -147,6 +147,10 @@ content box, keeping responsive sizing aligned with the iOS renderer.
|
|
|
147
147
|
When `flexShrink` is omitted, horizontal text labels and scroll hosts use the
|
|
148
148
|
same shrink defaults as the iOS renderer; buttons and editable fields retain
|
|
149
149
|
their intrinsic width unless explicitly configured.
|
|
150
|
+
An exact horizontal scroll viewport still measures its content unbounded and
|
|
151
|
+
retains that measured child width for the platform's second pass, so a wider
|
|
152
|
+
carousel keeps its overflow and receives the horizontal gesture before the tab
|
|
153
|
+
shell.
|
|
150
154
|
Wrapped containers also honor `alignContent` (`center`, `flex-end`,
|
|
151
155
|
`space-between`, `space-around`, `space-evenly`, and `stretch`) consistently
|
|
152
156
|
across hosts.
|
package/package.json
CHANGED
|
@@ -341,6 +341,18 @@ object CraftNative {
|
|
|
341
341
|
nodeData: FloatArray,
|
|
342
342
|
treeMeta: FloatArray,
|
|
343
343
|
): FloatArray?
|
|
344
|
+
private external fun nativeLayoutTreeV2(
|
|
345
|
+
width: Float,
|
|
346
|
+
height: Float,
|
|
347
|
+
nodeData: FloatArray,
|
|
348
|
+
treeMeta: FloatArray,
|
|
349
|
+
): FloatArray?
|
|
350
|
+
private external fun nativeLayoutTreeV3(
|
|
351
|
+
width: Float,
|
|
352
|
+
height: Float,
|
|
353
|
+
nodeData: FloatArray,
|
|
354
|
+
treeMeta: FloatArray,
|
|
355
|
+
): FloatArray?
|
|
344
356
|
private external fun nativeClipboardRead(activity: Activity): String?
|
|
345
357
|
private external fun nativeClipboardWrite(activity: Activity, text: String): Boolean
|
|
346
358
|
private external fun nativeOpenUrl(activity: Activity, url: String): Boolean
|
|
@@ -601,6 +613,28 @@ object CraftNative {
|
|
|
601
613
|
}
|
|
602
614
|
}
|
|
603
615
|
|
|
616
|
+
/** Resolve a reverse-aware tree in Zig, or null for Kotlin fallback. */
|
|
617
|
+
fun layoutTreeV2(width: Float, height: Float, nodeData: FloatArray, treeMeta: FloatArray): FloatArray? {
|
|
618
|
+
if (!isAvailable || nodeData.isEmpty() || treeMeta.isEmpty()) return null
|
|
619
|
+
return try {
|
|
620
|
+
nativeLayoutTreeV2(width, height, nodeData, treeMeta)
|
|
621
|
+
?.takeIf { it.size >= treeMeta.size / 2 * 4 && it.all { value -> value.isFinite() } }
|
|
622
|
+
} catch (e: UnsatisfiedLinkError) {
|
|
623
|
+
null
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/** Resolve a wrap-reverse-aware tree in Zig, or null for Kotlin fallback. */
|
|
628
|
+
fun layoutTreeV3(width: Float, height: Float, nodeData: FloatArray, treeMeta: FloatArray): FloatArray? {
|
|
629
|
+
if (!isAvailable || nodeData.isEmpty() || treeMeta.isEmpty()) return null
|
|
630
|
+
return try {
|
|
631
|
+
nativeLayoutTreeV3(width, height, nodeData, treeMeta)
|
|
632
|
+
?.takeIf { it.size >= treeMeta.size / 2 * 4 && it.all { value -> value.isFinite() } }
|
|
633
|
+
} catch (e: UnsatisfiedLinkError) {
|
|
634
|
+
null
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
604
638
|
/**
|
|
605
639
|
* The clipboard's text, or null to use the Kotlin implementation.
|
|
606
640
|
*
|
|
@@ -104,6 +104,7 @@ internal class CraftNativeCapabilities(
|
|
|
104
104
|
val maxPixels: Int,
|
|
105
105
|
val quality: Float,
|
|
106
106
|
val completion: (CraftNativeCapabilityResult) -> Unit,
|
|
107
|
+
val videoLimit: Long = 0,
|
|
107
108
|
)
|
|
108
109
|
|
|
109
110
|
init {
|
|
@@ -166,6 +167,7 @@ internal class CraftNativeCapabilities(
|
|
|
166
167
|
}
|
|
167
168
|
"Files" -> when (method) {
|
|
168
169
|
"pickImage" -> pickImage(requestToken, args, completion)
|
|
170
|
+
"pickVideo" -> pickVideo(requestToken, args, completion)
|
|
169
171
|
"download", "write", "remove" -> networkExecutor.execute {
|
|
170
172
|
complete(requestToken, files(requestToken, method, args), completion)
|
|
171
173
|
}
|
|
@@ -238,21 +240,28 @@ internal class CraftNativeCapabilities(
|
|
|
238
240
|
}
|
|
239
241
|
}
|
|
240
242
|
if (body != null || multipart is FileResult.Multipart) doOutput = true
|
|
241
|
-
if (multipart is FileResult.Multipart)
|
|
243
|
+
if (multipart is FileResult.Multipart) {
|
|
244
|
+
setRequestProperty("Content-Type", "multipart/form-data; boundary=${multipart.boundary}")
|
|
245
|
+
setFixedLengthStreamingMode(multipart.body.length())
|
|
246
|
+
readTimeout = 600_000
|
|
247
|
+
}
|
|
242
248
|
}
|
|
243
249
|
} catch (error: IllegalArgumentException) {
|
|
250
|
+
if (multipart is FileResult.Multipart) multipart.body.parentFile?.deleteRecursively()
|
|
244
251
|
return failure("INVALID_ARGUMENT", error.message ?: "fetch request is invalid")
|
|
245
252
|
} catch (error: Exception) {
|
|
253
|
+
if (multipart is FileResult.Multipart) multipart.body.parentFile?.deleteRecursively()
|
|
246
254
|
return failure("NETWORK_ERROR", error.message ?: "Network request failed")
|
|
247
255
|
}
|
|
248
256
|
fetchConnections[requestToken] = connection
|
|
249
257
|
if (cancelled.contains(requestToken)) {
|
|
250
258
|
fetchConnections.remove(requestToken)?.disconnect()
|
|
259
|
+
if (multipart is FileResult.Multipart) multipart.body.parentFile?.deleteRecursively()
|
|
251
260
|
return failure("CANCELLED", "Network request was cancelled")
|
|
252
261
|
}
|
|
253
262
|
return try {
|
|
254
263
|
if (body != null) connection.outputStream.use { it.write(body.toByteArray(Charsets.UTF_8)) }
|
|
255
|
-
if (multipart is FileResult.Multipart) connection.outputStream.use {
|
|
264
|
+
if (multipart is FileResult.Multipart) connection.outputStream.use { output -> multipart.body.inputStream().use { it.copyTo(output, 64 * 1024) } }
|
|
256
265
|
val status = connection.responseCode
|
|
257
266
|
val input = if (status >= 400) connection.errorStream else connection.inputStream
|
|
258
267
|
val output = ByteArrayOutputStream()
|
|
@@ -283,17 +292,21 @@ internal class CraftNativeCapabilities(
|
|
|
283
292
|
failure("NETWORK_ERROR", error.message ?: "Network request failed")
|
|
284
293
|
} finally {
|
|
285
294
|
fetchConnections.remove(requestToken)?.disconnect()
|
|
295
|
+
if (multipart is FileResult.Multipart) multipart.body.parentFile?.deleteRecursively()
|
|
286
296
|
}
|
|
287
297
|
}
|
|
288
298
|
|
|
289
299
|
private sealed interface FileResult {
|
|
290
|
-
data class Multipart(val body:
|
|
300
|
+
data class Multipart(val body: File, val boundary: String) : FileResult
|
|
291
301
|
data class Failure(val code: String, val message: String) : FileResult
|
|
292
302
|
}
|
|
293
303
|
|
|
294
304
|
private fun multipart(parts: JSONArray): FileResult {
|
|
295
305
|
val boundary = "craft-${UUID.randomUUID()}"
|
|
296
|
-
val
|
|
306
|
+
val file = File(fileRoot, UUID.randomUUID().toString()).apply { mkdirs() }.resolve("upload.multipart")
|
|
307
|
+
var kept = false
|
|
308
|
+
try {
|
|
309
|
+
FileOutputStream(file).use { body ->
|
|
297
310
|
fun append(value: String) { body.write(value.toByteArray(Charsets.UTF_8)) }
|
|
298
311
|
for (index in 0 until parts.length()) {
|
|
299
312
|
val part = parts.optJSONObject(index) ?: return FileResult.Failure("INVALID_ARGUMENT", "A form part must be an object")
|
|
@@ -303,14 +316,11 @@ internal class CraftNativeCapabilities(
|
|
|
303
316
|
val path = part.optString("path")
|
|
304
317
|
if (path.isNotBlank()) {
|
|
305
318
|
val file = ownedFile(path) ?: return FileResult.Failure("INVALID_ARGUMENT", "A form file must be one craft.files made, and still there")
|
|
306
|
-
val bytes = runCatching { file.readBytes() }.getOrElse {
|
|
307
|
-
return FileResult.Failure("INVALID_ARGUMENT", "A form file could not be read")
|
|
308
|
-
}
|
|
309
319
|
val filename = safeFileName(part.optString("filename").ifBlank { file.name })
|
|
310
320
|
val type = part.optString("type").ifBlank { URLConnection.guessContentTypeFromName(filename) ?: "application/octet-stream" }
|
|
311
321
|
append("Content-Disposition: form-data; name=\"${quoted(name)}\"; filename=\"${quoted(filename)}\"\r\n")
|
|
312
322
|
append("Content-Type: ${headerValue(type)}\r\n\r\n")
|
|
313
|
-
|
|
323
|
+
file.inputStream().use { it.copyTo(body, 64 * 1024) }
|
|
314
324
|
append("\r\n")
|
|
315
325
|
} else {
|
|
316
326
|
append("Content-Disposition: form-data; name=\"${quoted(name)}\"\r\n\r\n")
|
|
@@ -319,7 +329,14 @@ internal class CraftNativeCapabilities(
|
|
|
319
329
|
}
|
|
320
330
|
}
|
|
321
331
|
append("--$boundary--\r\n")
|
|
322
|
-
|
|
332
|
+
}
|
|
333
|
+
kept = true
|
|
334
|
+
return FileResult.Multipart(file, boundary)
|
|
335
|
+
} catch (error: Exception) {
|
|
336
|
+
return FileResult.Failure("WRITE_ERROR", error.message ?: "A form file could not be read")
|
|
337
|
+
} finally {
|
|
338
|
+
if (!kept) file.parentFile?.deleteRecursively()
|
|
339
|
+
}
|
|
323
340
|
}
|
|
324
341
|
|
|
325
342
|
private fun files(requestToken: String, method: String, args: JSONArray): CraftNativeCapabilityResult {
|
|
@@ -417,6 +434,64 @@ internal class CraftNativeCapabilities(
|
|
|
417
434
|
return success(JSONObject().put("ok", false).put("status", status).put("body", body.toString(Charsets.UTF_8.name())))
|
|
418
435
|
}
|
|
419
436
|
|
|
437
|
+
private fun pickVideo(requestToken: String, args: JSONArray, completion: (CraftNativeCapabilityResult) -> Unit) {
|
|
438
|
+
mainHandler.post {
|
|
439
|
+
val host = activity ?: return@post complete(requestToken, failure("NEEDS_ACTIVITY", "No screen is showing to pick from"), completion)
|
|
440
|
+
if (pendingFilePick != null) {
|
|
441
|
+
complete(requestToken, failure("BUSY", "Another file picker is already open"), completion)
|
|
442
|
+
return@post
|
|
443
|
+
}
|
|
444
|
+
val options = args.optJSONObject(0) ?: JSONObject()
|
|
445
|
+
val limit = options.optLong("maxBytes", 512L * 1024 * 1024).coerceIn(1, 512L * 1024 * 1024)
|
|
446
|
+
pendingFilePick = PendingFilePick(requestToken, "video", 0, 1f, completion, limit)
|
|
447
|
+
val photos = options.optString("source", "library") != "files" && Build.VERSION.SDK_INT >= 33
|
|
448
|
+
val intent = Intent(if (photos) MediaStore.ACTION_PICK_IMAGES else Intent.ACTION_OPEN_DOCUMENT).apply {
|
|
449
|
+
type = "video/*"
|
|
450
|
+
if (!photos) addCategory(Intent.CATEGORY_OPENABLE)
|
|
451
|
+
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
452
|
+
}
|
|
453
|
+
try { host.startActivityForResult(intent, CRAFT_NATIVE_FILE_PICK_REQUEST) }
|
|
454
|
+
catch (error: Exception) {
|
|
455
|
+
pendingFilePick = null
|
|
456
|
+
complete(requestToken, failure("PICKER_UNAVAILABLE", error.message ?: "Video picker could not be opened"), completion)
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** Copy with a bound even when a cloud provider does not report its size. */
|
|
462
|
+
private fun copyVideo(uri: Uri, limit: Long): CraftNativeCapabilityResult {
|
|
463
|
+
var target: File? = null
|
|
464
|
+
return try {
|
|
465
|
+
val name = context.contentResolver.query(uri, arrayOf(android.provider.OpenableColumns.DISPLAY_NAME), null, null, null)?.use {
|
|
466
|
+
if (it.moveToFirst()) it.getString(0) else null
|
|
467
|
+
} ?: "video.mp4"
|
|
468
|
+
val types = mapOf("mp4" to "video/mp4", "m4v" to "video/x-m4v", "mov" to "video/quicktime", "webm" to "video/webm")
|
|
469
|
+
val type = types[name.substringAfterLast('.', "").lowercase()]
|
|
470
|
+
?: return failure("INVALID_VIDEO", "Choose an MP4, MOV or WebM video.")
|
|
471
|
+
val destination = File(fileRoot, UUID.randomUUID().toString()).apply { mkdirs() }.resolve(safeFileName(name))
|
|
472
|
+
target = destination
|
|
473
|
+
val input = context.contentResolver.openInputStream(uri) ?: throw IllegalArgumentException("The video could not be read")
|
|
474
|
+
var size = 0L
|
|
475
|
+
input.use { source ->
|
|
476
|
+
destination.outputStream().use { output ->
|
|
477
|
+
val buffer = ByteArray(64 * 1024)
|
|
478
|
+
while (true) {
|
|
479
|
+
val count = source.read(buffer)
|
|
480
|
+
if (count < 0) break
|
|
481
|
+
size += count
|
|
482
|
+
require(size <= limit) { "Choose a video under ${limit / 1024 / 1024}MB." }
|
|
483
|
+
output.write(buffer, 0, count)
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
require(size > 0) { "Choose a nonempty video." }
|
|
488
|
+
success(descriptor(destination, type))
|
|
489
|
+
} catch (error: Exception) {
|
|
490
|
+
target?.parentFile?.deleteRecursively()
|
|
491
|
+
failure("READ_ERROR", error.message ?: "The video could not be read")
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
420
495
|
private fun pickImage(
|
|
421
496
|
requestToken: String,
|
|
422
497
|
args: JSONArray,
|
|
@@ -465,6 +540,14 @@ internal class CraftNativeCapabilities(
|
|
|
465
540
|
complete(pending.requestToken, success(JSONObject.NULL), pending.completion)
|
|
466
541
|
return true
|
|
467
542
|
}
|
|
543
|
+
if (pending.source == "video") {
|
|
544
|
+
networkExecutor.execute {
|
|
545
|
+
val result = data?.data?.let { copyVideo(it, pending.videoLimit) }
|
|
546
|
+
?: failure("PICKER_FAILED", "The selected video could not be read")
|
|
547
|
+
complete(pending.requestToken, result, pending.completion)
|
|
548
|
+
}
|
|
549
|
+
return true
|
|
550
|
+
}
|
|
468
551
|
val descriptor = if (pending.source == "camera") {
|
|
469
552
|
val bitmap = data?.extras?.get("data") as? Bitmap
|
|
470
553
|
bitmap?.let { writeImage(it, pending.maxPixels, pending.quality, "photo.jpg") }
|
|
@@ -580,6 +580,7 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
|
|
|
580
580
|
private var selectedTab = 0
|
|
581
581
|
private var swipeTracking = false
|
|
582
582
|
private var swipeIntercepted = false
|
|
583
|
+
private var horizontalScrollDeferred = false
|
|
583
584
|
private var swipeDownX = 0f
|
|
584
585
|
private var swipeDownY = 0f
|
|
585
586
|
private val swipeSlop = ViewConfiguration.get(context).scaledTouchSlop
|
|
@@ -590,6 +591,8 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
|
|
|
590
591
|
var onTabSwipeStart: ((Float, Float, Float) -> Boolean)? = null
|
|
591
592
|
var onTabSwipeMove: ((Float) -> Unit)? = null
|
|
592
593
|
var onTabSwipeEnd: ((Float, Boolean) -> Unit)? = null
|
|
594
|
+
/** Lets a native horizontal scroller own the gesture from its first down. */
|
|
595
|
+
var onTabSwipeShouldDefer: ((Float, Float) -> Boolean)? = null
|
|
593
596
|
var tabSwipeEnabled = true
|
|
594
597
|
|
|
595
598
|
init {
|
|
@@ -654,7 +657,10 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
|
|
|
654
657
|
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
|
655
658
|
when (event.actionMasked) {
|
|
656
659
|
MotionEvent.ACTION_DOWN -> {
|
|
657
|
-
|
|
660
|
+
val overHorizontalScroll = onTabSwipeShouldDefer?.invoke(event.x, event.y) == true
|
|
661
|
+
horizontalScrollDeferred = overHorizontalScroll
|
|
662
|
+
if (overHorizontalScroll) parent?.requestDisallowInterceptTouchEvent(true)
|
|
663
|
+
swipeTracking = tabs.size > 1 && tabSwipeEnabled && !overHorizontalScroll && tabBar.visibility == View.VISIBLE && event.y < tabBar.top
|
|
658
664
|
swipeIntercepted = false
|
|
659
665
|
swipeDownX = event.x
|
|
660
666
|
swipeDownY = event.y
|
|
@@ -668,7 +674,13 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
|
|
|
668
674
|
return swipeIntercepted
|
|
669
675
|
}
|
|
670
676
|
}
|
|
671
|
-
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP ->
|
|
677
|
+
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
|
|
678
|
+
swipeTracking = false
|
|
679
|
+
if (horizontalScrollDeferred) {
|
|
680
|
+
horizontalScrollDeferred = false
|
|
681
|
+
parent?.requestDisallowInterceptTouchEvent(false)
|
|
682
|
+
}
|
|
683
|
+
}
|
|
672
684
|
}
|
|
673
685
|
return swipeIntercepted
|
|
674
686
|
}
|
|
@@ -771,6 +771,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
771
771
|
private var gridAutoRows: CraftNativeLength? = null
|
|
772
772
|
private var reverse = false
|
|
773
773
|
private var wrapReverse = false
|
|
774
|
+
private var unboundedWidth = false
|
|
774
775
|
private var authoredPadding = intArrayOf(0, 0, 0, 0)
|
|
775
776
|
private var safeAreaPadding = intArrayOf(0, 0, 0, 0)
|
|
776
777
|
private var applyingPadding = false
|
|
@@ -947,6 +948,13 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
947
948
|
invalidateMeasurements()
|
|
948
949
|
}
|
|
949
950
|
|
|
951
|
+
/** A horizontal scroll host measures its content beyond the viewport. */
|
|
952
|
+
fun setUnboundedWidth(value: Boolean) {
|
|
953
|
+
if (unboundedWidth == value) return
|
|
954
|
+
unboundedWidth = value
|
|
955
|
+
invalidateMeasurements()
|
|
956
|
+
}
|
|
957
|
+
|
|
950
958
|
fun setChildStyle(view: View, style: JSONObject, density: Float) {
|
|
951
959
|
styles[view] = CraftNativeLayoutStyle.from(style, density)
|
|
952
960
|
invalidateMeasurements()
|
|
@@ -1237,15 +1245,197 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1237
1245
|
|
|
1238
1246
|
private fun sharedLength(value: CraftNativeLength?, base: Int?): Float = value?.resolve(base)?.toFloat() ?: Float.NaN
|
|
1239
1247
|
|
|
1248
|
+
/** A measured node in the host-neutral tree ABI, retaining its native view
|
|
1249
|
+
* so the returned absolute frames can be mounted back into the hierarchy. */
|
|
1250
|
+
private data class SharedTreeNode(
|
|
1251
|
+
val view: View?,
|
|
1252
|
+
val style: CraftNativeLayoutStyle,
|
|
1253
|
+
val intrinsicWidth: Float,
|
|
1254
|
+
val intrinsicHeight: Float,
|
|
1255
|
+
val baseline: Float,
|
|
1256
|
+
val direction: Int,
|
|
1257
|
+
val gap: Float,
|
|
1258
|
+
val rowGap: Float,
|
|
1259
|
+
val columnGap: Float,
|
|
1260
|
+
val wrap: Boolean,
|
|
1261
|
+
val justify: String,
|
|
1262
|
+
val alignItems: String,
|
|
1263
|
+
val alignContent: String,
|
|
1264
|
+
val padding: IntArray,
|
|
1265
|
+
val margins: IntArray,
|
|
1266
|
+
val grid: Boolean,
|
|
1267
|
+
val gridColumns: Int,
|
|
1268
|
+
val reverse: Boolean,
|
|
1269
|
+
val wrapReverse: Boolean,
|
|
1270
|
+
val children: List<SharedTreeNode>,
|
|
1271
|
+
)
|
|
1272
|
+
|
|
1273
|
+
private data class SharedTreeResult(
|
|
1274
|
+
val nodes: List<SharedTreeNode>,
|
|
1275
|
+
val parents: IntArray,
|
|
1276
|
+
val frames: FloatArray,
|
|
1277
|
+
)
|
|
1278
|
+
|
|
1279
|
+
/**
|
|
1280
|
+
* Flatten a flex or equal-track grid subtree for the shared ABI. The local
|
|
1281
|
+
* layout stays authoritative for grid-auto-rows,
|
|
1282
|
+
* absolute/modal children, and scroll hosts; those cases cannot be
|
|
1283
|
+
* represented without changing their native measurement contract.
|
|
1284
|
+
*/
|
|
1285
|
+
private fun sharedTreeChildren(width: Int, height: Int): List<Entry>? {
|
|
1286
|
+
if (gridAutoRows != null) return null
|
|
1287
|
+
if (grid) {
|
|
1288
|
+
val layout = gridLayout(width, height)
|
|
1289
|
+
if (layout.absolute.isNotEmpty()) return null
|
|
1290
|
+
return layout.rows.flatten()
|
|
1291
|
+
}
|
|
1292
|
+
val (lines, absolute) = entries(width, height)
|
|
1293
|
+
if (absolute.isNotEmpty()) return null
|
|
1294
|
+
return lines.flatMap { it.entries }
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
private fun sharedTreeNode(entry: Entry): SharedTreeNode? {
|
|
1298
|
+
val nested = entry.view as? CraftNativeFlexLayout
|
|
1299
|
+
val children = if (nested == null) {
|
|
1300
|
+
emptyList()
|
|
1301
|
+
} else {
|
|
1302
|
+
nested.sharedTreeChildren(entry.width, entry.height) ?: return null
|
|
1303
|
+
}
|
|
1304
|
+
val lp = entry.view.layoutParams as? MarginLayoutParams
|
|
1305
|
+
return SharedTreeNode(
|
|
1306
|
+
view = entry.view,
|
|
1307
|
+
style = entry.style,
|
|
1308
|
+
intrinsicWidth = entry.width.toFloat(),
|
|
1309
|
+
intrinsicHeight = entry.height.toFloat(),
|
|
1310
|
+
baseline = baselineOffset(entry.view, entry.height).toFloat(),
|
|
1311
|
+
direction = nested?.let { if (it.orientation == HORIZONTAL) 0 else 1 } ?: 0,
|
|
1312
|
+
gap = nested?.let { if (it.orientation == HORIZONTAL) it.columnGap else it.rowGap } ?: 0f,
|
|
1313
|
+
rowGap = nested?.rowGap ?: 0f,
|
|
1314
|
+
columnGap = nested?.columnGap ?: 0f,
|
|
1315
|
+
wrap = nested?.wrap ?: false,
|
|
1316
|
+
justify = nested?.justifyContent ?: "flex-start",
|
|
1317
|
+
alignItems = nested?.alignItems ?: "stretch",
|
|
1318
|
+
alignContent = nested?.alignContent ?: "flex-start",
|
|
1319
|
+
padding = nested?.let { intArrayOf(it.paddingLeft, it.paddingRight, it.paddingTop, it.paddingBottom) }
|
|
1320
|
+
?: intArrayOf(0, 0, 0, 0),
|
|
1321
|
+
margins = intArrayOf(lp?.leftMargin ?: 0, lp?.rightMargin ?: 0, lp?.topMargin ?: 0, lp?.bottomMargin ?: 0),
|
|
1322
|
+
grid = nested?.grid ?: false,
|
|
1323
|
+
gridColumns = nested?.gridColumns ?: 1,
|
|
1324
|
+
reverse = nested?.reverse ?: false,
|
|
1325
|
+
wrapReverse = nested?.wrapReverse ?: false,
|
|
1326
|
+
children = children.map { child -> sharedTreeNode(child) ?: return null },
|
|
1327
|
+
)
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
private fun sharedTreeFrames(entries: List<Entry>, width: Int, height: Int): SharedTreeResult? {
|
|
1331
|
+
if (entries.isEmpty() || width < 0 || height < 0 || gridAutoRows != null) return null
|
|
1332
|
+
val children = entries.map { sharedTreeNode(it) ?: return null }
|
|
1333
|
+
val root = SharedTreeNode(
|
|
1334
|
+
view = null,
|
|
1335
|
+
style = CraftNativeLayoutStyle(),
|
|
1336
|
+
intrinsicWidth = 0f,
|
|
1337
|
+
intrinsicHeight = 0f,
|
|
1338
|
+
baseline = 0f,
|
|
1339
|
+
direction = if (orientation == HORIZONTAL) 0 else 1,
|
|
1340
|
+
gap = if (orientation == HORIZONTAL) columnGap else rowGap,
|
|
1341
|
+
rowGap = rowGap,
|
|
1342
|
+
columnGap = columnGap,
|
|
1343
|
+
wrap = wrap,
|
|
1344
|
+
justify = justifyContent,
|
|
1345
|
+
alignItems = alignItems,
|
|
1346
|
+
alignContent = alignContent,
|
|
1347
|
+
padding = intArrayOf(paddingLeft, paddingRight, paddingTop, paddingBottom),
|
|
1348
|
+
margins = intArrayOf(0, 0, 0, 0),
|
|
1349
|
+
grid = grid,
|
|
1350
|
+
gridColumns = gridColumns,
|
|
1351
|
+
reverse = reverse,
|
|
1352
|
+
wrapReverse = wrapReverse,
|
|
1353
|
+
children = children,
|
|
1354
|
+
)
|
|
1355
|
+
val nodes = mutableListOf<SharedTreeNode>()
|
|
1356
|
+
val parents = mutableListOf<Int>()
|
|
1357
|
+
fun append(node: SharedTreeNode, parent: Int) {
|
|
1358
|
+
val index = nodes.size
|
|
1359
|
+
nodes += node
|
|
1360
|
+
parents += parent
|
|
1361
|
+
node.children.forEach { child -> append(child, index) }
|
|
1362
|
+
}
|
|
1363
|
+
append(root, -1)
|
|
1364
|
+
val reverseAware = nodes.any { it.reverse || it.wrapReverse }
|
|
1365
|
+
val wrapReverseAware = nodes.any { it.wrapReverse }
|
|
1366
|
+
val fields = if (reverseAware) 33 else 32
|
|
1367
|
+
val nodeData = FloatArray(nodes.size * fields) { Float.NaN }
|
|
1368
|
+
val treeMeta = FloatArray(nodes.size * 2)
|
|
1369
|
+
fun enumBits(value: Int): Float = Float.fromBits(value)
|
|
1370
|
+
fun put(base: Int, index: Int, value: Float) { nodeData[base + index] = value }
|
|
1371
|
+
fun putEnum(base: Int, index: Int, value: Int) { put(base, index, enumBits(value)) }
|
|
1372
|
+
var cursor = 0
|
|
1373
|
+
fun write(node: SharedTreeNode, parentWidth: Int, parentHeight: Int, rootNode: Boolean) {
|
|
1374
|
+
val index = cursor++
|
|
1375
|
+
val base = index * fields
|
|
1376
|
+
val measuredWidth = if (rootNode) width else node.style.width?.resolve(parentWidth) ?: node.intrinsicWidth.roundToInt()
|
|
1377
|
+
val measuredHeight = if (rootNode) height else node.style.height?.resolve(parentHeight) ?: node.intrinsicHeight.roundToInt()
|
|
1378
|
+
val contentWidth = max(0, measuredWidth - node.padding[0] - node.padding[1])
|
|
1379
|
+
val contentHeight = max(0, measuredHeight - node.padding[2] - node.padding[3])
|
|
1380
|
+
val mainSize = if (node.direction == 0) contentWidth else contentHeight
|
|
1381
|
+
putEnum(base, 0, node.direction)
|
|
1382
|
+
put(base, 1, if (rootNode) width.toFloat() else sharedLength(node.style.width, parentWidth))
|
|
1383
|
+
put(base, 2, if (rootNode) height.toFloat() else sharedLength(node.style.height, parentHeight))
|
|
1384
|
+
put(base, 3, if (rootNode) Float.NaN else node.intrinsicWidth)
|
|
1385
|
+
put(base, 4, if (rootNode) Float.NaN else node.intrinsicHeight)
|
|
1386
|
+
put(base, 5, if (rootNode) Float.NaN else node.baseline)
|
|
1387
|
+
put(base, 6, node.style.aspectRatio?.toFloat() ?: Float.NaN)
|
|
1388
|
+
put(base, 7, sharedLength(node.style.flexBasis, mainSize))
|
|
1389
|
+
put(base, 8, if (rootNode) 0f else node.style.flexGrow.toFloat().coerceAtLeast(0f))
|
|
1390
|
+
put(base, 9, if (rootNode) 0f else (node.style.flexShrink ?: 0.0).toFloat().coerceAtLeast(0f))
|
|
1391
|
+
put(base, 10, sharedLength(node.style.minWidth, contentWidth))
|
|
1392
|
+
put(base, 11, sharedLength(node.style.maxWidth, contentWidth))
|
|
1393
|
+
put(base, 12, sharedLength(node.style.minHeight, contentHeight))
|
|
1394
|
+
put(base, 13, sharedLength(node.style.maxHeight, contentHeight))
|
|
1395
|
+
put(base, 14, node.gap)
|
|
1396
|
+
putEnum(base, 15, sharedJustifyCode(node.justify))
|
|
1397
|
+
putEnum(base, 16, sharedAlignItemsCode(node.alignItems))
|
|
1398
|
+
putEnum(base, 17, if (rootNode) 0 else sharedAlignSelfCode(node.style.alignSelf))
|
|
1399
|
+
putEnum(base, 18, if (node.wrap) 1 else 0)
|
|
1400
|
+
putEnum(base, 19, sharedAlignContentCode(node.alignContent))
|
|
1401
|
+
put(base, 20, node.margins[2].toFloat())
|
|
1402
|
+
put(base, 21, node.margins[1].toFloat())
|
|
1403
|
+
put(base, 22, node.margins[3].toFloat())
|
|
1404
|
+
put(base, 23, node.margins[0].toFloat())
|
|
1405
|
+
put(base, 24, node.padding[2].toFloat())
|
|
1406
|
+
put(base, 25, node.padding[1].toFloat())
|
|
1407
|
+
put(base, 26, node.padding[3].toFloat())
|
|
1408
|
+
put(base, 27, node.padding[0].toFloat())
|
|
1409
|
+
putEnum(base, 28, if (node.grid) 1 else 0)
|
|
1410
|
+
put(base, 29, node.gridColumns.toFloat())
|
|
1411
|
+
put(base, 30, node.rowGap)
|
|
1412
|
+
put(base, 31, node.columnGap)
|
|
1413
|
+
if (reverseAware) putEnum(base, 32, (if (node.reverse) 1 else 0) or (if (node.wrapReverse) 2 else 0))
|
|
1414
|
+
val firstChild = cursor
|
|
1415
|
+
node.children.forEach { child -> write(child, contentWidth, contentHeight, false) }
|
|
1416
|
+
treeMeta[index * 2] = firstChild.toFloat()
|
|
1417
|
+
treeMeta[index * 2 + 1] = (cursor - firstChild).toFloat()
|
|
1418
|
+
}
|
|
1419
|
+
write(root, width, height, true)
|
|
1420
|
+
val frames = (if (wrapReverseAware) {
|
|
1421
|
+
CraftNative.layoutTreeV3(width.toFloat(), height.toFloat(), nodeData, treeMeta)
|
|
1422
|
+
} else if (reverseAware) {
|
|
1423
|
+
CraftNative.layoutTreeV2(width.toFloat(), height.toFloat(), nodeData, treeMeta)
|
|
1424
|
+
} else {
|
|
1425
|
+
CraftNative.layoutTree(width.toFloat(), height.toFloat(), nodeData, treeMeta)
|
|
1426
|
+
}) ?: return null
|
|
1427
|
+
return SharedTreeResult(nodes, parents.toIntArray(), frames)
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1240
1430
|
/**
|
|
1241
1431
|
* Use the host-neutral tree ABI when platform measurements are complete:
|
|
1242
|
-
* definite
|
|
1243
|
-
* independent row and column gaps.
|
|
1244
|
-
* missing runtimes, and malformed results
|
|
1245
|
-
* the established Kotlin layout below.
|
|
1432
|
+
* definite direct-child containers, including wrapped flows, equal-track
|
|
1433
|
+
* grids, and independent row and column gaps. Grid
|
|
1434
|
+
* auto-rows, absolute children, missing runtimes, and malformed results
|
|
1435
|
+
* deliberately fall through to the established Kotlin layout below.
|
|
1246
1436
|
*/
|
|
1247
1437
|
private fun sharedDirectFrames(entries: List<Entry>, width: Int, height: Int): FloatArray? {
|
|
1248
|
-
if (entries.isEmpty() || width < 0 || height < 0 || grid || reverse || wrapReverse) return null
|
|
1438
|
+
if (entries.isEmpty() || width < 0 || height < 0 || grid || reverse || wrapReverse || gridAutoRows != null) return null
|
|
1249
1439
|
val count = entries.size + 1
|
|
1250
1440
|
val fields = 32
|
|
1251
1441
|
val nodeData = FloatArray(count * fields) { Float.NaN }
|
|
@@ -1300,7 +1490,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1300
1490
|
put(base, 26, padding[3].toFloat())
|
|
1301
1491
|
put(base, 27, padding[0].toFloat())
|
|
1302
1492
|
putEnum(base, 28, 0)
|
|
1303
|
-
|
|
1493
|
+
put(base, 29, 1f)
|
|
1304
1494
|
put(base, 30, if (root) rowGapValue else 0f)
|
|
1305
1495
|
put(base, 31, if (root) columnGapValue else 0f)
|
|
1306
1496
|
}
|
|
@@ -1337,6 +1527,15 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1337
1527
|
else -> 0
|
|
1338
1528
|
}
|
|
1339
1529
|
|
|
1530
|
+
private fun sharedJustifyCode(value: String): Int = when (value) {
|
|
1531
|
+
"center" -> 1
|
|
1532
|
+
"flex-end" -> 2
|
|
1533
|
+
"space-between" -> 3
|
|
1534
|
+
"space-around" -> 4
|
|
1535
|
+
"space-evenly" -> 5
|
|
1536
|
+
else -> 0
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1340
1539
|
private fun alignContentCode(): Int = when (alignContent) {
|
|
1341
1540
|
"center" -> 1
|
|
1342
1541
|
"flex-end" -> 2
|
|
@@ -1347,6 +1546,16 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1347
1546
|
else -> 0
|
|
1348
1547
|
}
|
|
1349
1548
|
|
|
1549
|
+
private fun sharedAlignContentCode(value: String): Int = when (value) {
|
|
1550
|
+
"center" -> 1
|
|
1551
|
+
"flex-end" -> 2
|
|
1552
|
+
"space-between" -> 3
|
|
1553
|
+
"space-around" -> 4
|
|
1554
|
+
"stretch" -> 5
|
|
1555
|
+
"space-evenly" -> 6
|
|
1556
|
+
else -> 0
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1350
1559
|
/** Keep the old-runtime shim on the same bounded-flex water-fill rule as the Zig bridge. */
|
|
1351
1560
|
private fun fallbackFlexWidths(line: Line, available: Int, gap: Float): IntArray {
|
|
1352
1561
|
val horizontal = orientation == HORIZONTAL
|
|
@@ -1410,7 +1619,8 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1410
1619
|
}
|
|
1411
1620
|
|
|
1412
1621
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
1413
|
-
val
|
|
1622
|
+
val unboundedHorizontal = unboundedWidth && orientation == HORIZONTAL
|
|
1623
|
+
val widthMode = if (unboundedHorizontal) View.MeasureSpec.UNSPECIFIED else View.MeasureSpec.getMode(widthMeasureSpec)
|
|
1414
1624
|
widthDefinite = widthMode == View.MeasureSpec.EXACTLY
|
|
1415
1625
|
val width = View.MeasureSpec.getSize(widthMeasureSpec).takeUnless { widthMode == View.MeasureSpec.UNSPECIFIED } ?: 1_000_000
|
|
1416
1626
|
val height = View.MeasureSpec.getSize(heightMeasureSpec).takeUnless { View.MeasureSpec.getMode(heightMeasureSpec) == View.MeasureSpec.UNSPECIFIED } ?: 1_000_000
|
|
@@ -1437,7 +1647,8 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1437
1647
|
val absWidth = absolute.maxOfOrNull { it.width } ?: 0
|
|
1438
1648
|
val absHeight = absolute.maxOfOrNull { it.height } ?: 0
|
|
1439
1649
|
setMeasuredDimension(
|
|
1440
|
-
|
|
1650
|
+
if (unboundedHorizontal) max(desiredWidth + paddingLeft + paddingRight, absWidth + paddingLeft + paddingRight)
|
|
1651
|
+
else resolveSize(max(desiredWidth + paddingLeft + paddingRight, absWidth + paddingLeft + paddingRight), widthMeasureSpec),
|
|
1441
1652
|
resolveSize(max(desiredHeight + paddingTop + paddingBottom, absHeight + paddingTop + paddingBottom), heightMeasureSpec),
|
|
1442
1653
|
)
|
|
1443
1654
|
}
|
|
@@ -1547,6 +1758,23 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1547
1758
|
val (lines, absolute) = entries(width, height)
|
|
1548
1759
|
if (absolute.isEmpty() && lines.isNotEmpty() && (lines.size == 1 || wrap)) {
|
|
1549
1760
|
val sharedEntries = if (wrap) lines.flatMap { it.entries } else lines[0].entries
|
|
1761
|
+
val sharedTree = sharedTreeFrames(sharedEntries, width, height)
|
|
1762
|
+
if (sharedTree != null && (sharedTree.nodes.size > sharedEntries.size + 1 || grid)) {
|
|
1763
|
+
for (index in 1 until sharedTree.nodes.size) {
|
|
1764
|
+
val view = sharedTree.nodes[index].view ?: continue
|
|
1765
|
+
val offset = index * 4
|
|
1766
|
+
val parentOffset = sharedTree.parents[index] * 4
|
|
1767
|
+
val parentX = sharedTree.frames[parentOffset]
|
|
1768
|
+
val parentY = sharedTree.frames[parentOffset + 1]
|
|
1769
|
+
val x = (sharedTree.frames[offset] - parentX).roundToInt()
|
|
1770
|
+
val y = (sharedTree.frames[offset + 1] - parentY).roundToInt()
|
|
1771
|
+
val childWidth = sharedTree.frames[offset + 2].roundToInt()
|
|
1772
|
+
val childHeight = sharedTree.frames[offset + 3].roundToInt()
|
|
1773
|
+
place(view, x, y, x + childWidth, y + childHeight)
|
|
1774
|
+
}
|
|
1775
|
+
onLayoutChanged?.invoke()
|
|
1776
|
+
return
|
|
1777
|
+
}
|
|
1550
1778
|
val sharedFrames = sharedDirectFrames(sharedEntries, width, height)
|
|
1551
1779
|
if (sharedFrames != null) {
|
|
1552
1780
|
for ((index, entry) in sharedEntries.withIndex()) {
|
|
@@ -1808,10 +2036,18 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
1808
2036
|
vertical.isFillViewport = true
|
|
1809
2037
|
horizontal.isFillViewport = true
|
|
1810
2038
|
setOnRefreshListener { refreshHandler?.invoke() }
|
|
1811
|
-
|
|
2039
|
+
// The outer refresh host must stay touch-enabled so its real scroll
|
|
2040
|
+
// child can receive gestures even when no pull-to-refresh handler is
|
|
2041
|
+
// configured. Interception is gated below instead.
|
|
2042
|
+
isEnabled = true
|
|
1812
2043
|
setHorizontal(false)
|
|
1813
2044
|
}
|
|
1814
2045
|
|
|
2046
|
+
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
|
2047
|
+
if (refreshHandler == null) return false
|
|
2048
|
+
return super.onInterceptTouchEvent(event)
|
|
2049
|
+
}
|
|
2050
|
+
|
|
1815
2051
|
/**
|
|
1816
2052
|
* Along an axis its parent does not fix, a scroll view is as big as its
|
|
1817
2053
|
* content (up to what the parent allows): a horizontal strip is as tall
|
|
@@ -1824,10 +2060,11 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
1824
2060
|
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
|
|
1825
2061
|
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
|
|
1826
2062
|
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
2063
|
+
// Even when the viewport itself is exact, a horizontal scroll must
|
|
2064
|
+
// measure its content with an unspecified width. Returning straight
|
|
2065
|
+
// to SwipeRefreshLayout here makes HorizontalScrollView remeasure the
|
|
2066
|
+
// content at the viewport width, erasing overflow and disabling the
|
|
2067
|
+
// nested carousel gesture.
|
|
1831
2068
|
val sideways = horizontalMode == true
|
|
1832
2069
|
val unspecified = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
|
|
1833
2070
|
val horizontalPadding = paddingLeft + paddingRight
|
|
@@ -1838,6 +2075,18 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
1838
2075
|
if (!sideways || heightMode == MeasureSpec.UNSPECIFIED) unspecified
|
|
1839
2076
|
else MeasureSpec.makeMeasureSpec(max(0, heightSize - verticalPadding), MeasureSpec.AT_MOST),
|
|
1840
2077
|
)
|
|
2078
|
+
if (sideways) {
|
|
2079
|
+
// HorizontalScrollView may turn a WRAP_CONTENT child into an
|
|
2080
|
+
// AT_MOST viewport measurement during its own pass. Preserve the
|
|
2081
|
+
// intrinsic width we just measured as the child's layout width so
|
|
2082
|
+
// the second pass cannot collapse a carousel back to the viewport.
|
|
2083
|
+
val params = content.layoutParams ?: ViewGroup.LayoutParams(
|
|
2084
|
+
ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
2085
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
2086
|
+
)
|
|
2087
|
+
params.width = max(params.width, content.measuredWidth)
|
|
2088
|
+
content.layoutParams = params
|
|
2089
|
+
}
|
|
1841
2090
|
fun fit(mode: Int, size: Int, wanted: Int): Int = when (mode) {
|
|
1842
2091
|
MeasureSpec.EXACTLY -> size
|
|
1843
2092
|
MeasureSpec.AT_MOST -> min(size, wanted)
|
|
@@ -1853,7 +2102,7 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
1853
2102
|
|
|
1854
2103
|
fun setRefreshHandler(handler: (() -> Unit)?, refreshing: Boolean) {
|
|
1855
2104
|
refreshHandler = handler
|
|
1856
|
-
isEnabled =
|
|
2105
|
+
isEnabled = true
|
|
1857
2106
|
isRefreshing = handler != null && refreshing
|
|
1858
2107
|
if (handler == null) isRefreshing = false
|
|
1859
2108
|
}
|
|
@@ -1884,6 +2133,7 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
1884
2133
|
val scroller: ViewGroup = if (value) horizontal else vertical
|
|
1885
2134
|
scroller.removeAllViews()
|
|
1886
2135
|
content.orientation = if (value) LinearLayout.HORIZONTAL else LinearLayout.VERTICAL
|
|
2136
|
+
content.setUnboundedWidth(value)
|
|
1887
2137
|
scroller.addView(content, if (value)
|
|
1888
2138
|
ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
|
1889
2139
|
else ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT))
|
|
@@ -3272,6 +3522,16 @@ class MainActivity : AppCompatActivity() {
|
|
|
3272
3522
|
bars.onTabSwipeStart = { dx, x, y -> beginTabSwipe(dx, x, y) }
|
|
3273
3523
|
bars.onTabSwipeMove = { dx -> updateTabSwipe(dx) }
|
|
3274
3524
|
bars.onTabSwipeEnd = { dx, cancelled -> finishTabSwipe(dx, cancelled) }
|
|
3525
|
+
bars.onTabSwipeShouldDefer = { x, y ->
|
|
3526
|
+
val location = IntArray(2)
|
|
3527
|
+
bars.getLocationOnScreen(location)
|
|
3528
|
+
val screen = visibleScreen
|
|
3529
|
+
screen != null && horizontalScrollAtPoint(
|
|
3530
|
+
screen.view,
|
|
3531
|
+
location[0] + x,
|
|
3532
|
+
location[1] + y,
|
|
3533
|
+
)
|
|
3534
|
+
}
|
|
3275
3535
|
root.addView(bars)
|
|
3276
3536
|
chrome = bars
|
|
3277
3537
|
tabStacks.clear()
|
|
@@ -3320,7 +3580,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
3320
3580
|
val target = ensureTabRoot(targetIndex) ?: return false
|
|
3321
3581
|
val host = chrome?.content ?: return false
|
|
3322
3582
|
if (target === current || host.width <= 0) return false
|
|
3323
|
-
|
|
3583
|
+
val chromeLocation = IntArray(2)
|
|
3584
|
+
chrome?.getLocationOnScreen(chromeLocation)
|
|
3585
|
+
if (horizontalScrollCanConsume(current.view, chromeLocation[0] + x, chromeLocation[1] + y, dx)) return false
|
|
3324
3586
|
(target.view.parent as? ViewGroup)?.removeView(target.view)
|
|
3325
3587
|
host.addView(target.view, FrameLayout.LayoutParams(-1, -1))
|
|
3326
3588
|
target.view.translationX = if (direction > 0) host.width.toFloat() else -host.width.toFloat()
|
|
@@ -3375,15 +3637,36 @@ class MainActivity : AppCompatActivity() {
|
|
|
3375
3637
|
}.start()
|
|
3376
3638
|
}
|
|
3377
3639
|
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3640
|
+
/**
|
|
3641
|
+
* Returns true when the exact screen gesture point is over a horizontal
|
|
3642
|
+
* child that still has room in the gesture's direction. Screen coordinates
|
|
3643
|
+
* keep nested scroll offsets and translated parents out of the calculation.
|
|
3644
|
+
*/
|
|
3645
|
+
private fun horizontalScrollCanConsume(view: View, screenX: Float, screenY: Float, dx: Float): Boolean {
|
|
3646
|
+
if (!view.isShown) return false
|
|
3647
|
+
val location = IntArray(2)
|
|
3648
|
+
view.getLocationOnScreen(location)
|
|
3649
|
+
if (screenX < location[0] || screenY < location[1]
|
|
3650
|
+
|| screenX > location[0] + view.width || screenY > location[1] + view.height) return false
|
|
3651
|
+
if (view is android.widget.HorizontalScrollView && view.canScrollHorizontally(if (dx < 0) 1 else -1)) return true
|
|
3652
|
+
if (view !is ViewGroup) return false
|
|
3653
|
+
for (index in view.childCount - 1 downTo 0) {
|
|
3654
|
+
if (horizontalScrollCanConsume(view.getChildAt(index), screenX, screenY, dx)) return true
|
|
3655
|
+
}
|
|
3656
|
+
return false
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
/** Returns true when a native horizontal scroll host owns the touch point. */
|
|
3660
|
+
private fun horizontalScrollAtPoint(view: View, screenX: Float, screenY: Float): Boolean {
|
|
3661
|
+
if (!view.isShown) return false
|
|
3662
|
+
val location = IntArray(2)
|
|
3663
|
+
view.getLocationOnScreen(location)
|
|
3664
|
+
if (screenX < location[0] || screenY < location[1]
|
|
3665
|
+
|| screenX > location[0] + view.width || screenY > location[1] + view.height) return false
|
|
3666
|
+
if (view is android.widget.HorizontalScrollView) return true
|
|
3667
|
+
if (view !is ViewGroup) return false
|
|
3382
3668
|
for (index in view.childCount - 1 downTo 0) {
|
|
3383
|
-
|
|
3384
|
-
if (!child.isShown || localX < child.left || localX > child.right || localY < child.top || localY > child.bottom) continue
|
|
3385
|
-
if (child is android.widget.HorizontalScrollView && child.canScrollHorizontally(if (dx < 0) 1 else -1)) return true
|
|
3386
|
-
if (horizontalScrollCanConsume(child, host, localX - child.left, localY - child.top, dx)) return true
|
|
3669
|
+
if (horizontalScrollAtPoint(view.getChildAt(index), screenX, screenY)) return true
|
|
3387
3670
|
}
|
|
3388
3671
|
return false
|
|
3389
3672
|
}
|
|
@@ -4514,7 +4797,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4514
4797
|
)
|
|
4515
4798
|
} ?: run { trackTintList = null }
|
|
4516
4799
|
craftNativeOptionalString(props.opt("thumbColor"))
|
|
4517
|
-
?.let { ColorStateList.valueOf(color(it, Color.WHITE)) }
|
|
4800
|
+
?.let { thumbTintList = ColorStateList.valueOf(color(it, Color.WHITE)) }
|
|
4518
4801
|
?: run { thumbTintList = null }
|
|
4519
4802
|
val handler = craftNativeEventName(events, "onValueChange", "onChange")
|
|
4520
4803
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
@@ -4751,6 +5034,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
4751
5034
|
}
|
|
4752
5035
|
}
|
|
4753
5036
|
configureContainer(content, contentStyle)
|
|
5037
|
+
// Content-style reconciliation configures the flex host again;
|
|
5038
|
+
// restore the scroll axis after it so horizontal children are
|
|
5039
|
+
// measured beyond the viewport rather than clamped to it.
|
|
5040
|
+
content.orientation = if (horizontal) LinearLayout.HORIZONTAL else LinearLayout.VERTICAL
|
|
5041
|
+
content.setUnboundedWidth(horizontal)
|
|
4754
5042
|
val contentChildren = JSONArray()
|
|
4755
5043
|
for (index in 0 until children.length()) {
|
|
4756
5044
|
val child = children.opt(index)
|