@onekeyfe/react-native-native-list 3.0.157-alpha.254 → 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 +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
|
|
@@ -522,6 +602,8 @@ class NativeListView(
|
|
|
522
602
|
|
|
523
603
|
override fun onAttachedToWindow() {
|
|
524
604
|
super.onAttachedToWindow()
|
|
605
|
+
recyclerView.setTag(R.id.onekey_native_scroll_coordinator_offset_listener, coordinatorOffsetListener)
|
|
606
|
+
emitScrollPositionThresholdIfNeeded()
|
|
525
607
|
startStickyHeaderTracking()
|
|
526
608
|
updateSectionIndexAttachment()
|
|
527
609
|
val first = config?.items?.firstOrNull()
|
|
@@ -531,6 +613,9 @@ class NativeListView(
|
|
|
531
613
|
}
|
|
532
614
|
|
|
533
615
|
override fun onDetachedFromWindow() {
|
|
616
|
+
if (recyclerView.getTag(R.id.onekey_native_scroll_coordinator_offset_listener) === coordinatorOffsetListener) {
|
|
617
|
+
recyclerView.setTag(R.id.onekey_native_scroll_coordinator_offset_listener, null)
|
|
618
|
+
}
|
|
534
619
|
stopStickyHeaderTracking()
|
|
535
620
|
stopSectionIndexAttachmentTracking()
|
|
536
621
|
scheduleSectionIndexAttachmentToList()
|
|
@@ -562,6 +647,7 @@ class NativeListView(
|
|
|
562
647
|
}
|
|
563
648
|
}.toSet()
|
|
564
649
|
config = next
|
|
650
|
+
updateContainerSlots()
|
|
565
651
|
usesSelectorSourceScale = next.items.any { it.usesSelectorSourceScale }
|
|
566
652
|
adapter.usesSelectorSourceScale = usesSelectorSourceScale
|
|
567
653
|
adapter.theme = next.theme
|
|
@@ -592,13 +678,14 @@ class NativeListView(
|
|
|
592
678
|
previous?.items?.firstOrNull()?.type == "market" &&
|
|
593
679
|
next.items.firstOrNull()?.type == "market" &&
|
|
594
680
|
previous.items.firstOrNull()?.key != next.items.firstOrNull()?.key &&
|
|
595
|
-
layoutManager.findFirstVisibleItemPosition() == 0
|
|
681
|
+
rowPositionForScroll(layoutManager.findFirstVisibleItemPosition()) == 0
|
|
596
682
|
) {
|
|
597
|
-
layoutManager.findViewByPosition(0)?.let {
|
|
683
|
+
layoutManager.findViewByPosition(scrollPositionForRow(0))?.let {
|
|
598
684
|
layoutManager.getDecoratedTop(it) - recyclerView.paddingTop
|
|
599
685
|
}
|
|
600
686
|
} else null
|
|
601
687
|
config = next
|
|
688
|
+
updateContainerSlots()
|
|
602
689
|
usesSelectorSourceScale = next.items.any { it.usesSelectorSourceScale }
|
|
603
690
|
adapter.usesSelectorSourceScale = usesSelectorSourceScale
|
|
604
691
|
if (previous?.generation != next.generation) endReachedGeneration = null
|
|
@@ -612,7 +699,7 @@ class NativeListView(
|
|
|
612
699
|
updateLayout(next)
|
|
613
700
|
adapter.submitList(next.items) {
|
|
614
701
|
if (marketStartOffset != null) {
|
|
615
|
-
layoutManager.scrollToPositionWithOffset(0, marketStartOffset)
|
|
702
|
+
layoutManager.scrollToPositionWithOffset(scrollPositionForRow(0), marketStartOffset)
|
|
616
703
|
}
|
|
617
704
|
relayoutContents(preserveMarketPaginationAnchor)
|
|
618
705
|
performPendingScrollIfNeeded()
|
|
@@ -621,16 +708,17 @@ class NativeListView(
|
|
|
621
708
|
if (next.items.isNotEmpty()) checkEndReached()
|
|
622
709
|
}
|
|
623
710
|
refreshLayout.isEnabled = next.pullToRefresh
|
|
711
|
+
refreshLayout.setDistanceToTriggerSync(next.refreshTriggerDistance?.let { (it * density).roundToInt() } ?: defaultRefreshTriggerDistancePx)
|
|
624
712
|
refreshLayout.isRefreshing = next.refreshing
|
|
625
713
|
bindFooter(next)
|
|
626
714
|
updateReordering(next)
|
|
627
715
|
}
|
|
628
716
|
|
|
629
717
|
private fun captureVisibleMarketAnchor(): VisibleMarketAnchor? {
|
|
630
|
-
val position = layoutManager.findFirstVisibleItemPosition()
|
|
718
|
+
val position = rowPositionForScroll(layoutManager.findFirstVisibleItemPosition()).coerceAtLeast(0)
|
|
631
719
|
if (position == RecyclerView.NO_POSITION) return null
|
|
632
720
|
val item = adapter.itemAt(position) ?: return null
|
|
633
|
-
val view = layoutManager.findViewByPosition(position) ?: return null
|
|
721
|
+
val view = layoutManager.findViewByPosition(scrollPositionForRow(position)) ?: return null
|
|
634
722
|
return VisibleMarketAnchor(
|
|
635
723
|
key = item.key,
|
|
636
724
|
offset = layoutManager.getDecoratedTop(view) - recyclerView.paddingTop,
|
|
@@ -686,6 +774,7 @@ class NativeListView(
|
|
|
686
774
|
previous.rowPressToggles != next.rowPressToggles ||
|
|
687
775
|
previous.reorderable != next.reorderable ||
|
|
688
776
|
previous.pullToRefresh != next.pullToRefresh ||
|
|
777
|
+
previous.refreshTriggerDistance != next.refreshTriggerDistance ||
|
|
689
778
|
previous.refreshing != next.refreshing ||
|
|
690
779
|
previous.loadMore != next.loadMore ||
|
|
691
780
|
previous.endReachedThreshold != next.endReachedThreshold ||
|
|
@@ -837,6 +926,7 @@ class NativeListView(
|
|
|
837
926
|
if (!marketQuoteOnly) invalidateActionAnchor("snapshot")
|
|
838
927
|
val next = current.copy(items = nextItems, selectedKeys = selected)
|
|
839
928
|
config = next
|
|
929
|
+
updateContainerSlots()
|
|
840
930
|
usesSelectorSourceScale = next.items.any { it.usesSelectorSourceScale }
|
|
841
931
|
adapter.usesSelectorSourceScale = usesSelectorSourceScale
|
|
842
932
|
adapter.selectedKeys = selected
|
|
@@ -940,7 +1030,32 @@ class NativeListView(
|
|
|
940
1030
|
true
|
|
941
1031
|
}
|
|
942
1032
|
is ScrollRequest.End -> {
|
|
943
|
-
|
|
1033
|
+
val finalSlotHeight = when {
|
|
1034
|
+
footerSlotAdapter.itemCount > 0 -> slotHeights[2]
|
|
1035
|
+
emptySlotAdapter.itemCount > 0 -> slotHeights[1]
|
|
1036
|
+
adapter.itemCount == 0 && headerSlotAdapter.itemCount > 0 -> slotHeights[0]
|
|
1037
|
+
else -> null
|
|
1038
|
+
}
|
|
1039
|
+
if (finalSlotHeight != null) {
|
|
1040
|
+
val target = scrollContentAdapter.itemCount - 1
|
|
1041
|
+
if (request.animated) {
|
|
1042
|
+
val manager = layoutManager
|
|
1043
|
+
val scroller = object : LinearSmoothScroller(context) {
|
|
1044
|
+
override fun calculateDyToMakeVisible(view: View, snapPreference: Int): Int =
|
|
1045
|
+
nativeListEndAlignmentOffset(
|
|
1046
|
+
manager.height - manager.paddingBottom,
|
|
1047
|
+
manager.getDecoratedBottom(view),
|
|
1048
|
+
)
|
|
1049
|
+
}
|
|
1050
|
+
scroller.targetPosition = target
|
|
1051
|
+
layoutManager.startSmoothScroll(scroller)
|
|
1052
|
+
} else {
|
|
1053
|
+
layoutManager.scrollToPositionWithOffset(
|
|
1054
|
+
target,
|
|
1055
|
+
nativeListEndAlignmentOffset(viewportLength(layoutManager), finalSlotHeight),
|
|
1056
|
+
)
|
|
1057
|
+
}
|
|
1058
|
+
} else if (current.items.isNotEmpty()) {
|
|
944
1059
|
performIndexScroll(
|
|
945
1060
|
current.items.lastIndex,
|
|
946
1061
|
request.animated,
|
|
@@ -962,7 +1077,7 @@ class NativeListView(
|
|
|
962
1077
|
viewOffset: Double,
|
|
963
1078
|
) {
|
|
964
1079
|
val manager = recyclerView.layoutManager as? LinearLayoutManager ?: return
|
|
965
|
-
val visibleView = manager.findViewByPosition(index)
|
|
1080
|
+
val visibleView = manager.findViewByPosition(scrollPositionForRow(index))
|
|
966
1081
|
val viewPosition = if (alignment == "nearest") {
|
|
967
1082
|
val viewportStart = if (manager.orientation == RecyclerView.VERTICAL) {
|
|
968
1083
|
manager.paddingTop
|
|
@@ -983,7 +1098,7 @@ class NativeListView(
|
|
|
983
1098
|
else -> return
|
|
984
1099
|
}
|
|
985
1100
|
} else {
|
|
986
|
-
val firstVisible = manager.findFirstVisibleItemPosition()
|
|
1101
|
+
val firstVisible = rowPositionForScroll(manager.findFirstVisibleItemPosition())
|
|
987
1102
|
if (firstVisible != RecyclerView.NO_POSITION && index < firstVisible) 0.0 else 1.0
|
|
988
1103
|
}
|
|
989
1104
|
} else {
|
|
@@ -1010,13 +1125,13 @@ class NativeListView(
|
|
|
1010
1125
|
return smoothAlignmentDelta(manager, view, viewPosition, offsetPx)
|
|
1011
1126
|
}
|
|
1012
1127
|
}
|
|
1013
|
-
smoothScroller.targetPosition = index
|
|
1128
|
+
smoothScroller.targetPosition = scrollPositionForRow(index)
|
|
1014
1129
|
manager.startSmoothScroll(smoothScroller)
|
|
1015
1130
|
return
|
|
1016
1131
|
}
|
|
1017
1132
|
|
|
1018
1133
|
val provisionalOffset = (viewPosition * viewportLength(manager)).roundToInt() + offsetPx
|
|
1019
|
-
manager.scrollToPositionWithOffset(index, provisionalOffset)
|
|
1134
|
+
manager.scrollToPositionWithOffset(scrollPositionForRow(index), provisionalOffset)
|
|
1020
1135
|
relayoutRecyclerView()
|
|
1021
1136
|
alignAfterLayout(index, viewPosition, offsetPx)
|
|
1022
1137
|
}
|
|
@@ -1024,6 +1139,9 @@ class NativeListView(
|
|
|
1024
1139
|
private fun performOffsetScroll(offset: Double, animated: Boolean) {
|
|
1025
1140
|
val manager = recyclerView.layoutManager as? LinearLayoutManager ?: return
|
|
1026
1141
|
val target = (offset * density).roundToInt().coerceAtLeast(0)
|
|
1142
|
+
if (!animated) {
|
|
1143
|
+
logicalScrollDistancePx = target
|
|
1144
|
+
}
|
|
1027
1145
|
if (!animated) {
|
|
1028
1146
|
recyclerView.stopScroll()
|
|
1029
1147
|
manager.scrollToPositionWithOffset(0, -target)
|
|
@@ -1055,7 +1173,7 @@ class NativeListView(
|
|
|
1055
1173
|
) {
|
|
1056
1174
|
recyclerView.post {
|
|
1057
1175
|
val manager = recyclerView.layoutManager as? LinearLayoutManager ?: return@post
|
|
1058
|
-
val view = manager.findViewByPosition(index)
|
|
1176
|
+
val view = manager.findViewByPosition(scrollPositionForRow(index))
|
|
1059
1177
|
if (view == null) {
|
|
1060
1178
|
if (attemptsRemaining > 0) {
|
|
1061
1179
|
alignAfterLayout(index, viewPosition, viewOffset, attemptsRemaining - 1)
|
|
@@ -1166,6 +1284,7 @@ class NativeListView(
|
|
|
1166
1284
|
fun dispose() {
|
|
1167
1285
|
if (disposed) return
|
|
1168
1286
|
disposed = true
|
|
1287
|
+
onScrollPositionThresholdChange = null
|
|
1169
1288
|
if (!isAttachedToWindow) disposeResources()
|
|
1170
1289
|
}
|
|
1171
1290
|
|
|
@@ -1234,11 +1353,11 @@ class NativeListView(
|
|
|
1234
1353
|
stickyHeaderHost.visibility = GONE
|
|
1235
1354
|
return
|
|
1236
1355
|
}
|
|
1237
|
-
val first = layoutManager.findFirstVisibleItemPosition()
|
|
1356
|
+
val first = rowPositionForScroll(layoutManager.findFirstVisibleItemPosition())
|
|
1238
1357
|
var position = first
|
|
1239
1358
|
while (position >= 0 && adapter.itemAt(position)?.let(::isStickySectionHeader) != true) position--
|
|
1240
1359
|
val item = adapter.itemAt(position)
|
|
1241
|
-
if (item == null || first == RecyclerView.NO_POSITION || (layoutManager.findViewByPosition(position)?.top ?: -1) > 0) {
|
|
1360
|
+
if (item == null || first == RecyclerView.NO_POSITION || (layoutManager.findViewByPosition(scrollPositionForRow(position))?.top ?: -1) > 0) {
|
|
1242
1361
|
stickyHeaderHost.visibility = GONE
|
|
1243
1362
|
return
|
|
1244
1363
|
}
|
|
@@ -1267,7 +1386,7 @@ class NativeListView(
|
|
|
1267
1386
|
var nextTop = Int.MAX_VALUE
|
|
1268
1387
|
for (index in 0 until recyclerView.childCount) {
|
|
1269
1388
|
val child = recyclerView.getChildAt(index)
|
|
1270
|
-
val childPosition = recyclerView.getChildAdapterPosition(child)
|
|
1389
|
+
val childPosition = rowPositionForScroll(recyclerView.getChildAdapterPosition(child))
|
|
1271
1390
|
if (childPosition > position && adapter.itemAt(childPosition)?.let(::isStickySectionHeader) == true) nextTop = minOf(nextTop, child.top)
|
|
1272
1391
|
}
|
|
1273
1392
|
stickyHeaderHost.translationY = minOf(0, nextTop - height).toFloat()
|
|
@@ -1501,7 +1620,7 @@ class NativeListView(
|
|
|
1501
1620
|
|
|
1502
1621
|
private fun syncSectionIndexToVisibleRows() {
|
|
1503
1622
|
if (sectionIndexScrubbing || sectionIndexProgrammaticScroll || sectionIndexEntries.isEmpty()) return
|
|
1504
|
-
val firstVisible = layoutManager.findFirstVisibleItemPosition()
|
|
1623
|
+
val firstVisible = rowPositionForScroll(layoutManager.findFirstVisibleItemPosition())
|
|
1505
1624
|
if (firstVisible == RecyclerView.NO_POSITION) return
|
|
1506
1625
|
sectionIndexView.setActiveIndex(
|
|
1507
1626
|
sectionIndexEntries.indexOfLast { it.position <= firstVisible }.takeIf { it >= 0 },
|
|
@@ -1866,6 +1985,7 @@ class NativeListView(
|
|
|
1866
1985
|
}
|
|
1867
1986
|
|
|
1868
1987
|
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
|
|
1988
|
+
if (viewHolder !is NativeListViewHolder) return makeMovementFlags(0, 0)
|
|
1869
1989
|
val item = adapter.itemAt(viewHolder.bindingAdapterPosition)
|
|
1870
1990
|
if (item == null || !item.isReorderable) return makeMovementFlags(0, 0)
|
|
1871
1991
|
val dragFlags = if (next.orientation == "horizontal") ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT else ItemTouchHelper.UP or ItemTouchHelper.DOWN
|
|
@@ -1877,6 +1997,7 @@ class NativeListView(
|
|
|
1877
1997
|
source: RecyclerView.ViewHolder,
|
|
1878
1998
|
target: RecyclerView.ViewHolder,
|
|
1879
1999
|
): Boolean {
|
|
2000
|
+
if (source !is NativeListViewHolder || target !is NativeListViewHolder) return false
|
|
1880
2001
|
val from = source.bindingAdapterPosition
|
|
1881
2002
|
val to = target.bindingAdapterPosition
|
|
1882
2003
|
val base = pendingReorder ?: adapter.currentList
|
|
@@ -2088,7 +2209,7 @@ class NativeListView(
|
|
|
2088
2209
|
?.takeIf { it != RecyclerView.NO_POSITION }
|
|
2089
2210
|
?: destinationPosition
|
|
2090
2211
|
val destinationRow = recyclerView
|
|
2091
|
-
.findViewHolderForAdapterPosition(resolvedPosition)
|
|
2212
|
+
.findViewHolderForAdapterPosition(scrollPositionForRow(resolvedPosition))
|
|
2092
2213
|
?.let { it as? NativeListViewHolder }
|
|
2093
2214
|
?.rowView
|
|
2094
2215
|
val groupRow = destinationRow?.takeIf {
|
|
@@ -2251,8 +2372,20 @@ class NativeListView(
|
|
|
2251
2372
|
Choreographer.getInstance().postFrameCallback {
|
|
2252
2373
|
visibleEventScheduled = false
|
|
2253
2374
|
val manager = recyclerView.layoutManager as? LinearLayoutManager ?: return@postFrameCallback
|
|
2254
|
-
val
|
|
2255
|
-
|
|
2375
|
+
val visibleRows = (0 until recyclerView.childCount).mapNotNull { index ->
|
|
2376
|
+
val child = recyclerView.getChildAt(index)
|
|
2377
|
+
val holder = recyclerView.getChildViewHolder(child) as? NativeListViewHolder ?: return@mapNotNull null
|
|
2378
|
+
val visible = if (manager.orientation == RecyclerView.VERTICAL) {
|
|
2379
|
+
manager.getDecoratedBottom(child) > recyclerView.paddingTop &&
|
|
2380
|
+
manager.getDecoratedTop(child) < recyclerView.height - recyclerView.paddingBottom
|
|
2381
|
+
} else {
|
|
2382
|
+
manager.getDecoratedRight(child) > recyclerView.paddingLeft &&
|
|
2383
|
+
manager.getDecoratedLeft(child) < recyclerView.width - recyclerView.paddingRight
|
|
2384
|
+
}
|
|
2385
|
+
holder.bindingAdapterPosition.takeIf { visible && it != RecyclerView.NO_POSITION }
|
|
2386
|
+
}
|
|
2387
|
+
val first = visibleRows.minOrNull() ?: RecyclerView.NO_POSITION
|
|
2388
|
+
val last = visibleRows.maxOrNull() ?: RecyclerView.NO_POSITION
|
|
2256
2389
|
val firstKey = adapter.itemAt(first)?.key
|
|
2257
2390
|
val lastKey = adapter.itemAt(last)?.key
|
|
2258
2391
|
val signature = "$first:$last:${firstKey.orEmpty()}:${lastKey.orEmpty()}"
|
|
@@ -2271,7 +2404,7 @@ class NativeListView(
|
|
|
2271
2404
|
val current = config ?: return
|
|
2272
2405
|
if (!current.loadMore || current.generation == endReachedGeneration || adapter.itemCount == 0) return
|
|
2273
2406
|
val manager = recyclerView.layoutManager as? LinearLayoutManager ?: return
|
|
2274
|
-
val lastVisible = manager.findLastVisibleItemPosition()
|
|
2407
|
+
val lastVisible = rowPositionForScroll(manager.findLastVisibleItemPosition())
|
|
2275
2408
|
val thresholdItems = max(1, ceil(adapter.itemCount * current.endReachedThreshold).toInt())
|
|
2276
2409
|
if (lastVisible >= adapter.itemCount - thresholdItems) {
|
|
2277
2410
|
endReachedGeneration = current.generation
|
|
@@ -2311,7 +2444,7 @@ class NativeListView(
|
|
|
2311
2444
|
adapter.currentList.indexOfFirst { it.key == currentAnchor.key }
|
|
2312
2445
|
} ?: RecyclerView.NO_POSITION
|
|
2313
2446
|
if (anchor != null && anchorIndex >= 0) {
|
|
2314
|
-
layoutManager.scrollToPositionWithOffset(anchorIndex, anchor.offset)
|
|
2447
|
+
layoutManager.scrollToPositionWithOffset(scrollPositionForRow(anchorIndex), anchor.offset)
|
|
2315
2448
|
}
|
|
2316
2449
|
forceLayout()
|
|
2317
2450
|
measure(
|
|
@@ -2473,7 +2606,11 @@ private class ReorderPlaceholderDecoration(
|
|
|
2473
2606
|
val isWalletSidebar =
|
|
2474
2607
|
item.type == "identity" && item.json.optString("presentation") == "walletSidebar"
|
|
2475
2608
|
if (!isWalletSidebar && item.type != "walletGroup") return
|
|
2476
|
-
val view = parent.
|
|
2609
|
+
val view = (0 until parent.childCount).asSequence().map(parent::getChildAt)
|
|
2610
|
+
.firstOrNull { child ->
|
|
2611
|
+
val holder = parent.getChildViewHolder(child)
|
|
2612
|
+
holder is NativeListViewHolder && holder.bindingAdapterPosition == position
|
|
2613
|
+
} ?: return
|
|
2477
2614
|
bounds.set(
|
|
2478
2615
|
(view.left + insetPx).toFloat(),
|
|
2479
2616
|
view.top.toFloat(),
|
|
@@ -2744,7 +2881,7 @@ private class ItemSpacingDecoration(
|
|
|
2744
2881
|
parent: RecyclerView,
|
|
2745
2882
|
state: RecyclerView.State,
|
|
2746
2883
|
) {
|
|
2747
|
-
if (spacing <= 0) return
|
|
2884
|
+
if (spacing <= 0 || parent.getChildViewHolder(view) !is NativeListViewHolder) return
|
|
2748
2885
|
val horizontal = (parent.layoutManager as? LinearLayoutManager)?.orientation == RecyclerView.HORIZONTAL
|
|
2749
2886
|
val item = view.tag as? NativeListItem
|
|
2750
2887
|
val sourceWallet = item?.type == "identity" && item.json.optString("presentation") == "walletSidebar" && item.hasExplicitHeight
|
|
@@ -2766,7 +2903,7 @@ private class SelectorBackgroundDecoration(private val adapter: NativeListAdapte
|
|
|
2766
2903
|
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
|
2767
2904
|
for (index in 0 until parent.childCount) {
|
|
2768
2905
|
val child = parent.getChildAt(index)
|
|
2769
|
-
val item = adapter.itemAt(parent.
|
|
2906
|
+
val item = adapter.itemAt(parent.getChildViewHolder(child).takeIf { it is NativeListViewHolder }?.bindingAdapterPosition ?: RecyclerView.NO_POSITION) ?: continue
|
|
2770
2907
|
if (!item.json.optBoolean("backgroundFullWidth", false)) continue
|
|
2771
2908
|
val color = resolveNativeListSelectorBackgroundColor(
|
|
2772
2909
|
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
|
|