@onekeyfe/react-native-native-list 3.0.157-alpha.253 → 3.0.157-alpha.255
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/android/build.gradle +7 -0
- package/android/src/main/java/com/onekey/nativelist/HybridNativeList.kt +20 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListActivityDetailsView.kt +179 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListActivityRenderer.kt +23 -2
- package/android/src/main/java/com/onekey/nativelist/NativeListContainerSlotAdapter.kt +53 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListEndAlignment.kt +5 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListMediaPreviewSlot.kt +215 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListMediaTileRenderer.kt +15 -6
- package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +3 -1
- package/android/src/main/java/com/onekey/nativelist/NativeListScrollPositionTracker.kt +29 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListSectionHeaderRenderer.kt +16 -1
- package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +163 -26
- package/android/src/main/res/values/native_scroll_coordinator_ids.xml +4 -0
- package/android/src/test/java/com/margelo/nitro/nativelist/NativeListEndAlignmentTest.kt +34 -0
- package/android/src/test/java/com/margelo/nitro/nativelist/NativeListScrollPositionTrackerTest.kt +28 -0
- package/docs/DESIGN.md +4 -2
- package/docs/ROW_TEMPLATES.md +45 -0
- package/docs/SPEC.md +227 -1
- package/docs/STYLE_SPEC.md +8 -0
- package/ios/HybridNativeList.swift +30 -0
- package/ios/NativeListActivityDetailsView.swift +229 -0
- package/ios/NativeListActivityRenderer.swift +24 -2
- package/ios/NativeListMediaPreviewSlot.swift +199 -0
- package/ios/NativeListMediaTileRenderer.swift +10 -4
- package/ios/NativeListModels.swift +4 -1
- package/ios/NativeListScrollPositionTracker.swift +30 -0
- package/ios/NativeListSectionHeaderRenderer.swift +27 -1
- package/ios/RNCNativeListView.swift +159 -4
- package/ios/tests/NativeListActivityPresentationTests.swift +43 -0
- package/ios/tests/NativeListLifecycleTests.swift +213 -0
- package/ios/tests/NativeListMediaVisibilityTests.swift +42 -0
- package/ios/tests/NativeListScrollPositionTrackerTests.swift +25 -0
- package/lib/module/NativeList.js +56 -3
- package/lib/module/NativeList.web.js +5 -0
- package/lib/module/validation.js +64 -4
- package/lib/nitrogen/generated/android/c++/JFunc_void_bool.hpp +75 -0
- package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +39 -3
- package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +6 -0
- package/lib/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +15 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/Func_void_bool.kt +78 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +26 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/views/HybridNativeListManager.kt +24 -9
- package/lib/nitrogen/generated/android/nativelistOnLoad.cpp +2 -0
- package/lib/nitrogen/generated/ios/NativeListModule-Swift-Cxx-Bridge.cpp +8 -0
- package/lib/nitrogen/generated/ios/NativeListModule-Swift-Cxx-Bridge.hpp +37 -0
- package/lib/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +23 -2
- package/lib/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +32 -0
- package/lib/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
- package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +3 -0
- package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +54 -0
- package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +6 -0
- package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +8 -2
- package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +6 -0
- package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +11 -2
- package/lib/nitrogen/generated/shared/json/NativeListConfig.json +3 -0
- package/lib/typescript/src/NativeList.nitro.d.ts +3 -0
- package/lib/typescript/src/NativeList.types.d.ts +14 -1
- package/lib/typescript/src/models.d.ts +33 -5
- package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +75 -0
- package/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +39 -3
- package/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +6 -0
- package/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/Func_void_bool.kt +78 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +26 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/views/HybridNativeListManager.kt +24 -9
- package/nitrogen/generated/android/nativelistOnLoad.cpp +2 -0
- package/nitrogen/generated/ios/NativeListModule-Swift-Cxx-Bridge.cpp +8 -0
- package/nitrogen/generated/ios/NativeListModule-Swift-Cxx-Bridge.hpp +37 -0
- package/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +23 -2
- package/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +32 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +3 -0
- package/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +54 -0
- package/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +6 -0
- package/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +8 -2
- package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +6 -0
- package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +11 -2
- package/nitrogen/generated/shared/json/NativeListConfig.json +3 -0
- package/package.json +6 -5
- package/react-native-native-list.podspec +6 -0
- package/scripts/enable-container-slots.mjs +58 -0
- package/scripts/enable-container-slots.test.mjs +58 -0
- package/src/NativeList.nitro.ts +3 -0
- package/src/NativeList.tsx +84 -2
- package/src/NativeList.types.ts +14 -1
- package/src/NativeList.web.tsx +5 -0
- package/src/models.ts +38 -1
- package/src/validation.ts +146 -4
package/android/build.gradle
CHANGED
|
@@ -94,11 +94,18 @@ repositories {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
def kotlinVersion = getExtOrDefault("kotlinVersion")
|
|
97
|
+
// Keep native media preview formats aligned with react-native-video's default.
|
|
98
|
+
def media3Version = rootProject.ext.has("media3Version") ? rootProject.ext.get("media3Version") : "1.4.1"
|
|
97
99
|
|
|
98
100
|
dependencies {
|
|
99
101
|
implementation "com.facebook.react:react-android"
|
|
100
102
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
|
101
103
|
implementation "androidx.recyclerview:recyclerview:1.4.0"
|
|
104
|
+
implementation "androidx.media3:media3-exoplayer:$media3Version"
|
|
105
|
+
implementation "androidx.media3:media3-exoplayer-hls:$media3Version"
|
|
106
|
+
implementation "androidx.media3:media3-exoplayer-dash:$media3Version"
|
|
107
|
+
implementation "androidx.media3:media3-ui:$media3Version"
|
|
108
|
+
implementation "androidx.media3:media3-datasource-okhttp:$media3Version"
|
|
102
109
|
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
|
103
110
|
implementation project(":react-native-nitro-modules")
|
|
104
111
|
implementation project(":onekeyfe_react-native-native-logger")
|
|
@@ -11,6 +11,26 @@ class HybridNativeList(context: ThemedReactContext) : HybridNativeListSpec() {
|
|
|
11
11
|
|
|
12
12
|
override val view: View = hostView
|
|
13
13
|
|
|
14
|
+
override var scrollPositionThresholdsJson: String = ""
|
|
15
|
+
set(value) {
|
|
16
|
+
if (field == value) return
|
|
17
|
+
field = value
|
|
18
|
+
dispatchToUi { hostView.setScrollPositionThresholdsJson(value) }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
override var onScrollPositionThresholdChange: ((isBeyondThreshold: Boolean) -> Unit)? = null
|
|
22
|
+
set(value) {
|
|
23
|
+
field = value
|
|
24
|
+
dispatchToUi { hostView.onScrollPositionThresholdChange = value }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
override var containerSlotHeightsJson: String = "[]"
|
|
28
|
+
set(value) {
|
|
29
|
+
if (field == value) return
|
|
30
|
+
field = value
|
|
31
|
+
dispatchToUi { hostView.setContainerSlotHeightsJson(value) }
|
|
32
|
+
}
|
|
33
|
+
|
|
14
34
|
override var snapshotJson: String = ""
|
|
15
35
|
get() = field
|
|
16
36
|
set(value) {
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
package com.margelo.nitro.nativelist
|
|
2
|
+
|
|
3
|
+
import android.graphics.drawable.GradientDrawable
|
|
4
|
+
import android.text.Spannable
|
|
5
|
+
import android.text.SpannableStringBuilder
|
|
6
|
+
import android.text.style.AbsoluteSizeSpan
|
|
7
|
+
import android.view.Gravity
|
|
8
|
+
import android.view.View
|
|
9
|
+
import android.widget.LinearLayout
|
|
10
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
11
|
+
import kotlin.math.roundToInt
|
|
12
|
+
import org.json.JSONObject
|
|
13
|
+
|
|
14
|
+
/** Bounded rich activity fields; no wallet or transaction semantics live here. */
|
|
15
|
+
internal class NativeListActivityDetailsView(private val reactContext: ThemedReactContext) : LinearLayout(reactContext) {
|
|
16
|
+
var onAction: ((String, View, String, Int) -> Unit)? = null
|
|
17
|
+
private var sourceScale = false
|
|
18
|
+
private val main = LinearLayout(context).apply { orientation = HORIZONTAL; gravity = Gravity.CENTER_VERTICAL }
|
|
19
|
+
private val visual = NativeListLeadingVisual(reactContext)
|
|
20
|
+
private val identity = LinearLayout(context).apply { orientation = VERTICAL }
|
|
21
|
+
private val titleLine = LinearLayout(context).apply { orientation = HORIZONTAL; gravity = Gravity.CENTER_VERTICAL }
|
|
22
|
+
private val title = NativeListTextView(context)
|
|
23
|
+
private val description = NativeListTextView(context)
|
|
24
|
+
private val status = NativeListTextView(context)
|
|
25
|
+
private val amounts = LinearLayout(context).apply { orientation = VERTICAL }
|
|
26
|
+
private val fee = LinearLayout(context).apply { orientation = VERTICAL; gravity = Gravity.END }
|
|
27
|
+
private val feeLabel = NativeListTextView(context)
|
|
28
|
+
private val feeLine = LinearLayout(context).apply { orientation = HORIZONTAL; gravity = Gravity.END }
|
|
29
|
+
private val feePrimary = NativeListTextView(context)
|
|
30
|
+
private val feeSecondary = NativeListTextView(context)
|
|
31
|
+
private val actions = LinearLayout(context).apply { orientation = HORIZONTAL }
|
|
32
|
+
private val amountViews = mutableListOf<AmountView>()
|
|
33
|
+
private val badges = mutableListOf<View>()
|
|
34
|
+
|
|
35
|
+
init {
|
|
36
|
+
orientation = VERTICAL
|
|
37
|
+
titleLine.addView(title, LayoutParams(0, -2, 1f))
|
|
38
|
+
identity.addView(titleLine)
|
|
39
|
+
identity.addView(description)
|
|
40
|
+
identity.addView(status)
|
|
41
|
+
feeLine.addView(feePrimary); feeLine.addView(feeSecondary)
|
|
42
|
+
fee.addView(feeLabel); fee.addView(feeLine)
|
|
43
|
+
main.addView(visual)
|
|
44
|
+
main.addView(identity, LayoutParams(0, -2, 1f))
|
|
45
|
+
main.addView(amounts)
|
|
46
|
+
main.addView(fee)
|
|
47
|
+
addView(main, LayoutParams(-1, -2))
|
|
48
|
+
addView(actions, LayoutParams(-1, -2))
|
|
49
|
+
}
|
|
50
|
+
private fun dp(value: Int) = if (sourceScale) (value * resources.displayMetrics.density).roundToInt() else NativeListScale.dp(resources, value)
|
|
51
|
+
private fun px(value: Double) = (value * resources.displayMetrics.density).roundToInt()
|
|
52
|
+
private fun color(tone: String, theme: JSONObject?): Int {
|
|
53
|
+
val (key, fallback) = when (tone) {
|
|
54
|
+
"positive", "success" -> "positive" to "#218358"
|
|
55
|
+
"negative", "danger" -> "negative" to "#CE2C31"
|
|
56
|
+
"secondary" -> "secondaryText" to "#646464"
|
|
57
|
+
"warning" -> "warning" to "#AB6400"
|
|
58
|
+
"info" -> "info" to "#007BEF"
|
|
59
|
+
else -> "primaryText" to "#202020"
|
|
60
|
+
}
|
|
61
|
+
return parseNativeListColor(theme?.optString(key, fallback) ?: fallback)
|
|
62
|
+
}
|
|
63
|
+
private fun text(label: NativeListTextView, value: String, style: JSONObject?, size: Float,
|
|
64
|
+
ink: Int, line: Int, weight: String = "regular", lines: Int = 1) {
|
|
65
|
+
NativeListResolvedText.resolve(context, value, style, size, weight, ink, line, lines, sourceScale).bind(label)
|
|
66
|
+
}
|
|
67
|
+
fun bind(item: NativeListItem, theme: JSONObject?, sourceScale: Boolean) {
|
|
68
|
+
this.sourceScale = sourceScale
|
|
69
|
+
val data = item.json
|
|
70
|
+
val style = data.optJSONObject("style") ?: JSONObject()
|
|
71
|
+
val image = style.optJSONObject("image") ?: JSONObject()
|
|
72
|
+
val table = data.optString("presentation") == "table"
|
|
73
|
+
val gap = if (style.has("leadingGap")) px(style.optDouble("leadingGap")) else dp(12)
|
|
74
|
+
val iw = if (image.has("width")) px(image.optDouble("width")) else dp(40)
|
|
75
|
+
visual.layoutParams = LayoutParams(iw, if (image.has("height")) px(image.optDouble("height")) else dp(40)).apply { marginEnd = gap }
|
|
76
|
+
visual.bind(data.optJSONObject("leading") ?: JSONObject(), image, item.key, theme, false, sourceScale, data.optJSONObject("secondaryLeading"))
|
|
77
|
+
amounts.layoutParams = if (table) LayoutParams(0, -2, 1f).apply { marginStart = gap } else LayoutParams(-2, -2).apply { marginStart = gap }
|
|
78
|
+
text(title, data.optString("title"), style.optJSONObject("title"), 16f, color("primary", theme), 24, "medium")
|
|
79
|
+
text(description, data.optString("description"), style.optJSONObject("description"), 14f, color("secondary", theme), 20, lines = 2)
|
|
80
|
+
text(status, data.optString("status"), style.optJSONObject("status"), 12f, color("secondary", theme), 16)
|
|
81
|
+
description.setOnClickListener(null)
|
|
82
|
+
val descriptionKey = data.optString("descriptionActionKey")
|
|
83
|
+
description.isClickable = descriptionKey.isNotEmpty()
|
|
84
|
+
if (descriptionKey.isNotEmpty()) description.setOnClickListener { onAction?.invoke(descriptionKey, description, "description", 0) }
|
|
85
|
+
for (view in listOf(description, status)) (view.layoutParams as LayoutParams).topMargin = if (style.has("lineGap")) px(style.optDouble("lineGap")) else 0
|
|
86
|
+
badges.forEach(titleLine::removeView); badges.clear()
|
|
87
|
+
val badgeData = data.optJSONArray("badges")
|
|
88
|
+
for (index in 0 until (badgeData?.length() ?: 0)) {
|
|
89
|
+
val descriptor = badgeData!!.getJSONObject(index)
|
|
90
|
+
val label = NativeListTextView(context)
|
|
91
|
+
val ink = color(descriptor.optString("tone"), theme)
|
|
92
|
+
text(label, descriptor.optString("text"), null, 12f, ink, 16, "medium")
|
|
93
|
+
label.setPadding(dp(4), 0, dp(4), 0)
|
|
94
|
+
label.background = GradientDrawable().apply { setColor((ink and 0x00ffffff) or 0x19000000); cornerRadius = dp(4).toFloat() }
|
|
95
|
+
titleLine.addView(label, LayoutParams(-2, -2).apply { marginStart = if (style.has("titleBadgeGap")) px(style.optDouble("titleBadgeGap")) else dp(4) })
|
|
96
|
+
badges.add(label)
|
|
97
|
+
}
|
|
98
|
+
val amountData = data.optJSONArray("amounts")
|
|
99
|
+
val count = amountData?.length() ?: 0
|
|
100
|
+
while (amountViews.size > count) { val child = amountViews.removeAt(amountViews.lastIndex); child.recycle(); amounts.removeView(child) }
|
|
101
|
+
while (amountViews.size < count) { val child = AmountView(); amountViews.add(child); amounts.addView(child) }
|
|
102
|
+
for (index in 0 until count) {
|
|
103
|
+
val descriptor = amountData!!.getJSONObject(index)
|
|
104
|
+
amountViews[index].bind(descriptor, "${item.key}:${descriptor.optString("key")}", style, theme, table)
|
|
105
|
+
amountViews[index].layoutParams = LayoutParams(-1, -2).apply { topMargin = if (index == 0) 0 else if (style.has("trailingGap")) px(style.optDouble("trailingGap")) else dp(2) }
|
|
106
|
+
}
|
|
107
|
+
val feeData = data.optJSONObject("fee")
|
|
108
|
+
fee.visibility = if (feeData == null) GONE else VISIBLE
|
|
109
|
+
fee.alpha = if (feeData?.optBoolean("hidden") == true) 0f else 1f
|
|
110
|
+
fee.layoutParams = LayoutParams(-2, -2).apply { marginStart = gap }
|
|
111
|
+
text(feeLabel, feeData?.optString("label") ?: "", style.optJSONObject("secondaryAmount"), 12f, color("secondary", theme), 16)
|
|
112
|
+
text(feePrimary, feeData?.optString("primary") ?: "", style.optJSONObject("secondaryAmount"), 14f, color("primary", theme), 20)
|
|
113
|
+
text(feeSecondary, feeData?.optString("secondary") ?: "", style.optJSONObject("secondaryAmount"), 12f, color("secondary", theme), 16)
|
|
114
|
+
(feeSecondary.layoutParams as LayoutParams).marginStart = dp(4)
|
|
115
|
+
applySegments(feePrimary, feeData?.optJSONArray("primaryTextSegments"), style.optJSONObject("secondaryAmount")?.optDouble("fontSize", 14.0) ?: 14.0)
|
|
116
|
+
applySegments(feeSecondary, feeData?.optJSONArray("secondaryTextSegments"), style.optJSONObject("secondaryAmount")?.optDouble("fontSize", 12.0) ?: 12.0)
|
|
117
|
+
actions.removeAllViews()
|
|
118
|
+
val actionData = data.optJSONArray("footerActions")
|
|
119
|
+
actions.visibility = if ((actionData?.length() ?: 0) == 0) GONE else VISIBLE
|
|
120
|
+
actions.layoutParams = LayoutParams(-1, -2).apply { marginStart = iw + gap; topMargin = dp(8) }
|
|
121
|
+
for (index in 0 until (actionData?.length() ?: 0)) {
|
|
122
|
+
val descriptor = actionData!!.getJSONObject(index)
|
|
123
|
+
val button = NativeListTextView(context)
|
|
124
|
+
text(button, descriptor.optString("label"), null, 14f, color(descriptor.optString("tone"), theme), 20, "medium")
|
|
125
|
+
button.setPadding(dp(8), dp(4), dp(8), dp(4))
|
|
126
|
+
button.background = GradientDrawable().apply { setColor(parseNativeListColor(theme?.optString("strongBackground", "#F0F0F0") ?: "#F0F0F0")); cornerRadius = dp(8).toFloat() }
|
|
127
|
+
button.isEnabled = !data.optBoolean("disabled") && !descriptor.optBoolean("disabled")
|
|
128
|
+
button.setOnClickListener { onAction?.invoke(descriptor.optString("key"), button, "footerAction", index) }
|
|
129
|
+
actions.addView(button, LayoutParams(-2, -2).apply { marginEnd = dp(8) })
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
private fun applySegments(label: NativeListTextView, segments: org.json.JSONArray?, fontSize: Double) {
|
|
133
|
+
if (segments == null || segments.length() == 0) return
|
|
134
|
+
val fullText = (0 until segments.length()).joinToString("") { segments.getJSONObject(it).optString("text") }
|
|
135
|
+
val styled = if (label.text.toString() == fullText) SpannableStringBuilder(label.text) else SpannableStringBuilder(fullText)
|
|
136
|
+
var start = 0
|
|
137
|
+
for (index in 0 until segments.length()) {
|
|
138
|
+
val segment = segments.getJSONObject(index)
|
|
139
|
+
val end = start + segment.optString("text").length
|
|
140
|
+
if (segment.optString("style") == "subscript") {
|
|
141
|
+
// Match NumberSizeableText: shrink the zero count without moving its baseline.
|
|
142
|
+
val size = kotlin.math.ceil(fontSize * 0.6) * label.textSize / fontSize
|
|
143
|
+
styled.setSpan(AbsoluteSizeSpan(size.roundToInt()), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
144
|
+
}
|
|
145
|
+
start = end
|
|
146
|
+
}
|
|
147
|
+
label.text = styled
|
|
148
|
+
}
|
|
149
|
+
fun recycle() { visual.recycle(); amountViews.forEach { it.recycle() }; description.setOnClickListener(null); actions.removeAllViews() }
|
|
150
|
+
fun dispose() { recycle(); visual.dispose(); amountViews.forEach { it.dispose() } }
|
|
151
|
+
|
|
152
|
+
private inner class AmountView : LinearLayout(context) {
|
|
153
|
+
private val leading = NativeListLeadingVisual(reactContext)
|
|
154
|
+
private val labels = LinearLayout(context).apply { orientation = VERTICAL }
|
|
155
|
+
private val primary = NativeListTextView(context)
|
|
156
|
+
private val secondary = NativeListTextView(context)
|
|
157
|
+
init {
|
|
158
|
+
orientation = HORIZONTAL; gravity = Gravity.CENTER_VERTICAL
|
|
159
|
+
labels.addView(primary); labels.addView(secondary)
|
|
160
|
+
addView(leading); addView(labels, LayoutParams(0, -2, 1f))
|
|
161
|
+
}
|
|
162
|
+
fun bind(data: JSONObject, key: String, style: JSONObject, theme: JSONObject?, table: Boolean) {
|
|
163
|
+
val visualData = data.optJSONObject("leading")
|
|
164
|
+
leading.visibility = if (visualData == null) GONE else VISIBLE
|
|
165
|
+
leading.layoutParams = LayoutParams(dp(20), dp(20)).apply { marginEnd = dp(6) }
|
|
166
|
+
if (visualData != null) leading.bind(visualData, JSONObject().put("width", 20).put("height", 20), key, theme, false, sourceScale) else leading.recycle()
|
|
167
|
+
val primaryStyle = JSONObject(style.optJSONObject("primaryAmount")?.toString() ?: "{}")
|
|
168
|
+
if (!primaryStyle.has("alignment")) primaryStyle.put("alignment", if (table) "start" else "end")
|
|
169
|
+
text(primary, data.optString("text"), primaryStyle, 16f, color(data.optString("tone"), theme), 24, "medium")
|
|
170
|
+
applySegments(primary, data.optJSONArray("textSegments"), primaryStyle.optDouble("fontSize", 16.0))
|
|
171
|
+
val secondaryStyle = JSONObject(style.optJSONObject("secondaryAmount")?.toString() ?: "{}")
|
|
172
|
+
if (!secondaryStyle.has("alignment")) secondaryStyle.put("alignment", if (table) "start" else "end")
|
|
173
|
+
text(secondary, data.optString("secondaryText"), secondaryStyle, 14f, color("secondary", theme), 20)
|
|
174
|
+
applySegments(secondary, data.optJSONArray("secondaryTextSegments"), secondaryStyle.optDouble("fontSize", 14.0))
|
|
175
|
+
}
|
|
176
|
+
fun recycle() { leading.recycle() }
|
|
177
|
+
fun dispose() { leading.dispose() }
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -9,6 +9,7 @@ import org.json.JSONObject
|
|
|
9
9
|
|
|
10
10
|
internal class NativeListActivityRowView(context: ThemedReactContext) :
|
|
11
11
|
NativeListRendererRowView(context) {
|
|
12
|
+
private val details = NativeListActivityDetailsView(context)
|
|
12
13
|
private val content =
|
|
13
14
|
LinearLayout(context).apply {
|
|
14
15
|
orientation = HORIZONTAL
|
|
@@ -33,7 +34,7 @@ internal class NativeListActivityRowView(context: ThemedReactContext) :
|
|
|
33
34
|
private val values = List(2) { NativeListTextView(context) }
|
|
34
35
|
private val actions = LinearLayout(context).apply { orientation = HORIZONTAL }
|
|
35
36
|
private val buttons = List(3) { NativeListTextView(context) }
|
|
36
|
-
override val assetFields = listOf("leading", "secondaryLeading")
|
|
37
|
+
override val assetFields = listOf("leading", "secondaryLeading", "amounts")
|
|
37
38
|
|
|
38
39
|
init {
|
|
39
40
|
titleLine.addView(title, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
|
|
@@ -48,10 +49,13 @@ internal class NativeListActivityRowView(context: ThemedReactContext) :
|
|
|
48
49
|
content.addView(amounts)
|
|
49
50
|
addView(content, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
|
|
50
51
|
addView(actions)
|
|
52
|
+
addView(details, LayoutParams(-1, -2))
|
|
53
|
+
details.visibility = GONE
|
|
54
|
+
details.onAction = { key, view, source, slot -> emitAction(key, view, source, slot) }
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
override fun defaultHeight(item: NativeListItem, layout: String) =
|
|
54
|
-
if ((item.json.optJSONArray("footerActions")?.length() ?: 0) > 0) 104 else 60
|
|
58
|
+
if (item.json.has("amounts")) 0 else if ((item.json.optJSONArray("footerActions")?.length() ?: 0) > 0) 104 else 60
|
|
55
59
|
|
|
56
60
|
override fun bindContent(
|
|
57
61
|
item: NativeListItem,
|
|
@@ -60,6 +64,21 @@ internal class NativeListActivityRowView(context: ThemedReactContext) :
|
|
|
60
64
|
checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
|
|
61
65
|
) {
|
|
62
66
|
val data = item.json
|
|
67
|
+
val rich = data.has("amounts")
|
|
68
|
+
details.visibility = if (rich) VISIBLE else GONE
|
|
69
|
+
content.visibility = if (rich) GONE else VISIBLE
|
|
70
|
+
actions.visibility = if (rich) GONE else VISIBLE
|
|
71
|
+
if (rich) {
|
|
72
|
+
orientation = VERTICAL
|
|
73
|
+
val style = data.optJSONObject("style") ?: JSONObject()
|
|
74
|
+
val hp = if (style.has("horizontalPadding")) stylePx(style.optDouble("horizontalPadding")) else dp(if (data.optString("presentation") == "table") 16 else 12)
|
|
75
|
+
val vp = if (style.has("verticalPadding")) stylePx(style.optDouble("verticalPadding")) else dp(8)
|
|
76
|
+
setPadding(hp, vp, hp, vp)
|
|
77
|
+
visual.recycle()
|
|
78
|
+
details.bind(item, theme, sourceScale)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
details.recycle()
|
|
63
82
|
val style = data.optJSONObject("style") ?: JSONObject()
|
|
64
83
|
val image = style.optJSONObject("image") ?: JSONObject()
|
|
65
84
|
fun color(name: String, fallback: String) =
|
|
@@ -233,11 +252,13 @@ internal class NativeListActivityRowView(context: ThemedReactContext) :
|
|
|
233
252
|
|
|
234
253
|
override fun recycleContent() {
|
|
235
254
|
visual.recycle()
|
|
255
|
+
details.recycle()
|
|
236
256
|
buttons.forEach { it.setOnClickListener(null) }
|
|
237
257
|
}
|
|
238
258
|
|
|
239
259
|
override fun disposeContent() {
|
|
240
260
|
visual.dispose()
|
|
261
|
+
details.dispose()
|
|
241
262
|
buttons.forEach { it.setOnClickListener(null) }
|
|
242
263
|
}
|
|
243
264
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
package com.margelo.nitro.nativelist
|
|
2
|
+
|
|
3
|
+
import android.view.View
|
|
4
|
+
import android.view.ViewGroup
|
|
5
|
+
import android.widget.FrameLayout
|
|
6
|
+
import androidx.recyclerview.widget.RecyclerView
|
|
7
|
+
|
|
8
|
+
/** A persistent React container slot; never a template row or a selectable item. */
|
|
9
|
+
internal class NativeListContainerSlotAdapter : RecyclerView.Adapter<NativeListContainerSlotAdapter.Holder>() {
|
|
10
|
+
class Holder(val container: FrameLayout) : RecyclerView.ViewHolder(container)
|
|
11
|
+
private var content: View? = null
|
|
12
|
+
private var height = 0
|
|
13
|
+
private var enabled = true
|
|
14
|
+
|
|
15
|
+
fun update(view: View?, heightPx: Int, visible: Boolean) {
|
|
16
|
+
val previousCount = itemCount
|
|
17
|
+
val changed = content !== view || height != heightPx || enabled != visible
|
|
18
|
+
content = view
|
|
19
|
+
height = heightPx.coerceAtLeast(0)
|
|
20
|
+
enabled = visible
|
|
21
|
+
val nextCount = itemCount
|
|
22
|
+
if (previousCount == 0 && nextCount == 1) notifyItemInserted(0)
|
|
23
|
+
else if (previousCount == 1 && nextCount == 0) notifyItemRemoved(0)
|
|
24
|
+
else if (nextCount == 1 && changed) notifyItemChanged(0)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
override fun getItemCount(): Int = if (enabled && content != null && height > 0) 1 else 0
|
|
28
|
+
|
|
29
|
+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
|
|
30
|
+
val container = object : FrameLayout(parent.context) {
|
|
31
|
+
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
32
|
+
for (index in 0 until childCount) {
|
|
33
|
+
getChildAt(index).layout(0, 0, right - left, bottom - top)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
container.layoutParams = RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, height)
|
|
38
|
+
return Holder(container)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
override fun onBindViewHolder(holder: Holder, position: Int) {
|
|
42
|
+
holder.container.layoutParams = RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, height)
|
|
43
|
+
holder.container.removeAllViews()
|
|
44
|
+
content?.let { view ->
|
|
45
|
+
(view.parent as? ViewGroup)?.removeView(view)
|
|
46
|
+
holder.container.addView(view, FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, height))
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
override fun onViewRecycled(holder: Holder) {
|
|
51
|
+
holder.container.removeAllViews()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
package com.margelo.nitro.nativelist
|
|
2
|
+
|
|
3
|
+
import android.graphics.Rect
|
|
4
|
+
import android.view.ViewTreeObserver
|
|
5
|
+
import android.view.Gravity
|
|
6
|
+
import android.view.TextureView
|
|
7
|
+
import android.view.View
|
|
8
|
+
import android.widget.FrameLayout
|
|
9
|
+
import androidx.annotation.OptIn
|
|
10
|
+
import androidx.media3.common.MediaItem
|
|
11
|
+
import androidx.media3.common.PlaybackException
|
|
12
|
+
import androidx.media3.common.Player
|
|
13
|
+
import androidx.media3.common.VideoSize
|
|
14
|
+
import androidx.media3.common.util.UnstableApi
|
|
15
|
+
import androidx.media3.common.util.Util
|
|
16
|
+
import androidx.media3.datasource.DefaultDataSource
|
|
17
|
+
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
|
18
|
+
import androidx.media3.exoplayer.ExoPlayer
|
|
19
|
+
import androidx.media3.exoplayer.DefaultLoadControl
|
|
20
|
+
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
|
21
|
+
import androidx.media3.ui.AspectRatioFrameLayout
|
|
22
|
+
import com.facebook.react.modules.network.ForwardingCookieHandler
|
|
23
|
+
import com.facebook.react.modules.network.OkHttpClientProvider
|
|
24
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
25
|
+
import okhttp3.JavaNetCookieJar
|
|
26
|
+
import org.json.JSONObject
|
|
27
|
+
|
|
28
|
+
/** Uses Media3 like native Video, but never starts playback or requests audio focus. */
|
|
29
|
+
@OptIn(UnstableApi::class)
|
|
30
|
+
internal class NativeListMediaPreviewSlot(private val context: ThemedReactContext) {
|
|
31
|
+
val view: FrameLayout = object : FrameLayout(context) {
|
|
32
|
+
override fun onAttachedToWindow() { super.onAttachedToWindow(); observeViewport(); updateVisibility() }
|
|
33
|
+
override fun onDetachedFromWindow() { stopObservingViewport(); releasePlayer(); super.onDetachedFromWindow() }
|
|
34
|
+
override fun onWindowVisibilityChanged(visibility: Int) { super.onWindowVisibilityChanged(visibility); updateVisibility() }
|
|
35
|
+
override fun onVisibilityAggregated(isVisible: Boolean) { super.onVisibilityAggregated(isVisible); if (isVisible) updateVisibility() else releasePlayer() }
|
|
36
|
+
}
|
|
37
|
+
private val image = NativeListImageSlot(context)
|
|
38
|
+
private val video = AspectRatioFrameLayout(context)
|
|
39
|
+
private val texture = TextureView(context)
|
|
40
|
+
private var identity: String? = null
|
|
41
|
+
private var epoch = 0
|
|
42
|
+
private var candidateGeneration = 0
|
|
43
|
+
private var playerGeneration = 0
|
|
44
|
+
private var result: Boolean? = null
|
|
45
|
+
private var completion: ((Boolean) -> Unit)? = null
|
|
46
|
+
private var pendingCompletion: Runnable? = null
|
|
47
|
+
private var player: ExoPlayer? = null
|
|
48
|
+
private var resumeVideo: (() -> Unit)? = null
|
|
49
|
+
private var viewportObserver: ViewTreeObserver? = null
|
|
50
|
+
private val visibleRect = Rect()
|
|
51
|
+
private val scrollListener = ViewTreeObserver.OnScrollChangedListener { updateVisibility() }
|
|
52
|
+
private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener { updateVisibility() }
|
|
53
|
+
// Native property animations can move a retained pager page without a layout pass.
|
|
54
|
+
private val drawListener = ViewTreeObserver.OnPreDrawListener { updateVisibility(); true }
|
|
55
|
+
|
|
56
|
+
init {
|
|
57
|
+
view.isClickable = false
|
|
58
|
+
view.isFocusable = false
|
|
59
|
+
view.descendantFocusability = android.view.ViewGroup.FOCUS_BLOCK_DESCENDANTS
|
|
60
|
+
video.addView(texture, FrameLayout.LayoutParams(-1, -1))
|
|
61
|
+
view.addView(image.view, FrameLayout.LayoutParams(-1, -1))
|
|
62
|
+
view.addView(video, FrameLayout.LayoutParams(-1, -1, Gravity.CENTER))
|
|
63
|
+
video.visibility = View.GONE
|
|
64
|
+
video.setBackgroundColor(android.graphics.Color.BLACK)
|
|
65
|
+
texture.isOpaque = false
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fun bind(source: JSONObject, key: String, fit: String? = null,
|
|
69
|
+
placeholder: String = "#0000000F", probeOrder: List<String> = listOf("image"),
|
|
70
|
+
completion: ((Boolean) -> Unit)? = null) {
|
|
71
|
+
val next = nativeListCanonical(JSONObject().put("source", source).put("key", key)
|
|
72
|
+
.put("fit", fit ?: "").put("order", org.json.JSONArray(probeOrder)).put("placeholder", placeholder))
|
|
73
|
+
this.completion = completion
|
|
74
|
+
if (identity == next) {
|
|
75
|
+
result?.let { completion?.invoke(it) }
|
|
76
|
+
updateVisibility()
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
recycle()
|
|
80
|
+
identity = next
|
|
81
|
+
this.completion = completion
|
|
82
|
+
video.resizeMode = if ((fit ?: source.optString("contentFit")) == "cover") AspectRatioFrameLayout.RESIZE_MODE_ZOOM else AspectRatioFrameLayout.RESIZE_MODE_FIT
|
|
83
|
+
probe(source, key, fit, placeholder, probeOrder, 0, epoch)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private fun probe(source: JSONObject, key: String, fit: String?, placeholder: String,
|
|
87
|
+
order: List<String>, attempt: Int, token: Int) {
|
|
88
|
+
if (token != epoch) return
|
|
89
|
+
candidateGeneration++
|
|
90
|
+
val candidate = candidateGeneration
|
|
91
|
+
resumeVideo = null
|
|
92
|
+
stopObservingViewport()
|
|
93
|
+
releasePlayer()
|
|
94
|
+
if (attempt >= order.size) {
|
|
95
|
+
result = false
|
|
96
|
+
completion?.invoke(false)
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
fun finished(loaded: Boolean, expectedPlayerGeneration: Int? = null) {
|
|
100
|
+
if (epoch != token || candidateGeneration != candidate) return
|
|
101
|
+
pendingCompletion?.let(view::removeCallbacks)
|
|
102
|
+
// Glide forbids clearing/rebinding a request from either terminal callback.
|
|
103
|
+
// Media3 callbacks also defer release so the emitting player is not reentered.
|
|
104
|
+
val task = Runnable {
|
|
105
|
+
pendingCompletion = null
|
|
106
|
+
if (epoch != token || candidateGeneration != candidate ||
|
|
107
|
+
(expectedPlayerGeneration != null && playerGeneration != expectedPlayerGeneration)) return@Runnable
|
|
108
|
+
if (loaded) {
|
|
109
|
+
if (result != true) { result = true; completion?.invoke(true) }
|
|
110
|
+
} else probe(source, key, fit, placeholder, order, attempt + 1, token)
|
|
111
|
+
}
|
|
112
|
+
pendingCompletion = task
|
|
113
|
+
view.post(task)
|
|
114
|
+
}
|
|
115
|
+
if (order[attempt] == "image") {
|
|
116
|
+
video.visibility = View.GONE
|
|
117
|
+
image.view.visibility = View.VISIBLE
|
|
118
|
+
image.bind(source, key, fit = fit, placeholder = placeholder, completion = { loaded -> finished(loaded) })
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
image.recycle()
|
|
122
|
+
image.view.visibility = View.GONE
|
|
123
|
+
video.visibility = View.VISIBLE
|
|
124
|
+
val uri = source.optString("uri")
|
|
125
|
+
if (uri.isBlank()) { finished(false); return }
|
|
126
|
+
val headers = source.optJSONObject("headers")?.let { values -> values.keys().asSequence().associateWith { values.optString(it) } } ?: emptyMap()
|
|
127
|
+
resumeVideo = start@{
|
|
128
|
+
if (epoch != token || candidateGeneration != candidate || player != null) return@start
|
|
129
|
+
playerGeneration++
|
|
130
|
+
val generation = playerGeneration
|
|
131
|
+
try {
|
|
132
|
+
val client = OkHttpClientProvider.getOkHttpClient().newBuilder()
|
|
133
|
+
.cookieJar(JavaNetCookieJar(ForwardingCookieHandler(context))).build()
|
|
134
|
+
val http = OkHttpDataSource.Factory(client).setDefaultRequestProperties(headers)
|
|
135
|
+
if (headers.keys.none { it.equals("User-Agent", ignoreCase = true) }) http.setUserAgent(Util.getUserAgent(context, context.packageName))
|
|
136
|
+
val sources = DefaultMediaSourceFactory(DefaultDataSource.Factory(context, http))
|
|
137
|
+
val buffers = DefaultLoadControl.Builder()
|
|
138
|
+
.setBufferDurationsMs(250, 1000, 250, 250)
|
|
139
|
+
.setTargetBufferBytes(2 * 1024 * 1024)
|
|
140
|
+
.setPrioritizeTimeOverSizeThresholds(false)
|
|
141
|
+
.setBackBuffer(0, false).build()
|
|
142
|
+
val next = ExoPlayer.Builder(context.applicationContext)
|
|
143
|
+
.setMediaSourceFactory(sources).setLoadControl(buffers).build()
|
|
144
|
+
player = next
|
|
145
|
+
next.volume = 0f
|
|
146
|
+
next.playWhenReady = false
|
|
147
|
+
next.setAudioAttributes(androidx.media3.common.AudioAttributes.DEFAULT, false)
|
|
148
|
+
next.setVideoTextureView(texture)
|
|
149
|
+
next.addListener(object : Player.Listener {
|
|
150
|
+
override fun onRenderedFirstFrame() { if (playerGeneration == generation) finished(true, generation) }
|
|
151
|
+
override fun onPlayerError(error: PlaybackException) { if (playerGeneration == generation) finished(false, generation) }
|
|
152
|
+
override fun onVideoSizeChanged(size: VideoSize) {
|
|
153
|
+
if (playerGeneration == generation && size.height > 0) video.setAspectRatio(size.width * size.pixelWidthHeightRatio / size.height)
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
next.setMediaItem(MediaItem.fromUri(uri))
|
|
157
|
+
next.prepare()
|
|
158
|
+
next.pause()
|
|
159
|
+
} catch (_: Exception) {
|
|
160
|
+
releasePlayer()
|
|
161
|
+
finished(false)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
observeViewport()
|
|
165
|
+
updateVisibility()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private fun observeViewport() {
|
|
169
|
+
if (viewportObserver != null || resumeVideo == null || !view.isAttachedToWindow) return
|
|
170
|
+
view.viewTreeObserver.takeIf { it.isAlive }?.let {
|
|
171
|
+
it.addOnScrollChangedListener(scrollListener)
|
|
172
|
+
it.addOnGlobalLayoutListener(layoutListener)
|
|
173
|
+
it.addOnPreDrawListener(drawListener)
|
|
174
|
+
viewportObserver = it
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
private fun stopObservingViewport() {
|
|
178
|
+
viewportObserver?.takeIf { it.isAlive }?.let {
|
|
179
|
+
it.removeOnScrollChangedListener(scrollListener)
|
|
180
|
+
it.removeOnGlobalLayoutListener(layoutListener)
|
|
181
|
+
it.removeOnPreDrawListener(drawListener)
|
|
182
|
+
}
|
|
183
|
+
viewportObserver = null
|
|
184
|
+
}
|
|
185
|
+
private fun updateVisibility() {
|
|
186
|
+
var ancestor: View? = view
|
|
187
|
+
var opaque = true
|
|
188
|
+
while (ancestor != null) {
|
|
189
|
+
if (ancestor.alpha <= 0.01f) { opaque = false; break }
|
|
190
|
+
ancestor = ancestor.parent as? View
|
|
191
|
+
}
|
|
192
|
+
if (opaque && view.isAttachedToWindow && view.windowVisibility == View.VISIBLE && view.isShown &&
|
|
193
|
+
view.getGlobalVisibleRect(visibleRect) && !visibleRect.isEmpty) resumeVideo?.invoke()
|
|
194
|
+
else releasePlayer()
|
|
195
|
+
}
|
|
196
|
+
private fun releasePlayer() {
|
|
197
|
+
playerGeneration++
|
|
198
|
+
player?.let { it.pause(); it.clearVideoTextureView(texture); it.release() }
|
|
199
|
+
player = null
|
|
200
|
+
}
|
|
201
|
+
fun recycle() {
|
|
202
|
+
epoch++; candidateGeneration++
|
|
203
|
+
pendingCompletion?.let(view::removeCallbacks)
|
|
204
|
+
pendingCompletion = null
|
|
205
|
+
resumeVideo = null
|
|
206
|
+
stopObservingViewport()
|
|
207
|
+
releasePlayer()
|
|
208
|
+
image.recycle()
|
|
209
|
+
image.view.visibility = View.VISIBLE
|
|
210
|
+
video.visibility = View.GONE
|
|
211
|
+
video.setAspectRatio(0f)
|
|
212
|
+
identity = null; result = null; completion = null
|
|
213
|
+
}
|
|
214
|
+
fun dispose() { recycle(); image.dispose() }
|
|
215
|
+
}
|
|
@@ -14,7 +14,7 @@ import org.json.JSONObject
|
|
|
14
14
|
internal class NativeListMediaTileRowView(context: ThemedReactContext) :
|
|
15
15
|
NativeListRendererRowView(context) {
|
|
16
16
|
private val picture = FrameLayout(context)
|
|
17
|
-
private val image =
|
|
17
|
+
private val image = NativeListMediaPreviewSlot(context)
|
|
18
18
|
private val network = NativeListImageSlot(context)
|
|
19
19
|
private val errorIcon = OneKeyIconView(context)
|
|
20
20
|
private val title = NativeListTextView(context)
|
|
@@ -32,7 +32,7 @@ internal class NativeListMediaTileRowView(context: ThemedReactContext) :
|
|
|
32
32
|
override val defaultCornerRadius = 16
|
|
33
33
|
override val showsSelection = false
|
|
34
34
|
override val pressChangesBackground = false
|
|
35
|
-
override val assetFields = listOf("image", "networkImage")
|
|
35
|
+
override val assetFields = listOf("image", "media", "networkImage")
|
|
36
36
|
|
|
37
37
|
override fun horizontalWidth(item: NativeListItem) = dp(200)
|
|
38
38
|
|
|
@@ -141,15 +141,24 @@ internal class NativeListMediaTileRowView(context: ThemedReactContext) :
|
|
|
141
141
|
errorIcon.tintColor = parseNativeListColor("#00000044")
|
|
142
142
|
errorIcon.useSourceScale = sourceScale
|
|
143
143
|
errorIcon.layoutParams = FrameLayout.LayoutParams(dp(24), dp(24), Gravity.CENTER)
|
|
144
|
-
|
|
144
|
+
val media = item.json.optJSONObject("media")
|
|
145
|
+
val source = media?.optJSONObject("source") ?: item.json.optJSONObject("image")
|
|
146
|
+
val order = media?.optJSONArray("probeOrder")?.let { values ->
|
|
147
|
+
(0 until values.length()).map { values.optString(it) }
|
|
148
|
+
} ?: listOf("image")
|
|
149
|
+
if (state != "empty" && state != "error" && source != null)
|
|
145
150
|
image.bind(
|
|
146
|
-
|
|
151
|
+
source,
|
|
147
152
|
"${item.key}:media",
|
|
148
153
|
fit = imageStyle.optString("contentFit").takeIf { it.isNotEmpty() },
|
|
149
154
|
placeholder =
|
|
150
155
|
theme?.optString("strongBackground", "#0000000F")?.takeIf(String::isNotEmpty)
|
|
151
156
|
?: "#0000000F",
|
|
152
|
-
|
|
157
|
+
probeOrder = order,
|
|
158
|
+
) { loaded ->
|
|
159
|
+
errorIcon.visibility = if (loaded) GONE else VISIBLE
|
|
160
|
+
if (!loaded) image.view.setBackgroundColor(tint("strongBackground", "#0000000F"))
|
|
161
|
+
}
|
|
153
162
|
else image.recycle()
|
|
154
163
|
// Legacy: only the empty/error states paint a slot fill; loaded images show
|
|
155
164
|
// their own placeholder without a white backing.
|
|
@@ -157,7 +166,7 @@ internal class NativeListMediaTileRowView(context: ThemedReactContext) :
|
|
|
157
166
|
when (state) {
|
|
158
167
|
"error" -> GradientDrawable().apply { setColor(tint("strongBackground", "#0000000F")) }
|
|
159
168
|
"empty" -> GradientDrawable().apply { setColor(Color.WHITE) }
|
|
160
|
-
else -> null
|
|
169
|
+
else -> if (errorIcon.visibility == VISIBLE) GradientDrawable().apply { setColor(tint("strongBackground", "#0000000F")) } else null
|
|
161
170
|
}
|
|
162
171
|
image.view.outlineProvider =
|
|
163
172
|
object : ViewOutlineProvider() {
|
|
@@ -156,6 +156,7 @@ internal data class NativeListConfig(
|
|
|
156
156
|
val selectedKeys: LinkedHashSet<String>,
|
|
157
157
|
val reorderable: Boolean,
|
|
158
158
|
val pullToRefresh: Boolean,
|
|
159
|
+
val refreshTriggerDistance: Double? = null,
|
|
159
160
|
val refreshing: Boolean,
|
|
160
161
|
val loadMore: Boolean,
|
|
161
162
|
val endReachedThreshold: Double,
|
|
@@ -194,7 +195,7 @@ internal data class NativeListConfig(
|
|
|
194
195
|
generation = root.getInt("generation"),
|
|
195
196
|
layout = layout.getString("kind"),
|
|
196
197
|
orientation = layout.optString("orientation", "vertical"),
|
|
197
|
-
gridColumns = layout.optInt("gridColumns", 1).coerceIn(1,
|
|
198
|
+
gridColumns = layout.optInt("gridColumns", 1).coerceIn(1, 7),
|
|
198
199
|
stickyHeaders = layout.optBoolean("stickyHeaders", false),
|
|
199
200
|
contentPadding = layout.optInt("contentPadding", 0).coerceAtLeast(0),
|
|
200
201
|
contentPaddingHorizontal = layout.optionalNonNegativeInt("contentPaddingHorizontal"),
|
|
@@ -206,6 +207,7 @@ internal data class NativeListConfig(
|
|
|
206
207
|
selectedKeys = selectedKeys,
|
|
207
208
|
reorderable = capabilities?.optBoolean("reorderable", false) ?: false,
|
|
208
209
|
pullToRefresh = capabilities?.optBoolean("pullToRefresh", false) ?: false,
|
|
210
|
+
refreshTriggerDistance = capabilities?.optDouble("refreshTriggerDistance")?.takeIf { it.isFinite() && it > 0 },
|
|
209
211
|
refreshing = capabilities?.optBoolean("refreshing", false) ?: false,
|
|
210
212
|
loadMore = capabilities?.optBoolean("loadMore", false) ?: false,
|
|
211
213
|
endReachedThreshold = capabilities?.optDouble("endReachedThreshold", 0.2) ?: 0.2,
|