@onekeyfe/react-native-native-list 6.7.0-alpha.378 → 6.7.0-alpha.382
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/src/main/java/com/onekey/nativelist/HybridNativeList.kt +14 -1
- package/android/src/main/java/com/onekey/nativelist/NativeListAccessoryStack.kt +240 -11
- package/android/src/main/java/com/onekey/nativelist/NativeListActivityDetailsView.kt +7 -4
- package/android/src/main/java/com/onekey/nativelist/NativeListActivityRenderer.kt +6 -3
- package/android/src/main/java/com/onekey/nativelist/NativeListButtonGroupRenderer.kt +102 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListContainerSlotAdapter.kt +12 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListDataRowRenderer.kt +87 -7
- package/android/src/main/java/com/onekey/nativelist/NativeListIdentityRenderer.kt +177 -1
- package/android/src/main/java/com/onekey/nativelist/NativeListLeadingVisual.kt +43 -14
- package/android/src/main/java/com/onekey/nativelist/NativeListMessageRenderer.kt +25 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +33 -2
- package/android/src/main/java/com/onekey/nativelist/NativeListPrimitives.kt +22 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRendererRegistry.kt +2 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRendererRowView.kt +2 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListResolvedText.kt +3 -2
- package/android/src/main/java/com/onekey/nativelist/NativeListSectionHeaderRenderer.kt +69 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListTextView.kt +55 -1
- package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +1912 -107
- package/android/src/test/java/com/margelo/nitro/nativelist/NativeListPressPolicyTest.kt +1 -0
- package/docs/ROW_TEMPLATES.md +60 -9
- package/docs/SPEC.md +450 -5
- package/docs/STYLE_SPEC.md +6 -0
- package/ios/HybridNativeList.swift +21 -2
- package/ios/NativeListAccessoryStack.swift +227 -39
- package/ios/NativeListActivityDetailsView.swift +18 -8
- package/ios/NativeListActivityRenderer.swift +8 -2
- package/ios/NativeListButtonGroupRenderer.swift +89 -0
- package/ios/NativeListDataRowRenderer.swift +155 -27
- package/ios/NativeListDesignAssets.swift +3 -0
- package/ios/NativeListIdentityRenderer.swift +166 -30
- package/ios/NativeListLeadingVisual.swift +49 -14
- package/ios/NativeListMessageRenderer.swift +34 -1
- package/ios/NativeListModels.swift +42 -2
- package/ios/NativeListRendererRegistry.swift +1 -0
- package/ios/NativeListResolvedText.swift +2 -1
- package/ios/NativeListRowHost.swift +14 -0
- package/ios/NativeListSectionHeaderRenderer.swift +88 -0
- package/ios/RNCNativeListView.swift +1013 -55
- package/ios/Resources/NativeListIcons.xcassets/onekey_crypto_coin_outline.imageset/Contents.json +15 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_crypto_coin_outline.imageset/icon.svg +4 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_gas_solid.imageset/Contents.json +15 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_gas_solid.imageset/icon.svg +3 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_refresh_ccw_outline.imageset/Contents.json +15 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_refresh_ccw_outline.imageset/icon.svg +3 -0
- package/ios/tests/NativeListActivityPresentationTests.swift +56 -0
- package/ios/tests/NativeListLifecycleTests.swift +2720 -3
- package/lib/module/NativeList.js +175 -28
- package/lib/module/NativeList.web.js +129 -30
- package/lib/module/index.js +1 -1
- package/lib/module/models.js +1 -0
- package/lib/module/selection.js +1 -1
- package/lib/module/validation.js +421 -8
- package/lib/module/web/NativeListWebEngine.js +507 -19
- package/lib/module/web/templates/ActivityRowRenderer.js +8 -7
- package/lib/module/web/templates/ButtonGroupRowRenderer.js +47 -0
- package/lib/module/web/templates/DataRowRenderer.js +40 -5
- package/lib/module/web/templates/IdentityRowRenderer.js +90 -4
- package/lib/module/web/templates/MessageRowRenderer.js +18 -2
- package/lib/module/web/templates/RowContainerStyle.js +1 -0
- package/lib/module/web/templates/RowRendererRegistry.js +3 -0
- package/lib/module/web/templates/SectionHeaderRowRenderer.js +36 -2
- package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +26 -0
- package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +4 -0
- package/lib/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +10 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +20 -0
- package/lib/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +14 -0
- package/lib/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +12 -0
- package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +2 -0
- package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +43 -0
- package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +4 -0
- package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +4 -0
- package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +4 -0
- package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +6 -0
- package/lib/nitrogen/generated/shared/json/NativeListConfig.json +2 -0
- package/lib/typescript/src/NativeList.d.ts +1 -1
- package/lib/typescript/src/NativeList.nitro.d.ts +2 -0
- package/lib/typescript/src/NativeList.types.d.ts +18 -0
- package/lib/typescript/src/index.d.ts +2 -2
- package/lib/typescript/src/models.d.ts +105 -9
- package/lib/typescript/src/validation.d.ts +12 -0
- package/lib/typescript/src/web/NativeListWebEngine.d.ts +32 -0
- package/lib/typescript/src/web/templates/ButtonGroupRowRenderer.d.ts +14 -0
- package/lib/typescript/src/web/templates/IdentityRowRenderer.d.ts +1 -1
- package/lib/typescript/src/web/templates/MessageRowRenderer.d.ts +9 -3
- package/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +26 -0
- package/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +4 -0
- package/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +10 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +20 -0
- package/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +14 -0
- package/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +12 -0
- package/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +2 -0
- package/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +43 -0
- package/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +4 -0
- package/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +4 -0
- package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +4 -0
- package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +6 -0
- package/nitrogen/generated/shared/json/NativeListConfig.json +2 -0
- package/package.json +3 -3
- package/react-native-native-list.podspec +1 -0
- package/src/NativeList.nitro.ts +2 -0
- package/src/NativeList.tsx +313 -44
- package/src/NativeList.types.ts +22 -0
- package/src/NativeList.web.tsx +204 -48
- package/src/index.ts +2 -0
- package/src/models.ts +117 -3
- package/src/selection.ts +1 -0
- package/src/validation.ts +852 -6
- package/src/web/NativeListWebEngine.ts +803 -20
- package/src/web/templates/ActivityRowRenderer.ts +14 -6
- package/src/web/templates/ButtonGroupRowRenderer.ts +58 -0
- package/src/web/templates/DataRowRenderer.ts +93 -20
- package/src/web/templates/IdentityRowRenderer.ts +150 -5
- package/src/web/templates/MessageRowRenderer.ts +23 -7
- package/src/web/templates/RowContainerStyle.ts +2 -0
- package/src/web/templates/RowRendererRegistry.ts +11 -1
- package/src/web/templates/SectionHeaderRowRenderer.ts +47 -4
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
package com.margelo.nitro.nativelist
|
|
2
2
|
|
|
3
3
|
import android.animation.TimeInterpolator
|
|
4
|
+
import android.animation.ValueAnimator
|
|
5
|
+
import android.animation.Animator
|
|
6
|
+
import android.animation.AnimatorListenerAdapter
|
|
4
7
|
import android.content.Context
|
|
5
8
|
import android.graphics.Canvas
|
|
6
9
|
import android.graphics.Color
|
|
@@ -11,6 +14,7 @@ import android.graphics.RectF
|
|
|
11
14
|
import android.graphics.drawable.ShapeDrawable
|
|
12
15
|
import android.graphics.drawable.shapes.PathShape
|
|
13
16
|
import android.os.Bundle
|
|
17
|
+
import android.os.Build
|
|
14
18
|
import android.os.Handler
|
|
15
19
|
import android.os.Looper
|
|
16
20
|
import android.os.SystemClock
|
|
@@ -32,6 +36,7 @@ import android.widget.TextView
|
|
|
32
36
|
import androidx.core.view.ViewCompat
|
|
33
37
|
import androidx.core.view.WindowInsetsCompat
|
|
34
38
|
import androidx.recyclerview.widget.ConcatAdapter
|
|
39
|
+
import androidx.recyclerview.widget.DefaultItemAnimator
|
|
35
40
|
import androidx.recyclerview.widget.GridLayoutManager
|
|
36
41
|
import androidx.recyclerview.widget.ItemTouchHelper
|
|
37
42
|
import androidx.recyclerview.widget.LinearLayoutManager
|
|
@@ -39,6 +44,8 @@ import androidx.recyclerview.widget.LinearSmoothScroller
|
|
|
39
44
|
import androidx.recyclerview.widget.RecyclerView
|
|
40
45
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|
41
46
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
47
|
+
import com.facebook.react.uimanager.PointerEvents
|
|
48
|
+
import com.facebook.react.uimanager.ReactPointerEventsView
|
|
42
49
|
import com.margelo.nitro.nativelogger.OneKeyLog
|
|
43
50
|
import org.json.JSONArray
|
|
44
51
|
import org.json.JSONObject
|
|
@@ -62,13 +69,10 @@ private class NativeListGridLayoutManager(
|
|
|
62
69
|
var onChildLaidOut: ((View, Int) -> Unit)? = null
|
|
63
70
|
var onLayoutPassCompleted: (() -> Unit)? = null
|
|
64
71
|
var onBeforeDataLayout: (() -> Unit)? = null
|
|
65
|
-
var onDiagnosticLayout: ((String, RecyclerView.State) -> Unit)? = null
|
|
66
72
|
|
|
67
73
|
override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
|
|
68
|
-
onDiagnosticLayout?.invoke("layout-children-before", state)
|
|
69
74
|
if (!state.isPreLayout) onBeforeDataLayout?.invoke()
|
|
70
75
|
super.onLayoutChildren(recycler, state)
|
|
71
|
-
onDiagnosticLayout?.invoke("layout-children-after", state)
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
// Scroll fills and layout passes both place children here, including rows that a
|
|
@@ -80,7 +84,6 @@ private class NativeListGridLayoutManager(
|
|
|
80
84
|
|
|
81
85
|
override fun onLayoutCompleted(state: RecyclerView.State) {
|
|
82
86
|
super.onLayoutCompleted(state)
|
|
83
|
-
onDiagnosticLayout?.invoke("layout-completed", state)
|
|
84
87
|
onLayoutPassCompleted?.invoke()
|
|
85
88
|
}
|
|
86
89
|
|
|
@@ -105,6 +108,7 @@ class NativeListView(
|
|
|
105
108
|
|
|
106
109
|
fun setFontFamiliesJson(json: String) {
|
|
107
110
|
if (!fonts.configure(json)) return
|
|
111
|
+
if (structuralTail != null) finishStructuralTail(interrupted = true)
|
|
108
112
|
invalidateActionAnchor("layout")
|
|
109
113
|
measuredRowExtentsPx.clear()
|
|
110
114
|
measuredRowExtentSumPx = 0L
|
|
@@ -200,67 +204,243 @@ class NativeListView(
|
|
|
200
204
|
var onReorder: ((String) -> Unit)? = null
|
|
201
205
|
var onEndReached: ((String) -> Unit)? = null
|
|
202
206
|
var onVisibleRangeChanged: ((String) -> Unit)? = null
|
|
207
|
+
var onStructuralTransitionSettled: ((String) -> Unit)? = null
|
|
203
208
|
var keyboardDismissMode = NativeListKeyboardDismissMode.NONE
|
|
204
209
|
var keyboardShouldPersistTaps = NativeListKeyboardShouldPersistTaps.NEVER
|
|
205
210
|
private val density = resources.displayMetrics.density
|
|
206
|
-
private val recyclerView = RecyclerView(context)
|
|
211
|
+
private val recyclerView = object : RecyclerView(context) {
|
|
212
|
+
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
|
|
213
|
+
if (isLayoutSuppressed) {
|
|
214
|
+
setMeasuredDimension(measuredWidth, measuredHeight)
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
super.onMeasure(widthSpec, heightSpec)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
221
|
+
if (isLayoutSuppressed) return
|
|
222
|
+
super.onLayout(changed, left, top, right, bottom)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
207
225
|
private var configuredTopPaddingPx = 0
|
|
208
226
|
private var slotInsetPx = 0
|
|
227
|
+
private var slotRightInsetPx = 0
|
|
209
228
|
private var configuredBottomPaddingPx = 0
|
|
229
|
+
private data class SidecarLayout(val width: Int, val gap: Int, val maxContentWidth: Int)
|
|
230
|
+
private var sidecarLayout: SidecarLayout? = null
|
|
231
|
+
private var sidecarSpacerHeightPx = 0
|
|
232
|
+
private var sidecarSpacerScheduled = false
|
|
233
|
+
private var consumedStructuralTransitionId: String? = null
|
|
234
|
+
private var structuralAnimator: ValueAnimator? = null
|
|
235
|
+
private var structuralSpacerHeightPx = 0
|
|
236
|
+
private var structuralTailSignedExtentPx = 0
|
|
237
|
+
private var structuralStartSpacerPx = 0
|
|
238
|
+
private var structuralFooterVisualStartTop: Int? = null
|
|
239
|
+
private var structuralFooterVisualEndTop: Int? = null
|
|
240
|
+
private var structuralFooterVisualProgress = 0f
|
|
241
|
+
private var structuralFooterCompensatedHeightPx: Int? = null
|
|
242
|
+
private var structuralFooterObservedNativeHeightPx: Int? = null
|
|
243
|
+
private var acceptedFooterNaturalHeightPx: Int? = null
|
|
244
|
+
private data class PendingPreparingFooterFrame(
|
|
245
|
+
val heights: IntArray,
|
|
246
|
+
val naturalHeight: Int,
|
|
247
|
+
val observedNativeHeight: Int?,
|
|
248
|
+
val ownerOffset: Int,
|
|
249
|
+
val epoch: Int,
|
|
250
|
+
val footer: View?,
|
|
251
|
+
val handoffOnly: Boolean,
|
|
252
|
+
)
|
|
253
|
+
private var pendingPreparingFooterFrame: PendingPreparingFooterFrame? = null
|
|
254
|
+
private var preparingFooterFrameScheduled = false
|
|
255
|
+
private var structuralScrollInterrupted = false
|
|
256
|
+
private var structuralFooterHoldTop: Int? = null
|
|
257
|
+
private var structuralTransitionEpoch = 0
|
|
258
|
+
private class StructuralTail(
|
|
259
|
+
var items: MutableList<NativeListItem>,
|
|
260
|
+
val heights: IntArray,
|
|
261
|
+
spacings: IntArray,
|
|
262
|
+
val prefixCount: Int,
|
|
263
|
+
val finalExtent: Int,
|
|
264
|
+
) {
|
|
265
|
+
val starts = IntArray(items.size)
|
|
266
|
+
val indexByKey = items.withIndex().associate { it.value.key to it.index }
|
|
267
|
+
val hosts = mutableMapOf<Int, NativeListRowHost>()
|
|
268
|
+
var peakHosts = 0
|
|
269
|
+
val extent: Int
|
|
270
|
+
init {
|
|
271
|
+
var edge = 0
|
|
272
|
+
for (index in items.indices) {
|
|
273
|
+
starts[index] = edge
|
|
274
|
+
edge += heights[index] + spacings[index]
|
|
275
|
+
}
|
|
276
|
+
extent = edge
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
private val structuralTailLayer = object : FrameLayout(reactContext), ReactPointerEventsView {
|
|
280
|
+
override val pointerEvents: PointerEvents = PointerEvents.NONE
|
|
281
|
+
override fun dispatchTouchEvent(event: MotionEvent) = false
|
|
282
|
+
}.apply {
|
|
283
|
+
clipChildren = true
|
|
284
|
+
clipToPadding = true
|
|
285
|
+
isClickable = false
|
|
286
|
+
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
|
|
287
|
+
visibility = GONE
|
|
288
|
+
}
|
|
289
|
+
private var structuralTail: StructuralTail? = null
|
|
290
|
+
private var structuralTailRowsInAdapterPx = 0
|
|
291
|
+
private var structuralTailStartExtentPx = 0
|
|
292
|
+
private var structuralTailStartProgress = 0f
|
|
293
|
+
private var structuralTailStartContentY = 0
|
|
294
|
+
private var structuralTailStartOffset = 0
|
|
295
|
+
private var structuralTailTargetOffset = 0
|
|
296
|
+
private var disclosureFraction = 0f
|
|
297
|
+
private var disclosureStartFraction = 0f
|
|
298
|
+
private var disclosurePresentedContentPx = 0
|
|
299
|
+
private var disclosurePresentedInsetPx = 0
|
|
300
|
+
private var disclosureContentStartPx = 0
|
|
301
|
+
private var disclosureContentStartProgress = 0f
|
|
302
|
+
private var disclosureTransitioning = false
|
|
303
|
+
private var activeDisclosureTransitionId: String? = null
|
|
304
|
+
private var settledDisclosureTransitionId: String? = null
|
|
305
|
+
private var directDisclosureStatus = "interrupted"
|
|
306
|
+
private var directDisclosureTogglePositionPending = false
|
|
307
|
+
private var batchedDisclosureGeometryInvalid = false
|
|
308
|
+
private var structuralTailCommandActive = false
|
|
309
|
+
private var structuralTailFinalizing = false
|
|
310
|
+
private var structuralTailPreparing = false
|
|
311
|
+
private data class PendingStructuralTransition(
|
|
312
|
+
val transition: NativeListConfig.StructuralTransition,
|
|
313
|
+
val epoch: Int,
|
|
314
|
+
val anchorKey: String?,
|
|
315
|
+
val anchorTop: Int?,
|
|
316
|
+
val footerStartTop: Int?,
|
|
317
|
+
val heldHeightPx: Int,
|
|
318
|
+
val collapsing: Boolean,
|
|
319
|
+
val spacerInserted: Boolean = false,
|
|
320
|
+
val footerAnchorAppliedTop: Int? = null,
|
|
321
|
+
val rowAnchorAppliedTop: Int? = null,
|
|
322
|
+
)
|
|
323
|
+
private var pendingStructuralTransition: PendingStructuralTransition? = null
|
|
324
|
+
private var pendingStructuralStartScheduled = false
|
|
210
325
|
private val refreshLayout = SwipeRefreshLayout(context)
|
|
211
326
|
private val refreshIndicatorTravelPx = refreshLayout.progressViewEndOffset
|
|
212
327
|
private val defaultRefreshTriggerDistancePx = (64 * resources.displayMetrics.density).roundToInt()
|
|
213
328
|
private var refreshIndicatorOffsetPx = 0
|
|
214
329
|
private val contentContainer = FrameLayout(context)
|
|
330
|
+
private val sidecarHost = object : FrameLayout(context) {
|
|
331
|
+
private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
|
|
332
|
+
private var down: MotionEvent? = null
|
|
333
|
+
private var routing = false
|
|
334
|
+
|
|
335
|
+
private fun route(event: MotionEvent) {
|
|
336
|
+
val copy = MotionEvent.obtain(event)
|
|
337
|
+
copy.offsetLocation(x - refreshLayout.x, y - refreshLayout.y)
|
|
338
|
+
refreshLayout.dispatchTouchEvent(copy)
|
|
339
|
+
copy.recycle()
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
|
343
|
+
when (event.actionMasked) {
|
|
344
|
+
MotionEvent.ACTION_DOWN -> {
|
|
345
|
+
down?.recycle()
|
|
346
|
+
down = MotionEvent.obtain(event)
|
|
347
|
+
routing = false
|
|
348
|
+
}
|
|
349
|
+
MotionEvent.ACTION_MOVE -> {
|
|
350
|
+
if (routing) {
|
|
351
|
+
route(event)
|
|
352
|
+
return true
|
|
353
|
+
}
|
|
354
|
+
val start = down
|
|
355
|
+
if (start != null && abs(event.y - start.y) > touchSlop &&
|
|
356
|
+
abs(event.y - start.y) > abs(event.x - start.x)) {
|
|
357
|
+
val cancel = MotionEvent.obtain(event)
|
|
358
|
+
cancel.action = MotionEvent.ACTION_CANCEL
|
|
359
|
+
super.dispatchTouchEvent(cancel)
|
|
360
|
+
cancel.recycle()
|
|
361
|
+
route(start)
|
|
362
|
+
route(event)
|
|
363
|
+
routing = true
|
|
364
|
+
return true
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
|
368
|
+
if (routing) route(event)
|
|
369
|
+
down?.recycle()
|
|
370
|
+
down = null
|
|
371
|
+
if (routing) {
|
|
372
|
+
routing = false
|
|
373
|
+
return true
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
else -> if (routing) {
|
|
377
|
+
route(event)
|
|
378
|
+
return true
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return super.dispatchTouchEvent(event)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
215
384
|
private val adapter = NativeListAdapter(reactContext, fonts)
|
|
216
385
|
private val containerSlots = mutableListOf<View>()
|
|
217
|
-
private val slotHeights = IntArray(
|
|
386
|
+
private val slotHeights = IntArray(6)
|
|
387
|
+
private val expandedContentHost = object : FrameLayout(context), ReactPointerEventsView {
|
|
388
|
+
var naturalHeightPx = 0
|
|
389
|
+
override val pointerEvents: PointerEvents
|
|
390
|
+
get() = if (config?.disclosure?.expanded == true &&
|
|
391
|
+
!disclosureTransitioning && disclosureFraction >= 1f) PointerEvents.AUTO
|
|
392
|
+
else PointerEvents.NONE
|
|
393
|
+
|
|
394
|
+
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
395
|
+
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
|
|
396
|
+
MeasureSpec.getSize(heightMeasureSpec))
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
400
|
+
getChildAt(0)?.layout(0, 0, right - left, naturalHeightPx)
|
|
401
|
+
}
|
|
402
|
+
}.apply {
|
|
403
|
+
clipChildren = true
|
|
404
|
+
clipToPadding = true
|
|
405
|
+
isClickable = false
|
|
406
|
+
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
|
|
407
|
+
}
|
|
408
|
+
private val footerPresentationHost = object : FrameLayout(context) {
|
|
409
|
+
var naturalHeightPx = 0
|
|
410
|
+
var hiddenTopPx = 0
|
|
411
|
+
|
|
412
|
+
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
413
|
+
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
|
|
414
|
+
MeasureSpec.getSize(heightMeasureSpec))
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
418
|
+
val child = getChildAt(0) ?: return
|
|
419
|
+
child.layout(0, -hiddenTopPx, right - left, naturalHeightPx - hiddenTopPx)
|
|
420
|
+
}
|
|
421
|
+
}.apply {
|
|
422
|
+
clipChildren = true
|
|
423
|
+
clipToPadding = true
|
|
424
|
+
}
|
|
425
|
+
private var initialHeaderInsertionHandled = false
|
|
218
426
|
private var containerSlotRelayoutPending = false
|
|
219
427
|
private var containerSlotRelayoutScheduled = false
|
|
220
428
|
private val headerSlotAdapter = NativeListContainerSlotAdapter()
|
|
221
429
|
private val emptySlotAdapter = NativeListContainerSlotAdapter()
|
|
430
|
+
private val structuralSpacerView = View(context)
|
|
431
|
+
private val structuralSpacerAdapter = NativeListContainerSlotAdapter()
|
|
432
|
+
private val expandedContentAdapter = NativeListContainerSlotAdapter()
|
|
433
|
+
private val structureToggleAdapter = NativeListContainerSlotAdapter()
|
|
434
|
+
private val disclosureInsetView = View(context)
|
|
435
|
+
private val disclosureInsetAdapter = NativeListContainerSlotAdapter()
|
|
222
436
|
private val footerSlotAdapter = NativeListContainerSlotAdapter()
|
|
223
|
-
private val
|
|
224
|
-
|
|
225
|
-
private val
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (!homeOffsetDiagnosticEnabled) return
|
|
231
|
-
try {
|
|
232
|
-
val first = layoutManager.findFirstVisibleItemPosition()
|
|
233
|
-
val firstView = layoutManager.findViewByPosition(first)
|
|
234
|
-
val holder = firstView?.let { recyclerView.getChildViewHolder(it) }
|
|
235
|
-
val lastView = layoutManager.findViewByPosition(layoutManager.findLastVisibleItemPosition())
|
|
236
|
-
val lastHolder = lastView?.let { recyclerView.getChildViewHolder(it) }
|
|
237
|
-
fun adapterOrdinal(value: RecyclerView.Adapter<*>?): Int = when (value) {
|
|
238
|
-
headerSlotAdapter -> 0
|
|
239
|
-
adapter -> 1
|
|
240
|
-
emptySlotAdapter -> 2
|
|
241
|
-
footerSlotAdapter -> 3
|
|
242
|
-
else -> -1
|
|
243
|
-
}
|
|
244
|
-
val focused = recyclerView.findFocus()
|
|
245
|
-
val focusChild = focused?.let { recyclerView.findContainingItemView(it) }
|
|
246
|
-
val focusHolder = focusChild?.let { recyclerView.getChildViewHolder(it) }
|
|
247
|
-
OneKeyLog.debug("NativeList", "[HOME-OFFSET-DIAG] listPhase=$phase " +
|
|
248
|
-
"seq=${++homeOffsetDiagnosticSequence} wallMs=${System.currentTimeMillis()} monoMs=${SystemClock.uptimeMillis()} " +
|
|
249
|
-
"list=${System.identityHashCode(this)} view=${System.identityHashCode(recyclerView)} tag=$id " +
|
|
250
|
-
"generation=${config?.generation} configuredCount=${config?.items?.size} " +
|
|
251
|
-
"counts=${headerSlotAdapter.itemCount},${adapter.itemCount},${emptySlotAdapter.itemCount},${footerSlotAdapter.itemCount} " +
|
|
252
|
-
"total=${scrollContentAdapter.itemCount} pendingCommand=${pendingScrollRequest?.javaClass?.simpleName ?: "none"} " +
|
|
253
|
-
"stateCount=${state?.itemCount} preLayout=${state?.isPreLayout} structureChanged=${state?.didStructureChange()} predictive=${state?.willRunPredictiveAnimations()} " +
|
|
254
|
-
"first=$first last=${layoutManager.findLastVisibleItemPosition()} " +
|
|
255
|
-
"adapterOrdinal=${adapterOrdinal(holder?.bindingAdapter)} layout=${holder?.layoutPosition} absolute=${holder?.absoluteAdapterPosition} binding=${holder?.bindingAdapterPosition} lastAdapterOrdinal=${adapterOrdinal(lastHolder?.bindingAdapter)} lastLayout=${lastHolder?.layoutPosition} lastAbsolute=${lastHolder?.absoluteAdapterPosition} lastBinding=${lastHolder?.bindingAdapterPosition} " +
|
|
256
|
-
"anchorTop=${firstView?.let(layoutManager::getDecoratedTop)} paddingTop=${recyclerView.paddingTop} " +
|
|
257
|
-
"childHeight=${firstView?.height} height=${recyclerView.height} range=${recyclerView.computeVerticalScrollRange()} extent=${recyclerView.computeVerticalScrollExtent()} " +
|
|
258
|
-
"canUp=${recyclerView.canScrollVertically(-1)} canDown=${recyclerView.canScrollVertically(1)} " +
|
|
259
|
-
"focusClass=${focused?.javaClass?.simpleName ?: "none"} focusId=${focused?.id} focusAdapterOrdinal=${adapterOrdinal(focusHolder?.bindingAdapter)} focusLayout=${focusHolder?.layoutPosition} focusPosition=${focusHolder?.absoluteAdapterPosition} focusBinding=${focusHolder?.bindingAdapterPosition}")
|
|
260
|
-
} catch (_: Exception) {
|
|
261
|
-
// Diagnostic reads must not affect layout or command delivery.
|
|
262
|
-
}
|
|
263
|
-
}
|
|
437
|
+
private val sidecarSpacerView = View(context)
|
|
438
|
+
private val sidecarSpacerAdapter = NativeListContainerSlotAdapter()
|
|
439
|
+
private val scrollContentAdapter = ConcatAdapter(
|
|
440
|
+
headerSlotAdapter, adapter, emptySlotAdapter, structuralSpacerAdapter,
|
|
441
|
+
expandedContentAdapter, structureToggleAdapter, disclosureInsetAdapter,
|
|
442
|
+
footerSlotAdapter, sidecarSpacerAdapter,
|
|
443
|
+
)
|
|
264
444
|
|
|
265
445
|
fun mountContainerSlot(view: View, index: Int) {
|
|
266
446
|
containerSlots.add(index.coerceIn(0, containerSlots.size), view)
|
|
@@ -277,23 +457,283 @@ class NativeListView(
|
|
|
277
457
|
scheduleContainerSlotRelayout()
|
|
278
458
|
}
|
|
279
459
|
|
|
280
|
-
fun setContainerSlotHeightsJson(json: String) {
|
|
460
|
+
fun setContainerSlotHeightsJson(json: String, upcomingSnapshotJson: String? = null) {
|
|
281
461
|
val values = runCatching { JSONArray(json) }.getOrNull() ?: return
|
|
282
|
-
if (values.length() !=
|
|
283
|
-
|
|
462
|
+
if (values.length() != 4 && values.length() != 6) return
|
|
463
|
+
val nextHeights = IntArray(6)
|
|
464
|
+
for (index in 0 until values.length()) {
|
|
284
465
|
val value = values.optDouble(index)
|
|
285
466
|
if (!value.isFinite() || value < 0) return
|
|
467
|
+
nextHeights[index] = (value * density).roundToInt()
|
|
468
|
+
}
|
|
469
|
+
val disclosureTail = structuralTail != null && config?.disclosure != null &&
|
|
470
|
+
!structuralTailFinalizing
|
|
471
|
+
if (disclosureTail && upcomingSnapshotJson != null &&
|
|
472
|
+
(nextHeights[2] != slotHeights[2] || nextHeights[5] != slotHeights[5] ||
|
|
473
|
+
nextHeights[4] < disclosurePresentedContentPx)) {
|
|
474
|
+
// Nitro delivers both setters in this UI turn. Let the complete new snapshot
|
|
475
|
+
// replace the old command before deciding whether its geometry is supported.
|
|
476
|
+
batchedDisclosureGeometryInvalid = true
|
|
477
|
+
for (index in 0..5) slotHeights[index] = nextHeights[index]
|
|
478
|
+
acceptedFooterNaturalHeightPx = nextHeights[2]
|
|
479
|
+
return
|
|
286
480
|
}
|
|
481
|
+
val unsupportedDisclosureGeometry = disclosureTail &&
|
|
482
|
+
(nextHeights[2] != slotHeights[2] || nextHeights[5] != slotHeights[5] ||
|
|
483
|
+
nextHeights[4] < disclosurePresentedContentPx)
|
|
484
|
+
if (unsupportedDisclosureGeometry) {
|
|
485
|
+
// A changing fixed footer/toggle or a shrinking mounted C cannot be represented
|
|
486
|
+
// by the current visual segment. Commit the accepted endpoint before ACK.
|
|
487
|
+
structuralFooterCompensatedHeightPx = null
|
|
488
|
+
acceptedFooterNaturalHeightPx = nextHeights[2]
|
|
489
|
+
for (index in 0..5) slotHeights[index] = nextHeights[index]
|
|
490
|
+
updateContainerSlots()
|
|
491
|
+
finishStructuralTail(interrupted = true)
|
|
492
|
+
return
|
|
493
|
+
}
|
|
494
|
+
if (disclosureTail && nextHeights[4] != slotHeights[4]) {
|
|
495
|
+
disclosureContentStartPx = disclosurePresentedContentPx
|
|
496
|
+
disclosureContentStartProgress =
|
|
497
|
+
(structuralAnimator?.animatedValue as? Float) ?: 0f
|
|
498
|
+
}
|
|
499
|
+
if (structuralTail != null && structuralTailRowsInAdapterPx > 0 &&
|
|
500
|
+
listOf(0, 1, 3).all { slotHeights[it] == nextHeights[it] } &&
|
|
501
|
+
(pendingPreparingFooterFrame != null ||
|
|
502
|
+
(!structuralTailFinalizing && slotHeights[2] != nextHeights[2]))) {
|
|
503
|
+
enqueuePreparingFooterFrame(nextHeights, nextHeights[2])
|
|
504
|
+
return
|
|
505
|
+
}
|
|
506
|
+
pendingPreparingFooterFrame?.takeIf {
|
|
507
|
+
structuralTailRowsInAdapterPx > 0 && it.footer === containerSlots.getOrNull(2)
|
|
508
|
+
}?.let { pending ->
|
|
509
|
+
val preservesNewerHeight = nextHeights[2] == slotHeights[2] &&
|
|
510
|
+
pending.naturalHeight != nextHeights[2]
|
|
511
|
+
pendingPreparingFooterFrame = pending.copy(
|
|
512
|
+
heights = nextHeights.copyOf(),
|
|
513
|
+
naturalHeight = if (preservesNewerHeight) pending.naturalHeight else nextHeights[2],
|
|
514
|
+
observedNativeHeight = pending.observedNativeHeight.takeIf {
|
|
515
|
+
preservesNewerHeight || pending.naturalHeight == nextHeights[2]
|
|
516
|
+
},
|
|
517
|
+
)
|
|
518
|
+
}
|
|
519
|
+
if (structuralTailPreparing && (0..3).any { index ->
|
|
520
|
+
slotHeights[index] != nextHeights[index]
|
|
521
|
+
}) invalidateTailForContainerGeometry()
|
|
522
|
+
if (structuralTail != null && listOf(0, 1, 3).any { index ->
|
|
523
|
+
slotHeights[index] != nextHeights[index]
|
|
524
|
+
}) finishStructuralTail(interrupted = true)
|
|
525
|
+
val footerHeightPx = nextHeights[2]
|
|
526
|
+
if (footerHeightPx != slotHeights[2]) {
|
|
527
|
+
containerSlots.getOrNull(2)?.measuredHeight?.takeIf { it > 0 && it != footerHeightPx }
|
|
528
|
+
?.let { structuralFooterObservedNativeHeightPx = it }
|
|
529
|
+
reconcileStructuralFooterHeight(footerHeightPx)
|
|
530
|
+
acceptedFooterNaturalHeightPx = footerHeightPx
|
|
531
|
+
} else if (acceptedFooterNaturalHeightPx == null) {
|
|
532
|
+
acceptedFooterNaturalHeightPx = footerHeightPx
|
|
533
|
+
}
|
|
534
|
+
val hiddenDisclosureContentOnlyChange = config?.disclosure?.expanded == false &&
|
|
535
|
+
!disclosureTransitioning && structuralTail == null &&
|
|
536
|
+
!structuralTailPreparing && !structuralTailFinalizing &&
|
|
537
|
+
disclosurePresentedContentPx == 0 && disclosurePresentedInsetPx == 0 &&
|
|
538
|
+
expandedContentAdapter.itemCount == 0 &&
|
|
539
|
+
slotHeights[4] != nextHeights[4] &&
|
|
540
|
+
(0..5).all { index -> index == 4 || slotHeights[index] == nextHeights[index] }
|
|
287
541
|
var changed = false
|
|
288
|
-
for (index in 0..
|
|
289
|
-
val height =
|
|
542
|
+
for (index in 0..5) {
|
|
543
|
+
val height = nextHeights[index]
|
|
290
544
|
if (slotHeights[index] != height) changed = true
|
|
291
545
|
slotHeights[index] = height
|
|
292
546
|
}
|
|
547
|
+
if (hiddenDisclosureContentOnlyChange) {
|
|
548
|
+
expandedContentHost.naturalHeightPx = slotHeights[4]
|
|
549
|
+
layoutExpandedContentHostNow()
|
|
550
|
+
return
|
|
551
|
+
}
|
|
293
552
|
updateContainerSlots()
|
|
553
|
+
if (structuralTail != null) updateStructuralTailTarget()
|
|
554
|
+
updateSidecarGeometry()
|
|
555
|
+
scheduleSidecarSpacer()
|
|
294
556
|
if (changed) scheduleContainerSlotRelayout()
|
|
295
557
|
}
|
|
296
558
|
|
|
559
|
+
private fun enqueuePreparingFooterFrame(
|
|
560
|
+
heights: IntArray,
|
|
561
|
+
naturalHeight: Int,
|
|
562
|
+
observedNativeHeight: Int? = null,
|
|
563
|
+
) {
|
|
564
|
+
val footer = containerSlots.getOrNull(2)
|
|
565
|
+
val existing = pendingPreparingFooterFrame?.takeIf { it.footer === footer }
|
|
566
|
+
val preservesNewerHeight = existing != null && observedNativeHeight == null &&
|
|
567
|
+
naturalHeight == slotHeights[2] && existing.naturalHeight != naturalHeight
|
|
568
|
+
val acceptedHeight = if (preservesNewerHeight) existing.naturalHeight else naturalHeight
|
|
569
|
+
val observedHeight = observedNativeHeight ?:
|
|
570
|
+
existing?.observedNativeHeight?.takeIf { preservesNewerHeight || it == acceptedHeight }
|
|
571
|
+
pendingPreparingFooterFrame = PendingPreparingFooterFrame(
|
|
572
|
+
heights.copyOf(), acceptedHeight, observedHeight,
|
|
573
|
+
existing?.ownerOffset ?: structuralTailStartOffset,
|
|
574
|
+
structuralTransitionEpoch, footer, existing?.handoffOnly == true,
|
|
575
|
+
)
|
|
576
|
+
if (!commitPreparingFooterFrame() && pendingPreparingFooterFrame?.handoffOnly != true) {
|
|
577
|
+
schedulePreparingFooterFrame()
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
private fun schedulePreparingFooterFrame() {
|
|
582
|
+
if (preparingFooterFrameScheduled) return
|
|
583
|
+
preparingFooterFrameScheduled = true
|
|
584
|
+
recyclerView.post {
|
|
585
|
+
preparingFooterFrameScheduled = false
|
|
586
|
+
if (!commitPreparingFooterFrame()) recyclerView.postInvalidateOnAnimation()
|
|
587
|
+
}
|
|
588
|
+
recyclerView.postInvalidateOnAnimation()
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
private fun reconcileStructuralFooterHeight(nextHeightPx: Int) {
|
|
592
|
+
if (structuralTail != null) {
|
|
593
|
+
if (!structuralTailFinalizing) {
|
|
594
|
+
val delta = (structuralFooterCompensatedHeightPx ?:
|
|
595
|
+
acceptedFooterNaturalHeightPx ?: slotHeights[2]) - nextHeightPx
|
|
596
|
+
if (delta != 0) {
|
|
597
|
+
if (structuralTailPreparing) {
|
|
598
|
+
invalidateTailForContainerGeometry()
|
|
599
|
+
return
|
|
600
|
+
}
|
|
601
|
+
structuralFooterCompensatedHeightPx = nextHeightPx
|
|
602
|
+
val compensated = structuralTailSignedExtentPx + delta
|
|
603
|
+
structuralTailStartProgress = (structuralAnimator?.animatedValue as? Float) ?: 0f
|
|
604
|
+
structuralTailStartExtentPx = compensated
|
|
605
|
+
if (structuralTailRowsInAdapterPx == 0) structuralTailStartOffset = tailScrollExtentPx()
|
|
606
|
+
setStructuralTailExtent(compensated)
|
|
607
|
+
if (structuralTailRowsInAdapterPx == 0) updateStructuralTailWindow()
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return
|
|
611
|
+
}
|
|
612
|
+
val previousHeightPx = structuralFooterCompensatedHeightPx ?:
|
|
613
|
+
acceptedFooterNaturalHeightPx ?: slotHeights[2]
|
|
614
|
+
val footerDelta = previousHeightPx - nextHeightPx
|
|
615
|
+
if (footerDelta == 0) return
|
|
616
|
+
if (pendingStructuralTransition != null || structuralAnimator != null || structuralFooterHoldTop != null) {
|
|
617
|
+
structuralFooterCompensatedHeightPx = nextHeightPx
|
|
618
|
+
}
|
|
619
|
+
if (pendingStructuralTransition != null) {
|
|
620
|
+
pendingStructuralTransition = pendingStructuralTransition?.let {
|
|
621
|
+
val updated = it.copy(
|
|
622
|
+
footerStartTop = it.footerStartTop?.plus(footerDelta),
|
|
623
|
+
heldHeightPx = (it.heldHeightPx + footerDelta).coerceAtLeast(0),
|
|
624
|
+
)
|
|
625
|
+
if (updated.spacerInserted) setStructuralSpacerHeight(updated.heldHeightPx)
|
|
626
|
+
updated
|
|
627
|
+
}
|
|
628
|
+
structuralFooterHoldTop = structuralFooterHoldTop?.plus(footerDelta)
|
|
629
|
+
updateStructuralFooterHoldPosition()
|
|
630
|
+
} else if (structuralFooterHoldTop != null && structuralAnimator == null) {
|
|
631
|
+
setStructuralSpacerHeight(structuralSpacerHeightPx + footerDelta)
|
|
632
|
+
structuralFooterHoldTop = structuralFooterHoldTop?.plus(footerDelta)
|
|
633
|
+
updateStructuralFooterHoldPosition()
|
|
634
|
+
} else if (structuralAnimator != null) {
|
|
635
|
+
structuralStartSpacerPx = (structuralStartSpacerPx + footerDelta).coerceAtLeast(0)
|
|
636
|
+
structuralFooterVisualStartTop = structuralFooterVisualStartTop?.plus(footerDelta)
|
|
637
|
+
val fraction = (structuralAnimator?.animatedValue as? Float) ?: 0f
|
|
638
|
+
setStructuralSpacerHeight((structuralStartSpacerPx * (1f - fraction)).roundToInt())
|
|
639
|
+
updateStructuralFooterVisualPosition()
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
fun setSidecarLayoutJson(json: String) {
|
|
644
|
+
val next = if (json.isEmpty()) null else {
|
|
645
|
+
val value = runCatching { JSONObject(json) }.getOrNull() ?: return
|
|
646
|
+
val width = value.optDouble("width")
|
|
647
|
+
val gap = value.optDouble("gap")
|
|
648
|
+
val maxWidth = value.optDouble("maxContentWidth")
|
|
649
|
+
if (!width.isFinite() || width <= 0 || !gap.isFinite() || gap < 0 ||
|
|
650
|
+
!maxWidth.isFinite() || maxWidth <= width + gap) return
|
|
651
|
+
SidecarLayout((width * density).roundToInt(), (gap * density).roundToInt(),
|
|
652
|
+
(maxWidth * density).roundToInt())
|
|
653
|
+
}
|
|
654
|
+
if (next == sidecarLayout) return
|
|
655
|
+
if (structuralTail != null) finishStructuralTail(interrupted = true)
|
|
656
|
+
sidecarLayout = next
|
|
657
|
+
config?.let(::updateLayout)
|
|
658
|
+
updateSidecarGeometry()
|
|
659
|
+
scheduleSidecarSpacer()
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
private fun sidecarBaseInsetPx(): Int {
|
|
663
|
+
val layout = sidecarLayout ?: return 0
|
|
664
|
+
val width = recyclerView.width
|
|
665
|
+
return max(0, (width - minOf(width, layout.maxContentWidth)) / 2)
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
private fun updateSidecarGeometry() {
|
|
669
|
+
val layout = sidecarLayout
|
|
670
|
+
val root = containerSlots.getOrNull(3)
|
|
671
|
+
if (root !== sidecarHost.getChildAt(0)) {
|
|
672
|
+
sidecarHost.removeAllViews()
|
|
673
|
+
(root?.parent as? ViewGroup)?.removeView(root)
|
|
674
|
+
if (root != null) sidecarHost.addView(root)
|
|
675
|
+
}
|
|
676
|
+
val params = sidecarHost.layoutParams as? FrameLayout.LayoutParams
|
|
677
|
+
?: FrameLayout.LayoutParams(0, 0, Gravity.TOP or Gravity.RIGHT)
|
|
678
|
+
val nextWidth = layout?.width ?: 0
|
|
679
|
+
val nextHeight = if (layout != null) slotHeights[3] else 0
|
|
680
|
+
val isRtl = layoutDirection == LAYOUT_DIRECTION_RTL
|
|
681
|
+
val nextGravity = Gravity.TOP or (if (isRtl) Gravity.LEFT else Gravity.RIGHT)
|
|
682
|
+
val nextSideMargin = sidecarBaseInsetPx()
|
|
683
|
+
if (params.width != nextWidth || params.height != nextHeight ||
|
|
684
|
+
params.gravity != nextGravity ||
|
|
685
|
+
params.leftMargin != (if (isRtl) nextSideMargin else 0) ||
|
|
686
|
+
params.rightMargin != (if (isRtl) 0 else nextSideMargin)) {
|
|
687
|
+
params.width = nextWidth
|
|
688
|
+
params.height = nextHeight
|
|
689
|
+
params.gravity = nextGravity
|
|
690
|
+
params.leftMargin = if (isRtl) nextSideMargin else 0
|
|
691
|
+
params.rightMargin = if (isRtl) 0 else nextSideMargin
|
|
692
|
+
sidecarHost.layoutParams = params
|
|
693
|
+
}
|
|
694
|
+
sidecarHost.visibility = if (layout != null && root != null && slotHeights[3] > 0) VISIBLE else GONE
|
|
695
|
+
updateSidecarPosition()
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
private fun updateSidecarPosition(resync: Boolean = false) {
|
|
699
|
+
if (sidecarLayout == null) return
|
|
700
|
+
if (resync) (if (structuralTail == null) measuredScrollDistancePx()
|
|
701
|
+
else structuralTailMeasuredScrollDistancePx())?.let { logicalScrollDistancePx = it }
|
|
702
|
+
sidecarHost.translationY = (recyclerView.paddingTop - logicalScrollDistancePx).toFloat()
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
private fun sidecarLeftExtentPx(): Int =
|
|
706
|
+
(nativeListScrollPrefixPx(
|
|
707
|
+
scrollContentAdapter.itemCount - sidecarSpacerAdapter.itemCount,
|
|
708
|
+
layoutManager.spanCount,
|
|
709
|
+
layoutManager.spanSizeLookup::getSpanSize,
|
|
710
|
+
if (structuralTail == null) ::estimatedScrollExtentPx else ::structuralTailRowOrSlotExtentPx,
|
|
711
|
+
) + recyclerView.paddingTop + recyclerView.paddingBottom)
|
|
712
|
+
.coerceIn(0L, Int.MAX_VALUE.toLong()).toInt()
|
|
713
|
+
|
|
714
|
+
private fun scheduleSidecarSpacer() {
|
|
715
|
+
if (sidecarLayout == null && sidecarSpacerHeightPx == 0) return
|
|
716
|
+
if (disposed || sidecarSpacerScheduled) return
|
|
717
|
+
sidecarSpacerScheduled = true
|
|
718
|
+
recyclerView.post {
|
|
719
|
+
sidecarSpacerScheduled = false
|
|
720
|
+
updateSidecarSpacerNow()
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
private fun updateSidecarSpacerNow() {
|
|
725
|
+
if (disposed || recyclerView.width <= 0 || recyclerView.height <= 0) return
|
|
726
|
+
val desired = if (sidecarLayout != null) {
|
|
727
|
+
recyclerView.paddingTop + slotHeights[3] + recyclerView.paddingBottom
|
|
728
|
+
} else 0
|
|
729
|
+
val leftExtent = sidecarLeftExtentPx()
|
|
730
|
+
val next = (desired - leftExtent).coerceAtLeast(0)
|
|
731
|
+
if (abs(next - sidecarSpacerHeightPx) <= 1) return
|
|
732
|
+
sidecarSpacerHeightPx = next
|
|
733
|
+
sidecarSpacerAdapter.update(sidecarSpacerView, next, next > 0)
|
|
734
|
+
recyclerView.requestLayout()
|
|
735
|
+
}
|
|
736
|
+
|
|
297
737
|
private fun scheduleContainerSlotRelayout() {
|
|
298
738
|
if (disposed) return
|
|
299
739
|
containerSlotRelayoutPending = true
|
|
@@ -313,15 +753,185 @@ class NativeListView(
|
|
|
313
753
|
}
|
|
314
754
|
|
|
315
755
|
private fun updateContainerSlots() {
|
|
316
|
-
|
|
756
|
+
val headerWasAbsent = headerSlotAdapter.itemCount == 0
|
|
757
|
+
val firstVisible = layoutManager.findFirstVisibleItemPosition()
|
|
758
|
+
val firstView = layoutManager.findViewByPosition(firstVisible)
|
|
759
|
+
val restoreInitialHeaderTop = !initialHeaderInsertionHandled && headerWasAbsent &&
|
|
760
|
+
layoutManager.orientation == RecyclerView.VERTICAL &&
|
|
761
|
+
adapter.itemCount > 0 && readingIntentEpoch == 0L && pendingScrollRequest == null &&
|
|
762
|
+
acceptedScrollRequest == null && !executingScrollCommand &&
|
|
763
|
+
recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE && firstVisible == 0 &&
|
|
764
|
+
firstView != null && abs(layoutManager.getDecoratedTop(firstView) - recyclerView.paddingTop) <= 1 &&
|
|
765
|
+
!recyclerView.canScrollVertically(-1)
|
|
317
766
|
headerSlotAdapter.update(containerSlots.getOrNull(0), slotHeights[0], true)
|
|
318
767
|
emptySlotAdapter.update(containerSlots.getOrNull(1), slotHeights[1], config?.items?.isEmpty() != false)
|
|
319
|
-
|
|
320
|
-
|
|
768
|
+
syncDisclosurePresentation()
|
|
769
|
+
syncFooterPresentation()
|
|
770
|
+
updateSidecarGeometry()
|
|
771
|
+
if (headerWasAbsent && headerSlotAdapter.itemCount > 0) {
|
|
772
|
+
initialHeaderInsertionHandled = true
|
|
773
|
+
if (restoreInitialHeaderTop) {
|
|
774
|
+
// RecyclerView otherwise keeps the first body row at the top when this slot arrives late.
|
|
775
|
+
layoutManager.scrollToPositionWithOffset(0, 0)
|
|
776
|
+
}
|
|
777
|
+
}
|
|
321
778
|
layoutManager.spanSizeLookup.invalidateSpanIndexCache()
|
|
322
779
|
recyclerView.requestLayout()
|
|
323
780
|
}
|
|
324
781
|
|
|
782
|
+
private fun disclosureInsetExtentPx(): Int =
|
|
783
|
+
((config?.disclosure?.expandedTrailingInset ?: 0.0) * density)
|
|
784
|
+
.coerceIn(0.0, Int.MAX_VALUE.toDouble()).roundToInt()
|
|
785
|
+
|
|
786
|
+
private fun settleDisclosureCommand(status: String) {
|
|
787
|
+
val id = activeDisclosureTransitionId ?: return
|
|
788
|
+
if (disposed || id == settledDisclosureTransitionId ||
|
|
789
|
+
consumedStructuralTransitionId != id || structuralTail != null) return
|
|
790
|
+
settledDisclosureTransitionId = id
|
|
791
|
+
activeDisclosureTransitionId = null
|
|
792
|
+
directDisclosureTogglePositionPending = false
|
|
793
|
+
onStructuralTransitionSettled?.invoke(JSONObject().apply {
|
|
794
|
+
put("transitionId", id)
|
|
795
|
+
put("status", status)
|
|
796
|
+
put("settledExpanded", config?.disclosure?.expanded == true)
|
|
797
|
+
}.toString())
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
private fun settleCommittedDirectDisclosure(
|
|
801
|
+
next: NativeListConfig,
|
|
802
|
+
id: String?,
|
|
803
|
+
positionToggle: Boolean,
|
|
804
|
+
) {
|
|
805
|
+
if (id == null || disposed || config !== next ||
|
|
806
|
+
activeDisclosureTransitionId != id || consumedStructuralTransitionId != id ||
|
|
807
|
+
structuralTail != null || next.disclosure == null) return
|
|
808
|
+
relayoutRecyclerViewImmediately()
|
|
809
|
+
if (positionToggle && directDisclosureTogglePositionPending)
|
|
810
|
+
positionSettledDisclosureToggle()
|
|
811
|
+
settleDisclosureCommand(directDisclosureStatus)
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
private fun presentAcceptedDisclosureEndpoint(next: NativeListConfig) {
|
|
815
|
+
val disclosure = next.disclosure ?: return
|
|
816
|
+
disclosureFraction = if (disclosure.expanded) 1f else 0f
|
|
817
|
+
disclosurePresentedContentPx = if (disclosure.expanded) slotHeights[4] else 0
|
|
818
|
+
syncDisclosurePresentation()
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
private fun syncDisclosurePresentation() {
|
|
822
|
+
val disclosure = config?.disclosure
|
|
823
|
+
if (disclosure == null) {
|
|
824
|
+
disclosurePresentedContentPx = 0
|
|
825
|
+
disclosurePresentedInsetPx = 0
|
|
826
|
+
expandedContentAdapter.update(null, 0, false)
|
|
827
|
+
structureToggleAdapter.update(null, 0, false)
|
|
828
|
+
disclosureInsetAdapter.update(null, 0, false)
|
|
829
|
+
return
|
|
830
|
+
}
|
|
831
|
+
val content = containerSlots.getOrNull(4)
|
|
832
|
+
if (expandedContentHost.getChildAt(0) !== content) {
|
|
833
|
+
expandedContentHost.removeAllViews()
|
|
834
|
+
if (content != null) {
|
|
835
|
+
(content.parent as? ViewGroup)?.removeView(content)
|
|
836
|
+
expandedContentHost.addView(content, FrameLayout.LayoutParams(
|
|
837
|
+
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
expandedContentHost.naturalHeightPx = slotHeights[4]
|
|
841
|
+
val fraction = disclosureFraction.coerceIn(0f, 1f)
|
|
842
|
+
val contentHeight = if (disclosureTransitioning) disclosurePresentedContentPx else
|
|
843
|
+
if (disclosure.expanded) slotHeights[4] else 0
|
|
844
|
+
disclosurePresentedContentPx = contentHeight
|
|
845
|
+
disclosurePresentedInsetPx = (disclosureInsetExtentPx() * fraction).roundToInt()
|
|
846
|
+
val interactive = disclosure.expanded && !disclosureTransitioning && fraction >= 1f
|
|
847
|
+
expandedContentHost.importantForAccessibility = if (interactive)
|
|
848
|
+
View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
|
|
849
|
+
else View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
|
|
850
|
+
expandedContentAdapter.update(if (content == null) null else expandedContentHost,
|
|
851
|
+
contentHeight, true)
|
|
852
|
+
structureToggleAdapter.update(containerSlots.getOrNull(5), slotHeights[5], true)
|
|
853
|
+
disclosureInsetAdapter.update(disclosureInsetView,
|
|
854
|
+
disclosurePresentedInsetPx, true)
|
|
855
|
+
if (content != null) layoutExpandedContentHostNow()
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
private fun layoutExpandedContentHostNow() {
|
|
859
|
+
val wrapper = expandedContentHost
|
|
860
|
+
val child = wrapper.getChildAt(0) ?: return
|
|
861
|
+
val width = wrapper.width.takeIf { it > 0 } ?: wrapper.measuredWidth.takeIf { it > 0 } ?: return
|
|
862
|
+
val height = disclosurePresentedContentPx
|
|
863
|
+
if (!wrapper.isLayoutRequested && wrapper.measuredWidth == width &&
|
|
864
|
+
wrapper.measuredHeight == height && wrapper.height == height &&
|
|
865
|
+
child.left == 0 && child.top == 0 && child.right == width &&
|
|
866
|
+
child.bottom == wrapper.naturalHeightPx) return
|
|
867
|
+
wrapper.forceLayout()
|
|
868
|
+
wrapper.measure(View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
|
|
869
|
+
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY))
|
|
870
|
+
wrapper.layout(wrapper.left, wrapper.top, wrapper.left + width, wrapper.top + height)
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
private fun syncFooterPresentation() {
|
|
874
|
+
val footer = containerSlots.getOrNull(2)
|
|
875
|
+
if (footerPresentationHost.getChildAt(0) !== footer) {
|
|
876
|
+
invalidateTailForContainerGeometry()
|
|
877
|
+
structuralFooterCompensatedHeightPx = null
|
|
878
|
+
structuralFooterObservedNativeHeightPx = null
|
|
879
|
+
acceptedFooterNaturalHeightPx = null
|
|
880
|
+
footerPresentationHost.removeAllViews()
|
|
881
|
+
if (footer != null) {
|
|
882
|
+
(footer.parent as? ViewGroup)?.removeView(footer)
|
|
883
|
+
footerPresentationHost.addView(footer, FrameLayout.LayoutParams(
|
|
884
|
+
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
val naturalHeight = footerNaturalHeightPx()
|
|
888
|
+
val hiddenTop = footerHiddenTopPx()
|
|
889
|
+
if (footerPresentationHost.naturalHeightPx != naturalHeight ||
|
|
890
|
+
footerPresentationHost.hiddenTopPx != hiddenTop) {
|
|
891
|
+
footerPresentationHost.naturalHeightPx = naturalHeight
|
|
892
|
+
footerPresentationHost.hiddenTopPx = hiddenTop
|
|
893
|
+
footerPresentationHost.requestLayout()
|
|
894
|
+
}
|
|
895
|
+
footerSlotAdapter.update(if (footer == null) null else footerPresentationHost,
|
|
896
|
+
naturalHeight - hiddenTop, true)
|
|
897
|
+
if (footer != null) layoutFooterPresentationHostNow()
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
private fun layoutFooterPresentationHostNow() {
|
|
901
|
+
val wrapper = footerPresentationHost
|
|
902
|
+
val child = wrapper.getChildAt(0) ?: return
|
|
903
|
+
val width = wrapper.width.takeIf { it > 0 } ?: wrapper.measuredWidth.takeIf { it > 0 } ?: return
|
|
904
|
+
val height = footerPresentedHeightPx()
|
|
905
|
+
val childTop = -wrapper.hiddenTopPx
|
|
906
|
+
val childBottom = wrapper.naturalHeightPx - wrapper.hiddenTopPx
|
|
907
|
+
if (!wrapper.isLayoutRequested && wrapper.measuredWidth == width &&
|
|
908
|
+
wrapper.measuredHeight == height && wrapper.height == height &&
|
|
909
|
+
child.left == 0 && child.top == childTop && child.right == width &&
|
|
910
|
+
child.bottom == childBottom) return
|
|
911
|
+
wrapper.forceLayout()
|
|
912
|
+
wrapper.measure(View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
|
|
913
|
+
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY))
|
|
914
|
+
wrapper.layout(wrapper.left, wrapper.top, wrapper.left + width, wrapper.top + height)
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
private fun invalidateTailForContainerGeometry() {
|
|
918
|
+
pendingPreparingFooterFrame = null
|
|
919
|
+
if (structuralTail == null) return
|
|
920
|
+
if (!structuralTailFinalizing) finishStructuralTail(interrupted = true)
|
|
921
|
+
structuralFooterCompensatedHeightPx = null
|
|
922
|
+
structuralTailSignedExtentPx = structuralTailRowsInAdapterPx + structuralSpacerHeightPx
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
private fun footerNaturalHeightPx(): Int = if (structuralTail == null)
|
|
926
|
+
acceptedFooterNaturalHeightPx ?: slotHeights[2] else
|
|
927
|
+
structuralFooterCompensatedHeightPx ?: acceptedFooterNaturalHeightPx ?: slotHeights[2]
|
|
928
|
+
|
|
929
|
+
private fun footerHiddenTopPx(): Int = if (structuralTail == null) 0 else
|
|
930
|
+
(structuralTailRowsInAdapterPx - structuralTailSignedExtentPx)
|
|
931
|
+
.coerceIn(0, footerNaturalHeightPx())
|
|
932
|
+
|
|
933
|
+
private fun footerPresentedHeightPx(): Int = footerNaturalHeightPx() - footerHiddenTopPx()
|
|
934
|
+
|
|
325
935
|
private fun scrollPositionForRow(index: Int): Int = index + headerSlotAdapter.itemCount
|
|
326
936
|
|
|
327
937
|
private fun recordLaidOutRowExtent(child: View, extentPx: Int) {
|
|
@@ -345,7 +955,23 @@ class NativeListView(
|
|
|
345
955
|
if (position < headerCount) return slotHeights[0]
|
|
346
956
|
val row = position - headerCount
|
|
347
957
|
if (row >= adapter.itemCount) {
|
|
348
|
-
|
|
958
|
+
val slot = row - adapter.itemCount
|
|
959
|
+
return when {
|
|
960
|
+
slot < emptySlotAdapter.itemCount -> slotHeights[1]
|
|
961
|
+
slot < emptySlotAdapter.itemCount + structuralSpacerAdapter.itemCount -> structuralSpacerHeightPx
|
|
962
|
+
slot < emptySlotAdapter.itemCount + structuralSpacerAdapter.itemCount +
|
|
963
|
+
expandedContentAdapter.itemCount -> disclosurePresentedContentPx
|
|
964
|
+
slot < emptySlotAdapter.itemCount + structuralSpacerAdapter.itemCount +
|
|
965
|
+
expandedContentAdapter.itemCount + structureToggleAdapter.itemCount -> slotHeights[5]
|
|
966
|
+
slot < emptySlotAdapter.itemCount + structuralSpacerAdapter.itemCount +
|
|
967
|
+
expandedContentAdapter.itemCount + structureToggleAdapter.itemCount +
|
|
968
|
+
disclosureInsetAdapter.itemCount ->
|
|
969
|
+
disclosurePresentedInsetPx
|
|
970
|
+
slot < emptySlotAdapter.itemCount + structuralSpacerAdapter.itemCount +
|
|
971
|
+
expandedContentAdapter.itemCount + structureToggleAdapter.itemCount +
|
|
972
|
+
disclosureInsetAdapter.itemCount + footerSlotAdapter.itemCount -> footerPresentedHeightPx()
|
|
973
|
+
else -> sidecarSpacerHeightPx
|
|
974
|
+
}
|
|
349
975
|
}
|
|
350
976
|
val item = adapter.itemAt(row)
|
|
351
977
|
item?.key?.let(measuredRowExtentsPx::get)?.let { return it }
|
|
@@ -354,6 +980,847 @@ class NativeListView(
|
|
|
354
980
|
return if (measuredRowExtentsPx.isEmpty()) 0 else (measuredRowExtentSumPx / measuredRowExtentsPx.size).toInt()
|
|
355
981
|
}
|
|
356
982
|
|
|
983
|
+
private fun estimatedItemExtentPx(item: NativeListItem): Int {
|
|
984
|
+
measuredRowExtentsPx[item.key]?.let { return it }
|
|
985
|
+
val explicit = item.styledHeight ?: item.json.optDouble("height").takeIf { it.isFinite() }
|
|
986
|
+
if (explicit != null) return (explicit * density).roundToInt()
|
|
987
|
+
return if (measuredRowExtentsPx.isEmpty()) dp(56)
|
|
988
|
+
else (measuredRowExtentSumPx / measuredRowExtentsPx.size).toInt()
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
private fun setStructuralSpacerHeight(heightPx: Int) {
|
|
992
|
+
val next = heightPx.coerceAtLeast(0)
|
|
993
|
+
if (next == structuralSpacerHeightPx) return
|
|
994
|
+
structuralSpacerHeightPx = next
|
|
995
|
+
structuralSpacerAdapter.update(structuralSpacerView, next, true)
|
|
996
|
+
layoutManager.spanSizeLookup.invalidateSpanIndexCache()
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
private fun setStructuralTailExtent(extentPx: Int) {
|
|
1000
|
+
structuralTailSignedExtentPx = extentPx
|
|
1001
|
+
setStructuralSpacerHeight((extentPx - structuralTailRowsInAdapterPx).coerceAtLeast(0))
|
|
1002
|
+
syncFooterPresentation()
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
private fun prefixTailCount(old: NativeListConfig, next: NativeListConfig): Int? {
|
|
1006
|
+
if (old.orientation != "vertical" || next.orientation != "vertical" ||
|
|
1007
|
+
old.layout != next.layout || next.layout !in setOf("linear", "table") ||
|
|
1008
|
+
old.itemSpacing != next.itemSpacing ||
|
|
1009
|
+
old.contentPadding != next.contentPadding ||
|
|
1010
|
+
old.contentPaddingTop != next.contentPaddingTop ||
|
|
1011
|
+
old.contentPaddingBottom != next.contentPaddingBottom ||
|
|
1012
|
+
old.contentPaddingHorizontal != next.contentPaddingHorizontal ||
|
|
1013
|
+
old.theme?.toString() != next.theme?.toString() ||
|
|
1014
|
+
old.listStyle?.toString() != next.listStyle?.toString() ||
|
|
1015
|
+
old.items.size == next.items.size ||
|
|
1016
|
+
old.reorderable || next.reorderable ||
|
|
1017
|
+
structuralTailFinalizing || structuralTailRowsInAdapterPx > 0 ||
|
|
1018
|
+
old.items.any { it.usesSelectorSourceScale } !=
|
|
1019
|
+
next.items.any { it.usesSelectorSourceScale } ||
|
|
1020
|
+
(structuralAnimator != null && structuralTail == null) ||
|
|
1021
|
+
old.items.any { !it.hasExplicitHeight } ||
|
|
1022
|
+
next.items.any { !it.hasExplicitHeight } ||
|
|
1023
|
+
old.items.any { it.type !in setOf("identity", "dataRow") } ||
|
|
1024
|
+
next.items.any { it.type !in setOf("identity", "dataRow") } ||
|
|
1025
|
+
old.items.any { it.type == "sectionHeader" } ||
|
|
1026
|
+
next.items.any { it.type == "sectionHeader" }) return null
|
|
1027
|
+
val prefix = minOf(old.items.size, next.items.size)
|
|
1028
|
+
if (prefix == 0) return null
|
|
1029
|
+
if (structuralTail != null && prefix != structuralTail?.prefixCount) return null
|
|
1030
|
+
for (index in 0 until prefix) {
|
|
1031
|
+
val before = old.items[index]
|
|
1032
|
+
val after = next.items[index]
|
|
1033
|
+
if (before.key != after.key || before.rendererKey != after.rendererKey ||
|
|
1034
|
+
explicitTailRowHeight(before) != explicitTailRowHeight(after) ||
|
|
1035
|
+
explicitTailRowSpacingPx(before, old.itemSpacing) !=
|
|
1036
|
+
explicitTailRowSpacingPx(after, next.itemSpacing)) return null
|
|
1037
|
+
}
|
|
1038
|
+
return prefix
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
private fun explicitTailRowHeight(item: NativeListItem): Int =
|
|
1042
|
+
item.styledHeight?.let { (it * density).roundToInt() } ?: run {
|
|
1043
|
+
val value = item.json.optDouble("height")
|
|
1044
|
+
when (item.json.optString("heightRounding")) {
|
|
1045
|
+
"floor" -> (value * density).toInt()
|
|
1046
|
+
"nearest" -> (value * density).roundToInt()
|
|
1047
|
+
else -> if (usesSelectorSourceScale || item.usesSelectorSourceScale)
|
|
1048
|
+
(item.json.optInt("height") * density).roundToInt()
|
|
1049
|
+
else NativeListScale.dp(resources, item.json.optInt("height"))
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
private fun explicitTailRowSpacingPx(item: NativeListItem, spacing: Int): Int {
|
|
1054
|
+
if (item.type == "identity" &&
|
|
1055
|
+
item.json.optString("presentation") == "walletSidebar") {
|
|
1056
|
+
val height = item.styledHeight ?: item.json.optDouble("height")
|
|
1057
|
+
return ((height + spacing) * density).roundToInt() -
|
|
1058
|
+
(height * density).roundToInt()
|
|
1059
|
+
}
|
|
1060
|
+
return dp(spacing)
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
private fun exceedsStructuralTailHostLimit(
|
|
1064
|
+
old: NativeListConfig,
|
|
1065
|
+
next: NativeListConfig,
|
|
1066
|
+
prefixCount: Int,
|
|
1067
|
+
): Boolean {
|
|
1068
|
+
val items = (if (old.items.size > next.items.size) old.items else next.items)
|
|
1069
|
+
.drop(prefixCount)
|
|
1070
|
+
if (items.size <= 128) return false
|
|
1071
|
+
val starts = LongArray(items.size)
|
|
1072
|
+
val heights = IntArray(items.size)
|
|
1073
|
+
var cursor = 0L
|
|
1074
|
+
items.forEachIndexed { index, item ->
|
|
1075
|
+
starts[index] = cursor
|
|
1076
|
+
heights[index] = explicitTailRowHeight(item)
|
|
1077
|
+
cursor += heights[index] + explicitTailRowSpacingPx(item, next.itemSpacing)
|
|
1078
|
+
}
|
|
1079
|
+
val window = 3L * recyclerView.height
|
|
1080
|
+
return (0..(items.size - 129)).any { first ->
|
|
1081
|
+
starts[first + 128] - (starts[first] + heights[first]) <= window
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
private fun clearStructuralTail() {
|
|
1086
|
+
pendingPreparingFooterFrame = null
|
|
1087
|
+
structuralTail?.hosts?.values?.forEach { host ->
|
|
1088
|
+
structuralTailLayer.removeView(host)
|
|
1089
|
+
host.recycle()
|
|
1090
|
+
host.dispose()
|
|
1091
|
+
}
|
|
1092
|
+
structuralTail = null
|
|
1093
|
+
structuralTailFinalizing = false
|
|
1094
|
+
structuralTailPreparing = false
|
|
1095
|
+
structuralFooterCompensatedHeightPx = null
|
|
1096
|
+
structuralTailRowsInAdapterPx = 0
|
|
1097
|
+
setStructuralTailExtent(0)
|
|
1098
|
+
structuralTailLayer.removeAllViews()
|
|
1099
|
+
structuralTailLayer.visibility = GONE
|
|
1100
|
+
structuralTailLayer.layoutParams?.let { params ->
|
|
1101
|
+
params.width = 0
|
|
1102
|
+
params.height = 0
|
|
1103
|
+
structuralTailLayer.layoutParams = params
|
|
1104
|
+
}
|
|
1105
|
+
structuralTailLayer.forceLayout()
|
|
1106
|
+
structuralTailLayer.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.EXACTLY),
|
|
1107
|
+
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.EXACTLY))
|
|
1108
|
+
structuralTailLayer.layout(0, 0, 0, 0)
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
private fun structuralTailRowOrSlotExtentPx(position: Int): Int {
|
|
1112
|
+
val row = position - headerSlotAdapter.itemCount
|
|
1113
|
+
val item = adapter.itemAt(row)
|
|
1114
|
+
if (row < 0 || item == null || !item.hasExplicitHeight) return estimatedScrollExtentPx(position)
|
|
1115
|
+
return explicitTailRowHeight(item) +
|
|
1116
|
+
explicitTailRowSpacingPx(item, config?.itemSpacing ?: 0)
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
private fun structuralTailMeasuredScrollDistancePx(): Int? {
|
|
1120
|
+
if (layoutManager.orientation != RecyclerView.VERTICAL) return null
|
|
1121
|
+
val first = layoutManager.findFirstVisibleItemPosition()
|
|
1122
|
+
if (first == RecyclerView.NO_POSITION) return null
|
|
1123
|
+
val view = layoutManager.findViewByPosition(first) ?: return null
|
|
1124
|
+
val prefix = nativeListScrollPrefixPx(first, layoutManager.spanCount,
|
|
1125
|
+
layoutManager.spanSizeLookup::getSpanSize, ::structuralTailRowOrSlotExtentPx)
|
|
1126
|
+
val topMargin = (view.layoutParams as? ViewGroup.MarginLayoutParams)?.topMargin ?: 0
|
|
1127
|
+
val top = layoutManager.getDecoratedTop(view) - topMargin
|
|
1128
|
+
return (prefix + recyclerView.paddingTop - top).coerceIn(0L, Int.MAX_VALUE.toLong()).toInt()
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
private fun tailScrollExtentPx(): Int =
|
|
1132
|
+
(if (structuralTail != null) structuralTailMeasuredScrollDistancePx()
|
|
1133
|
+
else measuredScrollDistancePx()) ?: logicalScrollDistancePx
|
|
1134
|
+
|
|
1135
|
+
private fun updateStructuralTailWindow() {
|
|
1136
|
+
val tail = structuralTail ?: return
|
|
1137
|
+
if (structuralTailFinalizing || structuralTailRowsInAdapterPx > 0) return
|
|
1138
|
+
val scroll = tailScrollExtentPx()
|
|
1139
|
+
val viewport = recyclerView.height
|
|
1140
|
+
if (viewport <= 0) return
|
|
1141
|
+
val y = recyclerView.top + recyclerView.paddingTop + structuralTailStartContentY - scroll
|
|
1142
|
+
val width = (recyclerView.width - recyclerView.paddingLeft - recyclerView.paddingRight).coerceAtLeast(0)
|
|
1143
|
+
val params = structuralTailLayer.layoutParams as FrameLayout.LayoutParams
|
|
1144
|
+
val left = recyclerView.left + recyclerView.paddingLeft
|
|
1145
|
+
if (params.width != width || params.height != structuralSpacerHeightPx ||
|
|
1146
|
+
params.leftMargin != left || params.topMargin != y) {
|
|
1147
|
+
params.width = width
|
|
1148
|
+
params.height = structuralSpacerHeightPx
|
|
1149
|
+
params.leftMargin = left
|
|
1150
|
+
params.topMargin = y
|
|
1151
|
+
structuralTailLayer.layoutParams = params
|
|
1152
|
+
}
|
|
1153
|
+
structuralTailLayer.visibility = VISIBLE
|
|
1154
|
+
val low = (-y - viewport).coerceAtLeast(0)
|
|
1155
|
+
val high = minOf(structuralSpacerHeightPx, -y + 2 * viewport)
|
|
1156
|
+
var first = 0
|
|
1157
|
+
var end = tail.items.size
|
|
1158
|
+
while (first < end) {
|
|
1159
|
+
val middle = (first + end) / 2
|
|
1160
|
+
if (tail.starts[middle] + tail.heights[middle] < low) first = middle + 1
|
|
1161
|
+
else end = middle
|
|
1162
|
+
}
|
|
1163
|
+
var last = first
|
|
1164
|
+
end = tail.items.size
|
|
1165
|
+
while (last < end) {
|
|
1166
|
+
val middle = (last + end) / 2
|
|
1167
|
+
if (tail.starts[middle] <= high) last = middle + 1
|
|
1168
|
+
else end = middle
|
|
1169
|
+
}
|
|
1170
|
+
if (last - first > 128) {
|
|
1171
|
+
finishStructuralTail(interrupted = false)
|
|
1172
|
+
return
|
|
1173
|
+
}
|
|
1174
|
+
tail.hosts.keys.filter { it < first || it >= last }.forEach { index ->
|
|
1175
|
+
val host = tail.hosts.remove(index) ?: return@forEach
|
|
1176
|
+
structuralTailLayer.removeView(host)
|
|
1177
|
+
host.recycle()
|
|
1178
|
+
host.dispose()
|
|
1179
|
+
}
|
|
1180
|
+
for (index in first until last) {
|
|
1181
|
+
if (tail.hosts.containsKey(index)) continue
|
|
1182
|
+
val item = tail.items[index]
|
|
1183
|
+
val host = NativeListRendererRegistry.create(reactContext, item.rendererKey.ordinal, fonts)
|
|
1184
|
+
host.listStyle = config?.listStyle
|
|
1185
|
+
host.bind(item, config?.theme, config?.layout ?: "linear", "vertical",
|
|
1186
|
+
tail.prefixCount + index, item.json.optBoolean("selected") ||
|
|
1187
|
+
(config?.selectedKeys?.contains(item.key) == true), adapter.checkboxState,
|
|
1188
|
+
useSourceScale = usesSelectorSourceScale)
|
|
1189
|
+
host.isClickable = false
|
|
1190
|
+
host.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
|
|
1191
|
+
structuralTailLayer.addView(host, FrameLayout.LayoutParams(width, tail.heights[index],
|
|
1192
|
+
Gravity.TOP or Gravity.LEFT).apply { topMargin = tail.starts[index] })
|
|
1193
|
+
tail.hosts[index] = host
|
|
1194
|
+
tail.peakHosts = maxOf(tail.peakHosts, tail.hosts.size)
|
|
1195
|
+
}
|
|
1196
|
+
val height = structuralSpacerHeightPx
|
|
1197
|
+
val needsMeasure = structuralTailLayer.isLayoutRequested ||
|
|
1198
|
+
structuralTailLayer.measuredWidth != width ||
|
|
1199
|
+
structuralTailLayer.measuredHeight != height ||
|
|
1200
|
+
tail.hosts.values.any { it.isLayoutRequested }
|
|
1201
|
+
if (needsMeasure) {
|
|
1202
|
+
structuralTailLayer.measure(
|
|
1203
|
+
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
1204
|
+
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY),
|
|
1205
|
+
)
|
|
1206
|
+
}
|
|
1207
|
+
if (needsMeasure || structuralTailLayer.left != left || structuralTailLayer.top != y ||
|
|
1208
|
+
structuralTailLayer.right != left + width || structuralTailLayer.bottom != y + height) {
|
|
1209
|
+
structuralTailLayer.layout(left, y, left + width, y + height)
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
private fun updateStructuralTailTarget() {
|
|
1214
|
+
val tail = structuralTail ?: return
|
|
1215
|
+
val disclosure = config?.disclosure
|
|
1216
|
+
val finalChrome = if (disclosure == null) 0 else
|
|
1217
|
+
slotHeights[5] + if (disclosure.expanded) slotHeights[4] + disclosureInsetExtentPx() else 0
|
|
1218
|
+
val leftHeight = recyclerView.paddingTop + structuralTailStartContentY + tail.finalExtent +
|
|
1219
|
+
finalChrome + footerNaturalHeightPx() + recyclerView.paddingBottom
|
|
1220
|
+
val sidecarHeight = if (sidecarLayout == null) 0 else
|
|
1221
|
+
recyclerView.paddingTop + slotHeights[3] + recyclerView.paddingBottom
|
|
1222
|
+
val maximum = (maxOf(leftHeight, sidecarHeight) - recyclerView.height).coerceAtLeast(0)
|
|
1223
|
+
var target = structuralTailStartOffset.coerceIn(0, maximum)
|
|
1224
|
+
if (tail.finalExtent == 0 && disclosure != null && slotHeights[5] > 0) {
|
|
1225
|
+
val toggleTop = recyclerView.paddingTop + structuralTailStartContentY
|
|
1226
|
+
val toggleBottom = toggleTop + slotHeights[5]
|
|
1227
|
+
val sticky = stickyHeaderHost.height
|
|
1228
|
+
val lower = toggleBottom - recyclerView.height + recyclerView.paddingBottom
|
|
1229
|
+
val upper = toggleTop - recyclerView.paddingTop - sticky
|
|
1230
|
+
target = if (lower <= upper) target.coerceIn(lower, upper) else upper
|
|
1231
|
+
} else if (tail.finalExtent == 0 && footerNaturalHeightPx() > 0) {
|
|
1232
|
+
val footerTop = recyclerView.paddingTop + structuralTailStartContentY
|
|
1233
|
+
val footerBottom = footerTop + footerNaturalHeightPx()
|
|
1234
|
+
val sticky = stickyHeaderHost.height
|
|
1235
|
+
val lower = footerBottom - recyclerView.height + recyclerView.paddingBottom
|
|
1236
|
+
val upper = footerTop - recyclerView.paddingTop - sticky
|
|
1237
|
+
target = if (lower <= upper) target.coerceIn(lower, upper) else upper
|
|
1238
|
+
}
|
|
1239
|
+
structuralTailTargetOffset = target.coerceIn(0, maximum)
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
private fun positionSettledDisclosureToggle() {
|
|
1243
|
+
if (recyclerView.width <= 0 || recyclerView.height <= 0 ||
|
|
1244
|
+
structureToggleAdapter.itemCount == 0 || slotHeights[5] <= 0 ||
|
|
1245
|
+
recyclerView.scrollState != RecyclerView.SCROLL_STATE_IDLE ||
|
|
1246
|
+
pendingScrollRequest != null || acceptedScrollRequest != null ||
|
|
1247
|
+
executingScrollCommand) return
|
|
1248
|
+
val togglePosition = headerSlotAdapter.itemCount + adapter.itemCount +
|
|
1249
|
+
emptySlotAdapter.itemCount + structuralSpacerAdapter.itemCount +
|
|
1250
|
+
expandedContentAdapter.itemCount
|
|
1251
|
+
val toggleTop = (recyclerView.paddingTop.toLong() +
|
|
1252
|
+
nativeListScrollPrefixPx(togglePosition, layoutManager.spanCount,
|
|
1253
|
+
layoutManager.spanSizeLookup::getSpanSize, ::structuralTailRowOrSlotExtentPx))
|
|
1254
|
+
.coerceIn(0L, Int.MAX_VALUE.toLong()).toInt()
|
|
1255
|
+
val stickyTop = recyclerView.paddingTop + stickyHeaderHost.height
|
|
1256
|
+
val lower = toggleTop + slotHeights[5] - recyclerView.height + recyclerView.paddingBottom
|
|
1257
|
+
val upper = toggleTop - stickyTop
|
|
1258
|
+
val sidecarBottom = if (sidecarLayout == null) 0 else
|
|
1259
|
+
recyclerView.paddingTop + slotHeights[3] + recyclerView.paddingBottom
|
|
1260
|
+
val leftExtent = (nativeListScrollPrefixPx(
|
|
1261
|
+
scrollContentAdapter.itemCount - sidecarSpacerAdapter.itemCount,
|
|
1262
|
+
layoutManager.spanCount, layoutManager.spanSizeLookup::getSpanSize,
|
|
1263
|
+
::structuralTailRowOrSlotExtentPx) +
|
|
1264
|
+
recyclerView.paddingTop + recyclerView.paddingBottom)
|
|
1265
|
+
.coerceIn(0L, Int.MAX_VALUE.toLong()).toInt()
|
|
1266
|
+
val maximum = (maxOf(leftExtent, sidecarBottom) - recyclerView.height).coerceAtLeast(0)
|
|
1267
|
+
val current = structuralTailMeasuredScrollDistancePx() ?: tailScrollExtentPx()
|
|
1268
|
+
val target = (if (lower <= upper) current.coerceIn(lower, upper) else upper)
|
|
1269
|
+
.coerceIn(0, maximum)
|
|
1270
|
+
val ownsCommand = !structuralTailCommandActive
|
|
1271
|
+
if (ownsCommand) {
|
|
1272
|
+
notifyScrollCommand(false, 0)
|
|
1273
|
+
notifyScrollCommand(false, 1)
|
|
1274
|
+
}
|
|
1275
|
+
try {
|
|
1276
|
+
recyclerView.scrollBy(0, target - current)
|
|
1277
|
+
relayoutRecyclerViewImmediately()
|
|
1278
|
+
} finally {
|
|
1279
|
+
if (ownsCommand) notifyScrollCommand(false, 2)
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
private fun finishStructuralTail(interrupted: Boolean) {
|
|
1284
|
+
val next = config ?: return
|
|
1285
|
+
if (structuralTail == null || structuralTailFinalizing) return
|
|
1286
|
+
structuralTailFinalizing = true
|
|
1287
|
+
val epoch = ++structuralTransitionEpoch
|
|
1288
|
+
pendingPreparingFooterFrame = pendingPreparingFooterFrame?.takeIf {
|
|
1289
|
+
it.footer === containerSlots.getOrNull(2)
|
|
1290
|
+
}?.copy(epoch = epoch, handoffOnly = true)
|
|
1291
|
+
structuralAnimator?.cancel()
|
|
1292
|
+
structuralAnimator = null
|
|
1293
|
+
recyclerView.itemAnimator = null
|
|
1294
|
+
structuralFooterHoldTop = null
|
|
1295
|
+
structuralFooterVisualStartTop = null
|
|
1296
|
+
structuralFooterVisualEndTop = null
|
|
1297
|
+
footerSlotAdapter.setVisualTranslationY(0f)
|
|
1298
|
+
if (interrupted && structuralTailCommandActive) {
|
|
1299
|
+
structuralTailCommandActive = false
|
|
1300
|
+
notifyScrollCommand(false, 2)
|
|
1301
|
+
}
|
|
1302
|
+
adapter.submitList(next.items) {
|
|
1303
|
+
if (epoch != structuralTransitionEpoch || disposed) return@submitList
|
|
1304
|
+
fun handoff() {
|
|
1305
|
+
if (epoch != structuralTransitionEpoch || disposed) return
|
|
1306
|
+
if (recyclerView.isComputingLayout) {
|
|
1307
|
+
recyclerView.post { handoff() }
|
|
1308
|
+
return
|
|
1309
|
+
}
|
|
1310
|
+
val acceptedPendingFooter = acceptPendingFooterValues(adjustTailStart = false)
|
|
1311
|
+
presentAcceptedDisclosureEndpoint(next)
|
|
1312
|
+
if (recyclerView.width <= 0 || recyclerView.height <= 0) {
|
|
1313
|
+
clearStructuralTail()
|
|
1314
|
+
if (acceptedPendingFooter) updateContainerSlots()
|
|
1315
|
+
if (structuralTailCommandActive) {
|
|
1316
|
+
structuralTailCommandActive = false
|
|
1317
|
+
notifyScrollCommand(false, 2)
|
|
1318
|
+
}
|
|
1319
|
+
if (next.disclosure != null) {
|
|
1320
|
+
disclosureTransitioning = false
|
|
1321
|
+
syncDisclosurePresentation()
|
|
1322
|
+
settleDisclosureCommand(if (interrupted) "interrupted" else "completed")
|
|
1323
|
+
}
|
|
1324
|
+
scheduleVisibleEvent()
|
|
1325
|
+
checkEndReached()
|
|
1326
|
+
return
|
|
1327
|
+
}
|
|
1328
|
+
structuralTailRowsInAdapterPx = 0
|
|
1329
|
+
setStructuralTailExtent(0)
|
|
1330
|
+
updateSidecarSpacerNow()
|
|
1331
|
+
relayoutRecyclerViewImmediately()
|
|
1332
|
+
if (epoch != structuralTransitionEpoch || disposed) return
|
|
1333
|
+
if (!interrupted && recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE) {
|
|
1334
|
+
val sidecarBottom = if (sidecarLayout == null) 0 else
|
|
1335
|
+
recyclerView.paddingTop + slotHeights[3] + recyclerView.paddingBottom
|
|
1336
|
+
val maximum = (maxOf(sidecarLeftExtentPx(), sidecarBottom) -
|
|
1337
|
+
recyclerView.height).coerceAtLeast(0)
|
|
1338
|
+
val target = structuralTailTargetOffset.coerceIn(0, maximum)
|
|
1339
|
+
recyclerView.scrollBy(0, target - tailScrollExtentPx())
|
|
1340
|
+
}
|
|
1341
|
+
if (epoch != structuralTransitionEpoch || disposed) return
|
|
1342
|
+
clearStructuralTail()
|
|
1343
|
+
if (acceptedPendingFooter) updateContainerSlots()
|
|
1344
|
+
performPendingScrollIfNeeded()
|
|
1345
|
+
if (structuralTailCommandActive) {
|
|
1346
|
+
structuralTailCommandActive = false
|
|
1347
|
+
notifyScrollCommand(false, 2)
|
|
1348
|
+
}
|
|
1349
|
+
if (next.disclosure != null) {
|
|
1350
|
+
disclosureTransitioning = false
|
|
1351
|
+
syncDisclosurePresentation()
|
|
1352
|
+
settleDisclosureCommand(if (interrupted) "interrupted" else "completed")
|
|
1353
|
+
}
|
|
1354
|
+
scheduleVisibleEvent()
|
|
1355
|
+
checkEndReached()
|
|
1356
|
+
}
|
|
1357
|
+
handoff()
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
private fun discardStructuralTail() {
|
|
1362
|
+
if (structuralTail == null) return
|
|
1363
|
+
structuralTransitionEpoch += 1
|
|
1364
|
+
structuralAnimator?.cancel()
|
|
1365
|
+
structuralAnimator = null
|
|
1366
|
+
if (structuralTailCommandActive) {
|
|
1367
|
+
structuralTailCommandActive = false
|
|
1368
|
+
notifyScrollCommand(false, 2)
|
|
1369
|
+
}
|
|
1370
|
+
clearStructuralTail()
|
|
1371
|
+
setStructuralSpacerHeight(0)
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
private fun holdStructuralTailUntilListCommit(): Int {
|
|
1375
|
+
val epoch = ++structuralTransitionEpoch
|
|
1376
|
+
pendingPreparingFooterFrame = pendingPreparingFooterFrame?.takeIf {
|
|
1377
|
+
it.footer === containerSlots.getOrNull(2)
|
|
1378
|
+
}?.copy(epoch = epoch, handoffOnly = true)
|
|
1379
|
+
structuralAnimator?.cancel()
|
|
1380
|
+
structuralAnimator = null
|
|
1381
|
+
structuralTailFinalizing = true
|
|
1382
|
+
if (structuralTailCommandActive) {
|
|
1383
|
+
structuralTailCommandActive = false
|
|
1384
|
+
notifyScrollCommand(false, 2)
|
|
1385
|
+
}
|
|
1386
|
+
return epoch
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
private fun completeHeldTailCommit(
|
|
1390
|
+
next: NativeListConfig,
|
|
1391
|
+
epoch: Int,
|
|
1392
|
+
completion: () -> Unit,
|
|
1393
|
+
positionToggle: Boolean,
|
|
1394
|
+
) {
|
|
1395
|
+
if (epoch != structuralTransitionEpoch || disposed) return
|
|
1396
|
+
if (recyclerView.isComputingLayout) {
|
|
1397
|
+
recyclerView.post { completeHeldTailCommit(next, epoch, completion, positionToggle) }
|
|
1398
|
+
return
|
|
1399
|
+
}
|
|
1400
|
+
val acceptedPendingFooter = acceptPendingFooterValues(adjustTailStart = false)
|
|
1401
|
+
presentAcceptedDisclosureEndpoint(next)
|
|
1402
|
+
structuralTailRowsInAdapterPx = 0
|
|
1403
|
+
setStructuralTailExtent(0)
|
|
1404
|
+
if (acceptedPendingFooter) updateContainerSlots()
|
|
1405
|
+
updateSidecarSpacerNow()
|
|
1406
|
+
relayoutRecyclerViewImmediately()
|
|
1407
|
+
if (positionToggle) positionSettledDisclosureToggle()
|
|
1408
|
+
if (epoch != structuralTransitionEpoch || disposed) return
|
|
1409
|
+
clearStructuralTail()
|
|
1410
|
+
disclosureTransitioning = false
|
|
1411
|
+
syncDisclosurePresentation()
|
|
1412
|
+
completion()
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
private fun applyStructuralTailFrame(tail: StructuralTail, epoch: Int, progress: Float): Boolean {
|
|
1416
|
+
if (epoch != structuralTransitionEpoch || disposed || structuralTail !== tail ||
|
|
1417
|
+
structuralTailFinalizing) return true
|
|
1418
|
+
if (recyclerView.isComputingLayout) return false
|
|
1419
|
+
val segmentProgress = if (structuralTailStartProgress >= 1f) 1f else
|
|
1420
|
+
((progress - structuralTailStartProgress) /
|
|
1421
|
+
(1f - structuralTailStartProgress)).coerceIn(0f, 1f)
|
|
1422
|
+
config?.disclosure?.let { disclosure ->
|
|
1423
|
+
val target = if (disclosure.expanded) 1f else 0f
|
|
1424
|
+
disclosureFraction = disclosureStartFraction +
|
|
1425
|
+
(target - disclosureStartFraction) * segmentProgress
|
|
1426
|
+
val contentProgress = if (disclosureContentStartProgress >= 1f) 1f else
|
|
1427
|
+
((progress - disclosureContentStartProgress) /
|
|
1428
|
+
(1f - disclosureContentStartProgress)).coerceIn(0f, 1f)
|
|
1429
|
+
val contentTarget = if (disclosure.expanded) slotHeights[4] else 0
|
|
1430
|
+
disclosurePresentedContentPx = (disclosureContentStartPx +
|
|
1431
|
+
(contentTarget - disclosureContentStartPx) * contentProgress).roundToInt()
|
|
1432
|
+
syncDisclosurePresentation()
|
|
1433
|
+
}
|
|
1434
|
+
setStructuralTailExtent((structuralTailStartExtentPx +
|
|
1435
|
+
(tail.finalExtent - structuralTailStartExtentPx) * segmentProgress).roundToInt())
|
|
1436
|
+
updateSidecarSpacerNow()
|
|
1437
|
+
relayoutRecyclerViewImmediately()
|
|
1438
|
+
if (epoch != structuralTransitionEpoch || disposed || structuralTail !== tail) return true
|
|
1439
|
+
if (!structuralScrollInterrupted && recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE) {
|
|
1440
|
+
val sidecarBottom = if (sidecarLayout == null) 0 else
|
|
1441
|
+
recyclerView.paddingTop + slotHeights[3] + recyclerView.paddingBottom
|
|
1442
|
+
val maximum = (maxOf(sidecarLeftExtentPx(), sidecarBottom) -
|
|
1443
|
+
recyclerView.height).coerceAtLeast(0)
|
|
1444
|
+
val desired = (structuralTailStartOffset +
|
|
1445
|
+
(structuralTailTargetOffset - structuralTailStartOffset) * segmentProgress)
|
|
1446
|
+
.roundToInt().coerceIn(0, maximum)
|
|
1447
|
+
val requestedScroll = desired - tailScrollExtentPx()
|
|
1448
|
+
recyclerView.scrollBy(0, requestedScroll)
|
|
1449
|
+
}
|
|
1450
|
+
updateStructuralTailWindow()
|
|
1451
|
+
return true
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
private fun commitPreparingFooterFrame(): Boolean {
|
|
1455
|
+
val pending = pendingPreparingFooterFrame ?: return true
|
|
1456
|
+
if (structuralTail == null || structuralTailRowsInAdapterPx <= 0 ||
|
|
1457
|
+
pending.epoch != structuralTransitionEpoch || disposed ||
|
|
1458
|
+
pending.footer !== containerSlots.getOrNull(2)) {
|
|
1459
|
+
if (pendingPreparingFooterFrame === pending) pendingPreparingFooterFrame = null
|
|
1460
|
+
return true
|
|
1461
|
+
}
|
|
1462
|
+
if (pending.handoffOnly || recyclerView.isComputingLayout) return false
|
|
1463
|
+
pendingPreparingFooterFrame = null
|
|
1464
|
+
if (pending.observedNativeHeight != null) {
|
|
1465
|
+
structuralFooterObservedNativeHeightPx = pending.observedNativeHeight
|
|
1466
|
+
} else {
|
|
1467
|
+
containerSlots.getOrNull(2)?.measuredHeight
|
|
1468
|
+
?.takeIf { it > 0 && it != pending.naturalHeight }
|
|
1469
|
+
?.let { structuralFooterObservedNativeHeightPx = it }
|
|
1470
|
+
}
|
|
1471
|
+
reconcileStructuralFooterHeight(pending.naturalHeight)
|
|
1472
|
+
acceptedFooterNaturalHeightPx = pending.naturalHeight
|
|
1473
|
+
for (index in 0..5) slotHeights[index] = pending.heights[index]
|
|
1474
|
+
updateContainerSlots()
|
|
1475
|
+
updateStructuralTailTarget()
|
|
1476
|
+
updateSidecarGeometry()
|
|
1477
|
+
updateSidecarSpacerNow()
|
|
1478
|
+
relayoutRecyclerViewImmediately()
|
|
1479
|
+
if (pending.epoch != structuralTransitionEpoch || disposed ||
|
|
1480
|
+
pending.footer !== containerSlots.getOrNull(2)) {
|
|
1481
|
+
return pendingPreparingFooterFrame == null
|
|
1482
|
+
}
|
|
1483
|
+
val sidecarBottom = if (sidecarLayout == null) 0 else
|
|
1484
|
+
recyclerView.paddingTop + slotHeights[3] + recyclerView.paddingBottom
|
|
1485
|
+
val maximum = (maxOf(sidecarLeftExtentPx(), sidecarBottom) -
|
|
1486
|
+
recyclerView.height).coerceAtLeast(0)
|
|
1487
|
+
val desired = pending.ownerOffset.coerceIn(0, maximum)
|
|
1488
|
+
recyclerView.scrollBy(0, desired - tailScrollExtentPx())
|
|
1489
|
+
relayoutRecyclerViewImmediately()
|
|
1490
|
+
updateStructuralTailWindow()
|
|
1491
|
+
return pendingPreparingFooterFrame == null
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
private fun acceptPendingFooterValues(adjustTailStart: Boolean): Boolean {
|
|
1495
|
+
val pending = pendingPreparingFooterFrame ?: return false
|
|
1496
|
+
if (pending.footer !== containerSlots.getOrNull(2) || disposed ||
|
|
1497
|
+
pending.epoch != structuralTransitionEpoch) {
|
|
1498
|
+
pendingPreparingFooterFrame = null
|
|
1499
|
+
return false
|
|
1500
|
+
}
|
|
1501
|
+
val latestNativeHeight = if (pending.observedNativeHeight != null)
|
|
1502
|
+
pending.footer?.measuredHeight?.takeIf { it > 0 } else null
|
|
1503
|
+
val naturalHeight = if (latestNativeHeight != null &&
|
|
1504
|
+
latestNativeHeight != pending.observedNativeHeight) {
|
|
1505
|
+
(ceil(latestNativeHeight / density.toDouble()) * density).roundToInt()
|
|
1506
|
+
} else pending.naturalHeight
|
|
1507
|
+
if (adjustTailStart) {
|
|
1508
|
+
val previousHeight = structuralFooterCompensatedHeightPx ?:
|
|
1509
|
+
acceptedFooterNaturalHeightPx ?: slotHeights[2]
|
|
1510
|
+
val delta = previousHeight - naturalHeight
|
|
1511
|
+
structuralTailSignedExtentPx += delta
|
|
1512
|
+
structuralTailStartExtentPx += delta
|
|
1513
|
+
}
|
|
1514
|
+
structuralFooterObservedNativeHeightPx = latestNativeHeight ?:
|
|
1515
|
+
containerSlots.getOrNull(2)?.measuredHeight?.takeIf {
|
|
1516
|
+
it > 0 && it != naturalHeight
|
|
1517
|
+
}
|
|
1518
|
+
acceptedFooterNaturalHeightPx = naturalHeight
|
|
1519
|
+
structuralFooterCompensatedHeightPx = naturalHeight
|
|
1520
|
+
for (index in 0..5) slotHeights[index] = pending.heights[index]
|
|
1521
|
+
pendingPreparingFooterFrame = null
|
|
1522
|
+
return true
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
private fun startPrefixTailTransition(
|
|
1526
|
+
previous: NativeListConfig,
|
|
1527
|
+
next: NativeListConfig,
|
|
1528
|
+
transition: NativeListConfig.StructuralTransition,
|
|
1529
|
+
prefixCount: Int,
|
|
1530
|
+
) {
|
|
1531
|
+
val previousTail = structuralTail
|
|
1532
|
+
val carriedExtent = previousTail?.let { structuralTailSignedExtentPx }
|
|
1533
|
+
val startOffset = structuralTailMeasuredScrollDistancePx() ?: tailScrollExtentPx()
|
|
1534
|
+
if (previousTail == null) {
|
|
1535
|
+
cancelStructuralTransition()
|
|
1536
|
+
} else {
|
|
1537
|
+
structuralTransitionEpoch += 1
|
|
1538
|
+
structuralAnimator?.cancel()
|
|
1539
|
+
structuralAnimator = null
|
|
1540
|
+
if (structuralTailCommandActive) {
|
|
1541
|
+
structuralTailCommandActive = false
|
|
1542
|
+
notifyScrollCommand(false, 2)
|
|
1543
|
+
}
|
|
1544
|
+
// The old presentation remains visible until the new prefix and spacer commit together.
|
|
1545
|
+
structuralTailPreparing = true
|
|
1546
|
+
}
|
|
1547
|
+
val epoch = structuralTransitionEpoch
|
|
1548
|
+
val collapsing = previous.items.size > next.items.size
|
|
1549
|
+
val items = (if (collapsing) previous.items else next.items)
|
|
1550
|
+
.drop(prefixCount).toMutableList()
|
|
1551
|
+
val heights = IntArray(items.size) { explicitTailRowHeight(items[it]) }
|
|
1552
|
+
val spacings = IntArray(items.size) {
|
|
1553
|
+
explicitTailRowSpacingPx(items[it], next.itemSpacing)
|
|
1554
|
+
}
|
|
1555
|
+
val tail = StructuralTail(items, heights, spacings, prefixCount,
|
|
1556
|
+
if (collapsing) 0 else heights.sum() + spacings.sum())
|
|
1557
|
+
val startExtent = carriedExtent ?: if (collapsing) tail.extent else 0
|
|
1558
|
+
disclosureStartFraction = disclosureFraction
|
|
1559
|
+
if (next.disclosure != null) {
|
|
1560
|
+
disclosureContentStartPx = disclosurePresentedContentPx
|
|
1561
|
+
disclosureContentStartProgress = 0f
|
|
1562
|
+
disclosureTransitioning = true
|
|
1563
|
+
}
|
|
1564
|
+
val startContentY = (if (headerSlotAdapter.itemCount > 0) slotHeights[0] else 0) +
|
|
1565
|
+
next.items.take(prefixCount).sumOf {
|
|
1566
|
+
explicitTailRowHeight(it) + explicitTailRowSpacingPx(it, next.itemSpacing)
|
|
1567
|
+
}
|
|
1568
|
+
if (previousTail == null) {
|
|
1569
|
+
structuralTail = tail
|
|
1570
|
+
structuralTailRowsInAdapterPx = if (collapsing) tail.extent else 0
|
|
1571
|
+
structuralTailStartExtentPx = startExtent
|
|
1572
|
+
structuralTailStartProgress = 0f
|
|
1573
|
+
structuralTailStartContentY = startContentY
|
|
1574
|
+
structuralTailStartOffset = startOffset
|
|
1575
|
+
structuralTailTargetOffset = startOffset
|
|
1576
|
+
setStructuralTailExtent(startExtent)
|
|
1577
|
+
}
|
|
1578
|
+
structuralScrollInterrupted = false
|
|
1579
|
+
structuralFooterCompensatedHeightPx = null
|
|
1580
|
+
recyclerView.itemAnimator = null
|
|
1581
|
+
acceptRowIndex(next, previous)
|
|
1582
|
+
config = next
|
|
1583
|
+
pruneMeasuredRowExtents(next.items)
|
|
1584
|
+
updateContainerSlots()
|
|
1585
|
+
usesSelectorSourceScale = next.items.any { it.usesSelectorSourceScale }
|
|
1586
|
+
adapter.usesSelectorSourceScale = usesSelectorSourceScale
|
|
1587
|
+
adapter.theme = next.theme
|
|
1588
|
+
adapter.listStyle = next.listStyle
|
|
1589
|
+
adapter.layout = next.layout
|
|
1590
|
+
adapter.orientation = next.orientation
|
|
1591
|
+
adapter.selectedKeys = next.selectedKeys
|
|
1592
|
+
if (previous.generation != next.generation) endReachedGeneration = null
|
|
1593
|
+
configureSectionIndex(next)
|
|
1594
|
+
updateLayout(next)
|
|
1595
|
+
bindFooter(next)
|
|
1596
|
+
updateReordering(next)
|
|
1597
|
+
refreshLayout.isEnabled = next.pullToRefresh
|
|
1598
|
+
refreshLayout.setDistanceToTriggerSync(
|
|
1599
|
+
next.refreshTriggerDistance?.let { (it * density).roundToInt() }
|
|
1600
|
+
?: defaultRefreshTriggerDistancePx,
|
|
1601
|
+
)
|
|
1602
|
+
refreshLayout.isRefreshing = next.refreshing
|
|
1603
|
+
structuralTailCommandActive = true
|
|
1604
|
+
notifyScrollCommand(false, 0)
|
|
1605
|
+
notifyScrollCommand(false, 1)
|
|
1606
|
+
adapter.submitList(next.items.take(prefixCount)) {
|
|
1607
|
+
fun commitPrefix() {
|
|
1608
|
+
if (epoch != structuralTransitionEpoch || disposed ||
|
|
1609
|
+
structuralTail !== (previousTail ?: tail) || structuralTailFinalizing) return
|
|
1610
|
+
if (recyclerView.isComputingLayout) {
|
|
1611
|
+
recyclerView.post { commitPrefix() }
|
|
1612
|
+
return
|
|
1613
|
+
}
|
|
1614
|
+
if (previousTail != null) {
|
|
1615
|
+
clearStructuralTail()
|
|
1616
|
+
structuralTail = tail
|
|
1617
|
+
structuralTailStartExtentPx = startExtent
|
|
1618
|
+
structuralTailStartProgress = 0f
|
|
1619
|
+
structuralTailStartContentY = startContentY
|
|
1620
|
+
structuralTailStartOffset = startOffset
|
|
1621
|
+
structuralTailTargetOffset = startOffset
|
|
1622
|
+
}
|
|
1623
|
+
acceptPendingFooterValues(adjustTailStart = true)
|
|
1624
|
+
structuralTailPreparing = false
|
|
1625
|
+
structuralTailRowsInAdapterPx = 0
|
|
1626
|
+
setStructuralTailExtent(structuralTailStartExtentPx)
|
|
1627
|
+
updateStructuralTailTarget()
|
|
1628
|
+
if (recyclerView.width <= 0 || recyclerView.height <= 0) {
|
|
1629
|
+
finishStructuralTail(interrupted = false)
|
|
1630
|
+
return
|
|
1631
|
+
}
|
|
1632
|
+
applyStructuralTailFrame(tail, epoch, 0f)
|
|
1633
|
+
if (epoch != structuralTransitionEpoch || disposed || structuralTail !== tail ||
|
|
1634
|
+
structuralTailFinalizing) return
|
|
1635
|
+
var lastAppliedProgress = 0f
|
|
1636
|
+
var frameScheduled = false
|
|
1637
|
+
fun applyProgress(progress: Float) {
|
|
1638
|
+
if (epoch != structuralTransitionEpoch || disposed || structuralTail !== tail ||
|
|
1639
|
+
structuralTailFinalizing) return
|
|
1640
|
+
val nextProgress = maxOf(progress, lastAppliedProgress)
|
|
1641
|
+
if (!applyStructuralTailFrame(tail, epoch, nextProgress)) {
|
|
1642
|
+
if (!frameScheduled) {
|
|
1643
|
+
frameScheduled = true
|
|
1644
|
+
recyclerView.post {
|
|
1645
|
+
frameScheduled = false
|
|
1646
|
+
val current = structuralAnimator?.animatedValue as? Float ?: nextProgress
|
|
1647
|
+
applyProgress(current)
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
return
|
|
1651
|
+
}
|
|
1652
|
+
lastAppliedProgress = nextProgress
|
|
1653
|
+
}
|
|
1654
|
+
structuralAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
|
|
1655
|
+
duration = transition.durationMs
|
|
1656
|
+
interpolator = android.view.animation.AccelerateDecelerateInterpolator()
|
|
1657
|
+
addUpdateListener { animation ->
|
|
1658
|
+
if (epoch != structuralTransitionEpoch || disposed || structuralTail !== tail) return@addUpdateListener
|
|
1659
|
+
if (Build.VERSION.SDK_INT >= 26 && !ValueAnimator.areAnimatorsEnabled()) {
|
|
1660
|
+
finishStructuralTail(interrupted = false)
|
|
1661
|
+
return@addUpdateListener
|
|
1662
|
+
}
|
|
1663
|
+
applyProgress(animation.animatedValue as Float)
|
|
1664
|
+
}
|
|
1665
|
+
addListener(object : AnimatorListenerAdapter() {
|
|
1666
|
+
override fun onAnimationEnd(animation: Animator) {
|
|
1667
|
+
if (epoch == structuralTransitionEpoch && structuralTail === tail)
|
|
1668
|
+
finishStructuralTail(interrupted = structuralScrollInterrupted)
|
|
1669
|
+
}
|
|
1670
|
+
})
|
|
1671
|
+
start()
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
commitPrefix()
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
private fun updatePrefixTailSnapshot(
|
|
1679
|
+
previous: NativeListConfig,
|
|
1680
|
+
next: NativeListConfig,
|
|
1681
|
+
acceptIndex: Boolean = false,
|
|
1682
|
+
): Boolean {
|
|
1683
|
+
val tail = structuralTail ?: return false
|
|
1684
|
+
if (structuralTailPreparing || structuralTailFinalizing ||
|
|
1685
|
+
structuralTailRowsInAdapterPx > 0 ||
|
|
1686
|
+
previous.items.size != next.items.size ||
|
|
1687
|
+
previous.disclosure != next.disclosure ||
|
|
1688
|
+
previous.layout != next.layout || previous.orientation != next.orientation ||
|
|
1689
|
+
previous.theme?.toString() != next.theme?.toString() ||
|
|
1690
|
+
previous.listStyle?.toString() != next.listStyle?.toString() ||
|
|
1691
|
+
previous.itemSpacing != next.itemSpacing ||
|
|
1692
|
+
previous.contentPadding != next.contentPadding ||
|
|
1693
|
+
previous.contentPaddingTop != next.contentPaddingTop ||
|
|
1694
|
+
previous.contentPaddingBottom != next.contentPaddingBottom ||
|
|
1695
|
+
previous.contentPaddingHorizontal != next.contentPaddingHorizontal) return false
|
|
1696
|
+
for (index in next.items.indices) {
|
|
1697
|
+
val before = previous.items[index]
|
|
1698
|
+
val after = next.items[index]
|
|
1699
|
+
if (before.key != after.key || before.rendererKey != after.rendererKey ||
|
|
1700
|
+
!after.hasExplicitHeight || explicitTailRowHeight(before) != explicitTailRowHeight(after) ||
|
|
1701
|
+
explicitTailRowSpacingPx(before, previous.itemSpacing) !=
|
|
1702
|
+
explicitTailRowSpacingPx(after, next.itemSpacing))
|
|
1703
|
+
return false
|
|
1704
|
+
}
|
|
1705
|
+
if (acceptIndex) acceptRowIndex(next, previous)
|
|
1706
|
+
config = next
|
|
1707
|
+
adapter.selectedKeys = next.selectedKeys
|
|
1708
|
+
adapter.submitList(next.items.take(tail.prefixCount)) {
|
|
1709
|
+
if (structuralTail !== tail || disposed) return@submitList
|
|
1710
|
+
scheduleVisibleEvent()
|
|
1711
|
+
}
|
|
1712
|
+
if (tail.finalExtent > 0) {
|
|
1713
|
+
for (index in tail.items.indices) {
|
|
1714
|
+
val item = next.items[tail.prefixCount + index]
|
|
1715
|
+
if (tail.items[index].content == item.content) continue
|
|
1716
|
+
tail.items[index] = item
|
|
1717
|
+
tail.hosts[index]?.let { host ->
|
|
1718
|
+
host.bind(item, next.theme, next.layout, "vertical", tail.prefixCount + index,
|
|
1719
|
+
item.json.optBoolean("selected") || next.selectedKeys.contains(item.key),
|
|
1720
|
+
adapter.checkboxState, useSourceScale = usesSelectorSourceScale)
|
|
1721
|
+
host.isClickable = false
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
bindFooter(next)
|
|
1726
|
+
updateStructuralTailTarget()
|
|
1727
|
+
return true
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
private fun updateStructuralFooterHoldPosition() {
|
|
1731
|
+
val targetTop = structuralFooterHoldTop ?: return
|
|
1732
|
+
if (footerSlotAdapter.itemCount == 0) return
|
|
1733
|
+
val footerPosition = scrollContentAdapter.itemCount - 1 - sidecarSpacerAdapter.itemCount
|
|
1734
|
+
layoutManager.findViewByPosition(footerPosition)?.let { footer ->
|
|
1735
|
+
footerSlotAdapter.setVisualTranslationY((targetTop - layoutManager.getDecoratedTop(footer)).toFloat())
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
private fun updateStructuralFooterVisualPosition() {
|
|
1740
|
+
val startTop = structuralFooterVisualStartTop ?: return
|
|
1741
|
+
val endTop = structuralFooterVisualEndTop ?: return
|
|
1742
|
+
if (footerSlotAdapter.itemCount == 0) return
|
|
1743
|
+
val footerPosition = scrollContentAdapter.itemCount - 1 - sidecarSpacerAdapter.itemCount
|
|
1744
|
+
layoutManager.findViewByPosition(footerPosition)?.let { footer ->
|
|
1745
|
+
val desiredTop = startTop + (endTop - startTop) * structuralFooterVisualProgress
|
|
1746
|
+
footerSlotAdapter.setVisualTranslationY(desiredTop - layoutManager.getDecoratedTop(footer))
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
private fun cancelStructuralTransition() {
|
|
1751
|
+
if (structuralTail != null) discardStructuralTail()
|
|
1752
|
+
structuralTransitionEpoch += 1
|
|
1753
|
+
pendingStructuralTransition = null
|
|
1754
|
+
pendingStructuralStartScheduled = false
|
|
1755
|
+
structuralAnimator?.cancel()
|
|
1756
|
+
structuralAnimator = null
|
|
1757
|
+
recyclerView.itemAnimator = null
|
|
1758
|
+
setStructuralSpacerHeight(0)
|
|
1759
|
+
footerSlotAdapter.setVisualTranslationY(0f)
|
|
1760
|
+
structuralFooterHoldTop = null
|
|
1761
|
+
structuralFooterVisualStartTop = null
|
|
1762
|
+
structuralFooterVisualEndTop = null
|
|
1763
|
+
structuralFooterVisualProgress = 0f
|
|
1764
|
+
structuralFooterCompensatedHeightPx = null
|
|
1765
|
+
structuralStartSpacerPx = 0
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
private fun startPendingStructuralTransition() {
|
|
1769
|
+
val pending = pendingStructuralTransition ?: return
|
|
1770
|
+
if (pending.epoch != structuralTransitionEpoch || disposed || pendingStructuralStartScheduled) return
|
|
1771
|
+
val expected = config?.items?.map { it.key } ?: return
|
|
1772
|
+
if (adapter.currentList.map { it.key } != expected) return
|
|
1773
|
+
pendingStructuralStartScheduled = true
|
|
1774
|
+
recyclerView.postOnAnimation {
|
|
1775
|
+
if (pending.epoch != structuralTransitionEpoch) return@postOnAnimation
|
|
1776
|
+
pendingStructuralStartScheduled = false
|
|
1777
|
+
val latest = pendingStructuralTransition
|
|
1778
|
+
if (latest?.epoch == pending.epoch && !disposed) {
|
|
1779
|
+
if (recyclerView.isComputingLayout) {
|
|
1780
|
+
startPendingStructuralTransition()
|
|
1781
|
+
return@postOnAnimation
|
|
1782
|
+
}
|
|
1783
|
+
alignPendingStructuralAnchor(latest)
|
|
1784
|
+
if (latest.anchorTop != null || (latest.collapsing && latest.footerStartTop != null)) {
|
|
1785
|
+
relayoutRecyclerViewImmediately()
|
|
1786
|
+
}
|
|
1787
|
+
pendingStructuralTransition = null
|
|
1788
|
+
startStructuralTransition(
|
|
1789
|
+
latest.transition, latest.epoch, latest.anchorKey, latest.anchorTop,
|
|
1790
|
+
latest.footerStartTop,
|
|
1791
|
+
)
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
private fun alignPendingStructuralFooter(pending: PendingStructuralTransition): Boolean {
|
|
1797
|
+
val top = pending.footerStartTop ?: return false
|
|
1798
|
+
if (!pending.collapsing || !pending.spacerInserted || footerSlotAdapter.itemCount == 0 || structuralScrollInterrupted) return false
|
|
1799
|
+
if (pending.footerAnchorAppliedTop == top) return false
|
|
1800
|
+
val footerPosition = scrollContentAdapter.itemCount - 1 - sidecarSpacerAdapter.itemCount
|
|
1801
|
+
layoutManager.scrollToPositionWithOffset(footerPosition, top - recyclerView.paddingTop)
|
|
1802
|
+
pendingStructuralTransition = pending.copy(footerAnchorAppliedTop = top)
|
|
1803
|
+
return true
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
private fun alignPendingStructuralRow(pending: PendingStructuralTransition): Boolean {
|
|
1807
|
+
val key = pending.anchorKey ?: return false
|
|
1808
|
+
val top = pending.anchorTop ?: return false
|
|
1809
|
+
if (structuralScrollInterrupted || pending.rowAnchorAppliedTop == top) return false
|
|
1810
|
+
val desiredItems = config?.items ?: return false
|
|
1811
|
+
val rowIndex = desiredItems.indexOfFirst { it.key == key }
|
|
1812
|
+
if (rowIndex < 0 || adapter.itemCount != desiredItems.size ||
|
|
1813
|
+
adapter.currentList.getOrNull(rowIndex)?.key != key) return false
|
|
1814
|
+
layoutManager.scrollToPositionWithOffset(scrollPositionForRow(rowIndex), top - recyclerView.paddingTop)
|
|
1815
|
+
pendingStructuralTransition = pending.copy(rowAnchorAppliedTop = top)
|
|
1816
|
+
return true
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
private fun alignPendingStructuralAnchor(pending: PendingStructuralTransition) {
|
|
1820
|
+
if (pending.anchorTop != null) alignPendingStructuralRow(pending)
|
|
1821
|
+
else alignPendingStructuralFooter(pending)
|
|
1822
|
+
}
|
|
1823
|
+
|
|
357
1824
|
private fun measuredScrollDistancePx(): Int? {
|
|
358
1825
|
if (layoutManager.orientation != RecyclerView.VERTICAL) return null
|
|
359
1826
|
val first = layoutManager.findFirstVisibleItemPosition()
|
|
@@ -527,6 +1994,39 @@ class NativeListView(
|
|
|
527
1994
|
override fun onDetachedFromWindow() { down?.recycle(); down = null; super.onDetachedFromWindow() }
|
|
528
1995
|
}
|
|
529
1996
|
private val stickyPreDraw = android.view.ViewTreeObserver.OnPreDrawListener {
|
|
1997
|
+
if (!commitPreparingFooterFrame()) {
|
|
1998
|
+
if (pendingPreparingFooterFrame?.handoffOnly != true) schedulePreparingFooterFrame()
|
|
1999
|
+
return@OnPreDrawListener false
|
|
2000
|
+
}
|
|
2001
|
+
if (pendingStructuralTransition != null || structuralAnimator != null || structuralTail != null) {
|
|
2002
|
+
val measuredHeightPx = containerSlots.getOrNull(2)?.measuredHeight ?: 0
|
|
2003
|
+
if (measuredHeightPx > 0 && measuredHeightPx != structuralFooterObservedNativeHeightPx) {
|
|
2004
|
+
// React layout can change the footer's contents before its onLayout height reaches JS.
|
|
2005
|
+
val roundedDip = ceil(measuredHeightPx / density.toDouble())
|
|
2006
|
+
val naturalHeight = (roundedDip * density).roundToInt()
|
|
2007
|
+
if (structuralTail != null && config?.disclosure != null &&
|
|
2008
|
+
naturalHeight != footerNaturalHeightPx()) {
|
|
2009
|
+
structuralFooterObservedNativeHeightPx = measuredHeightPx
|
|
2010
|
+
acceptedFooterNaturalHeightPx = naturalHeight
|
|
2011
|
+
structuralFooterCompensatedHeightPx = null
|
|
2012
|
+
finishStructuralTail(interrupted = true)
|
|
2013
|
+
return@OnPreDrawListener false
|
|
2014
|
+
}
|
|
2015
|
+
if (structuralTail != null && structuralTailRowsInAdapterPx > 0 &&
|
|
2016
|
+
!structuralTailFinalizing) {
|
|
2017
|
+
enqueuePreparingFooterFrame(slotHeights.copyOf(), naturalHeight,
|
|
2018
|
+
observedNativeHeight = measuredHeightPx)
|
|
2019
|
+
if (pendingPreparingFooterFrame != null) return@OnPreDrawListener false
|
|
2020
|
+
} else {
|
|
2021
|
+
structuralFooterObservedNativeHeightPx = measuredHeightPx
|
|
2022
|
+
reconcileStructuralFooterHeight(naturalHeight)
|
|
2023
|
+
acceptedFooterNaturalHeightPx = naturalHeight
|
|
2024
|
+
if (structuralTail == null || structuralTailFinalizing) syncFooterPresentation()
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
if (structuralTail != null && !structuralTailFinalizing) layoutFooterPresentationHostNow()
|
|
2029
|
+
if (config?.disclosure != null) layoutExpandedContentHostNow()
|
|
530
2030
|
updateStickyHeader()
|
|
531
2031
|
finishScrollCommandIfSettled()
|
|
532
2032
|
true
|
|
@@ -572,7 +2072,6 @@ class NativeListView(
|
|
|
572
2072
|
private var dispatchingScrollRequest: ScrollRequest? = null
|
|
573
2073
|
private var commandLayoutRequest: ScrollRequest? = null
|
|
574
2074
|
private var commandAlignmentRequest: ScrollRequest? = null
|
|
575
|
-
private var commandFinishDiagnosticReason: String? = null
|
|
576
2075
|
private var readingIntentEpoch = 0L
|
|
577
2076
|
private var pendingBodyStartEpoch: Long? = null
|
|
578
2077
|
private val actionAnchorInstanceId = UUID.randomUUID().toString()
|
|
@@ -584,6 +2083,17 @@ class NativeListView(
|
|
|
584
2083
|
private var lastMarketPaginationAnchorLogAtMs = 0L
|
|
585
2084
|
private var disposed = false
|
|
586
2085
|
private var resourcesDisposed = false
|
|
2086
|
+
private var acceptedKeyIndex: Map<String, Int>? = null
|
|
2087
|
+
|
|
2088
|
+
private fun acceptRowIndex(next: NativeListConfig, previous: NativeListConfig?) {
|
|
2089
|
+
if (previous != null && acceptedKeyIndex != null &&
|
|
2090
|
+
previous.generation == next.generation && previous.items.size == next.items.size &&
|
|
2091
|
+
previous.items.indices.all { index ->
|
|
2092
|
+
previous.items[index].key == next.items[index].key &&
|
|
2093
|
+
previous.items[index].type == next.items[index].type
|
|
2094
|
+
}) return
|
|
2095
|
+
acceptedKeyIndex = next.items.withIndex().associate { it.value.key to it.index }
|
|
2096
|
+
}
|
|
587
2097
|
|
|
588
2098
|
init {
|
|
589
2099
|
orientation = VERTICAL
|
|
@@ -595,18 +2105,20 @@ class NativeListView(
|
|
|
595
2105
|
recyclerView.addItemDecoration(reorderPlaceholderDecoration)
|
|
596
2106
|
// OneKey patch: full-width selector header backgrounds do not change row content insets.
|
|
597
2107
|
recyclerView.addItemDecoration(SelectorBackgroundDecoration(adapter))
|
|
598
|
-
recyclerView.addItemDecoration(ContainerSlotInsetDecoration { slotInsetPx })
|
|
599
|
-
if (homeOffsetDiagnosticEnabled) layoutManager.onDiagnosticLayout = ::homeOffsetDiagnostic
|
|
2108
|
+
recyclerView.addItemDecoration(ContainerSlotInsetDecoration { slotInsetPx to slotRightInsetPx })
|
|
600
2109
|
layoutManager.onBeforeDataLayout = {
|
|
601
2110
|
if (pendingBodyStartEpoch == readingIntentEpoch && adapter.itemCount > 0 && pendingScrollRequest == null) {
|
|
602
2111
|
layoutManager.scrollToPositionWithOffset(0, 0)
|
|
603
2112
|
pendingBodyStartEpoch = null
|
|
604
|
-
homeOffsetDiagnostic("first-fill-body-start")
|
|
605
2113
|
}
|
|
2114
|
+
pendingStructuralTransition?.let(::alignPendingStructuralAnchor)
|
|
606
2115
|
}
|
|
607
2116
|
layoutManager.onChildLaidOut = ::recordLaidOutRowExtent
|
|
608
2117
|
layoutManager.onLayoutPassCompleted = {
|
|
2118
|
+
if (structuralFooterHoldTop != null) updateStructuralFooterHoldPosition()
|
|
2119
|
+
else if (structuralAnimator != null) updateStructuralFooterVisualPosition()
|
|
609
2120
|
emitScrollPositionThresholdIfNeeded(resync = true)
|
|
2121
|
+
updateSidecarPosition(resync = true)
|
|
610
2122
|
if (containerSlotRelayoutPending) scheduleContainerSlotRelayout()
|
|
611
2123
|
if (commandLayoutRequest === acceptedScrollRequest) commandLayoutRequest = null
|
|
612
2124
|
finishScrollCommandIfSettled()
|
|
@@ -618,7 +2130,7 @@ class NativeListView(
|
|
|
618
2130
|
val rowPosition = rowPositionForScroll(position)
|
|
619
2131
|
if (rowPosition !in 0 until adapter.itemCount) return layoutManager.spanCount
|
|
620
2132
|
val type = adapter.itemAt(rowPosition)?.type
|
|
621
|
-
return if (type == "sectionHeader" || type == "system" || type == "action") {
|
|
2133
|
+
return if (type == "sectionHeader" || type == "system" || type == "action" || type == "buttonGroup") {
|
|
622
2134
|
layoutManager.spanCount
|
|
623
2135
|
} else {
|
|
624
2136
|
1
|
|
@@ -636,6 +2148,9 @@ class NativeListView(
|
|
|
636
2148
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
637
2149
|
),
|
|
638
2150
|
)
|
|
2151
|
+
contentContainer.addView(structuralTailLayer, FrameLayout.LayoutParams(0, 0, Gravity.TOP or Gravity.LEFT))
|
|
2152
|
+
contentContainer.addView(sidecarHost,
|
|
2153
|
+
FrameLayout.LayoutParams(0, 0, Gravity.TOP or Gravity.RIGHT))
|
|
639
2154
|
stickyHeaderHost.visibility = GONE
|
|
640
2155
|
stickyHeaderHost.addView(stickyHeaderView, FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT))
|
|
641
2156
|
contentContainer.addView(stickyHeaderHost, FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.TOP))
|
|
@@ -700,6 +2215,9 @@ class NativeListView(
|
|
|
700
2215
|
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
|
701
2216
|
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
|
702
2217
|
if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
|
|
2218
|
+
if (structuralTail != null) finishStructuralTail(interrupted = true)
|
|
2219
|
+
if (structuralAnimator != null || pendingStructuralTransition != null)
|
|
2220
|
+
structuralScrollInterrupted = true
|
|
703
2221
|
cancelExecutingScrollCommand()
|
|
704
2222
|
readingIntentEpoch++
|
|
705
2223
|
pendingBodyStartEpoch = null
|
|
@@ -724,6 +2242,21 @@ class NativeListView(
|
|
|
724
2242
|
|
|
725
2243
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
|
726
2244
|
logicalScrollDistancePx = (logicalScrollDistancePx + dy).coerceAtLeast(0)
|
|
2245
|
+
if (structuralScrollInterrupted && dy != 0) {
|
|
2246
|
+
if (pendingStructuralTransition != null) {
|
|
2247
|
+
pendingStructuralTransition = pendingStructuralTransition?.let {
|
|
2248
|
+
it.copy(footerStartTop = it.footerStartTop?.minus(dy))
|
|
2249
|
+
}
|
|
2250
|
+
structuralFooterHoldTop = structuralFooterHoldTop?.minus(dy)
|
|
2251
|
+
updateStructuralFooterHoldPosition()
|
|
2252
|
+
} else if (structuralAnimator != null) {
|
|
2253
|
+
structuralFooterVisualStartTop = structuralFooterVisualStartTop?.minus(dy)
|
|
2254
|
+
structuralFooterVisualEndTop = structuralFooterVisualEndTop?.minus(dy)
|
|
2255
|
+
updateStructuralFooterVisualPosition()
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
updateSidecarPosition()
|
|
2259
|
+
if (structuralTail != null) updateStructuralTailWindow()
|
|
727
2260
|
emitScrollPositionThresholdIfNeeded(resync = dx == 0 && dy == 0)
|
|
728
2261
|
if (dx != 0 || dy != 0) invalidateActionAnchor("scroll")
|
|
729
2262
|
syncSectionIndexToVisibleRows()
|
|
@@ -781,6 +2314,11 @@ class NativeListView(
|
|
|
781
2314
|
}
|
|
782
2315
|
val nextWidth = right - left
|
|
783
2316
|
val nextHeight = bottom - top
|
|
2317
|
+
val widthChanged = lastLayoutWidth != nextWidth
|
|
2318
|
+
val directionChanged = layoutDirection != lastLayoutDirection
|
|
2319
|
+
if (structuralTail != null && lastLayoutWidth >= 0 &&
|
|
2320
|
+
(widthChanged || lastLayoutHeight != nextHeight || directionChanged))
|
|
2321
|
+
finishStructuralTail(interrupted = true)
|
|
784
2322
|
if (
|
|
785
2323
|
lastLayoutWidth >= 0 &&
|
|
786
2324
|
(nextWidth != lastLayoutWidth || nextHeight != lastLayoutHeight || layoutDirection != lastLayoutDirection)
|
|
@@ -790,6 +2328,9 @@ class NativeListView(
|
|
|
790
2328
|
lastLayoutWidth = nextWidth
|
|
791
2329
|
lastLayoutHeight = nextHeight
|
|
792
2330
|
lastLayoutDirection = layoutDirection
|
|
2331
|
+
if ((widthChanged || directionChanged) && sidecarLayout != null) config?.let(::updateLayout)
|
|
2332
|
+
updateSidecarGeometry()
|
|
2333
|
+
scheduleSidecarSpacer()
|
|
793
2334
|
updateSectionIndexAttachment()
|
|
794
2335
|
performPendingScrollIfNeeded()
|
|
795
2336
|
}
|
|
@@ -821,6 +2362,27 @@ class NativeListView(
|
|
|
821
2362
|
}
|
|
822
2363
|
|
|
823
2364
|
override fun onDetachedFromWindow() {
|
|
2365
|
+
val disclosureId = activeDisclosureTransitionId
|
|
2366
|
+
if (structuralTail != null) {
|
|
2367
|
+
discardStructuralTail()
|
|
2368
|
+
val epoch = structuralTransitionEpoch
|
|
2369
|
+
config?.let { accepted ->
|
|
2370
|
+
adapter.submitList(accepted.items) {
|
|
2371
|
+
if (!disposed && epoch == structuralTransitionEpoch &&
|
|
2372
|
+
activeDisclosureTransitionId == disclosureId && disclosureId != null) {
|
|
2373
|
+
disclosureFraction = if (accepted.disclosure?.expanded == true) 1f else 0f
|
|
2374
|
+
disclosureTransitioning = false
|
|
2375
|
+
syncDisclosurePresentation()
|
|
2376
|
+
settleDisclosureCommand("interrupted")
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
} else if (disclosureId != null && !disposed) {
|
|
2381
|
+
disclosureFraction = if (config?.disclosure?.expanded == true) 1f else 0f
|
|
2382
|
+
disclosureTransitioning = false
|
|
2383
|
+
syncDisclosurePresentation()
|
|
2384
|
+
settleDisclosureCommand("interrupted")
|
|
2385
|
+
}
|
|
824
2386
|
val cancelTag = R.id.onekey_native_scroll_coordinator_command_cancel_listener
|
|
825
2387
|
if (recyclerView.getTag(cancelTag) === coordinatorCommandCancelListener) recyclerView.setTag(cancelTag, null)
|
|
826
2388
|
cancelExecutingScrollCommand()
|
|
@@ -841,9 +2403,94 @@ class NativeListView(
|
|
|
841
2403
|
} catch (_: Exception) {
|
|
842
2404
|
return
|
|
843
2405
|
}
|
|
844
|
-
homeOffsetDiagnostic("snapshot-before-nextCount=${next.items.size}")
|
|
845
2406
|
invalidateActionAnchor("snapshot")
|
|
846
2407
|
val previous = config
|
|
2408
|
+
val transition = next.structuralTransition
|
|
2409
|
+
val isNewTransition = transition != null && transition.id != consumedStructuralTransitionId
|
|
2410
|
+
if (transition != null) consumedStructuralTransitionId = transition.id
|
|
2411
|
+
val disclosureGeometryInvalid = batchedDisclosureGeometryInvalid ||
|
|
2412
|
+
(!isNewTransition && structuralTail != null &&
|
|
2413
|
+
previous?.disclosure != next.disclosure)
|
|
2414
|
+
batchedDisclosureGeometryInvalid = false
|
|
2415
|
+
if (!isNewTransition &&
|
|
2416
|
+
(next.disclosure == null || previous?.disclosure?.expanded != next.disclosure.expanded)) {
|
|
2417
|
+
activeDisclosureTransitionId = null
|
|
2418
|
+
directDisclosureTogglePositionPending = false
|
|
2419
|
+
}
|
|
2420
|
+
val oldKeys = previous?.items?.map { it.key } ?: emptyList()
|
|
2421
|
+
val nextKeys = next.items.map { it.key }
|
|
2422
|
+
if (next.disclosure != null && isNewTransition && previous != null) {
|
|
2423
|
+
activeDisclosureTransitionId = transition?.id
|
|
2424
|
+
directDisclosureStatus = if (oldKeys == nextKeys || nextKeys.isEmpty() ||
|
|
2425
|
+
transition?.durationMs == 0L ||
|
|
2426
|
+
(Build.VERSION.SDK_INT >= 26 && !ValueAnimator.areAnimatorsEnabled()))
|
|
2427
|
+
"completed" else "interrupted"
|
|
2428
|
+
directDisclosureTogglePositionPending = !next.disclosure.expanded &&
|
|
2429
|
+
!disclosureGeometryInvalid && directDisclosureStatus == "completed"
|
|
2430
|
+
} else if (next.disclosure == null) {
|
|
2431
|
+
activeDisclosureTransitionId = null
|
|
2432
|
+
directDisclosureTogglePositionPending = false
|
|
2433
|
+
}
|
|
2434
|
+
if (disclosureGeometryInvalid && activeDisclosureTransitionId != null) {
|
|
2435
|
+
directDisclosureStatus = "interrupted"
|
|
2436
|
+
directDisclosureTogglePositionPending = false
|
|
2437
|
+
}
|
|
2438
|
+
val positionDirectCollapse = directDisclosureTogglePositionPending
|
|
2439
|
+
var animateStructure = !disclosureGeometryInvalid && previous != null &&
|
|
2440
|
+
isNewTransition && oldKeys != nextKeys &&
|
|
2441
|
+
previous.orientation == "vertical" &&
|
|
2442
|
+
(previous.layout == "linear" || previous.layout == "table") &&
|
|
2443
|
+
next.orientation == "vertical" && (next.layout == "linear" || next.layout == "table") &&
|
|
2444
|
+
(transition?.durationMs ?: 0) > 0 &&
|
|
2445
|
+
(if (Build.VERSION.SDK_INT >= 26) ValueAnimator.areAnimatorsEnabled() else
|
|
2446
|
+
android.provider.Settings.Global.getFloat(context.contentResolver, "animator_duration_scale", 1f) > 0f)
|
|
2447
|
+
val prefixCount = if (animateStructure && previous != null && recyclerView.width > 0 &&
|
|
2448
|
+
recyclerView.height > 0) prefixTailCount(previous, next) else null
|
|
2449
|
+
val exceedsTailHostLimit = prefixCount != null && previous != null &&
|
|
2450
|
+
exceedsStructuralTailHostLimit(previous, next, prefixCount)
|
|
2451
|
+
if (exceedsTailHostLimit) animateStructure = false
|
|
2452
|
+
if (next.disclosure != null && prefixCount == null) animateStructure = false
|
|
2453
|
+
if (prefixCount != null && !exceedsTailHostLimit && previous != null && transition != null) {
|
|
2454
|
+
startPrefixTailTransition(previous, next, transition, prefixCount)
|
|
2455
|
+
return
|
|
2456
|
+
}
|
|
2457
|
+
var heldTailEpoch: Int? = null
|
|
2458
|
+
if (structuralTail != null) {
|
|
2459
|
+
if (!disclosureGeometryInvalid && !animateStructure && oldKeys == nextKeys &&
|
|
2460
|
+
(Build.VERSION.SDK_INT < 26 || ValueAnimator.areAnimatorsEnabled()) &&
|
|
2461
|
+
previous != null && updatePrefixTailSnapshot(previous, next, acceptIndex = true)) {
|
|
2462
|
+
return
|
|
2463
|
+
}
|
|
2464
|
+
heldTailEpoch = holdStructuralTailUntilListCommit()
|
|
2465
|
+
// Keep the accepted presentation until the full adapter list has committed.
|
|
2466
|
+
animateStructure = false
|
|
2467
|
+
}
|
|
2468
|
+
val nextKeySet = nextKeys.toHashSet()
|
|
2469
|
+
val visibleAnchor = if (animateStructure) {
|
|
2470
|
+
val first = layoutManager.findFirstVisibleItemPosition()
|
|
2471
|
+
val last = layoutManager.findLastVisibleItemPosition()
|
|
2472
|
+
(first..last).firstNotNullOfOrNull { position ->
|
|
2473
|
+
val rowIndex = rowPositionForScroll(position)
|
|
2474
|
+
oldKeys.getOrNull(rowIndex)?.takeIf(nextKeySet::contains)?.let { key ->
|
|
2475
|
+
layoutManager.findViewByPosition(position)?.let { key to layoutManager.getDecoratedTop(it) }
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
} else null
|
|
2479
|
+
val anchorKey = visibleAnchor?.first ?: transition?.anchorKey?.takeIf(nextKeySet::contains)
|
|
2480
|
+
val anchorTop = visibleAnchor?.second
|
|
2481
|
+
val footerStartTop = if (animateStructure && footerSlotAdapter.itemCount > 0) {
|
|
2482
|
+
layoutManager.findViewByPosition(scrollContentAdapter.itemCount - 1 - sidecarSpacerAdapter.itemCount)?.let {
|
|
2483
|
+
layoutManager.getDecoratedTop(it) + it.translationY.roundToInt()
|
|
2484
|
+
}
|
|
2485
|
+
} else null
|
|
2486
|
+
val oldKeySet = oldKeys.toHashSet()
|
|
2487
|
+
val removedExtent = if (animateStructure) previous?.items?.sumOf { item ->
|
|
2488
|
+
if (nextKeySet.contains(item.key)) 0 else estimatedItemExtentPx(item)
|
|
2489
|
+
} ?: 0 else 0
|
|
2490
|
+
val addedExtent = if (animateStructure) next.items.sumOf { item ->
|
|
2491
|
+
if (oldKeySet.contains(item.key)) 0 else estimatedItemExtentPx(item)
|
|
2492
|
+
} else 0
|
|
2493
|
+
if (!animateStructure && oldKeys != nextKeys && heldTailEpoch == null) cancelStructuralTransition()
|
|
847
2494
|
if (previous?.items?.isEmpty() == true && next.items.isNotEmpty() && adapter.itemCount == 0 &&
|
|
848
2495
|
pendingScrollRequest == null && recyclerView.scrollState != RecyclerView.SCROLL_STATE_DRAGGING &&
|
|
849
2496
|
!recyclerView.canScrollVertically(-1) && layoutManager.findFirstVisibleItemPosition() == 0) {
|
|
@@ -866,7 +2513,12 @@ class NativeListView(
|
|
|
866
2513
|
previous.items[index].content != next.items[index].content
|
|
867
2514
|
}
|
|
868
2515
|
}.toSet()
|
|
2516
|
+
acceptRowIndex(next, previous)
|
|
869
2517
|
config = next
|
|
2518
|
+
if (heldTailEpoch == null) {
|
|
2519
|
+
disclosureFraction = if (next.disclosure?.expanded == true) 1f else 0f
|
|
2520
|
+
disclosureTransitioning = false
|
|
2521
|
+
}
|
|
870
2522
|
updateContainerSlots()
|
|
871
2523
|
usesSelectorSourceScale = next.items.any { it.usesSelectorSourceScale }
|
|
872
2524
|
adapter.usesSelectorSourceScale = usesSelectorSourceScale
|
|
@@ -878,15 +2530,23 @@ class NativeListView(
|
|
|
878
2530
|
// Rows rebind through the adapter's listStyle invalidation; the fixed footer is bound
|
|
879
2531
|
// outside the adapter, so a listStyle-only snapshot must rebind it here.
|
|
880
2532
|
if (previous.listStyle?.toString() != next.listStyle?.toString()) bindFooter(next)
|
|
2533
|
+
val directDisclosureId = activeDisclosureTransitionId
|
|
881
2534
|
adapter.submitList(next.items) {
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
2535
|
+
val committed: () -> Unit = {
|
|
2536
|
+
// OneKey patch: DiffUtil has dispatched its payloads; release the old serialized rows.
|
|
2537
|
+
next.items.forEach { it.selectionUpdateFromContent = null }
|
|
2538
|
+
recyclerView.post {
|
|
2539
|
+
scheduleSidecarSpacer()
|
|
2540
|
+
startPendingStructuralTransition()
|
|
2541
|
+
bindVisibleSelection(changedSummaryKeys)
|
|
2542
|
+
bindFooterSelection()
|
|
2543
|
+
performPendingScrollIfNeeded()
|
|
2544
|
+
settleCommittedDirectDisclosure(next, directDisclosureId, heldTailEpoch == null)
|
|
2545
|
+
}
|
|
889
2546
|
}
|
|
2547
|
+
if (heldTailEpoch != null) completeHeldTailCommit(next, heldTailEpoch, committed,
|
|
2548
|
+
positionDirectCollapse)
|
|
2549
|
+
else committed()
|
|
890
2550
|
}
|
|
891
2551
|
return
|
|
892
2552
|
}
|
|
@@ -905,7 +2565,12 @@ class NativeListView(
|
|
|
905
2565
|
layoutManager.getDecoratedTop(it) - recyclerView.paddingTop
|
|
906
2566
|
}
|
|
907
2567
|
} else null
|
|
2568
|
+
acceptRowIndex(next, previous)
|
|
908
2569
|
config = next
|
|
2570
|
+
if (heldTailEpoch == null) {
|
|
2571
|
+
disclosureFraction = if (next.disclosure?.expanded == true) 1f else 0f
|
|
2572
|
+
disclosureTransitioning = false
|
|
2573
|
+
}
|
|
909
2574
|
pruneMeasuredRowExtents(next.items)
|
|
910
2575
|
updateContainerSlots()
|
|
911
2576
|
usesSelectorSourceScale = next.items.any { it.usesSelectorSourceScale }
|
|
@@ -919,16 +2584,52 @@ class NativeListView(
|
|
|
919
2584
|
adapter.selectedKeys = next.selectedKeys
|
|
920
2585
|
configureSectionIndex(next)
|
|
921
2586
|
updateLayout(next)
|
|
2587
|
+
if (animateStructure && transition != null) {
|
|
2588
|
+
structuralTransitionEpoch += 1
|
|
2589
|
+
pendingStructuralStartScheduled = false
|
|
2590
|
+
structuralAnimator?.cancel()
|
|
2591
|
+
structuralAnimator = null
|
|
2592
|
+
structuralScrollInterrupted = false
|
|
2593
|
+
structuralFooterHoldTop = footerStartTop
|
|
2594
|
+
structuralFooterCompensatedHeightPx = slotHeights[2]
|
|
2595
|
+
val held = (structuralSpacerHeightPx + removedExtent - addedExtent).coerceAtLeast(0)
|
|
2596
|
+
pendingStructuralTransition = PendingStructuralTransition(
|
|
2597
|
+
transition, structuralTransitionEpoch, anchorKey, anchorTop, footerStartTop, held,
|
|
2598
|
+
removedExtent > addedExtent,
|
|
2599
|
+
)
|
|
2600
|
+
recyclerView.itemAnimator = DefaultItemAnimator().apply {
|
|
2601
|
+
addDuration = transition.durationMs
|
|
2602
|
+
removeDuration = transition.durationMs
|
|
2603
|
+
moveDuration = 0
|
|
2604
|
+
changeDuration = 0
|
|
2605
|
+
supportsChangeAnimations = false
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
val directDisclosureId = activeDisclosureTransitionId
|
|
922
2609
|
adapter.submitList(next.items) {
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
2610
|
+
val committed: () -> Unit = {
|
|
2611
|
+
pendingStructuralTransition?.takeIf { it.epoch == structuralTransitionEpoch }?.let {
|
|
2612
|
+
setStructuralSpacerHeight(it.heldHeightPx)
|
|
2613
|
+
pendingStructuralTransition = it.copy(spacerInserted = true)
|
|
2614
|
+
pendingStructuralTransition?.let(::alignPendingStructuralAnchor)
|
|
2615
|
+
}
|
|
2616
|
+
if (marketStartOffset != null) {
|
|
2617
|
+
layoutManager.scrollToPositionWithOffset(scrollPositionForRow(0), marketStartOffset)
|
|
2618
|
+
}
|
|
2619
|
+
relayoutContents(preserveMarketPaginationAnchor)
|
|
2620
|
+
scheduleSidecarSpacer()
|
|
2621
|
+
startPendingStructuralTransition()
|
|
2622
|
+
performPendingScrollIfNeeded()
|
|
2623
|
+
syncSectionIndexToVisibleRows()
|
|
2624
|
+
scheduleVisibleEvent()
|
|
2625
|
+
if (next.items.isNotEmpty()) checkEndReached()
|
|
2626
|
+
if (directDisclosureId != null) recyclerView.post {
|
|
2627
|
+
settleCommittedDirectDisclosure(next, directDisclosureId, heldTailEpoch == null)
|
|
2628
|
+
}
|
|
926
2629
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
scheduleVisibleEvent()
|
|
931
|
-
if (next.items.isNotEmpty()) checkEndReached()
|
|
2630
|
+
if (heldTailEpoch != null) completeHeldTailCommit(next, heldTailEpoch, committed,
|
|
2631
|
+
positionDirectCollapse)
|
|
2632
|
+
else committed()
|
|
932
2633
|
}
|
|
933
2634
|
refreshLayout.isEnabled = next.pullToRefresh
|
|
934
2635
|
refreshLayout.setDistanceToTriggerSync(next.refreshTriggerDistance?.let { (it * density).roundToInt() } ?: defaultRefreshTriggerDistancePx)
|
|
@@ -937,6 +2638,85 @@ class NativeListView(
|
|
|
937
2638
|
updateReordering(next)
|
|
938
2639
|
}
|
|
939
2640
|
|
|
2641
|
+
private fun startStructuralTransition(
|
|
2642
|
+
transition: NativeListConfig.StructuralTransition,
|
|
2643
|
+
epoch: Int,
|
|
2644
|
+
anchorKey: String?,
|
|
2645
|
+
anchorTop: Int?,
|
|
2646
|
+
footerStartTop: Int?,
|
|
2647
|
+
) {
|
|
2648
|
+
structuralFooterHoldTop = null
|
|
2649
|
+
val startSpacer = structuralSpacerHeightPx
|
|
2650
|
+
structuralStartSpacerPx = startSpacer
|
|
2651
|
+
structuralFooterVisualStartTop = footerStartTop
|
|
2652
|
+
structuralFooterVisualProgress = 0f
|
|
2653
|
+
val startOffset = measuredScrollDistancePx()?.also { logicalScrollDistancePx = it }
|
|
2654
|
+
?: logicalScrollDistancePx
|
|
2655
|
+
val anchorIndex = anchorKey?.let { key -> adapter.currentList.indexOfFirst { it.key == key } }
|
|
2656
|
+
val currentAnchorTop = anchorIndex?.takeIf { it >= 0 }?.let { index ->
|
|
2657
|
+
layoutManager.findViewByPosition(scrollPositionForRow(index))?.let(layoutManager::getDecoratedTop)
|
|
2658
|
+
}
|
|
2659
|
+
val anchorDelta = if (anchorTop != null && currentAnchorTop != null) currentAnchorTop - anchorTop else 0
|
|
2660
|
+
val leftFinal = (sidecarLeftExtentPx() - startSpacer).coerceAtLeast(0)
|
|
2661
|
+
val sidecarBottom = if (sidecarLayout != null)
|
|
2662
|
+
recyclerView.paddingTop + slotHeights[3] + recyclerView.paddingBottom else 0
|
|
2663
|
+
val finalMax = (max(leftFinal, sidecarBottom) - recyclerView.height).coerceAtLeast(0)
|
|
2664
|
+
var targetOffset = (startOffset + anchorDelta).coerceIn(0, finalMax)
|
|
2665
|
+
if (anchorTop == null && anchorIndex != null && anchorIndex >= 0) {
|
|
2666
|
+
val fallbackTop = nativeListScrollPrefixPx(
|
|
2667
|
+
scrollPositionForRow(anchorIndex),
|
|
2668
|
+
layoutManager.spanCount,
|
|
2669
|
+
layoutManager.spanSizeLookup::getSpanSize,
|
|
2670
|
+
::estimatedScrollExtentPx,
|
|
2671
|
+
).coerceIn(0L, Int.MAX_VALUE.toLong()).toInt()
|
|
2672
|
+
targetOffset = minOf(targetOffset, fallbackTop)
|
|
2673
|
+
}
|
|
2674
|
+
structuralFooterVisualEndTop = if (footerStartTop != null && footerSlotAdapter.itemCount > 0) {
|
|
2675
|
+
val footerPosition = scrollContentAdapter.itemCount - 1 - sidecarSpacerAdapter.itemCount
|
|
2676
|
+
val finalPrefix = nativeListScrollPrefixPx(
|
|
2677
|
+
footerPosition,
|
|
2678
|
+
layoutManager.spanCount,
|
|
2679
|
+
layoutManager.spanSizeLookup::getSpanSize,
|
|
2680
|
+
::estimatedScrollExtentPx,
|
|
2681
|
+
) - startSpacer
|
|
2682
|
+
(finalPrefix + recyclerView.paddingTop - targetOffset)
|
|
2683
|
+
.coerceIn(Int.MIN_VALUE.toLong(), Int.MAX_VALUE.toLong()).toInt()
|
|
2684
|
+
} else null
|
|
2685
|
+
structuralAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
|
|
2686
|
+
duration = transition.durationMs
|
|
2687
|
+
interpolator = android.view.animation.AccelerateDecelerateInterpolator()
|
|
2688
|
+
addUpdateListener { animation ->
|
|
2689
|
+
if (epoch != structuralTransitionEpoch || disposed) return@addUpdateListener
|
|
2690
|
+
val fraction = animation.animatedValue as Float
|
|
2691
|
+
structuralFooterVisualProgress = fraction
|
|
2692
|
+
setStructuralSpacerHeight((structuralStartSpacerPx * (1f - fraction)).roundToInt())
|
|
2693
|
+
measuredScrollDistancePx()?.let { logicalScrollDistancePx = it }
|
|
2694
|
+
val finalLeftExtent = (sidecarLeftExtentPx() - structuralSpacerHeightPx).coerceAtLeast(0)
|
|
2695
|
+
val finalMaximum = (max(finalLeftExtent, sidecarBottom) - recyclerView.height).coerceAtLeast(0)
|
|
2696
|
+
val desired = (startOffset + (minOf(targetOffset, finalMaximum) - startOffset) * fraction).roundToInt()
|
|
2697
|
+
if (!structuralScrollInterrupted && recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE)
|
|
2698
|
+
recyclerView.scrollBy(0, desired - logicalScrollDistancePx)
|
|
2699
|
+
updateStructuralFooterVisualPosition()
|
|
2700
|
+
}
|
|
2701
|
+
addListener(object : AnimatorListenerAdapter() {
|
|
2702
|
+
override fun onAnimationEnd(animation: Animator) {
|
|
2703
|
+
if (epoch != structuralTransitionEpoch) return
|
|
2704
|
+
structuralAnimator = null
|
|
2705
|
+
recyclerView.itemAnimator = null
|
|
2706
|
+
setStructuralSpacerHeight(0)
|
|
2707
|
+
scheduleSidecarSpacer()
|
|
2708
|
+
footerSlotAdapter.setVisualTranslationY(0f)
|
|
2709
|
+
structuralFooterVisualStartTop = null
|
|
2710
|
+
structuralFooterVisualEndTop = null
|
|
2711
|
+
structuralFooterVisualProgress = 0f
|
|
2712
|
+
structuralFooterCompensatedHeightPx = null
|
|
2713
|
+
structuralStartSpacerPx = 0
|
|
2714
|
+
}
|
|
2715
|
+
})
|
|
2716
|
+
start()
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
|
|
940
2720
|
private fun captureVisibleMarketAnchor(): VisibleMarketAnchor? {
|
|
941
2721
|
val position = rowPositionForScroll(layoutManager.findFirstVisibleItemPosition()).coerceAtLeast(0)
|
|
942
2722
|
if (position == RecyclerView.NO_POSITION) return null
|
|
@@ -1111,20 +2891,22 @@ class NativeListView(
|
|
|
1111
2891
|
|
|
1112
2892
|
fun applyPatches(patchesJson: String) {
|
|
1113
2893
|
val current = config ?: return
|
|
2894
|
+
val indexByKey = acceptedKeyIndex ?: return
|
|
1114
2895
|
val patches = try {
|
|
1115
2896
|
JSONArray(patchesJson)
|
|
1116
2897
|
} catch (_: Exception) {
|
|
1117
2898
|
return
|
|
1118
2899
|
}
|
|
1119
|
-
val indexByKey = current.items.withIndex().associate { it.value.key to it.index }
|
|
1120
2900
|
val pending = ArrayList<Pair<Int, JSONObject>>(patches.length())
|
|
1121
2901
|
val seen = HashSet<String>()
|
|
1122
2902
|
for (index in 0 until patches.length()) {
|
|
1123
2903
|
val patch = patches.optJSONObject(index) ?: return
|
|
1124
2904
|
val key = patch.optString("key")
|
|
1125
2905
|
val itemIndex = indexByKey[key] ?: return
|
|
2906
|
+
val item = current.items.getOrNull(itemIndex) ?: return
|
|
2907
|
+
if (item.key != key) return
|
|
1126
2908
|
if (!seen.add(key)) return
|
|
1127
|
-
if (patch.optString("type") !=
|
|
2909
|
+
if (patch.optString("type") != item.type) return
|
|
1128
2910
|
val changes = patch.optJSONObject("changes") ?: return
|
|
1129
2911
|
pending.add(itemIndex to changes)
|
|
1130
2912
|
}
|
|
@@ -1148,12 +2930,27 @@ class NativeListView(
|
|
|
1148
2930
|
}
|
|
1149
2931
|
if (!marketQuoteOnly) invalidateActionAnchor("snapshot")
|
|
1150
2932
|
val next = current.copy(items = nextItems, selectedKeys = selected)
|
|
2933
|
+
if (structuralTail != null && updatePrefixTailSnapshot(current, next)) return
|
|
2934
|
+
val heldTailEpoch = if (structuralTail != null) holdStructuralTailUntilListCommit() else null
|
|
1151
2935
|
config = next
|
|
1152
2936
|
updateContainerSlots()
|
|
1153
2937
|
usesSelectorSourceScale = next.items.any { it.usesSelectorSourceScale }
|
|
1154
2938
|
adapter.usesSelectorSourceScale = usesSelectorSourceScale
|
|
1155
2939
|
adapter.selectedKeys = selected
|
|
1156
|
-
|
|
2940
|
+
val directDisclosureId = activeDisclosureTransitionId
|
|
2941
|
+
adapter.submitList(nextItems) {
|
|
2942
|
+
if (heldTailEpoch != null) {
|
|
2943
|
+
if (heldTailEpoch != structuralTransitionEpoch || disposed) return@submitList
|
|
2944
|
+
val acceptedPendingFooter = acceptPendingFooterValues(adjustTailStart = false)
|
|
2945
|
+
clearStructuralTail()
|
|
2946
|
+
if (acceptedPendingFooter) updateContainerSlots()
|
|
2947
|
+
setStructuralSpacerHeight(0)
|
|
2948
|
+
}
|
|
2949
|
+
relayoutContents()
|
|
2950
|
+
if (directDisclosureId != null) recyclerView.post {
|
|
2951
|
+
settleCommittedDirectDisclosure(next, directDisclosureId, heldTailEpoch == null)
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
1157
2954
|
bindFooter(next)
|
|
1158
2955
|
}
|
|
1159
2956
|
|
|
@@ -1215,12 +3012,12 @@ class NativeListView(
|
|
|
1215
3012
|
is ScrollRequest.End -> true
|
|
1216
3013
|
}
|
|
1217
3014
|
if (!valid) return
|
|
3015
|
+
if (structuralTail != null) finishStructuralTail(interrupted = true)
|
|
1218
3016
|
executingScrollCommand = false
|
|
1219
3017
|
dispatchingScrollRequest = null
|
|
1220
3018
|
commandLayoutRequest = null
|
|
1221
3019
|
commandAlignmentRequest = null
|
|
1222
3020
|
acceptedScrollRequest = request
|
|
1223
|
-
commandFinishDiagnosticReason = null
|
|
1224
3021
|
readingIntentEpoch++
|
|
1225
3022
|
pendingBodyStartEpoch = null
|
|
1226
3023
|
val explicitTop = when (request) {
|
|
@@ -1240,6 +3037,7 @@ class NativeListView(
|
|
|
1240
3037
|
|
|
1241
3038
|
private val coordinatorCommandCancelListener = Runnable {
|
|
1242
3039
|
if (disposed) return@Runnable
|
|
3040
|
+
if (structuralTail != null) finishStructuralTail(interrupted = true)
|
|
1243
3041
|
pendingScrollRequest = null
|
|
1244
3042
|
readingIntentEpoch++
|
|
1245
3043
|
pendingBodyStartEpoch = null
|
|
@@ -1261,15 +3059,6 @@ class NativeListView(
|
|
|
1261
3059
|
|
|
1262
3060
|
private fun finishScrollCommandIfSettled() {
|
|
1263
3061
|
val request = acceptedScrollRequest ?: return
|
|
1264
|
-
if (homeOffsetDiagnosticEnabled && executingScrollCommand) {
|
|
1265
|
-
val reason = "pendingOwn=${pendingScrollRequest === request}-dispatchOwn=${dispatchingScrollRequest === request}" +
|
|
1266
|
-
"-layoutOwn=${commandLayoutRequest === request}-alignOwn=${commandAlignmentRequest === request}" +
|
|
1267
|
-
"-computing=${recyclerView.isComputingLayout}-scrollState=${recyclerView.scrollState}-smooth=${layoutManager.isSmoothScrolling}"
|
|
1268
|
-
if (commandFinishDiagnosticReason != reason) {
|
|
1269
|
-
commandFinishDiagnosticReason = reason
|
|
1270
|
-
homeOffsetDiagnostic("command-finish-gates-$reason")
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
3062
|
if (
|
|
1274
3063
|
!executingScrollCommand || pendingScrollRequest === request ||
|
|
1275
3064
|
dispatchingScrollRequest === request || commandLayoutRequest === request ||
|
|
@@ -1285,7 +3074,6 @@ class NativeListView(
|
|
|
1285
3074
|
private fun requestScroll(request: ScrollRequest) {
|
|
1286
3075
|
acceptScrollRequest(request)
|
|
1287
3076
|
if (config != null && acceptedScrollRequest !== request) return
|
|
1288
|
-
homeOffsetDiagnostic("command-request-kind=${request.javaClass.simpleName}")
|
|
1289
3077
|
if (performScroll(request)) {
|
|
1290
3078
|
pendingScrollRequest = null
|
|
1291
3079
|
finishScrollCommandIfSettled()
|
|
@@ -1301,7 +3089,6 @@ class NativeListView(
|
|
|
1301
3089
|
if (pendingScrollRequest === request) pendingScrollRequest = null
|
|
1302
3090
|
return
|
|
1303
3091
|
}
|
|
1304
|
-
homeOffsetDiagnostic("command-pending-kind=${request.javaClass.simpleName}")
|
|
1305
3092
|
if (performScroll(request)) {
|
|
1306
3093
|
pendingScrollRequest = null
|
|
1307
3094
|
finishScrollCommandIfSettled()
|
|
@@ -1309,7 +3096,6 @@ class NativeListView(
|
|
|
1309
3096
|
}
|
|
1310
3097
|
|
|
1311
3098
|
private fun performScroll(request: ScrollRequest): Boolean {
|
|
1312
|
-
homeOffsetDiagnostic("command-perform-kind=${request.javaClass.simpleName}")
|
|
1313
3099
|
val current = config ?: return false
|
|
1314
3100
|
if (adapter.itemCount != current.items.size) return false
|
|
1315
3101
|
if (recyclerView.width <= 0 || recyclerView.height <= 0) return false
|
|
@@ -1353,7 +3139,8 @@ class NativeListView(
|
|
|
1353
3139
|
}
|
|
1354
3140
|
is ScrollRequest.End -> {
|
|
1355
3141
|
val finalSlotHeight = when {
|
|
1356
|
-
|
|
3142
|
+
sidecarSpacerAdapter.itemCount > 0 -> sidecarSpacerHeightPx
|
|
3143
|
+
footerSlotAdapter.itemCount > 0 -> footerPresentedHeightPx()
|
|
1357
3144
|
emptySlotAdapter.itemCount > 0 -> slotHeights[1]
|
|
1358
3145
|
adapter.itemCount == 0 && headerSlotAdapter.itemCount > 0 -> slotHeights[0]
|
|
1359
3146
|
else -> null
|
|
@@ -1489,13 +3276,11 @@ class NativeListView(
|
|
|
1489
3276
|
val startView = manager.findViewByPosition(0)
|
|
1490
3277
|
val paddingStart = if (manager.orientation == RecyclerView.VERTICAL) manager.paddingTop else manager.paddingLeft
|
|
1491
3278
|
val relativeStart = startView?.let { decoratedStart(manager, it) - paddingStart }
|
|
1492
|
-
homeOffsetDiagnostic("command-top-dispatch-first=${manager.findFirstVisibleItemPosition()}-relativeStart=$relativeStart-smooth=${manager.isSmoothScrolling}")
|
|
1493
3279
|
if (
|
|
1494
3280
|
startView != null && manager.findFirstVisibleItemPosition() == 0 && relativeStart == 0 &&
|
|
1495
3281
|
recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE &&
|
|
1496
3282
|
!recyclerView.isComputingLayout && !manager.isSmoothScrolling
|
|
1497
3283
|
) {
|
|
1498
|
-
homeOffsetDiagnostic("command-top-noop")
|
|
1499
3284
|
return
|
|
1500
3285
|
}
|
|
1501
3286
|
recyclerView.smoothScrollToPosition(0)
|
|
@@ -1648,6 +3433,8 @@ class NativeListView(
|
|
|
1648
3433
|
fun dispose() {
|
|
1649
3434
|
if (disposed) return
|
|
1650
3435
|
disposed = true
|
|
3436
|
+
acceptedKeyIndex = null
|
|
3437
|
+
if (structuralTail != null) discardStructuralTail()
|
|
1651
3438
|
val cancelTag = R.id.onekey_native_scroll_coordinator_command_cancel_listener
|
|
1652
3439
|
if (recyclerView.getTag(cancelTag) === coordinatorCommandCancelListener) recyclerView.setTag(cancelTag, null)
|
|
1653
3440
|
containerSlotRelayoutPending = false
|
|
@@ -1658,6 +3445,8 @@ class NativeListView(
|
|
|
1658
3445
|
private fun disposeResources() {
|
|
1659
3446
|
if (resourcesDisposed) return
|
|
1660
3447
|
resourcesDisposed = true
|
|
3448
|
+
cancelStructuralTransition()
|
|
3449
|
+
recyclerView.itemAnimator = null
|
|
1661
3450
|
stopSectionIndexAttachmentTracking()
|
|
1662
3451
|
scheduleSectionIndexAttachmentToList()
|
|
1663
3452
|
stopReorderRelayoutLoop()
|
|
@@ -1667,7 +3456,6 @@ class NativeListView(
|
|
|
1667
3456
|
visibleEventScheduled = false
|
|
1668
3457
|
layoutManager.onChildLaidOut = null
|
|
1669
3458
|
layoutManager.onLayoutPassCompleted = null
|
|
1670
|
-
layoutManager.onDiagnosticLayout = null
|
|
1671
3459
|
layoutManager.onBeforeDataLayout = null
|
|
1672
3460
|
pendingBodyStartEpoch = null
|
|
1673
3461
|
executingScrollCommand = false
|
|
@@ -1722,12 +3510,26 @@ class NativeListView(
|
|
|
1722
3510
|
val coordinatorBottomPadding = (recyclerView.paddingBottom - configuredBottomPaddingPx).coerceAtLeast(0)
|
|
1723
3511
|
// Slot roots are laid out by React at the unscaled JS inset, without the index gutter.
|
|
1724
3512
|
slotInsetPx = (horizontalPadding * density).roundToInt()
|
|
3513
|
+
val sidecar = sidecarLayout
|
|
3514
|
+
val contentWidth = recyclerView.width
|
|
3515
|
+
if (sidecar != null && contentWidth > 0) {
|
|
3516
|
+
slotInsetPx = (contentWidth - minOf(contentWidth, sidecar.maxContentWidth)) / 2
|
|
3517
|
+
}
|
|
3518
|
+
val sidecarSpace = (sidecar?.width ?: 0) + (sidecar?.gap ?: 0)
|
|
3519
|
+
val baseSlotInset = slotInsetPx
|
|
3520
|
+
if (sidecar != null && layoutDirection == LAYOUT_DIRECTION_RTL) {
|
|
3521
|
+
slotInsetPx += sidecarSpace
|
|
3522
|
+
slotRightInsetPx = baseSlotInset
|
|
3523
|
+
} else {
|
|
3524
|
+
slotRightInsetPx = baseSlotInset + sidecarSpace
|
|
3525
|
+
}
|
|
1725
3526
|
configuredTopPaddingPx = dp(topPadding)
|
|
1726
3527
|
configuredBottomPaddingPx = dp(bottomPadding)
|
|
1727
3528
|
recyclerView.setPaddingRelative(
|
|
1728
|
-
dp(horizontalPadding),
|
|
3529
|
+
if (sidecar == null) dp(horizontalPadding) else baseSlotInset + dp(horizontalPadding),
|
|
1729
3530
|
configuredTopPaddingPx + coordinatorTopPadding,
|
|
1730
|
-
dp(horizontalPadding + indexGutter)
|
|
3531
|
+
if (sidecar == null) dp(horizontalPadding + indexGutter)
|
|
3532
|
+
else baseSlotInset + sidecarSpace + dp(horizontalPadding + indexGutter),
|
|
1731
3533
|
configuredBottomPaddingPx + coordinatorBottomPadding,
|
|
1732
3534
|
)
|
|
1733
3535
|
recyclerView.clipToPadding = false
|
|
@@ -2117,15 +3919,16 @@ class NativeListView(
|
|
|
2117
3919
|
actionAnchor = record
|
|
2118
3920
|
// OneKey patch: selector menus anchor to the glyph slot, not its expanded press target.
|
|
2119
3921
|
val anchorInset = origin.anchorInsetPixels
|
|
3922
|
+
val helpBounds = (origin.sourceView as? NativeListTextView)?.helpAnchorBounds
|
|
2120
3923
|
return JSONObject()
|
|
2121
3924
|
.put("token", token)
|
|
2122
3925
|
.put(
|
|
2123
3926
|
"windowRect",
|
|
2124
3927
|
JSONObject()
|
|
2125
|
-
.put("x", (location[0] + anchorInset) / density)
|
|
2126
|
-
.put("y", (location[1] + anchorInset) / density)
|
|
2127
|
-
.put("width", (origin.sourceView.width - anchorInset * 2) / density)
|
|
2128
|
-
.put("height", (origin.sourceView.height - anchorInset * 2) / density),
|
|
3928
|
+
.put("x", (location[0] + (helpBounds?.left ?: anchorInset.toFloat())) / density)
|
|
3929
|
+
.put("y", (location[1] + (helpBounds?.top ?: anchorInset.toFloat())) / density)
|
|
3930
|
+
.put("width", (helpBounds?.width() ?: (origin.sourceView.width - anchorInset * 2).toFloat()) / density)
|
|
3931
|
+
.put("height", (helpBounds?.height() ?: (origin.sourceView.height - anchorInset * 2).toFloat()) / density),
|
|
2129
3932
|
)
|
|
2130
3933
|
.put("source", origin.source)
|
|
2131
3934
|
.put("generation", generation)
|
|
@@ -2666,6 +4469,7 @@ class NativeListView(
|
|
|
2666
4469
|
if (from != RecyclerView.NO_POSITION && to != RecyclerView.NO_POSITION && from != to && reordered != null) {
|
|
2667
4470
|
val moved = reordered.getOrNull(to)
|
|
2668
4471
|
if (moved != null) {
|
|
4472
|
+
acceptedKeyIndex = reordered.withIndex().associate { it.value.key to it.index }
|
|
2669
4473
|
config = config?.copy(items = reordered)
|
|
2670
4474
|
val payload = JSONObject()
|
|
2671
4475
|
.put("key", moved.key)
|
|
@@ -2849,6 +4653,7 @@ class NativeListView(
|
|
|
2849
4653
|
}
|
|
2850
4654
|
|
|
2851
4655
|
private fun checkEndReached() {
|
|
4656
|
+
if (structuralTail != null) return
|
|
2852
4657
|
val current = config ?: return
|
|
2853
4658
|
if (!current.loadMore || current.generation == endReachedGeneration || adapter.itemCount == 0) return
|
|
2854
4659
|
val manager = recyclerView.layoutManager as? LinearLayoutManager ?: return
|
|
@@ -3354,7 +5159,7 @@ private class ItemSpacingDecoration(
|
|
|
3354
5159
|
* from each edge. Row padding may be scaled or widened by the section-index gutter, so slot
|
|
3355
5160
|
* holders offset themselves (possibly negatively) back to the JavaScript inset.
|
|
3356
5161
|
*/
|
|
3357
|
-
private class ContainerSlotInsetDecoration(private val
|
|
5162
|
+
private class ContainerSlotInsetDecoration(private val insetsPx: () -> Pair<Int, Int>) : RecyclerView.ItemDecoration() {
|
|
3358
5163
|
override fun getItemOffsets(
|
|
3359
5164
|
outRect: android.graphics.Rect,
|
|
3360
5165
|
view: View,
|
|
@@ -3362,9 +5167,9 @@ private class ContainerSlotInsetDecoration(private val insetPx: () -> Int) : Rec
|
|
|
3362
5167
|
state: RecyclerView.State,
|
|
3363
5168
|
) {
|
|
3364
5169
|
if (parent.getChildViewHolder(view) !is NativeListContainerSlotAdapter.Holder) return
|
|
3365
|
-
val
|
|
3366
|
-
outRect.left =
|
|
3367
|
-
outRect.right =
|
|
5170
|
+
val (leftInset, rightInset) = insetsPx()
|
|
5171
|
+
outRect.left = leftInset - parent.paddingLeft
|
|
5172
|
+
outRect.right = rightInset - parent.paddingRight
|
|
3368
5173
|
}
|
|
3369
5174
|
}
|
|
3370
5175
|
|