@onekeyfe/react-native-native-list 3.0.157-alpha.254 → 3.0.157-alpha.258
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 +165 -26
- package/android/src/main/res/values/native_scroll_coordinator_ids.xml +5 -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 +241 -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 +123 -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/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
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
package com.margelo.nitro.nativelist
|
|
2
|
+
|
|
3
|
+
/** A per-view hysteresis observer. Null means no bridge event is needed. */
|
|
4
|
+
internal class NativeListScrollPositionTracker {
|
|
5
|
+
private var start: Double? = null
|
|
6
|
+
private var end: Double? = null
|
|
7
|
+
private var previous: Boolean? = null
|
|
8
|
+
|
|
9
|
+
fun configure(start: Double?, end: Double?) {
|
|
10
|
+
this.start = null
|
|
11
|
+
this.end = null
|
|
12
|
+
previous = null
|
|
13
|
+
if (start == null || end == null || !start.isFinite() || !end.isFinite() || start < 0 || end <= start) return
|
|
14
|
+
this.start = start
|
|
15
|
+
this.end = end
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
fun resetDelivery() { previous = null }
|
|
19
|
+
|
|
20
|
+
fun update(offset: Double): Boolean? {
|
|
21
|
+
val start = start ?: return null
|
|
22
|
+
val end = end ?: return null
|
|
23
|
+
if (!offset.isFinite()) return null
|
|
24
|
+
val next = if (previous == true) offset > start else offset >= end
|
|
25
|
+
if (previous == next) return null
|
|
26
|
+
previous = next
|
|
27
|
+
return next
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package com.margelo.nitro.nativelist
|
|
2
2
|
|
|
3
|
+
import android.content.res.ColorStateList
|
|
3
4
|
import android.graphics.Paint
|
|
4
5
|
import android.graphics.drawable.GradientDrawable
|
|
5
6
|
import android.text.Spannable
|
|
@@ -12,6 +13,7 @@ import android.view.Gravity
|
|
|
12
13
|
import android.view.View
|
|
13
14
|
import android.view.ViewGroup
|
|
14
15
|
import android.widget.LinearLayout
|
|
16
|
+
import android.widget.ProgressBar
|
|
15
17
|
import android.widget.TextView
|
|
16
18
|
import androidx.core.widget.TextViewCompat
|
|
17
19
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
@@ -24,6 +26,10 @@ internal class NativeListSectionHeaderRowView(context: ThemedReactContext) :
|
|
|
24
26
|
private val mainColumn = LinearLayout(context)
|
|
25
27
|
private val titleLine = PackedTitleLineLayout(context)
|
|
26
28
|
private val title = DottedUnderlineTextView(context)
|
|
29
|
+
private val titleSpinner = ProgressBar(context, null, android.R.attr.progressBarStyleSmall).apply {
|
|
30
|
+
isIndeterminate = true
|
|
31
|
+
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
|
32
|
+
}
|
|
27
33
|
private val subtitle = NativeListTextView(context)
|
|
28
34
|
private val trailingColumn = LinearLayout(context)
|
|
29
35
|
private val trailingViews = listOf(NativeListTextView(context))
|
|
@@ -64,7 +70,8 @@ internal class NativeListSectionHeaderRowView(context: ThemedReactContext) :
|
|
|
64
70
|
layout == "table" -> 28
|
|
65
71
|
item.json.optString("variant") == "summary" -> 80
|
|
66
72
|
item.json.optString("variant") == "gallery" -> 32
|
|
67
|
-
item.json.optString("variant") == "history" ->
|
|
73
|
+
item.json.optString("variant") == "history" ->
|
|
74
|
+
if (item.json.optBoolean("titleLoading")) 20 else 16
|
|
68
75
|
item.json.optString("value").isNotEmpty() && item.json.optJSONObject("checkbox") != null -> 40
|
|
69
76
|
else -> 36
|
|
70
77
|
}
|
|
@@ -284,6 +291,7 @@ internal class NativeListSectionHeaderRowView(context: ThemedReactContext) :
|
|
|
284
291
|
}
|
|
285
292
|
}
|
|
286
293
|
applyTextStyle(item)
|
|
294
|
+
titleSpinner.indeterminateTintList = ColorStateList.valueOf(title.currentTextColor)
|
|
287
295
|
style
|
|
288
296
|
.optJSONObject("container")
|
|
289
297
|
?.optString("contentVerticalAlignment")
|
|
@@ -403,6 +411,7 @@ internal class NativeListSectionHeaderRowView(context: ThemedReactContext) :
|
|
|
403
411
|
override fun recycleContent() {
|
|
404
412
|
checkboxControl.reset()
|
|
405
413
|
currentItem = null
|
|
414
|
+
titleSpinner.visibility = GONE
|
|
406
415
|
}
|
|
407
416
|
|
|
408
417
|
override fun disposeContent() {
|
|
@@ -437,6 +446,12 @@ internal class NativeListSectionHeaderRowView(context: ThemedReactContext) :
|
|
|
437
446
|
// Linear/sectioned snapshots reserve the ListItem mx=8 at RecyclerView
|
|
438
447
|
// level, so header-local insets below are source px minus that outer inset.
|
|
439
448
|
val headerHorizontalInset = 12
|
|
449
|
+
if (item.json.optBoolean("titleLoading")) {
|
|
450
|
+
titleSpinner.visibility = VISIBLE
|
|
451
|
+
addView(titleSpinner, LayoutParams(dp(20), dp(20)).apply { marginEnd = dp(8) })
|
|
452
|
+
} else {
|
|
453
|
+
titleSpinner.visibility = GONE
|
|
454
|
+
}
|
|
440
455
|
addView(mainColumn, weighted())
|
|
441
456
|
showText(title, item.json.optString("title"), 1)
|
|
442
457
|
if (hasDottedTitle) {
|
|
@@ -31,6 +31,7 @@ import android.widget.SeekBar
|
|
|
31
31
|
import android.widget.TextView
|
|
32
32
|
import androidx.core.view.ViewCompat
|
|
33
33
|
import androidx.core.view.WindowInsetsCompat
|
|
34
|
+
import androidx.recyclerview.widget.ConcatAdapter
|
|
34
35
|
import androidx.recyclerview.widget.GridLayoutManager
|
|
35
36
|
import androidx.recyclerview.widget.ItemTouchHelper
|
|
36
37
|
import androidx.recyclerview.widget.LinearLayoutManager
|
|
@@ -111,6 +112,37 @@ class NativeListView(
|
|
|
111
112
|
data class End(val animated: Boolean) : ScrollRequest()
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
private val coordinatorOffsetListener = Runnable { emitScrollPositionThresholdIfNeeded() }
|
|
116
|
+
private var logicalScrollDistancePx = 0
|
|
117
|
+
private val scrollPositionTracker = NativeListScrollPositionTracker()
|
|
118
|
+
var onScrollPositionThresholdChange: ((Boolean) -> Unit)? = null
|
|
119
|
+
set(value) {
|
|
120
|
+
field = value
|
|
121
|
+
scrollPositionTracker.resetDelivery()
|
|
122
|
+
emitScrollPositionThresholdIfNeeded()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
fun setScrollPositionThresholdsJson(json: String) {
|
|
126
|
+
val config = runCatching { JSONObject(json) }.getOrNull()
|
|
127
|
+
scrollPositionTracker.configure(config?.optDouble("start"), config?.optDouble("end"))
|
|
128
|
+
emitScrollPositionThresholdIfNeeded()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private fun emitScrollPositionThresholdIfNeeded() {
|
|
132
|
+
if (disposed) return
|
|
133
|
+
val callback = onScrollPositionThresholdChange ?: return
|
|
134
|
+
if (layoutManager.findFirstVisibleItemPosition() == 0) {
|
|
135
|
+
layoutManager.findViewByPosition(0)?.let { first ->
|
|
136
|
+
logicalScrollDistancePx = (recyclerView.paddingTop - layoutManager.getDecoratedTop(first)).coerceAtLeast(0)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
val contributions = recyclerView.getTag(R.id.onekey_native_scroll_coordinator_consumed_offsets) as? Map<*, *>
|
|
140
|
+
val parentDistance = contributions?.values?.sumOf { (it as? Int)?.coerceAtLeast(0)?.toLong() ?: 0L } ?: 0L
|
|
141
|
+
val distance = logicalScrollDistancePx.toLong() + parentDistance
|
|
142
|
+
val next = scrollPositionTracker.update(distance.toDouble() / density) ?: return
|
|
143
|
+
callback(next)
|
|
144
|
+
}
|
|
145
|
+
|
|
114
146
|
var onRowAction: ((String) -> Unit)? = null
|
|
115
147
|
var onActionAnchorInvalidated: ((String) -> Unit)? = null
|
|
116
148
|
var onSelectionDelta: ((String) -> Unit)? = null
|
|
@@ -125,9 +157,53 @@ class NativeListView(
|
|
|
125
157
|
private var configuredBottomPaddingPx = 0
|
|
126
158
|
private val refreshLayout = SwipeRefreshLayout(context)
|
|
127
159
|
private val refreshIndicatorTravelPx = refreshLayout.progressViewEndOffset
|
|
160
|
+
private val defaultRefreshTriggerDistancePx = (64 * resources.displayMetrics.density).roundToInt()
|
|
128
161
|
private var refreshIndicatorOffsetPx = 0
|
|
129
162
|
private val contentContainer = FrameLayout(context)
|
|
130
163
|
private val adapter = NativeListAdapter(reactContext)
|
|
164
|
+
private val containerSlots = mutableListOf<View>()
|
|
165
|
+
private val slotHeights = IntArray(3)
|
|
166
|
+
private val headerSlotAdapter = NativeListContainerSlotAdapter()
|
|
167
|
+
private val emptySlotAdapter = NativeListContainerSlotAdapter()
|
|
168
|
+
private val footerSlotAdapter = NativeListContainerSlotAdapter()
|
|
169
|
+
private val scrollContentAdapter = ConcatAdapter(headerSlotAdapter, adapter, emptySlotAdapter, footerSlotAdapter)
|
|
170
|
+
|
|
171
|
+
fun mountContainerSlot(view: View, index: Int) {
|
|
172
|
+
containerSlots.add(index.coerceIn(0, containerSlots.size), view)
|
|
173
|
+
updateContainerSlots()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fun containerSlotCount(): Int = containerSlots.size
|
|
177
|
+
fun containerSlotAt(index: Int): View = containerSlots[index]
|
|
178
|
+
fun unmountContainerSlot(index: Int) {
|
|
179
|
+
val view = containerSlots.removeAt(index)
|
|
180
|
+
(view.parent as? ViewGroup)?.removeView(view)
|
|
181
|
+
updateContainerSlots()
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
fun setContainerSlotHeightsJson(json: String) {
|
|
185
|
+
val values = runCatching { JSONArray(json) }.getOrNull() ?: return
|
|
186
|
+
if (values.length() != 3) return
|
|
187
|
+
for (index in 0..2) {
|
|
188
|
+
val value = values.optDouble(index)
|
|
189
|
+
if (!value.isFinite() || value < 0) return
|
|
190
|
+
}
|
|
191
|
+
for (index in 0..2) slotHeights[index] = (values.getDouble(index) * density).roundToInt()
|
|
192
|
+
updateContainerSlots()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private fun updateContainerSlots() {
|
|
196
|
+
headerSlotAdapter.update(containerSlots.getOrNull(0), slotHeights[0], true)
|
|
197
|
+
emptySlotAdapter.update(containerSlots.getOrNull(1), slotHeights[1], config?.items?.isEmpty() != false)
|
|
198
|
+
footerSlotAdapter.update(containerSlots.getOrNull(2), slotHeights[2], true)
|
|
199
|
+
layoutManager.spanSizeLookup.invalidateSpanIndexCache()
|
|
200
|
+
recyclerView.requestLayout()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private fun scrollPositionForRow(index: Int): Int = index + headerSlotAdapter.itemCount
|
|
204
|
+
private fun rowPositionForScroll(index: Int): Int =
|
|
205
|
+
if (index == RecyclerView.NO_POSITION) RecyclerView.NO_POSITION else index - headerSlotAdapter.itemCount
|
|
206
|
+
|
|
131
207
|
private val layoutManager = NativeListGridLayoutManager(context, 1)
|
|
132
208
|
// OneKey patch: vertical lists retain vertical drags and leave horizontal drags to a parent pager.
|
|
133
209
|
private val pagerGestureTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
|
|
@@ -333,7 +409,7 @@ class NativeListView(
|
|
|
333
409
|
|
|
334
410
|
init {
|
|
335
411
|
orientation = VERTICAL
|
|
336
|
-
recyclerView.adapter =
|
|
412
|
+
recyclerView.adapter = scrollContentAdapter
|
|
337
413
|
recyclerView.layoutManager = layoutManager
|
|
338
414
|
recyclerView.itemAnimator = null
|
|
339
415
|
recyclerView.addOnItemTouchListener(keyboardTapTouchListener)
|
|
@@ -345,7 +421,9 @@ class NativeListView(
|
|
|
345
421
|
layoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
|
346
422
|
override fun getSpanSize(position: Int): Int {
|
|
347
423
|
if (config?.layout != "grid") return 1
|
|
348
|
-
val
|
|
424
|
+
val rowPosition = rowPositionForScroll(position)
|
|
425
|
+
if (rowPosition !in 0 until adapter.itemCount) return layoutManager.spanCount
|
|
426
|
+
val type = adapter.itemAt(rowPosition)?.type
|
|
349
427
|
return if (type == "sectionHeader" || type == "system" || type == "action") {
|
|
350
428
|
layoutManager.spanCount
|
|
351
429
|
} else {
|
|
@@ -443,6 +521,8 @@ class NativeListView(
|
|
|
443
521
|
}
|
|
444
522
|
|
|
445
523
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
|
524
|
+
logicalScrollDistancePx = (logicalScrollDistancePx + dy).coerceAtLeast(0)
|
|
525
|
+
emitScrollPositionThresholdIfNeeded()
|
|
446
526
|
if (dx != 0 || dy != 0) invalidateActionAnchor("scroll")
|
|
447
527
|
syncSectionIndexToVisibleRows()
|
|
448
528
|
scheduleVisibleEvent()
|
|
@@ -469,7 +549,7 @@ class NativeListView(
|
|
|
469
549
|
|
|
470
550
|
private fun keyboardTapIsHandled(recyclerView: RecyclerView, event: MotionEvent): Boolean {
|
|
471
551
|
val child = recyclerView.findChildViewUnder(event.x, event.y) ?: return false
|
|
472
|
-
val position = recyclerView.getChildAdapterPosition(child)
|
|
552
|
+
val position = rowPositionForScroll(recyclerView.getChildAdapterPosition(child))
|
|
473
553
|
val item = adapter.itemAt(position) ?: return false
|
|
474
554
|
val clickable = clickableDescendantAt(child, event.rawX.toInt(), event.rawY.toInt())
|
|
475
555
|
val accessoryHandlesTap = clickable != null && clickable !== child
|
|
@@ -512,6 +592,8 @@ class NativeListView(
|
|
|
512
592
|
}
|
|
513
593
|
|
|
514
594
|
private fun updateRefreshIndicatorOffset(offset: Int) {
|
|
595
|
+
// Let an ancestor render above its shared header without guessing caller offsets.
|
|
596
|
+
refreshLayout.setTag(R.id.onekey_native_scroll_coordinator_refresh_inset, offset)
|
|
515
597
|
if (refreshIndicatorOffsetPx == offset) return
|
|
516
598
|
refreshIndicatorOffsetPx = offset
|
|
517
599
|
val refreshing = refreshLayout.isRefreshing
|
|
@@ -522,6 +604,8 @@ class NativeListView(
|
|
|
522
604
|
|
|
523
605
|
override fun onAttachedToWindow() {
|
|
524
606
|
super.onAttachedToWindow()
|
|
607
|
+
recyclerView.setTag(R.id.onekey_native_scroll_coordinator_offset_listener, coordinatorOffsetListener)
|
|
608
|
+
emitScrollPositionThresholdIfNeeded()
|
|
525
609
|
startStickyHeaderTracking()
|
|
526
610
|
updateSectionIndexAttachment()
|
|
527
611
|
val first = config?.items?.firstOrNull()
|
|
@@ -531,6 +615,9 @@ class NativeListView(
|
|
|
531
615
|
}
|
|
532
616
|
|
|
533
617
|
override fun onDetachedFromWindow() {
|
|
618
|
+
if (recyclerView.getTag(R.id.onekey_native_scroll_coordinator_offset_listener) === coordinatorOffsetListener) {
|
|
619
|
+
recyclerView.setTag(R.id.onekey_native_scroll_coordinator_offset_listener, null)
|
|
620
|
+
}
|
|
534
621
|
stopStickyHeaderTracking()
|
|
535
622
|
stopSectionIndexAttachmentTracking()
|
|
536
623
|
scheduleSectionIndexAttachmentToList()
|
|
@@ -562,6 +649,7 @@ class NativeListView(
|
|
|
562
649
|
}
|
|
563
650
|
}.toSet()
|
|
564
651
|
config = next
|
|
652
|
+
updateContainerSlots()
|
|
565
653
|
usesSelectorSourceScale = next.items.any { it.usesSelectorSourceScale }
|
|
566
654
|
adapter.usesSelectorSourceScale = usesSelectorSourceScale
|
|
567
655
|
adapter.theme = next.theme
|
|
@@ -592,13 +680,14 @@ class NativeListView(
|
|
|
592
680
|
previous?.items?.firstOrNull()?.type == "market" &&
|
|
593
681
|
next.items.firstOrNull()?.type == "market" &&
|
|
594
682
|
previous.items.firstOrNull()?.key != next.items.firstOrNull()?.key &&
|
|
595
|
-
layoutManager.findFirstVisibleItemPosition() == 0
|
|
683
|
+
rowPositionForScroll(layoutManager.findFirstVisibleItemPosition()) == 0
|
|
596
684
|
) {
|
|
597
|
-
layoutManager.findViewByPosition(0)?.let {
|
|
685
|
+
layoutManager.findViewByPosition(scrollPositionForRow(0))?.let {
|
|
598
686
|
layoutManager.getDecoratedTop(it) - recyclerView.paddingTop
|
|
599
687
|
}
|
|
600
688
|
} else null
|
|
601
689
|
config = next
|
|
690
|
+
updateContainerSlots()
|
|
602
691
|
usesSelectorSourceScale = next.items.any { it.usesSelectorSourceScale }
|
|
603
692
|
adapter.usesSelectorSourceScale = usesSelectorSourceScale
|
|
604
693
|
if (previous?.generation != next.generation) endReachedGeneration = null
|
|
@@ -612,7 +701,7 @@ class NativeListView(
|
|
|
612
701
|
updateLayout(next)
|
|
613
702
|
adapter.submitList(next.items) {
|
|
614
703
|
if (marketStartOffset != null) {
|
|
615
|
-
layoutManager.scrollToPositionWithOffset(0, marketStartOffset)
|
|
704
|
+
layoutManager.scrollToPositionWithOffset(scrollPositionForRow(0), marketStartOffset)
|
|
616
705
|
}
|
|
617
706
|
relayoutContents(preserveMarketPaginationAnchor)
|
|
618
707
|
performPendingScrollIfNeeded()
|
|
@@ -621,16 +710,17 @@ class NativeListView(
|
|
|
621
710
|
if (next.items.isNotEmpty()) checkEndReached()
|
|
622
711
|
}
|
|
623
712
|
refreshLayout.isEnabled = next.pullToRefresh
|
|
713
|
+
refreshLayout.setDistanceToTriggerSync(next.refreshTriggerDistance?.let { (it * density).roundToInt() } ?: defaultRefreshTriggerDistancePx)
|
|
624
714
|
refreshLayout.isRefreshing = next.refreshing
|
|
625
715
|
bindFooter(next)
|
|
626
716
|
updateReordering(next)
|
|
627
717
|
}
|
|
628
718
|
|
|
629
719
|
private fun captureVisibleMarketAnchor(): VisibleMarketAnchor? {
|
|
630
|
-
val position = layoutManager.findFirstVisibleItemPosition()
|
|
720
|
+
val position = rowPositionForScroll(layoutManager.findFirstVisibleItemPosition()).coerceAtLeast(0)
|
|
631
721
|
if (position == RecyclerView.NO_POSITION) return null
|
|
632
722
|
val item = adapter.itemAt(position) ?: return null
|
|
633
|
-
val view = layoutManager.findViewByPosition(position) ?: return null
|
|
723
|
+
val view = layoutManager.findViewByPosition(scrollPositionForRow(position)) ?: return null
|
|
634
724
|
return VisibleMarketAnchor(
|
|
635
725
|
key = item.key,
|
|
636
726
|
offset = layoutManager.getDecoratedTop(view) - recyclerView.paddingTop,
|
|
@@ -686,6 +776,7 @@ class NativeListView(
|
|
|
686
776
|
previous.rowPressToggles != next.rowPressToggles ||
|
|
687
777
|
previous.reorderable != next.reorderable ||
|
|
688
778
|
previous.pullToRefresh != next.pullToRefresh ||
|
|
779
|
+
previous.refreshTriggerDistance != next.refreshTriggerDistance ||
|
|
689
780
|
previous.refreshing != next.refreshing ||
|
|
690
781
|
previous.loadMore != next.loadMore ||
|
|
691
782
|
previous.endReachedThreshold != next.endReachedThreshold ||
|
|
@@ -837,6 +928,7 @@ class NativeListView(
|
|
|
837
928
|
if (!marketQuoteOnly) invalidateActionAnchor("snapshot")
|
|
838
929
|
val next = current.copy(items = nextItems, selectedKeys = selected)
|
|
839
930
|
config = next
|
|
931
|
+
updateContainerSlots()
|
|
840
932
|
usesSelectorSourceScale = next.items.any { it.usesSelectorSourceScale }
|
|
841
933
|
adapter.usesSelectorSourceScale = usesSelectorSourceScale
|
|
842
934
|
adapter.selectedKeys = selected
|
|
@@ -940,7 +1032,32 @@ class NativeListView(
|
|
|
940
1032
|
true
|
|
941
1033
|
}
|
|
942
1034
|
is ScrollRequest.End -> {
|
|
943
|
-
|
|
1035
|
+
val finalSlotHeight = when {
|
|
1036
|
+
footerSlotAdapter.itemCount > 0 -> slotHeights[2]
|
|
1037
|
+
emptySlotAdapter.itemCount > 0 -> slotHeights[1]
|
|
1038
|
+
adapter.itemCount == 0 && headerSlotAdapter.itemCount > 0 -> slotHeights[0]
|
|
1039
|
+
else -> null
|
|
1040
|
+
}
|
|
1041
|
+
if (finalSlotHeight != null) {
|
|
1042
|
+
val target = scrollContentAdapter.itemCount - 1
|
|
1043
|
+
if (request.animated) {
|
|
1044
|
+
val manager = layoutManager
|
|
1045
|
+
val scroller = object : LinearSmoothScroller(context) {
|
|
1046
|
+
override fun calculateDyToMakeVisible(view: View, snapPreference: Int): Int =
|
|
1047
|
+
nativeListEndAlignmentOffset(
|
|
1048
|
+
manager.height - manager.paddingBottom,
|
|
1049
|
+
manager.getDecoratedBottom(view),
|
|
1050
|
+
)
|
|
1051
|
+
}
|
|
1052
|
+
scroller.targetPosition = target
|
|
1053
|
+
layoutManager.startSmoothScroll(scroller)
|
|
1054
|
+
} else {
|
|
1055
|
+
layoutManager.scrollToPositionWithOffset(
|
|
1056
|
+
target,
|
|
1057
|
+
nativeListEndAlignmentOffset(viewportLength(layoutManager), finalSlotHeight),
|
|
1058
|
+
)
|
|
1059
|
+
}
|
|
1060
|
+
} else if (current.items.isNotEmpty()) {
|
|
944
1061
|
performIndexScroll(
|
|
945
1062
|
current.items.lastIndex,
|
|
946
1063
|
request.animated,
|
|
@@ -962,7 +1079,7 @@ class NativeListView(
|
|
|
962
1079
|
viewOffset: Double,
|
|
963
1080
|
) {
|
|
964
1081
|
val manager = recyclerView.layoutManager as? LinearLayoutManager ?: return
|
|
965
|
-
val visibleView = manager.findViewByPosition(index)
|
|
1082
|
+
val visibleView = manager.findViewByPosition(scrollPositionForRow(index))
|
|
966
1083
|
val viewPosition = if (alignment == "nearest") {
|
|
967
1084
|
val viewportStart = if (manager.orientation == RecyclerView.VERTICAL) {
|
|
968
1085
|
manager.paddingTop
|
|
@@ -983,7 +1100,7 @@ class NativeListView(
|
|
|
983
1100
|
else -> return
|
|
984
1101
|
}
|
|
985
1102
|
} else {
|
|
986
|
-
val firstVisible = manager.findFirstVisibleItemPosition()
|
|
1103
|
+
val firstVisible = rowPositionForScroll(manager.findFirstVisibleItemPosition())
|
|
987
1104
|
if (firstVisible != RecyclerView.NO_POSITION && index < firstVisible) 0.0 else 1.0
|
|
988
1105
|
}
|
|
989
1106
|
} else {
|
|
@@ -1010,13 +1127,13 @@ class NativeListView(
|
|
|
1010
1127
|
return smoothAlignmentDelta(manager, view, viewPosition, offsetPx)
|
|
1011
1128
|
}
|
|
1012
1129
|
}
|
|
1013
|
-
smoothScroller.targetPosition = index
|
|
1130
|
+
smoothScroller.targetPosition = scrollPositionForRow(index)
|
|
1014
1131
|
manager.startSmoothScroll(smoothScroller)
|
|
1015
1132
|
return
|
|
1016
1133
|
}
|
|
1017
1134
|
|
|
1018
1135
|
val provisionalOffset = (viewPosition * viewportLength(manager)).roundToInt() + offsetPx
|
|
1019
|
-
manager.scrollToPositionWithOffset(index, provisionalOffset)
|
|
1136
|
+
manager.scrollToPositionWithOffset(scrollPositionForRow(index), provisionalOffset)
|
|
1020
1137
|
relayoutRecyclerView()
|
|
1021
1138
|
alignAfterLayout(index, viewPosition, offsetPx)
|
|
1022
1139
|
}
|
|
@@ -1024,6 +1141,9 @@ class NativeListView(
|
|
|
1024
1141
|
private fun performOffsetScroll(offset: Double, animated: Boolean) {
|
|
1025
1142
|
val manager = recyclerView.layoutManager as? LinearLayoutManager ?: return
|
|
1026
1143
|
val target = (offset * density).roundToInt().coerceAtLeast(0)
|
|
1144
|
+
if (!animated) {
|
|
1145
|
+
logicalScrollDistancePx = target
|
|
1146
|
+
}
|
|
1027
1147
|
if (!animated) {
|
|
1028
1148
|
recyclerView.stopScroll()
|
|
1029
1149
|
manager.scrollToPositionWithOffset(0, -target)
|
|
@@ -1055,7 +1175,7 @@ class NativeListView(
|
|
|
1055
1175
|
) {
|
|
1056
1176
|
recyclerView.post {
|
|
1057
1177
|
val manager = recyclerView.layoutManager as? LinearLayoutManager ?: return@post
|
|
1058
|
-
val view = manager.findViewByPosition(index)
|
|
1178
|
+
val view = manager.findViewByPosition(scrollPositionForRow(index))
|
|
1059
1179
|
if (view == null) {
|
|
1060
1180
|
if (attemptsRemaining > 0) {
|
|
1061
1181
|
alignAfterLayout(index, viewPosition, viewOffset, attemptsRemaining - 1)
|
|
@@ -1166,6 +1286,7 @@ class NativeListView(
|
|
|
1166
1286
|
fun dispose() {
|
|
1167
1287
|
if (disposed) return
|
|
1168
1288
|
disposed = true
|
|
1289
|
+
onScrollPositionThresholdChange = null
|
|
1169
1290
|
if (!isAttachedToWindow) disposeResources()
|
|
1170
1291
|
}
|
|
1171
1292
|
|
|
@@ -1234,11 +1355,11 @@ class NativeListView(
|
|
|
1234
1355
|
stickyHeaderHost.visibility = GONE
|
|
1235
1356
|
return
|
|
1236
1357
|
}
|
|
1237
|
-
val first = layoutManager.findFirstVisibleItemPosition()
|
|
1358
|
+
val first = rowPositionForScroll(layoutManager.findFirstVisibleItemPosition())
|
|
1238
1359
|
var position = first
|
|
1239
1360
|
while (position >= 0 && adapter.itemAt(position)?.let(::isStickySectionHeader) != true) position--
|
|
1240
1361
|
val item = adapter.itemAt(position)
|
|
1241
|
-
if (item == null || first == RecyclerView.NO_POSITION || (layoutManager.findViewByPosition(position)?.top ?: -1) > 0) {
|
|
1362
|
+
if (item == null || first == RecyclerView.NO_POSITION || (layoutManager.findViewByPosition(scrollPositionForRow(position))?.top ?: -1) > 0) {
|
|
1242
1363
|
stickyHeaderHost.visibility = GONE
|
|
1243
1364
|
return
|
|
1244
1365
|
}
|
|
@@ -1267,7 +1388,7 @@ class NativeListView(
|
|
|
1267
1388
|
var nextTop = Int.MAX_VALUE
|
|
1268
1389
|
for (index in 0 until recyclerView.childCount) {
|
|
1269
1390
|
val child = recyclerView.getChildAt(index)
|
|
1270
|
-
val childPosition = recyclerView.getChildAdapterPosition(child)
|
|
1391
|
+
val childPosition = rowPositionForScroll(recyclerView.getChildAdapterPosition(child))
|
|
1271
1392
|
if (childPosition > position && adapter.itemAt(childPosition)?.let(::isStickySectionHeader) == true) nextTop = minOf(nextTop, child.top)
|
|
1272
1393
|
}
|
|
1273
1394
|
stickyHeaderHost.translationY = minOf(0, nextTop - height).toFloat()
|
|
@@ -1501,7 +1622,7 @@ class NativeListView(
|
|
|
1501
1622
|
|
|
1502
1623
|
private fun syncSectionIndexToVisibleRows() {
|
|
1503
1624
|
if (sectionIndexScrubbing || sectionIndexProgrammaticScroll || sectionIndexEntries.isEmpty()) return
|
|
1504
|
-
val firstVisible = layoutManager.findFirstVisibleItemPosition()
|
|
1625
|
+
val firstVisible = rowPositionForScroll(layoutManager.findFirstVisibleItemPosition())
|
|
1505
1626
|
if (firstVisible == RecyclerView.NO_POSITION) return
|
|
1506
1627
|
sectionIndexView.setActiveIndex(
|
|
1507
1628
|
sectionIndexEntries.indexOfLast { it.position <= firstVisible }.takeIf { it >= 0 },
|
|
@@ -1866,6 +1987,7 @@ class NativeListView(
|
|
|
1866
1987
|
}
|
|
1867
1988
|
|
|
1868
1989
|
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
|
|
1990
|
+
if (viewHolder !is NativeListViewHolder) return makeMovementFlags(0, 0)
|
|
1869
1991
|
val item = adapter.itemAt(viewHolder.bindingAdapterPosition)
|
|
1870
1992
|
if (item == null || !item.isReorderable) return makeMovementFlags(0, 0)
|
|
1871
1993
|
val dragFlags = if (next.orientation == "horizontal") ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT else ItemTouchHelper.UP or ItemTouchHelper.DOWN
|
|
@@ -1877,6 +1999,7 @@ class NativeListView(
|
|
|
1877
1999
|
source: RecyclerView.ViewHolder,
|
|
1878
2000
|
target: RecyclerView.ViewHolder,
|
|
1879
2001
|
): Boolean {
|
|
2002
|
+
if (source !is NativeListViewHolder || target !is NativeListViewHolder) return false
|
|
1880
2003
|
val from = source.bindingAdapterPosition
|
|
1881
2004
|
val to = target.bindingAdapterPosition
|
|
1882
2005
|
val base = pendingReorder ?: adapter.currentList
|
|
@@ -2088,7 +2211,7 @@ class NativeListView(
|
|
|
2088
2211
|
?.takeIf { it != RecyclerView.NO_POSITION }
|
|
2089
2212
|
?: destinationPosition
|
|
2090
2213
|
val destinationRow = recyclerView
|
|
2091
|
-
.findViewHolderForAdapterPosition(resolvedPosition)
|
|
2214
|
+
.findViewHolderForAdapterPosition(scrollPositionForRow(resolvedPosition))
|
|
2092
2215
|
?.let { it as? NativeListViewHolder }
|
|
2093
2216
|
?.rowView
|
|
2094
2217
|
val groupRow = destinationRow?.takeIf {
|
|
@@ -2251,8 +2374,20 @@ class NativeListView(
|
|
|
2251
2374
|
Choreographer.getInstance().postFrameCallback {
|
|
2252
2375
|
visibleEventScheduled = false
|
|
2253
2376
|
val manager = recyclerView.layoutManager as? LinearLayoutManager ?: return@postFrameCallback
|
|
2254
|
-
val
|
|
2255
|
-
|
|
2377
|
+
val visibleRows = (0 until recyclerView.childCount).mapNotNull { index ->
|
|
2378
|
+
val child = recyclerView.getChildAt(index)
|
|
2379
|
+
val holder = recyclerView.getChildViewHolder(child) as? NativeListViewHolder ?: return@mapNotNull null
|
|
2380
|
+
val visible = if (manager.orientation == RecyclerView.VERTICAL) {
|
|
2381
|
+
manager.getDecoratedBottom(child) > recyclerView.paddingTop &&
|
|
2382
|
+
manager.getDecoratedTop(child) < recyclerView.height - recyclerView.paddingBottom
|
|
2383
|
+
} else {
|
|
2384
|
+
manager.getDecoratedRight(child) > recyclerView.paddingLeft &&
|
|
2385
|
+
manager.getDecoratedLeft(child) < recyclerView.width - recyclerView.paddingRight
|
|
2386
|
+
}
|
|
2387
|
+
holder.bindingAdapterPosition.takeIf { visible && it != RecyclerView.NO_POSITION }
|
|
2388
|
+
}
|
|
2389
|
+
val first = visibleRows.minOrNull() ?: RecyclerView.NO_POSITION
|
|
2390
|
+
val last = visibleRows.maxOrNull() ?: RecyclerView.NO_POSITION
|
|
2256
2391
|
val firstKey = adapter.itemAt(first)?.key
|
|
2257
2392
|
val lastKey = adapter.itemAt(last)?.key
|
|
2258
2393
|
val signature = "$first:$last:${firstKey.orEmpty()}:${lastKey.orEmpty()}"
|
|
@@ -2271,7 +2406,7 @@ class NativeListView(
|
|
|
2271
2406
|
val current = config ?: return
|
|
2272
2407
|
if (!current.loadMore || current.generation == endReachedGeneration || adapter.itemCount == 0) return
|
|
2273
2408
|
val manager = recyclerView.layoutManager as? LinearLayoutManager ?: return
|
|
2274
|
-
val lastVisible = manager.findLastVisibleItemPosition()
|
|
2409
|
+
val lastVisible = rowPositionForScroll(manager.findLastVisibleItemPosition())
|
|
2275
2410
|
val thresholdItems = max(1, ceil(adapter.itemCount * current.endReachedThreshold).toInt())
|
|
2276
2411
|
if (lastVisible >= adapter.itemCount - thresholdItems) {
|
|
2277
2412
|
endReachedGeneration = current.generation
|
|
@@ -2311,7 +2446,7 @@ class NativeListView(
|
|
|
2311
2446
|
adapter.currentList.indexOfFirst { it.key == currentAnchor.key }
|
|
2312
2447
|
} ?: RecyclerView.NO_POSITION
|
|
2313
2448
|
if (anchor != null && anchorIndex >= 0) {
|
|
2314
|
-
layoutManager.scrollToPositionWithOffset(anchorIndex, anchor.offset)
|
|
2449
|
+
layoutManager.scrollToPositionWithOffset(scrollPositionForRow(anchorIndex), anchor.offset)
|
|
2315
2450
|
}
|
|
2316
2451
|
forceLayout()
|
|
2317
2452
|
measure(
|
|
@@ -2473,7 +2608,11 @@ private class ReorderPlaceholderDecoration(
|
|
|
2473
2608
|
val isWalletSidebar =
|
|
2474
2609
|
item.type == "identity" && item.json.optString("presentation") == "walletSidebar"
|
|
2475
2610
|
if (!isWalletSidebar && item.type != "walletGroup") return
|
|
2476
|
-
val view = parent.
|
|
2611
|
+
val view = (0 until parent.childCount).asSequence().map(parent::getChildAt)
|
|
2612
|
+
.firstOrNull { child ->
|
|
2613
|
+
val holder = parent.getChildViewHolder(child)
|
|
2614
|
+
holder is NativeListViewHolder && holder.bindingAdapterPosition == position
|
|
2615
|
+
} ?: return
|
|
2477
2616
|
bounds.set(
|
|
2478
2617
|
(view.left + insetPx).toFloat(),
|
|
2479
2618
|
view.top.toFloat(),
|
|
@@ -2744,7 +2883,7 @@ private class ItemSpacingDecoration(
|
|
|
2744
2883
|
parent: RecyclerView,
|
|
2745
2884
|
state: RecyclerView.State,
|
|
2746
2885
|
) {
|
|
2747
|
-
if (spacing <= 0) return
|
|
2886
|
+
if (spacing <= 0 || parent.getChildViewHolder(view) !is NativeListViewHolder) return
|
|
2748
2887
|
val horizontal = (parent.layoutManager as? LinearLayoutManager)?.orientation == RecyclerView.HORIZONTAL
|
|
2749
2888
|
val item = view.tag as? NativeListItem
|
|
2750
2889
|
val sourceWallet = item?.type == "identity" && item.json.optString("presentation") == "walletSidebar" && item.hasExplicitHeight
|
|
@@ -2766,7 +2905,7 @@ private class SelectorBackgroundDecoration(private val adapter: NativeListAdapte
|
|
|
2766
2905
|
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
|
2767
2906
|
for (index in 0 until parent.childCount) {
|
|
2768
2907
|
val child = parent.getChildAt(index)
|
|
2769
|
-
val item = adapter.itemAt(parent.
|
|
2908
|
+
val item = adapter.itemAt(parent.getChildViewHolder(child).takeIf { it is NativeListViewHolder }?.bindingAdapterPosition ?: RecyclerView.NO_POSITION) ?: continue
|
|
2770
2909
|
if (!item.json.optBoolean("backgroundFullWidth", false)) continue
|
|
2771
2910
|
val color = resolveNativeListSelectorBackgroundColor(
|
|
2772
2911
|
rowType = item.type,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
package com.margelo.nitro.nativelist
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertEquals
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
|
|
6
|
+
class NativeListEndAlignmentTest {
|
|
7
|
+
@Test fun tallFooterEndsAtViewportBottom() {
|
|
8
|
+
val viewportHeight = 600
|
|
9
|
+
val footerHeight = 1000
|
|
10
|
+
val start = nativeListEndAlignmentOffset(viewportHeight, footerHeight)
|
|
11
|
+
assertEquals(-400, start)
|
|
12
|
+
assertEquals(viewportHeight, start + footerHeight)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@Test fun tallEmptySlotUsesTheSameEndAlignmentWithPadding() {
|
|
16
|
+
val viewportTop = 80
|
|
17
|
+
val viewportBottom = 680
|
|
18
|
+
val emptyHeight = 900
|
|
19
|
+
val start = viewportTop + nativeListEndAlignmentOffset(viewportBottom - viewportTop, emptyHeight)
|
|
20
|
+
assertEquals(viewportBottom, start + emptyHeight)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@Test fun animatedCorrectionMovesDecoratedBottomToViewportBottom() {
|
|
24
|
+
val decoratedBottom = 1240
|
|
25
|
+
val viewportBottom = 680
|
|
26
|
+
val correction = nativeListEndAlignmentOffset(viewportBottom, decoratedBottom)
|
|
27
|
+
assertEquals(-560, correction)
|
|
28
|
+
assertEquals(viewportBottom, decoratedBottom + correction)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@Test fun shortFooterRetainsBottomAlignment() {
|
|
32
|
+
assertEquals(480, nativeListEndAlignmentOffset(600, 120))
|
|
33
|
+
}
|
|
34
|
+
}
|
package/android/src/test/java/com/margelo/nitro/nativelist/NativeListScrollPositionTrackerTest.kt
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
package com.margelo.nitro.nativelist
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertEquals
|
|
4
|
+
import org.junit.Assert.assertNull
|
|
5
|
+
import org.junit.Test
|
|
6
|
+
|
|
7
|
+
class NativeListScrollPositionTrackerTest {
|
|
8
|
+
@Test fun hysteresisAndDisabledLifecycle() {
|
|
9
|
+
val tracker = NativeListScrollPositionTracker()
|
|
10
|
+
assertNull(tracker.update(200.0))
|
|
11
|
+
tracker.configure(48.0, 160.0)
|
|
12
|
+
assertEquals(false, tracker.update(0.0))
|
|
13
|
+
assertNull(tracker.update(159.0))
|
|
14
|
+
assertEquals(true, tracker.update(160.0))
|
|
15
|
+
assertNull(tracker.update(100.0))
|
|
16
|
+
assertEquals(false, tracker.update(48.0))
|
|
17
|
+
assertNull(tracker.update(49.0))
|
|
18
|
+
tracker.configure(null, null)
|
|
19
|
+
assertNull(tracker.update(200.0))
|
|
20
|
+
tracker.configure(48.0, 160.0)
|
|
21
|
+
assertEquals(true, tracker.update(200.0))
|
|
22
|
+
tracker.resetDelivery()
|
|
23
|
+
assertEquals(true, tracker.update(200.0))
|
|
24
|
+
assertNull(tracker.update(Double.NaN))
|
|
25
|
+
tracker.configure(48.0, 48.0)
|
|
26
|
+
assertNull(tracker.update(48.0))
|
|
27
|
+
}
|
|
28
|
+
}
|
package/docs/DESIGN.md
CHANGED
|
@@ -43,8 +43,10 @@ applying them.
|
|
|
43
43
|
Section headers and scrolling footer/status rows use the flattened row stream.
|
|
44
44
|
`fixedFooter` is a separate descriptor outside that stream, and `emptyState` is
|
|
45
45
|
used when content is empty; both reuse row binders and currently accept only
|
|
46
|
-
`action` or `system` through the public API.
|
|
47
|
-
|
|
46
|
+
`action` or `system` through the public API. Native vertical lists additionally
|
|
47
|
+
expose `listHeader`, `listEmpty`, and `listFooter` React container slots. These
|
|
48
|
+
three fixed roots remain outside row identity and scroll with the native list;
|
|
49
|
+
see SPEC.md for mounting, index translation, measurement and platform limits. `groupId` and `groupPosition` preserve continuous card geometry
|
|
48
50
|
without nesting lists inside cells. These placement/grouping rules belong to the
|
|
49
51
|
container even when the displayed content uses an existing row template.
|
|
50
52
|
|