@craft-native/android 0.0.123 → 0.0.125
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 +29 -5
- package/dist/index.d.ts +3 -0
- package/dist/index.js +23 -4
- package/dist/index.js.map +3 -3
- package/package.json +1 -1
- package/templates/AndroidManifest.xml.template +1 -1
- package/templates/CraftNativeFlatList.kt.template +151 -7
- package/templates/CraftNativeRefreshableFlatList.kt.template +29 -0
- package/templates/MainActivity.kt.template +17 -0
- package/templates/MainActivityNative.kt.template +1085 -84
- package/templates/build.gradle.kts.app.template +1 -0
|
@@ -14,7 +14,7 @@ import kotlin.math.ceil
|
|
|
14
14
|
|
|
15
15
|
internal typealias CraftNativeRenderItem = (node: JSONObject, identity: String, previous: View?) -> View
|
|
16
16
|
|
|
17
|
-
/** A stable-key RecyclerView host for stx
|
|
17
|
+
/** A stable-key RecyclerView host for stx native FlatList nodes. */
|
|
18
18
|
internal class CraftNativeFlatList(context: Context) : RecyclerView(context) {
|
|
19
19
|
private data class Item(
|
|
20
20
|
val identity: String,
|
|
@@ -37,12 +37,16 @@ internal class CraftNativeFlatList(context: Context) : RecyclerView(context) {
|
|
|
37
37
|
override fun getItemId(position: Int): Long = stableId(getItem(position).identity)
|
|
38
38
|
|
|
39
39
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = Holder(FrameLayout(parent.context).apply {
|
|
40
|
-
layoutParams = LayoutParams(
|
|
40
|
+
layoutParams = LayoutParams(
|
|
41
|
+
if (horizontal) LayoutParams.WRAP_CONTENT else LayoutParams.MATCH_PARENT,
|
|
42
|
+
LayoutParams.WRAP_CONTENT,
|
|
43
|
+
)
|
|
41
44
|
})
|
|
42
45
|
|
|
43
46
|
override fun onBindViewHolder(holder: Holder, position: Int) {
|
|
44
47
|
val item = getItem(position)
|
|
45
48
|
if (holder.identity != null && holder.identity != item.identity) release(holder)
|
|
49
|
+
holder.host.layoutParams = holderLayoutParams()
|
|
46
50
|
val view = renderItem?.invoke(item.node, item.identity, holder.hostedView) ?: View(holder.host.context)
|
|
47
51
|
val renderedLayout = view.layoutParams
|
|
48
52
|
val hostedLayout = FrameLayout.LayoutParams(
|
|
@@ -83,19 +87,134 @@ internal class CraftNativeFlatList(context: Context) : RecyclerView(context) {
|
|
|
83
87
|
private var endReached: (() -> Unit)? = null
|
|
84
88
|
private var threshold = 0.1
|
|
85
89
|
private var endReachedSignature: String? = null
|
|
90
|
+
private var dataContentSignature = ""
|
|
86
91
|
private var horizontal = false
|
|
87
92
|
private var columns = 1
|
|
93
|
+
private var keyboardShouldPersistTaps = "never"
|
|
88
94
|
private var submissionGeneration = 0
|
|
89
95
|
private var isSubmittingList = false
|
|
96
|
+
private var pendingThemeRefresh = false
|
|
97
|
+
var onScrollEvent: (() -> Unit)? = null
|
|
98
|
+
var onScrollBeginDrag: (() -> Unit)? = null
|
|
99
|
+
var onScrollEndDrag: (() -> Unit)? = null
|
|
100
|
+
var onMomentumScrollBegin: (() -> Unit)? = null
|
|
101
|
+
var onMomentumScrollEnd: (() -> Unit)? = null
|
|
102
|
+
var onLayoutChanged: (() -> Unit)? = null
|
|
103
|
+
|
|
104
|
+
private fun holderLayoutParams() = LayoutParams(
|
|
105
|
+
if (horizontal) LayoutParams.WRAP_CONTENT else LayoutParams.MATCH_PARENT,
|
|
106
|
+
LayoutParams.WRAP_CONTENT,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
private fun updateVisibleHolderLayoutParams() {
|
|
110
|
+
for (index in 0 until childCount) {
|
|
111
|
+
(getChildViewHolder(getChildAt(index)) as? Holder)?.host?.layoutParams = holderLayoutParams()
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
116
|
+
super.onLayout(changed, left, top, right, bottom)
|
|
117
|
+
onLayoutChanged?.invoke()
|
|
118
|
+
}
|
|
90
119
|
|
|
91
120
|
init {
|
|
92
121
|
adapter = listAdapter
|
|
93
122
|
itemAnimator = null
|
|
94
123
|
addOnScrollListener(object : OnScrollListener() {
|
|
95
|
-
|
|
124
|
+
private var dragging = false
|
|
125
|
+
private var momentum = false
|
|
126
|
+
|
|
127
|
+
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
|
128
|
+
onScrollEvent?.invoke()
|
|
129
|
+
onLayoutChanged?.invoke()
|
|
130
|
+
evaluateEndReached()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
|
134
|
+
when (newState) {
|
|
135
|
+
RecyclerView.SCROLL_STATE_DRAGGING -> if (!dragging) {
|
|
136
|
+
dragging = true
|
|
137
|
+
onScrollBeginDrag?.invoke()
|
|
138
|
+
}
|
|
139
|
+
RecyclerView.SCROLL_STATE_SETTLING -> {
|
|
140
|
+
if (dragging) {
|
|
141
|
+
dragging = false
|
|
142
|
+
onScrollEndDrag?.invoke()
|
|
143
|
+
}
|
|
144
|
+
if (!momentum) {
|
|
145
|
+
momentum = true
|
|
146
|
+
onMomentumScrollBegin?.invoke()
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
RecyclerView.SCROLL_STATE_IDLE -> {
|
|
150
|
+
if (dragging) {
|
|
151
|
+
dragging = false
|
|
152
|
+
onScrollEndDrag?.invoke()
|
|
153
|
+
}
|
|
154
|
+
if (momentum) {
|
|
155
|
+
momentum = false
|
|
156
|
+
onMomentumScrollEnd?.invoke()
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
96
161
|
})
|
|
97
162
|
}
|
|
98
163
|
|
|
164
|
+
fun setKeyboardDismissMode(mode: String, dismissKeyboard: () -> Unit) {
|
|
165
|
+
if (mode !in setOf("on-drag", "interactive") && keyboardShouldPersistTaps == "always") {
|
|
166
|
+
setOnTouchListener(null)
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
var dismissed = false
|
|
170
|
+
setOnTouchListener { _, event ->
|
|
171
|
+
when (event.actionMasked) {
|
|
172
|
+
android.view.MotionEvent.ACTION_DOWN -> dismissed = false
|
|
173
|
+
android.view.MotionEvent.ACTION_MOVE -> if (!dismissed) {
|
|
174
|
+
dismissed = true
|
|
175
|
+
dismissKeyboard()
|
|
176
|
+
}
|
|
177
|
+
android.view.MotionEvent.ACTION_UP -> if (shouldDismissKeyboard(event)) dismissKeyboard()
|
|
178
|
+
}
|
|
179
|
+
false
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
fun setKeyboardShouldPersistTaps(value: String?) {
|
|
184
|
+
keyboardShouldPersistTaps = value?.takeIf { it in setOf("always", "handled", "never") } ?: "never"
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private fun tapTarget(view: View, x: Float, y: Float): View? {
|
|
188
|
+
if (x < 0 || y < 0 || x >= view.width || y >= view.height || view.visibility != View.VISIBLE) return null
|
|
189
|
+
if (view is ViewGroup) {
|
|
190
|
+
for (index in view.childCount - 1 downTo 0) {
|
|
191
|
+
val child = view.getChildAt(index)
|
|
192
|
+
tapTarget(child, x - child.left, y - child.top)?.let { return it }
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return view
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private fun shouldDismissKeyboard(event: android.view.MotionEvent): Boolean {
|
|
199
|
+
if (keyboardShouldPersistTaps == "always") return false
|
|
200
|
+
if (keyboardShouldPersistTaps == "never") return true
|
|
201
|
+
return tapTarget(this, event.x, event.y)?.isClickable != true
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
fun setContentContainerStyle(style: JSONObject?, density: Float) {
|
|
205
|
+
val padding = (style?.optDouble("padding", 0.0) ?: 0.0) * density
|
|
206
|
+
val horizontal = (style?.optDouble("paddingHorizontal", padding / density) ?: (padding / density)) * density
|
|
207
|
+
val vertical = (style?.optDouble("paddingVertical", padding / density) ?: (padding / density)) * density
|
|
208
|
+
setPadding(
|
|
209
|
+
((style?.optDouble("paddingLeft", horizontal / density) ?: (horizontal / density)) * density).toInt(),
|
|
210
|
+
((style?.optDouble("paddingTop", vertical / density) ?: (vertical / density)) * density).toInt(),
|
|
211
|
+
((style?.optDouble("paddingRight", horizontal / density) ?: (horizontal / density)) * density).toInt(),
|
|
212
|
+
((style?.optDouble("paddingBottom", vertical / density) ?: (vertical / density)) * density).toInt(),
|
|
213
|
+
)
|
|
214
|
+
clipToPadding = true
|
|
215
|
+
requestLayout()
|
|
216
|
+
}
|
|
217
|
+
|
|
99
218
|
fun apply(
|
|
100
219
|
nodes: List<JSONObject>,
|
|
101
220
|
horizontal: Boolean,
|
|
@@ -116,7 +235,10 @@ internal class CraftNativeFlatList(context: Context) : RecyclerView(context) {
|
|
|
116
235
|
val retainedState = if (layoutChanged) null else layoutManager?.onSaveInstanceState()
|
|
117
236
|
this.horizontal = horizontal
|
|
118
237
|
this.columns = nextColumns
|
|
119
|
-
if (layoutChanged || layoutManager == null)
|
|
238
|
+
if (layoutChanged || layoutManager == null) {
|
|
239
|
+
configureLayoutManager()
|
|
240
|
+
updateVisibleHolderLayoutParams()
|
|
241
|
+
}
|
|
120
242
|
|
|
121
243
|
val seen = mutableMapOf<String, Int>()
|
|
122
244
|
val next = nodes.mapIndexed { index, node ->
|
|
@@ -132,7 +254,10 @@ internal class CraftNativeFlatList(context: Context) : RecyclerView(context) {
|
|
|
132
254
|
val nextIds = next.mapTo(mutableSetOf()) { it.identity }
|
|
133
255
|
(oldIds - nextIds).forEach(recycleItem)
|
|
134
256
|
val dataItems = next.filterNot { it.chrome }
|
|
135
|
-
val contentSignature =
|
|
257
|
+
val contentSignature = dataItems.joinToString("") {
|
|
258
|
+
"${it.identity.length}:${it.identity}${it.signature.length}:${it.signature}"
|
|
259
|
+
}
|
|
260
|
+
dataContentSignature = contentSignature
|
|
136
261
|
if (contentSignature != endReachedSignature) endReachedSignature = null
|
|
137
262
|
val generation = ++submissionGeneration
|
|
138
263
|
isSubmittingList = true
|
|
@@ -140,6 +265,10 @@ internal class CraftNativeFlatList(context: Context) : RecyclerView(context) {
|
|
|
140
265
|
if (generation != submissionGeneration) return@submitList
|
|
141
266
|
retainedState?.let { layoutManager?.onRestoreInstanceState(it) }
|
|
142
267
|
isSubmittingList = false
|
|
268
|
+
if (pendingThemeRefresh) {
|
|
269
|
+
pendingThemeRefresh = false
|
|
270
|
+
refreshThemeDefaults()
|
|
271
|
+
}
|
|
143
272
|
evaluateEndReached()
|
|
144
273
|
}
|
|
145
274
|
}
|
|
@@ -148,12 +277,28 @@ internal class CraftNativeFlatList(context: Context) : RecyclerView(context) {
|
|
|
148
277
|
listAdapter.currentList.forEach { recycleItem?.invoke(it.identity) }
|
|
149
278
|
val generation = ++submissionGeneration
|
|
150
279
|
isSubmittingList = true
|
|
280
|
+
pendingThemeRefresh = false
|
|
151
281
|
listAdapter.submitList(emptyList()) {
|
|
152
282
|
if (generation == submissionGeneration) isSubmittingList = false
|
|
153
283
|
}
|
|
154
284
|
renderItem = null
|
|
155
285
|
recycleItem = null
|
|
156
286
|
endReached = null
|
|
287
|
+
dataContentSignature = ""
|
|
288
|
+
onScrollEvent = null
|
|
289
|
+
onScrollBeginDrag = null
|
|
290
|
+
onScrollEndDrag = null
|
|
291
|
+
onMomentumScrollBegin = null
|
|
292
|
+
onMomentumScrollEnd = null
|
|
293
|
+
onLayoutChanged = null
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
fun refreshThemeDefaults() {
|
|
297
|
+
if (isSubmittingList) {
|
|
298
|
+
pendingThemeRefresh = true
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
if (listAdapter.itemCount > 0) listAdapter.notifyItemRangeChanged(0, listAdapter.itemCount)
|
|
157
302
|
}
|
|
158
303
|
|
|
159
304
|
private fun configureLayoutManager() {
|
|
@@ -179,8 +324,7 @@ internal class CraftNativeFlatList(context: Context) : RecyclerView(context) {
|
|
|
179
324
|
val remaining = dataPositions.size - lastDataOrdinal - 1
|
|
180
325
|
val thresholdItems = maxOf(1, ceil(dataPositions.size * threshold).toInt())
|
|
181
326
|
if (remaining > thresholdItems) return
|
|
182
|
-
val
|
|
183
|
-
val signature = "${dataPositions.size}:$lastIdentity"
|
|
327
|
+
val signature = dataContentSignature
|
|
184
328
|
if (endReachedSignature == signature) return
|
|
185
329
|
endReachedSignature = signature
|
|
186
330
|
endReached?.invoke()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
package {{PACKAGE_NAME}}
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|
5
|
+
|
|
6
|
+
/** Swipe-to-refresh host for a native FlatList. */
|
|
7
|
+
internal class CraftNativeRefreshableFlatList(context: Context) : SwipeRefreshLayout(context) {
|
|
8
|
+
val list = CraftNativeFlatList(context)
|
|
9
|
+
private var refreshHandler: (() -> Unit)? = null
|
|
10
|
+
|
|
11
|
+
init {
|
|
12
|
+
addView(list, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
|
|
13
|
+
setOnRefreshListener { refreshHandler?.invoke() }
|
|
14
|
+
isEnabled = false
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
fun configureRefresh(handler: (() -> Unit)?, refreshing: Boolean) {
|
|
18
|
+
refreshHandler = handler
|
|
19
|
+
isEnabled = handler != null
|
|
20
|
+
isRefreshing = handler != null && refreshing
|
|
21
|
+
if (handler == null) isRefreshing = false
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
override fun onDetachedFromWindow() {
|
|
25
|
+
refreshHandler = null
|
|
26
|
+
isRefreshing = false
|
|
27
|
+
super.onDetachedFromWindow()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -17,6 +17,7 @@ import android.webkit.WebSettings
|
|
|
17
17
|
import android.webkit.WebView
|
|
18
18
|
import android.webkit.WebViewClient
|
|
19
19
|
import androidx.appcompat.app.AppCompatActivity
|
|
20
|
+
import androidx.appcompat.app.AppCompatDelegate
|
|
20
21
|
import androidx.core.view.WindowCompat
|
|
21
22
|
import androidx.webkit.WebViewAssetLoader
|
|
22
23
|
import org.json.JSONObject
|
|
@@ -75,6 +76,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
75
76
|
|
|
76
77
|
@SuppressLint("SetJavaScriptEnabled")
|
|
77
78
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
79
|
+
applyConfiguredAppearance()
|
|
78
80
|
super.onCreate(savedInstanceState)
|
|
79
81
|
|
|
80
82
|
// Enable edge-to-edge
|
|
@@ -111,6 +113,21 @@ class MainActivity : AppCompatActivity() {
|
|
|
111
113
|
setupNetworkMonitoring()
|
|
112
114
|
}
|
|
113
115
|
|
|
116
|
+
private fun applyConfiguredAppearance() {
|
|
117
|
+
val configured = configJson()
|
|
118
|
+
val mode = when (configured?.optString("appearance")) {
|
|
119
|
+
"light" -> AppCompatDelegate.MODE_NIGHT_NO
|
|
120
|
+
"dark" -> AppCompatDelegate.MODE_NIGHT_YES
|
|
121
|
+
"system" -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
|
122
|
+
else -> if (configured?.optBoolean("darkMode", true) == false) {
|
|
123
|
+
AppCompatDelegate.MODE_NIGHT_NO
|
|
124
|
+
} else {
|
|
125
|
+
AppCompatDelegate.MODE_NIGHT_YES
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
AppCompatDelegate.setDefaultNightMode(mode)
|
|
129
|
+
}
|
|
130
|
+
|
|
114
131
|
@SuppressLint("SetJavaScriptEnabled")
|
|
115
132
|
private fun setupWebView() {
|
|
116
133
|
webView.settings.apply {
|