@onekeyfe/react-native-pager-view 3.0.172 → 3.0.174
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/reactnativepagerview/CollapsiblePagerHost.kt +506 -56
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerViewManager.kt +4 -0
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerView.kt +14 -0
- package/android/src/main/res/values/native_scroll_coordinator_ids.xml +2 -0
- package/docs/SPEC.md +54 -48
- package/ios/RNCCollapsiblePagerViewComponentView.mm +245 -32
- package/ios/RNCNativeScrollerComponentView.mm +6 -0
- package/ios/tests/RNCCollapsiblePagerFirstFrameTests.mm +85 -49
- package/ios/tests/RNCCollapsiblePagerRefreshControlTests.mm +14 -5
- package/lib/module/CollapsiblePagerView.js +6 -0
- package/lib/module/CollapsiblePagerView.web.js +81 -0
- package/lib/module/CollapsiblePagerViewNativeComponent.ts +6 -0
- package/lib/typescript/src/CollapsiblePagerView.d.ts +6 -0
- package/lib/typescript/src/CollapsiblePagerView.web.d.ts +11 -0
- package/lib/typescript/src/CollapsiblePagerViewNativeComponent.d.ts +1 -0
- package/package.json +2 -2
- package/src/CollapsiblePagerView.tsx +18 -0
- package/src/CollapsiblePagerView.web.tsx +170 -36
- package/src/CollapsiblePagerViewNativeComponent.ts +6 -0
|
@@ -29,6 +29,7 @@ import com.facebook.react.views.scroll.ReactScrollView
|
|
|
29
29
|
import com.margelo.nitro.nativelogger.OneKeyLog
|
|
30
30
|
import java.lang.ref.WeakReference
|
|
31
31
|
import java.util.WeakHashMap
|
|
32
|
+
import java.util.function.BiConsumer
|
|
32
33
|
import kotlin.math.max
|
|
33
34
|
import kotlin.math.min
|
|
34
35
|
import kotlin.math.roundToInt
|
|
@@ -132,6 +133,26 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
132
133
|
private var refreshForegroundAncestorAlpha = 1f
|
|
133
134
|
private val pageOffsets = HashMap<String, Int>()
|
|
134
135
|
private val pageHeaderOffsets = HashMap<String, Int>()
|
|
136
|
+
private class RecyclerReadingAnchor(view: RecyclerView, val adapter: RecyclerView.Adapter<*>, val position: Int, val top: Int) {
|
|
137
|
+
val view = WeakReference(view)
|
|
138
|
+
}
|
|
139
|
+
private val recyclerReadingAnchors = HashMap<String, RecyclerReadingAnchor>()
|
|
140
|
+
private val displayRecyclerAnchors = HashMap<String, RecyclerReadingAnchor>()
|
|
141
|
+
private var transitionSourceRecyclerAnchor: RecyclerReadingAnchor? = null
|
|
142
|
+
private val commandBodyKeys = HashSet<String>()
|
|
143
|
+
private var explicitTopCommand: Pair<String, PageRestoreRequest>? = null
|
|
144
|
+
private class PagerTopRequest(view: View, val key: String, val pageIndex: Int, val generation: Int, val target: Int, val token: Int) {
|
|
145
|
+
val view = WeakReference(view)
|
|
146
|
+
}
|
|
147
|
+
private var pagerTopRequest: PagerTopRequest? = null
|
|
148
|
+
private var pagerTopToken = 0
|
|
149
|
+
private var dispatchingPagerTop = false
|
|
150
|
+
private val suspendedBodyKeys = HashSet<String>()
|
|
151
|
+
private val alignedBodyKeys = HashSet<String>()
|
|
152
|
+
private val transitionAlignedViews = WeakHashMap<View, String>()
|
|
153
|
+
private var transitionSourceDisplayBody = 0
|
|
154
|
+
private val displayBodyOverrides = HashMap<String, Int>()
|
|
155
|
+
private val deferredRecyclerGeometry = WeakHashMap<RecyclerView, Triple<Int, Int, Int>>()
|
|
135
156
|
private var transitionHeaderOffsets: Map<String, Int>? = null
|
|
136
157
|
private var transitionPageOffsets: Map<String, Int>? = null
|
|
137
158
|
private var transitionSourcePage = 0
|
|
@@ -152,6 +173,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
152
173
|
return PageRestoreRequest(view, pageIndex, attachmentGeneration).also {
|
|
153
174
|
pageRestoreRequests[key] = it
|
|
154
175
|
pendingPageRestores.add(key)
|
|
176
|
+
offsetDiag("request-register", view, key, pageIndex, it)
|
|
155
177
|
}
|
|
156
178
|
}
|
|
157
179
|
|
|
@@ -163,9 +185,11 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
163
185
|
private fun clearPageRestoreForView(key: String, view: View, request: PageRestoreRequest? = null) {
|
|
164
186
|
if (request != null && pageRestoreRequests[key] !== request) return
|
|
165
187
|
if (pageRestoreRequests[key]?.view?.get() !== view) return
|
|
188
|
+
offsetDiag("request-clear-before", view, key, request = request ?: pageRestoreRequests[key])
|
|
166
189
|
pageRestoreRequests.remove(key)
|
|
167
190
|
pendingPageRestores.remove(key)
|
|
168
191
|
ownedScrollerRestoreRequests.remove(key)
|
|
192
|
+
offsetDiag("request-clear-after", view, key, request = request)
|
|
169
193
|
}
|
|
170
194
|
private var observedRecyclerView: RecyclerView? = null
|
|
171
195
|
private var observedRecyclerKey: String? = null
|
|
@@ -201,8 +225,14 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
201
225
|
// Apply its header insets before that list first draws.
|
|
202
226
|
prepareAdjacentPages()
|
|
203
227
|
attachPrimaryScrollObserver()
|
|
228
|
+
finishPagerTopIfSettled()
|
|
204
229
|
// OneKey patch: a filtered short page can reduce the existing collapse range.
|
|
205
230
|
setHeaderOffset(headerOffsetPx)
|
|
231
|
+
if (offsetDiagEnabled() && offsetDiagNextDraw.isNotEmpty()) {
|
|
232
|
+
val samples = offsetDiagNextDraw.entries.map { it.key to it.value }
|
|
233
|
+
offsetDiagNextDraw.clear()
|
|
234
|
+
samples.forEach { (view, owner) -> offsetDiag("restore-next-predraw", view, owner.first, owner.second) }
|
|
235
|
+
}
|
|
206
236
|
true
|
|
207
237
|
}
|
|
208
238
|
|
|
@@ -238,6 +268,8 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
238
268
|
ownedScrollerRestoreRequests.remove(key)
|
|
239
269
|
}
|
|
240
270
|
}
|
|
271
|
+
recyclerReadingAnchors.keys.retainAll(value.toSet())
|
|
272
|
+
displayRecyclerAnchors.keys.retainAll(value.toSet())
|
|
241
273
|
field = value
|
|
242
274
|
}
|
|
243
275
|
var retainedPages: String = "[]"
|
|
@@ -282,16 +314,48 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
282
314
|
}
|
|
283
315
|
|
|
284
316
|
private var offsetDiagSequence = 0L
|
|
317
|
+
private val offsetDiagNextDraw = WeakHashMap<View, Pair<String, Int>>()
|
|
285
318
|
|
|
286
|
-
private fun
|
|
287
|
-
|
|
319
|
+
private fun offsetDiagEnabled(): Boolean =
|
|
320
|
+
(context.applicationInfo.flags and android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE) != 0
|
|
321
|
+
|
|
322
|
+
private fun offsetDiagAfterDraw(view: View, key: String, pageIndex: Int) {
|
|
323
|
+
if (offsetDiagEnabled()) offsetDiagNextDraw[view] = key to pageIndex
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private fun offsetDiag(
|
|
327
|
+
phase: String,
|
|
328
|
+
view: View? = null,
|
|
329
|
+
key: String = currentPageKey(),
|
|
330
|
+
pageIndex: Int = pageKeys.indexOf(key),
|
|
331
|
+
request: PageRestoreRequest? = pageRestoreRequests[key],
|
|
332
|
+
) {
|
|
333
|
+
if (!offsetDiagEnabled()) return
|
|
288
334
|
val safeKey = if (key in setOf("portfolio", "perps", "defi", "nft", "history")) key else "other"
|
|
289
335
|
val observed = observedOwnedScroller ?: observedNativeScroller ?: observedRecyclerView
|
|
336
|
+
val observedKey = observedOwnedScrollerKey ?: observedNativeScrollerKey ?: observedRecyclerKey
|
|
337
|
+
val recycler = view as? RecyclerView
|
|
338
|
+
val manager = recycler?.layoutManager as? LinearLayoutManager
|
|
339
|
+
val first = manager?.findFirstVisibleItemPosition()
|
|
340
|
+
val last = manager?.findLastVisibleItemPosition()
|
|
341
|
+
val anchor = first?.takeIf { it != RecyclerView.NO_POSITION }?.let { manager?.findViewByPosition(it) }
|
|
342
|
+
val child = (view as? ViewGroup)?.getChildAt(0)
|
|
290
343
|
OneKeyLog.debug("CollapsiblePager", "[HOME-OFFSET-DIAG] phase=$phase seq=${++offsetDiagSequence} " +
|
|
291
344
|
"wallMs=${System.currentTimeMillis()} monoMs=${SystemClock.uptimeMillis()} host=$hostIdentity " +
|
|
292
|
-
"generation=$attachmentGeneration
|
|
345
|
+
"generation=$attachmentGeneration selectedPage=$selectedPage pagerPage=${pager.currentItem} targetPage=$pageIndex key=$safeKey " +
|
|
346
|
+
"ownerPage=${pageKeys.indexOf(observedKey)} header=$headerOffsetPx headerHeight=$headerHeightPx stickyHeight=$stickyHeaderHeightPx " +
|
|
347
|
+
"transition=${transitionHeaderOffsets != null} synthetic=$syntheticPageRestore pending=${key in pendingPageRestores} " +
|
|
348
|
+
"requestId=${request?.let { System.identityHashCode(it) }} requestGeneration=${request?.generation} " +
|
|
349
|
+
"requestPage=${request?.pageIndex} requestCurrent=${request != null && pageRestoreRequests[key] === request} " +
|
|
293
350
|
"view=${view?.let { System.identityHashCode(it) }} tag=${view?.id} observed=${observed?.let { System.identityHashCode(it) }} " +
|
|
294
|
-
"
|
|
351
|
+
"attached=${view?.isAttachedToWindow} estimatedOffset=${view?.let(::verticalScrollOffset)} " +
|
|
352
|
+
"cachePresent=${pageOffsets.containsKey(key)} cache=${pageOffsets[key]} snapshot=${transitionPageOffsets?.get(key)} " +
|
|
353
|
+
"boundsHeight=${view?.height} measuredHeight=${view?.measuredHeight} childHeight=${child?.height} childTop=${child?.top} " +
|
|
354
|
+
"paddingTop=${view?.paddingTop} paddingBottom=${view?.paddingBottom} childCount=${(view as? ViewGroup)?.childCount} " +
|
|
355
|
+
"itemCount=${recycler?.adapter?.itemCount} first=$first last=$last anchorTag=${anchor?.id} " +
|
|
356
|
+
"anchorTop=${anchor?.let { manager?.getDecoratedTop(it) }} anchorRelative=${anchor?.let { manager?.getDecoratedTop(it)?.minus(recycler?.paddingTop ?: 0) }} " +
|
|
357
|
+
"range=${recycler?.computeVerticalScrollRange()} extent=${recycler?.computeVerticalScrollExtent()} " +
|
|
358
|
+
"canUp=${view?.canScrollVertically(-1)} canDown=${view?.canScrollVertically(1)} scrollState=${recycler?.scrollState} computing=${recycler?.isComputingLayout}")
|
|
295
359
|
}
|
|
296
360
|
|
|
297
361
|
private fun outerPagerIndex(): Int {
|
|
@@ -430,7 +494,17 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
430
494
|
detachPrimaryScrollObserver()
|
|
431
495
|
pageRestoreRequests.clear()
|
|
432
496
|
pendingPageRestores.clear()
|
|
497
|
+
commandBodyKeys.clear()
|
|
498
|
+
explicitTopCommand = null
|
|
499
|
+
suspendedBodyKeys.clear()
|
|
500
|
+
alignedBodyKeys.clear()
|
|
501
|
+
transitionAlignedViews.clear()
|
|
502
|
+
deferredRecyclerGeometry.clear()
|
|
503
|
+
displayBodyOverrides.clear()
|
|
504
|
+
displayRecyclerAnchors.clear()
|
|
433
505
|
ownedScrollerRestoreRequests.clear()
|
|
506
|
+
recyclerReadingAnchors.clear()
|
|
507
|
+
transitionSourceRecyclerAnchor = null
|
|
434
508
|
// Weak keys may disappear after size is read; Collection.toArray tolerates this.
|
|
435
509
|
for (recycler in originalRecyclerPadding.keys.toTypedArray()) {
|
|
436
510
|
restoreRecyclerInsets(recycler)
|
|
@@ -454,30 +528,28 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
454
528
|
fun reactChildAt(index: Int) = logicalChildren[index]
|
|
455
529
|
|
|
456
530
|
internal fun beginPageTransition(target: Int? = null) {
|
|
531
|
+
cancelPagerTop()
|
|
457
532
|
if (!nativeSmoothHeaderScrollEnabled || headerHeightPx <= 0) return
|
|
458
533
|
if (transitionHeaderOffsets == null) {
|
|
534
|
+
transitionSourceDisplayBody = primaryScrollableForPage(selectedPage)?.let(::verticalScrollOffset) ?: 0
|
|
535
|
+
transitionSourceRecyclerAnchor = (primaryScrollableForPage(selectedPage) as? RecyclerView)?.let(::captureRecyclerAnchor)
|
|
459
536
|
pageHeaderOffsets[currentPageKey()] = headerOffsetPx
|
|
460
537
|
detachPrimaryScrollObserver()
|
|
461
538
|
transitionSourcePage = selectedPage
|
|
462
539
|
transitionHeaderOffsets = pageHeaderOffsets.toMap()
|
|
463
540
|
transitionPageOffsets = pageOffsets.toMap()
|
|
464
541
|
}
|
|
465
|
-
if (target != null)
|
|
542
|
+
if (target != null) {
|
|
543
|
+
transitionTargetPage = target
|
|
544
|
+
alignPageBody(target)
|
|
545
|
+
}
|
|
546
|
+
offsetDiag("transition-begin-source=$transitionSourcePage", primaryScrollableForPage(selectedPage), pageKey(target ?: selectedPage), target ?: selectedPage)
|
|
466
547
|
}
|
|
467
548
|
|
|
468
549
|
internal fun onPagerScrolled(position: Int, offset: Float) {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
val second = target ?: (position + 1).coerceAtMost(adapter.itemCount - 1)
|
|
473
|
-
val fraction = if (target != null && target != first) {
|
|
474
|
-
((position + offset - first) / (target - first)).coerceIn(0f, 1f)
|
|
475
|
-
} else offset
|
|
476
|
-
val firstH = headers[pageKey(first)] ?: 0
|
|
477
|
-
val secondH = headers[pageKey(second)] ?: 0
|
|
478
|
-
syntheticPageRestore = true
|
|
479
|
-
setHeaderOffset((firstH + (secondH - firstH) * fraction).roundToInt())
|
|
480
|
-
syntheticPageRestore = false
|
|
550
|
+
if (transitionHeaderOffsets == null) return
|
|
551
|
+
alignPageBody(position)
|
|
552
|
+
if (position + 1 < adapter.itemCount) alignPageBody(position + 1)
|
|
481
553
|
updateNativeTabProgress(position, offset)
|
|
482
554
|
}
|
|
483
555
|
|
|
@@ -495,18 +567,30 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
495
567
|
releaseRefreshForeground()
|
|
496
568
|
selectedPage = position.coerceIn(0, adapter.itemCount - 1)
|
|
497
569
|
syntheticPageRestore = true
|
|
498
|
-
setHeaderOffset(headers[
|
|
570
|
+
setHeaderOffset(headers[pageKey(transitionSourcePage)] ?: headerOffsetPx)
|
|
499
571
|
transitionHeaderOffsets = null
|
|
500
572
|
transitionPageOffsets = null
|
|
501
573
|
transitionTargetPage = null
|
|
502
574
|
syntheticPageRestore = false
|
|
503
|
-
|
|
575
|
+
transitionAlignedViews.clear()
|
|
576
|
+
if (selectedPage == transitionSourcePage) {
|
|
577
|
+
displayBodyOverrides[currentPageKey()] = transitionSourceDisplayBody
|
|
578
|
+
transitionSourceRecyclerAnchor?.let { displayRecyclerAnchors[currentPageKey()] = it }
|
|
579
|
+
} else {
|
|
580
|
+
displayBodyOverrides.remove(currentPageKey())
|
|
581
|
+
displayRecyclerAnchors.remove(currentPageKey())
|
|
582
|
+
}
|
|
583
|
+
transitionSourceRecyclerAnchor = null
|
|
584
|
+
if (headerOffsetPx == headerHeightPx) suspendedBodyKeys.remove(currentPageKey())
|
|
585
|
+
else if ((pageOffsets[currentPageKey()] ?: 0) > 0) suspendedBodyKeys.add(currentPageKey())
|
|
586
|
+
alignPageBody(selectedPage, force = true)
|
|
587
|
+
offsetDiag("transition-finish", primaryScrollableForPage(selectedPage), currentPageKey(), selectedPage)
|
|
504
588
|
prepareAdjacentPages()
|
|
505
589
|
attachPrimaryScrollObserver()
|
|
506
590
|
}
|
|
507
591
|
|
|
508
592
|
fun selectPage(position: Int) {
|
|
509
|
-
offsetDiag("select-begin
|
|
593
|
+
offsetDiag("select-begin", observedOwnedScroller ?: observedNativeScroller ?: observedRecyclerView, pageKey(position), position)
|
|
510
594
|
pendingRefreshPull = null
|
|
511
595
|
if (adapter.itemCount == 0) return
|
|
512
596
|
if (nativeSmoothHeaderScrollEnabled && headerHeightPx > 0) {
|
|
@@ -650,7 +734,287 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
650
734
|
}
|
|
651
735
|
}
|
|
652
736
|
|
|
737
|
+
private fun restorationBodyOffset(key: String): Int {
|
|
738
|
+
val saved = if (transitionPageOffsets != null && key == pageKey(transitionSourcePage)) transitionSourceDisplayBody
|
|
739
|
+
else displayBodyOverrides[key] ?: transitionPageOffsets?.get(key) ?: pageOffsets[key] ?: 0
|
|
740
|
+
return if (nativeSmoothHeaderScrollEnabled && headerOffsetPx < headerHeightPx) 0 else saved
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
private fun captureRecyclerAnchor(view: RecyclerView): RecyclerReadingAnchor? {
|
|
744
|
+
val manager = view.layoutManager as? LinearLayoutManager ?: return null
|
|
745
|
+
if (manager.orientation != RecyclerView.VERTICAL) return null
|
|
746
|
+
val position = manager.findFirstVisibleItemPosition()
|
|
747
|
+
val child = manager.findViewByPosition(position) ?: return null
|
|
748
|
+
val adapter = view.adapter ?: return null
|
|
749
|
+
return RecyclerReadingAnchor(view, adapter, position, manager.getDecoratedTop(child) - view.paddingTop)
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
private fun saveRecyclerReadingAnchor(key: String, view: RecyclerView) {
|
|
753
|
+
captureRecyclerAnchor(view)?.let { recyclerReadingAnchors[key] = it }
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
private fun restoreRecyclerAnchor(view: RecyclerView, key: String, index: Int, request: PageRestoreRequest?): Boolean {
|
|
757
|
+
if (!nativeSmoothHeaderScrollEnabled || pageKey(index) != key ||
|
|
758
|
+
primaryScrollableForPage(index) !== view || !isCurrentPageRestore(key, request) ||
|
|
759
|
+
!isAttachedToWindow || !view.isAttachedToWindow || view.width <= 0 || view.height <= 0 ||
|
|
760
|
+
isInLayout || view.isInLayout || view.isComputingLayout) return false
|
|
761
|
+
val manager = view.layoutManager as? LinearLayoutManager
|
|
762
|
+
val reading = recyclerReadingAnchors[key]?.takeIf { it.view.get() === view && it.adapter === view.adapter }
|
|
763
|
+
val display = (if (transitionPageOffsets != null && index == transitionSourcePage) transitionSourceRecyclerAnchor
|
|
764
|
+
else displayRecyclerAnchors[key])?.takeIf { it.view.get() === view && it.adapter === view.adapter }
|
|
765
|
+
if (manager == null || manager.orientation != RecyclerView.VERTICAL ||
|
|
766
|
+
(headerOffsetPx == headerHeightPx && reading == null && display == null && restorationBodyOffset(key) > 0)) {
|
|
767
|
+
val saved = restorationBodyOffset(key)
|
|
768
|
+
val current = view.computeVerticalScrollOffset()
|
|
769
|
+
val previousSynthetic = syntheticPageRestore
|
|
770
|
+
syntheticPageRestore = true
|
|
771
|
+
try { if (saved != current) view.scrollBy(0, saved - current) }
|
|
772
|
+
finally { syntheticPageRestore = previousSynthetic }
|
|
773
|
+
return saved <= view.computeVerticalScrollOffset() || view.canScrollVertically(1)
|
|
774
|
+
}
|
|
775
|
+
val target = if (headerOffsetPx < headerHeightPx) null else display ?: reading
|
|
776
|
+
val position = target?.position ?: 0
|
|
777
|
+
val top = target?.top ?: 0
|
|
778
|
+
if (position !in 0 until (view.adapter?.itemCount ?: 0)) return false
|
|
779
|
+
fun reached(): Boolean {
|
|
780
|
+
val actual = captureRecyclerAnchor(view) ?: return false
|
|
781
|
+
return actual.position == position && actual.top == top
|
|
782
|
+
}
|
|
783
|
+
if (!reached()) {
|
|
784
|
+
if (!isAttachedToWindow || !view.isAttachedToWindow || width <= 0 || height <= 0 ||
|
|
785
|
+
view.width <= 0 || view.height <= 0 || isInLayout || view.isInLayout || view.isComputingLayout ||
|
|
786
|
+
pageKey(index) != key || primaryScrollableForPage(index) !== view || !isCurrentPageRestore(key, request)) return false
|
|
787
|
+
val previousSynthetic = syntheticPageRestore
|
|
788
|
+
syntheticPageRestore = true
|
|
789
|
+
try {
|
|
790
|
+
manager.scrollToPositionWithOffset(position, top)
|
|
791
|
+
// Use the existing inset commit boundary once, before the target's first frame.
|
|
792
|
+
view.forceLayout()
|
|
793
|
+
view.measure(MeasureSpec.makeMeasureSpec(view.width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(view.height, MeasureSpec.EXACTLY))
|
|
794
|
+
view.layout(view.left, view.top, view.right, view.bottom)
|
|
795
|
+
} finally {
|
|
796
|
+
syntheticPageRestore = previousSynthetic
|
|
797
|
+
}
|
|
798
|
+
offsetDiag("recycler-anchor-restore-position=$position-top=$top", view, key, index, request)
|
|
799
|
+
}
|
|
800
|
+
val readingStillUnreachable = display != null && reading != null &&
|
|
801
|
+
(reading.position != position || reading.top != top) && !view.canScrollVertically(1)
|
|
802
|
+
return reached() && !readingStillUnreachable
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
private fun alignPageBody(index: Int, force: Boolean = false) {
|
|
806
|
+
if (!nativeSmoothHeaderScrollEnabled || index !in 0 until adapter.itemCount) return
|
|
807
|
+
val view = primaryScrollableForPage(index) ?: return
|
|
808
|
+
val key = pageKey(index)
|
|
809
|
+
if (!force && transitionAlignedViews[view] == key) return
|
|
810
|
+
transitionAlignedViews[view] = key
|
|
811
|
+
if (transitionPageOffsets != null && index != transitionSourcePage) {
|
|
812
|
+
displayBodyOverrides.remove(key)
|
|
813
|
+
displayRecyclerAnchors.remove(key)
|
|
814
|
+
}
|
|
815
|
+
alignedBodyKeys.add(key)
|
|
816
|
+
if (headerOffsetPx < headerHeightPx && (pageOffsets[key] ?: 0) > 0) suspendedBodyKeys.add(key)
|
|
817
|
+
val previousSynthetic = syntheticPageRestore
|
|
818
|
+
syntheticPageRestore = true
|
|
819
|
+
when (view) {
|
|
820
|
+
is NativeScrollerViewport -> {
|
|
821
|
+
ownedScrollerKeys.remove(view)
|
|
822
|
+
applyOwnedScrollerInsets(view, index)
|
|
823
|
+
view.host.restoreScrollOffset(restorationBodyOffset(key))
|
|
824
|
+
}
|
|
825
|
+
is RecyclerView -> {
|
|
826
|
+
restoredRecyclerKeys.remove(view)
|
|
827
|
+
applyRecyclerInsets(view, index)
|
|
828
|
+
restoreRecyclerAnchor(view, key, index, pageRestoreRequests[key])
|
|
829
|
+
}
|
|
830
|
+
is ScrollView -> {
|
|
831
|
+
restoredScrollerKeys.remove(view)
|
|
832
|
+
applyNativeScrollerInsets(view, index)
|
|
833
|
+
if (view.height > 0 && view.childCount > 0) view.scrollTo(view.scrollX, restorationBodyOffset(key))
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
syntheticPageRestore = previousSynthetic
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
private fun takeOverBodyForVerticalConsumption() {
|
|
840
|
+
cancelPagerTop()
|
|
841
|
+
if (!nativeSmoothHeaderScrollEnabled || transitionHeaderOffsets != null || syntheticPageRestore) return
|
|
842
|
+
val key = currentPageKey()
|
|
843
|
+
commandBodyKeys.remove(key)
|
|
844
|
+
explicitTopCommand = null
|
|
845
|
+
suspendedBodyKeys.remove(key)
|
|
846
|
+
alignedBodyKeys.remove(key)
|
|
847
|
+
displayBodyOverrides.remove(key)
|
|
848
|
+
displayRecyclerAnchors.remove(key)
|
|
849
|
+
pageOffsets[key] = primaryScrollableForPage(selectedPage)?.let(::verticalScrollOffset) ?: 0
|
|
850
|
+
(primaryScrollableForPage(selectedPage) as? RecyclerView)?.let { saveRecyclerReadingAnchor(key, it) }
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
internal fun acceptObservedScrollCommand(view: View, explicitTop: Boolean, phase: Int = 0) {
|
|
854
|
+
if (dispatchingPagerTop && matchesPagerTop(view)) return
|
|
855
|
+
if (phase == 0 && view === (observedRecyclerView ?: observedOwnedScroller ?: observedNativeScroller)) cancelPagerTop()
|
|
856
|
+
if (phase != 0 && matchesPagerTop(view)) { finishPagerTopIfSettled(); return }
|
|
857
|
+
val owner = observedRecyclerView ?: observedOwnedScroller ?: observedNativeScroller
|
|
858
|
+
if (!nativeSmoothHeaderScrollEnabled || view !== owner || transitionHeaderOffsets != null || syntheticPageRestore ||
|
|
859
|
+
!isAttachedToWindow || !view.isAttachedToWindow) return
|
|
860
|
+
val key = currentPageKey()
|
|
861
|
+
val observedKey = observedRecyclerKey ?: observedOwnedScrollerKey ?: observedNativeScrollerKey
|
|
862
|
+
if (observedKey != key) return
|
|
863
|
+
if (phase == 1) { commandBodyKeys.add(key); return }
|
|
864
|
+
if (phase == 2) {
|
|
865
|
+
if (!commandBodyKeys.remove(key)) return
|
|
866
|
+
val completingExplicitTop = hasExplicitTopCommand(view, key)
|
|
867
|
+
val manager = (view as? RecyclerView)?.layoutManager as? LinearLayoutManager
|
|
868
|
+
val first = manager?.findViewByPosition(0)
|
|
869
|
+
val atExplicitTop = completingExplicitTop && manager != null && manager.orientation == RecyclerView.VERTICAL &&
|
|
870
|
+
manager.findFirstVisibleItemPosition() == 0 && first != null &&
|
|
871
|
+
manager.getDecoratedTop(first) - view.paddingTop == 0
|
|
872
|
+
explicitTopCommand = null
|
|
873
|
+
pageOffsets[key] = if (atExplicitTop) 0 else verticalScrollOffset(view)
|
|
874
|
+
if (view is RecyclerView) saveRecyclerReadingAnchor(key, view)
|
|
875
|
+
if (atExplicitTop) setHeaderOffset(0)
|
|
876
|
+
else if ((pageOffsets[key] ?: 0) > 0) setHeaderOffset(headerHeightPx)
|
|
877
|
+
offsetDiag("command-result-complete", view, key)
|
|
878
|
+
return
|
|
879
|
+
}
|
|
880
|
+
commandBodyKeys.remove(key)
|
|
881
|
+
explicitTopCommand = if (explicitTop && view is RecyclerView) {
|
|
882
|
+
key to PageRestoreRequest(view, selectedPage, attachmentGeneration)
|
|
883
|
+
} else null
|
|
884
|
+
pageRestoreRequests.remove(key)
|
|
885
|
+
pendingPageRestores.remove(key)
|
|
886
|
+
ownedScrollerRestoreRequests.remove(key)
|
|
887
|
+
if (view is RecyclerView) deferredRecyclerGeometry.remove(view)
|
|
888
|
+
(view as? NativeScrollerViewport)?.host?.cancelPendingScrollRestore()
|
|
889
|
+
suspendedBodyKeys.remove(key)
|
|
890
|
+
alignedBodyKeys.remove(key)
|
|
891
|
+
displayBodyOverrides.remove(key)
|
|
892
|
+
displayRecyclerAnchors.remove(key)
|
|
893
|
+
if (view is RecyclerView) saveRecyclerReadingAnchor(key, view)
|
|
894
|
+
pageOffsets[key] = if (explicitTop) 0 else verticalScrollOffset(view)
|
|
895
|
+
if (explicitTop) setHeaderOffset(0)
|
|
896
|
+
offsetDiag("command-body-takeover-top=$explicitTop", view, key)
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
private fun matchesPagerTop(view: View): Boolean {
|
|
900
|
+
val request = pagerTopRequest ?: return false
|
|
901
|
+
val owner = observedRecyclerView ?: observedOwnedScroller ?: observedNativeScroller
|
|
902
|
+
val observedKey = observedRecyclerKey ?: observedOwnedScrollerKey ?: observedNativeScrollerKey
|
|
903
|
+
return request.view.get() === view && owner === view && observedKey == request.key &&
|
|
904
|
+
request.key == currentPageKey() && request.pageIndex == selectedPage &&
|
|
905
|
+
request.generation == attachmentGeneration && view.isAttachedToWindow && isAttachedToWindow
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
private fun cancelPagerTop() {
|
|
909
|
+
pagerTopRequest?.let { commandBodyKeys.remove(it.key) }
|
|
910
|
+
pagerTopRequest = null
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
private fun recyclerIsAtBodyTop(view: RecyclerView): Boolean {
|
|
914
|
+
val manager = view.layoutManager as? LinearLayoutManager ?: return !view.canScrollVertically(-1)
|
|
915
|
+
val first = manager.findViewByPosition(0) ?: return view.adapter?.itemCount == 0
|
|
916
|
+
return manager.orientation == RecyclerView.VERTICAL && manager.findFirstVisibleItemPosition() == 0 &&
|
|
917
|
+
manager.getDecoratedTop(first) - view.paddingTop == 0
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
private fun finishPagerTopIfSettled() {
|
|
921
|
+
val request = pagerTopRequest ?: return
|
|
922
|
+
val view = request.view.get() ?: run { cancelPagerTop(); return }
|
|
923
|
+
if (!matchesPagerTop(view)) { cancelPagerTop(); return }
|
|
924
|
+
if (dispatchingPagerTop) return
|
|
925
|
+
val settled = when (view) {
|
|
926
|
+
is RecyclerView -> !view.isComputingLayout && view.scrollState == RecyclerView.SCROLL_STATE_IDLE &&
|
|
927
|
+
view.layoutManager?.isSmoothScrolling != true && recyclerIsAtBodyTop(view)
|
|
928
|
+
is NativeScrollerViewport -> !view.isReadingMotionActive() && view.scrollY == 0
|
|
929
|
+
is ScrollView -> view.scrollY == 0
|
|
930
|
+
else -> false
|
|
931
|
+
}
|
|
932
|
+
if (!settled) return
|
|
933
|
+
pageOffsets[request.key] = 0
|
|
934
|
+
if (view is RecyclerView) saveRecyclerReadingAnchor(request.key, view)
|
|
935
|
+
setHeaderOffset(if (request.target == 1) headerHeightPx else 0)
|
|
936
|
+
offsetDiag("pager-top-complete-target=${request.target}-token=${request.token}", view, request.key)
|
|
937
|
+
cancelPagerTop()
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
internal fun scrollToTop(target: Int, animated: Boolean) {
|
|
941
|
+
if (target !in 0..1 || pager.scrollState != ViewPager2.SCROLL_STATE_IDLE || transitionHeaderOffsets != null ||
|
|
942
|
+
syntheticPageRestore || selectedPage !in 0 until adapter.itemCount || !isAttachedToWindow) return
|
|
943
|
+
val view = observedRecyclerView ?: observedOwnedScroller ?: observedNativeScroller ?: return
|
|
944
|
+
val key = currentPageKey()
|
|
945
|
+
val ownerKey = observedRecyclerKey ?: observedOwnedScrollerKey ?: observedNativeScrollerKey
|
|
946
|
+
if (ownerKey != key || !view.isAttachedToWindow || primaryScrollableForPage(selectedPage) !== view) return
|
|
947
|
+
// Finish old child notifications before installing the new Pager-owned target.
|
|
948
|
+
cancelPagerTop()
|
|
949
|
+
if (view is RecyclerView) {
|
|
950
|
+
(view.getTag(R.id.onekey_native_scroll_coordinator_command_cancel_listener) as? Runnable)?.run()
|
|
951
|
+
view.stopScroll()
|
|
952
|
+
} else if (view is NativeScrollerViewport) {
|
|
953
|
+
view.stopMotion()
|
|
954
|
+
view.host.cancelPendingScrollRestore()
|
|
955
|
+
}
|
|
956
|
+
explicitTopCommand = null
|
|
957
|
+
pageRestoreRequests.remove(key)
|
|
958
|
+
pendingPageRestores.remove(key)
|
|
959
|
+
ownedScrollerRestoreRequests.remove(key)
|
|
960
|
+
suspendedBodyKeys.remove(key)
|
|
961
|
+
alignedBodyKeys.remove(key)
|
|
962
|
+
displayBodyOverrides.remove(key)
|
|
963
|
+
displayRecyclerAnchors.remove(key)
|
|
964
|
+
recyclerReadingAnchors.remove(key)
|
|
965
|
+
if (view is RecyclerView) deferredRecyclerGeometry.remove(view)
|
|
966
|
+
pageOffsets[key] = 0
|
|
967
|
+
val request = PagerTopRequest(view, key, selectedPage, attachmentGeneration, target, ++pagerTopToken)
|
|
968
|
+
pagerTopRequest = request
|
|
969
|
+
commandBodyKeys.add(key)
|
|
970
|
+
setHeaderOffset(if (target == 1) headerHeightPx else 0)
|
|
971
|
+
offsetDiag("pager-top-begin-target=$target-animated=$animated-token=${request.token}", view, key)
|
|
972
|
+
dispatchingPagerTop = true
|
|
973
|
+
try {
|
|
974
|
+
when (view) {
|
|
975
|
+
is RecyclerView -> {
|
|
976
|
+
val manager = view.layoutManager as? LinearLayoutManager
|
|
977
|
+
if (animated && !recyclerIsAtBodyTop(view)) view.smoothScrollToPosition(0)
|
|
978
|
+
else if (manager != null && manager.orientation == RecyclerView.VERTICAL) {
|
|
979
|
+
manager.scrollToPositionWithOffset(0, 0)
|
|
980
|
+
if (view.width > 0 && view.height > 0 && !isInLayout && !view.isInLayout && !view.isComputingLayout) {
|
|
981
|
+
view.measure(MeasureSpec.makeMeasureSpec(view.width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(view.height, MeasureSpec.EXACTLY))
|
|
982
|
+
view.layout(view.left, view.top, view.right, view.bottom)
|
|
983
|
+
}
|
|
984
|
+
} else view.scrollToPosition(0)
|
|
985
|
+
}
|
|
986
|
+
is NativeScrollerViewport -> view.host.scrollToPosition(0, 0, animated)
|
|
987
|
+
is ScrollView -> if (animated) view.smoothScrollTo(0, 0) else view.scrollTo(0, 0)
|
|
988
|
+
}
|
|
989
|
+
} finally { dispatchingPagerTop = false }
|
|
990
|
+
finishPagerTopIfSettled()
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
private fun hasExplicitTopCommand(view: View, key: String): Boolean {
|
|
994
|
+
val command = explicitTopCommand ?: return false
|
|
995
|
+
val request = command.second
|
|
996
|
+
return view is RecyclerView && observedRecyclerView === view && observedRecyclerKey == key &&
|
|
997
|
+
key == currentPageKey() && command.first == key && request.view.get() === view &&
|
|
998
|
+
request.pageIndex == selectedPage && request.generation == attachmentGeneration
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
private fun publishScrollCommandListener(recycler: RecyclerView, remove: Boolean = false) {
|
|
1002
|
+
val tag = R.id.onekey_native_scroll_coordinator_command_listener
|
|
1003
|
+
@Suppress("UNCHECKED_CAST")
|
|
1004
|
+
val existing = recycler.getTag(tag) as? MutableMap<Any, BiConsumer<Boolean, Int>>
|
|
1005
|
+
val listeners = existing ?: if (remove) return else mutableMapOf<Any, BiConsumer<Boolean, Int>>().also { recycler.setTag(tag, it) }
|
|
1006
|
+
if (remove) listeners.remove(this)
|
|
1007
|
+
else {
|
|
1008
|
+
val generation = attachmentGeneration
|
|
1009
|
+
val key = currentPageKey()
|
|
1010
|
+
listeners[this] = BiConsumer { explicitTop, phase ->
|
|
1011
|
+
if (generation == attachmentGeneration && key == currentPageKey()) acceptObservedScrollCommand(recycler, explicitTop, phase)
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
653
1016
|
private fun maximumHeaderOffset(): Int {
|
|
1017
|
+
if (nativeSmoothHeaderScrollEnabled) return headerHeightPx
|
|
654
1018
|
val scrollable = observedRecyclerView
|
|
655
1019
|
?: observedOwnedScroller
|
|
656
1020
|
?: observedNativeScroller
|
|
@@ -669,7 +1033,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
669
1033
|
private fun setHeaderOffset(offset: Int) {
|
|
670
1034
|
// val next = offset.coerceIn(0, headerHeightPx)
|
|
671
1035
|
val pending = nativeSmoothHeaderScrollEnabled && currentPageKey() in pendingPageRestores
|
|
672
|
-
val maximum = if (transitionHeaderOffsets != null || pending) headerHeightPx else maximumHeaderOffset()
|
|
1036
|
+
val maximum = if (transitionHeaderOffsets != null || pending || pagerTopRequest != null) headerHeightPx else maximumHeaderOffset()
|
|
673
1037
|
val next = offset.coerceIn(0, maximum)
|
|
674
1038
|
if (headerOffsetPx == next) return
|
|
675
1039
|
headerOffsetPx = next
|
|
@@ -801,7 +1165,10 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
801
1165
|
detachPrimaryScrollObserver(recordOffset = !nativeSmoothHeaderScrollEnabled)
|
|
802
1166
|
// A replacement scroller starts at the beginning while retaining header collapse.
|
|
803
1167
|
offsetDiag("primary-replacement-cache-before", previous)
|
|
804
|
-
if (!nativeSmoothHeaderScrollEnabled)
|
|
1168
|
+
if (!nativeSmoothHeaderScrollEnabled) {
|
|
1169
|
+
offsetDiag("cache-write-replacement-new=0", previous)
|
|
1170
|
+
pageOffsets[currentPageKey()] = 0
|
|
1171
|
+
}
|
|
805
1172
|
offsetDiag("primary-replacement-cache-after", previous)
|
|
806
1173
|
}
|
|
807
1174
|
when (val scrollable = primaryScrollableForPage(selectedPage)) {
|
|
@@ -822,20 +1189,32 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
822
1189
|
offsetDiag("recycler-attach-before", recycler)
|
|
823
1190
|
observedRecyclerView = recycler
|
|
824
1191
|
observedRecyclerKey = currentPageKey()
|
|
1192
|
+
publishScrollCommandListener(recycler)
|
|
825
1193
|
applyRecyclerInsets(recycler, selectedPage)
|
|
826
1194
|
publishConsumedOffset(recycler)
|
|
827
1195
|
val key = currentPageKey()
|
|
828
1196
|
val listener = object : RecyclerView.OnScrollListener() {
|
|
1197
|
+
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
|
1198
|
+
if (newState == RecyclerView.SCROLL_STATE_IDLE) finishPagerTopIfSettled()
|
|
1199
|
+
}
|
|
829
1200
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
|
830
1201
|
if (observedScrollListener !== this || observedRecyclerKey != key ||
|
|
831
1202
|
key != currentPageKey() || !isAttachedToWindow ||
|
|
832
|
-
transitionHeaderOffsets != null || syntheticPageRestore || key in pendingPageRestores)
|
|
1203
|
+
transitionHeaderOffsets != null || syntheticPageRestore || key in pendingPageRestores || key in alignedBodyKeys) {
|
|
1204
|
+
if (offsetDiagEnabled() && (dx != 0 || dy != 0 || pageOffsets[key] != recyclerView.computeVerticalScrollOffset())) {
|
|
1205
|
+
offsetDiag("recycler-observer-skip-dx=$dx-dy=$dy-listenerCurrent=${observedScrollListener === this}", recyclerView, key)
|
|
1206
|
+
}
|
|
1207
|
+
return
|
|
1208
|
+
}
|
|
1209
|
+
if (nativeSmoothHeaderScrollEnabled && key !in commandBodyKeys &&
|
|
1210
|
+
(dy == 0 || recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE)) return
|
|
833
1211
|
val contentOffset = recyclerView.computeVerticalScrollOffset()
|
|
834
|
-
if (pageOffsets[
|
|
1212
|
+
if (pageOffsets[key] != contentOffset) offsetDiag("cache-write-recycler-observer-new=$contentOffset-dx=$dx-dy=$dy", recyclerView, key)
|
|
835
1213
|
pageOffsets[key] = contentOffset
|
|
1214
|
+
if (nativeSmoothHeaderScrollEnabled) saveRecyclerReadingAnchor(key, recyclerView)
|
|
836
1215
|
// Nested touch scrolling moves list content only after the header has
|
|
837
1216
|
// collapsed. Programmatic list jumps bypass those parent callbacks.
|
|
838
|
-
if (contentOffset > 0 && headerOffsetPx < headerHeightPx) {
|
|
1217
|
+
if (contentOffset > 0 && headerOffsetPx < headerHeightPx && !hasExplicitTopCommand(recyclerView, key) && !matchesPagerTop(recyclerView)) {
|
|
839
1218
|
offsetDiag("recycler-positive-collapse-before", recyclerView)
|
|
840
1219
|
setHeaderOffset(headerHeightPx)
|
|
841
1220
|
offsetDiag("recycler-positive-collapse-after", recyclerView)
|
|
@@ -851,15 +1230,21 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
851
1230
|
}
|
|
852
1231
|
|
|
853
1232
|
private fun detachRecyclerObserver(recordOffset: Boolean) {
|
|
1233
|
+
cancelPagerTop()
|
|
1234
|
+
explicitTopCommand = null
|
|
854
1235
|
val recycler = observedRecyclerView
|
|
855
1236
|
val listener = observedScrollListener
|
|
856
1237
|
if (recycler != null) {
|
|
857
|
-
offsetDiag("recycler-detach-save-before-record=$recordOffset", recycler)
|
|
858
|
-
if (recordOffset && transitionPageOffsets == null) observedRecyclerKey?.let {
|
|
859
|
-
if (it !in pendingPageRestores)
|
|
1238
|
+
offsetDiag("recycler-detach-save-before-record=$recordOffset", recycler, observedRecyclerKey ?: currentPageKey())
|
|
1239
|
+
if (recordOffset && !nativeSmoothHeaderScrollEnabled && transitionPageOffsets == null) observedRecyclerKey?.let {
|
|
1240
|
+
if (it !in pendingPageRestores) {
|
|
1241
|
+
offsetDiag("cache-write-recycler-detach-new=${recycler.computeVerticalScrollOffset()}", recycler, it)
|
|
1242
|
+
pageOffsets[it] = recycler.computeVerticalScrollOffset()
|
|
1243
|
+
}
|
|
860
1244
|
}
|
|
861
|
-
offsetDiag("recycler-detach-save-after", recycler)
|
|
1245
|
+
offsetDiag("recycler-detach-save-after", recycler, observedRecyclerKey ?: currentPageKey())
|
|
862
1246
|
publishConsumedOffset(recycler, remove = true)
|
|
1247
|
+
publishScrollCommandListener(recycler, remove = true)
|
|
863
1248
|
}
|
|
864
1249
|
if (recycler != null && listener != null) recycler.removeOnScrollListener(listener)
|
|
865
1250
|
if (recycler != null) {
|
|
@@ -892,11 +1277,12 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
892
1277
|
return
|
|
893
1278
|
}
|
|
894
1279
|
if (key != currentPageKey() || !isAttachedToWindow ||
|
|
895
|
-
transitionHeaderOffsets != null || syntheticPageRestore || key in pendingPageRestores) return
|
|
1280
|
+
transitionHeaderOffsets != null || syntheticPageRestore || key in pendingPageRestores || key in alignedBodyKeys) return
|
|
1281
|
+
if (nativeSmoothHeaderScrollEnabled && nestedScrollingParentHelper.nestedScrollAxes == 0 && key !in commandBodyKeys) return
|
|
896
1282
|
val contentOffset = scroller.scrollY
|
|
897
|
-
if (pageOffsets[
|
|
1283
|
+
if (pageOffsets[key] != contentOffset) offsetDiag("cache-write-scrollview-observer-new=$contentOffset", scroller, key)
|
|
898
1284
|
pageOffsets[key] = contentOffset
|
|
899
|
-
if (contentOffset > 0 && headerOffsetPx < headerHeightPx) {
|
|
1285
|
+
if (contentOffset > 0 && headerOffsetPx < headerHeightPx && !matchesPagerTop(scroller)) {
|
|
900
1286
|
offsetDiag("scrollview-positive-collapse-before", scroller)
|
|
901
1287
|
setHeaderOffset(headerHeightPx)
|
|
902
1288
|
offsetDiag("scrollview-positive-collapse-after", scroller)
|
|
@@ -914,13 +1300,17 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
914
1300
|
}
|
|
915
1301
|
|
|
916
1302
|
private fun detachNativeScrollerObserver(recordOffset: Boolean) {
|
|
1303
|
+
cancelPagerTop()
|
|
917
1304
|
val scroller = observedNativeScroller
|
|
918
1305
|
val listener = observedNativeScrollerListener
|
|
919
|
-
offsetDiag("scrollview-detach-save-before-record=$recordOffset", scroller)
|
|
920
|
-
if (scroller != null && recordOffset && transitionPageOffsets == null) observedNativeScrollerKey?.let {
|
|
921
|
-
if (it !in pendingPageRestores)
|
|
1306
|
+
offsetDiag("scrollview-detach-save-before-record=$recordOffset", scroller, observedNativeScrollerKey ?: currentPageKey())
|
|
1307
|
+
if (scroller != null && recordOffset && !nativeSmoothHeaderScrollEnabled && transitionPageOffsets == null) observedNativeScrollerKey?.let {
|
|
1308
|
+
if (it !in pendingPageRestores) {
|
|
1309
|
+
offsetDiag("cache-write-scrollview-detach-new=${scroller.scrollY}", scroller, it)
|
|
1310
|
+
pageOffsets[it] = scroller.scrollY
|
|
1311
|
+
}
|
|
922
1312
|
}
|
|
923
|
-
offsetDiag("scrollview-detach-save-after", scroller)
|
|
1313
|
+
offsetDiag("scrollview-detach-save-after", scroller, observedNativeScrollerKey ?: currentPageKey())
|
|
924
1314
|
if (scroller != null && listener != null) {
|
|
925
1315
|
removeScrollChangedListener(observedNativeScrollerTreeObserver, scroller, listener)
|
|
926
1316
|
}
|
|
@@ -963,18 +1353,21 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
963
1353
|
val restoreRequest = registerPageRestore(key, scroller, pageIndex)
|
|
964
1354
|
if (nativeSmoothHeaderScrollEnabled) ownedScrollerRestoreRequests[key] = WeakReference(scroller) to null
|
|
965
1355
|
val generation = attachmentGeneration
|
|
1356
|
+
offsetDiag("owned-enqueue", scroller, key, pageIndex, restoreRequest)
|
|
966
1357
|
postForCurrentAttachment {
|
|
1358
|
+
offsetDiag("owned-execute", scroller, key, pageIndex, restoreRequest)
|
|
967
1359
|
if (isCurrentPageRestore(key, restoreRequest) && ownedScrollerKeys[scroller] == key && generation == attachmentGeneration &&
|
|
968
1360
|
scroller.isAttachedToWindow && (!nativeSmoothHeaderScrollEnabled ||
|
|
969
1361
|
(key in pendingPageRestores && ownedScrollerRestoreRequests[key]?.first?.get() === scroller))) {
|
|
970
|
-
offsetDiag("owned-restore-before", scroller, key)
|
|
971
|
-
val saved =
|
|
1362
|
+
offsetDiag("owned-restore-before", scroller, key, pageIndex, restoreRequest)
|
|
1363
|
+
val saved = restorationBodyOffset(key)
|
|
972
1364
|
scroller.host.restoreScrollOffset(saved)
|
|
973
1365
|
if (nativeSmoothHeaderScrollEnabled) {
|
|
974
1366
|
ownedScrollerRestoreRequests[key] = WeakReference(scroller) to generation
|
|
975
1367
|
recordOwnedScrollerOffset(scroller, key)
|
|
976
1368
|
}
|
|
977
|
-
offsetDiag("owned-restore-after", scroller, key)
|
|
1369
|
+
offsetDiag("owned-restore-after-saved=$saved", scroller, key, pageIndex, restoreRequest)
|
|
1370
|
+
offsetDiagAfterDraw(scroller, key, pageIndex)
|
|
978
1371
|
}
|
|
979
1372
|
}
|
|
980
1373
|
}
|
|
@@ -1027,11 +1420,12 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1027
1420
|
clearPageRestoreForView(key, scroller)
|
|
1028
1421
|
}
|
|
1029
1422
|
if (observedOwnedScroller !== scroller || observedOwnedScrollerKey != key ||
|
|
1030
|
-
key != currentPageKey() || transitionHeaderOffsets != null || syntheticPageRestore) return
|
|
1423
|
+
key != currentPageKey() || transitionHeaderOffsets != null || syntheticPageRestore || key in alignedBodyKeys) return
|
|
1424
|
+
if (nativeSmoothHeaderScrollEnabled && !scroller.isReadingMotionActive() && key !in commandBodyKeys) return
|
|
1031
1425
|
val contentOffset = scroller.scrollY
|
|
1032
|
-
if (pageOffsets[key] != contentOffset) offsetDiag("owned-
|
|
1426
|
+
if (pageOffsets[key] != contentOffset) offsetDiag("cache-write-owned-observer-new=$contentOffset", scroller, key)
|
|
1033
1427
|
pageOffsets[key] = contentOffset
|
|
1034
|
-
if (contentOffset > 0 && headerOffsetPx < headerHeightPx) {
|
|
1428
|
+
if (contentOffset > 0 && headerOffsetPx < headerHeightPx && !matchesPagerTop(scroller)) {
|
|
1035
1429
|
offsetDiag("owned-positive-collapse-before", scroller, key)
|
|
1036
1430
|
setHeaderOffset(headerHeightPx)
|
|
1037
1431
|
offsetDiag("owned-positive-collapse-after", scroller, key)
|
|
@@ -1039,14 +1433,18 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1039
1433
|
}
|
|
1040
1434
|
|
|
1041
1435
|
private fun detachOwnedScrollerObserver(recordOffset: Boolean) {
|
|
1436
|
+
cancelPagerTop()
|
|
1042
1437
|
val scroller = observedOwnedScroller ?: return
|
|
1043
1438
|
scroller.stopMotion()
|
|
1044
1439
|
publishConsumedOffset(scroller, remove = true)
|
|
1045
|
-
offsetDiag("owned-detach-save-before-record=$recordOffset", scroller)
|
|
1046
|
-
if (recordOffset && transitionPageOffsets == null) observedOwnedScrollerKey?.let {
|
|
1047
|
-
if (it !in pendingPageRestores)
|
|
1440
|
+
offsetDiag("owned-detach-save-before-record=$recordOffset", scroller, observedOwnedScrollerKey ?: currentPageKey())
|
|
1441
|
+
if (recordOffset && !nativeSmoothHeaderScrollEnabled && transitionPageOffsets == null) observedOwnedScrollerKey?.let {
|
|
1442
|
+
if (it !in pendingPageRestores) {
|
|
1443
|
+
offsetDiag("cache-write-owned-detach-new=${scroller.scrollY}", scroller, it)
|
|
1444
|
+
pageOffsets[it] = scroller.scrollY
|
|
1445
|
+
}
|
|
1048
1446
|
}
|
|
1049
|
-
offsetDiag("owned-detach-save-after", scroller)
|
|
1447
|
+
offsetDiag("owned-detach-save-after", scroller, observedOwnedScrollerKey ?: currentPageKey())
|
|
1050
1448
|
observedOwnedScrollerListener?.let {
|
|
1051
1449
|
removeScrollChangedListener(observedOwnedScrollerTreeObserver, scroller, it)
|
|
1052
1450
|
}
|
|
@@ -1083,6 +1481,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1083
1481
|
val anchorOffset = if (first != RecyclerView.NO_POSITION) {
|
|
1084
1482
|
manager?.findViewByPosition(first)?.let { manager.getDecoratedTop(it) - recycler.paddingTop }
|
|
1085
1483
|
} else null
|
|
1484
|
+
offsetDiag("recycler-inset-before-nextTop=$top-anchorPosition=$first-anchorOffset=$anchorOffset", recycler, pageKey(pageIndex), pageIndex)
|
|
1086
1485
|
recycler.setPadding(original.left, top, original.right, bottom)
|
|
1087
1486
|
if (anchorOffset != null) manager?.scrollToPositionWithOffset(first, anchorOffset)
|
|
1088
1487
|
// React Native can stop this nested requestLayout at its React-owned parent.
|
|
@@ -1096,15 +1495,33 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1096
1495
|
recycler.layout(recycler.left, recycler.top, recycler.right, recycler.bottom)
|
|
1097
1496
|
}
|
|
1098
1497
|
}
|
|
1498
|
+
if (original.appliedTopInset != topInset) offsetDiag("recycler-inset-after-old=${original.appliedTopInset}-new=$topInset", recycler, pageKey(pageIndex), pageIndex)
|
|
1099
1499
|
original.appliedTopInset = topInset
|
|
1100
1500
|
val key = pageKey(pageIndex)
|
|
1501
|
+
val geometry = Triple(recycler.computeVerticalScrollRange(), recycler.computeVerticalScrollExtent(), recycler.childCount)
|
|
1502
|
+
deferredRecyclerGeometry[recycler]?.let { deferred ->
|
|
1503
|
+
if (geometry != deferred) {
|
|
1504
|
+
restoredRecyclerKeys.remove(recycler)
|
|
1505
|
+
deferredRecyclerGeometry.remove(recycler)
|
|
1506
|
+
if (nativeSmoothHeaderScrollEnabled && headerOffsetPx == headerHeightPx) {
|
|
1507
|
+
displayBodyOverrides.remove(key)
|
|
1508
|
+
displayRecyclerAnchors.remove(key)
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1101
1512
|
if (restoredRecyclerKeys[recycler] != key) {
|
|
1102
1513
|
restoredRecyclerKeys[recycler] = key
|
|
1103
1514
|
val restoreRequest = registerPageRestore(key, recycler, pageIndex)
|
|
1104
1515
|
val generation = attachmentGeneration
|
|
1516
|
+
offsetDiag("recycler-enqueue", recycler, key, pageIndex, restoreRequest)
|
|
1517
|
+
if (nativeSmoothHeaderScrollEnabled && recycler.height > 0 && recycler.childCount > 0) {
|
|
1518
|
+
// Consume an available reading anchor before this pre-draw reaches the first frame.
|
|
1519
|
+
restoreRecyclerAnchor(recycler, key, pageIndex, restoreRequest)
|
|
1520
|
+
}
|
|
1105
1521
|
recycler.post {
|
|
1522
|
+
offsetDiag("recycler-execute", recycler, key, pageIndex, restoreRequest)
|
|
1106
1523
|
if (!isCurrentPageRestore(key, restoreRequest)) {
|
|
1107
|
-
offsetDiag("post-skip-cancelled
|
|
1524
|
+
offsetDiag("recycler-post-skip-cancelled", recycler, key, pageIndex, restoreRequest)
|
|
1108
1525
|
return@post
|
|
1109
1526
|
}
|
|
1110
1527
|
if (!isAttachedToWindow || generation != attachmentGeneration ||
|
|
@@ -1114,18 +1531,31 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1114
1531
|
clearPageRestoreForView(key, recycler, restoreRequest)
|
|
1115
1532
|
return@post
|
|
1116
1533
|
}
|
|
1117
|
-
val saved =
|
|
1534
|
+
val saved = restorationBodyOffset(key)
|
|
1118
1535
|
val current = recycler.computeVerticalScrollOffset()
|
|
1119
|
-
offsetDiag("recycler-restore-before-saved=$saved", recycler, key)
|
|
1536
|
+
offsetDiag("recycler-restore-before-saved=$saved-current=${current}-requestedDelta=${saved - current}", recycler, key, pageIndex, restoreRequest)
|
|
1120
1537
|
if (nativeSmoothHeaderScrollEnabled && (recycler.height <= 0 || recycler.childCount == 0)) {
|
|
1121
1538
|
restoredRecyclerKeys.remove(recycler)
|
|
1122
1539
|
if (pageRestoreRequests[key] === restoreRequest) pageRestoreRequests.remove(key)
|
|
1123
1540
|
return@post
|
|
1124
1541
|
}
|
|
1125
|
-
if (
|
|
1542
|
+
if (nativeSmoothHeaderScrollEnabled) {
|
|
1543
|
+
if (!restoreRecyclerAnchor(recycler, key, pageIndex, restoreRequest)) {
|
|
1544
|
+
if (isInLayout || recycler.isInLayout || recycler.isComputingLayout) {
|
|
1545
|
+
restoredRecyclerKeys.remove(recycler)
|
|
1546
|
+
if (pageRestoreRequests[key] === restoreRequest) pageRestoreRequests.remove(key)
|
|
1547
|
+
return@post
|
|
1548
|
+
}
|
|
1549
|
+
deferredRecyclerGeometry[recycler] = Triple(recycler.computeVerticalScrollRange(), recycler.computeVerticalScrollExtent(), recycler.childCount)
|
|
1550
|
+
offsetDiag("recycler-anchor-pending-saved=$saved", recycler, key, pageIndex, restoreRequest)
|
|
1551
|
+
return@post
|
|
1552
|
+
}
|
|
1553
|
+
} else if (saved != current) recycler.scrollBy(0, saved - current)
|
|
1554
|
+
deferredRecyclerGeometry.remove(recycler)
|
|
1126
1555
|
if (nativeSmoothHeaderScrollEnabled) clearPageRestoreForView(key, recycler, restoreRequest)
|
|
1127
1556
|
else pendingPageRestores.remove(key)
|
|
1128
|
-
offsetDiag("recycler-restore-after", recycler, key)
|
|
1557
|
+
offsetDiag("recycler-restore-after-saved=$saved", recycler, key, pageIndex, restoreRequest)
|
|
1558
|
+
offsetDiagAfterDraw(recycler, key, pageIndex)
|
|
1129
1559
|
}
|
|
1130
1560
|
}
|
|
1131
1561
|
}
|
|
@@ -1173,9 +1603,11 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1173
1603
|
restoredScrollerKeys[scroller] = key
|
|
1174
1604
|
val restoreRequest = registerPageRestore(key, scroller, pageIndex)
|
|
1175
1605
|
val generation = attachmentGeneration
|
|
1606
|
+
offsetDiag("scrollview-enqueue", scroller, key, pageIndex, restoreRequest)
|
|
1176
1607
|
scroller.post {
|
|
1608
|
+
offsetDiag("scrollview-execute", scroller, key, pageIndex, restoreRequest)
|
|
1177
1609
|
if (!isCurrentPageRestore(key, restoreRequest)) {
|
|
1178
|
-
offsetDiag("post-skip-cancelled
|
|
1610
|
+
offsetDiag("scrollview-post-skip-cancelled", scroller, key, pageIndex, restoreRequest)
|
|
1179
1611
|
return@post
|
|
1180
1612
|
}
|
|
1181
1613
|
if (!isAttachedToWindow || generation != attachmentGeneration ||
|
|
@@ -1185,8 +1617,8 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1185
1617
|
clearPageRestoreForView(key, scroller, restoreRequest)
|
|
1186
1618
|
return@post
|
|
1187
1619
|
}
|
|
1188
|
-
val saved =
|
|
1189
|
-
offsetDiag("scrollview-restore-before-saved=$saved", scroller, key)
|
|
1620
|
+
val saved = restorationBodyOffset(key)
|
|
1621
|
+
offsetDiag("scrollview-restore-before-saved=$saved-current=${scroller.scrollY}-requestedDelta=${saved - scroller.scrollY}", scroller, key, pageIndex, restoreRequest)
|
|
1190
1622
|
if (nativeSmoothHeaderScrollEnabled &&
|
|
1191
1623
|
(scroller.height <= 0 || (scroller.getChildAt(0)?.height ?: 0) <= 0)) {
|
|
1192
1624
|
restoredScrollerKeys.remove(scroller)
|
|
@@ -1196,7 +1628,8 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1196
1628
|
if (scroller.scrollY != saved) scroller.scrollTo(scroller.scrollX, saved)
|
|
1197
1629
|
if (nativeSmoothHeaderScrollEnabled) clearPageRestoreForView(key, scroller, restoreRequest)
|
|
1198
1630
|
else pendingPageRestores.remove(key)
|
|
1199
|
-
offsetDiag("scrollview-restore-after", scroller, key)
|
|
1631
|
+
offsetDiag("scrollview-restore-after-saved=$saved", scroller, key, pageIndex, restoreRequest)
|
|
1632
|
+
offsetDiagAfterDraw(scroller, key, pageIndex)
|
|
1200
1633
|
}
|
|
1201
1634
|
}
|
|
1202
1635
|
}
|
|
@@ -1638,9 +2071,13 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1638
2071
|
if (type == ViewCompat.TYPE_TOUCH) pendingRefreshPull = null
|
|
1639
2072
|
nestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes, type)
|
|
1640
2073
|
if (type == ViewCompat.TYPE_TOUCH) {
|
|
2074
|
+
val request = pageRestoreRequests[currentPageKey()]
|
|
2075
|
+
offsetDiag("touch-cancel-before", target, request = request)
|
|
2076
|
+
observedRecyclerView?.let { deferredRecyclerGeometry.remove(it) }
|
|
1641
2077
|
pageRestoreRequests.remove(currentPageKey())
|
|
1642
2078
|
pendingPageRestores.remove(currentPageKey())
|
|
1643
2079
|
ownedScrollerRestoreRequests.remove(currentPageKey())
|
|
2080
|
+
offsetDiag("touch-cancel-after", target, request = request)
|
|
1644
2081
|
}
|
|
1645
2082
|
}
|
|
1646
2083
|
|
|
@@ -1658,6 +2095,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1658
2095
|
// val used = min(dy, headerHeightPx - headerOffsetPx)
|
|
1659
2096
|
if (dy > 0 && headerOffsetPx < maximumOffset) {
|
|
1660
2097
|
val used = min(dy, maximumOffset - headerOffsetPx)
|
|
2098
|
+
if (type == ViewCompat.TYPE_TOUCH && used != 0) takeOverBodyForVerticalConsumption()
|
|
1661
2099
|
setHeaderOffset(headerOffsetPx + used)
|
|
1662
2100
|
consumed[1] += used
|
|
1663
2101
|
} else if (dy < 0 && headerOffsetPx > 0) {
|
|
@@ -1669,6 +2107,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1669
2107
|
}
|
|
1670
2108
|
if (!canScrollUp) {
|
|
1671
2109
|
val used = max(dy, -headerOffsetPx)
|
|
2110
|
+
if (type == ViewCompat.TYPE_TOUCH && used != 0) takeOverBodyForVerticalConsumption()
|
|
1672
2111
|
setHeaderOffset(headerOffsetPx + used)
|
|
1673
2112
|
consumed[1] += used
|
|
1674
2113
|
}
|
|
@@ -1684,8 +2123,11 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1684
2123
|
type: Int,
|
|
1685
2124
|
consumed: IntArray,
|
|
1686
2125
|
) {
|
|
1687
|
-
if (!isCurrentPageTarget(target)
|
|
2126
|
+
if (!isCurrentPageTarget(target)) return
|
|
2127
|
+
if (type == ViewCompat.TYPE_TOUCH && dyConsumed != 0) takeOverBodyForVerticalConsumption()
|
|
2128
|
+
if (dyUnconsumed >= 0) return
|
|
1688
2129
|
val used = max(dyUnconsumed, -headerOffsetPx)
|
|
2130
|
+
if (type == ViewCompat.TYPE_TOUCH && used != 0) takeOverBodyForVerticalConsumption()
|
|
1689
2131
|
setHeaderOffset(headerOffsetPx + used)
|
|
1690
2132
|
consumed[1] += used
|
|
1691
2133
|
if (type == ViewCompat.TYPE_TOUCH && dyUnconsumed < used &&
|
|
@@ -1763,6 +2205,14 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1763
2205
|
detachPrimaryScrollObserver()
|
|
1764
2206
|
pageRestoreRequests.clear()
|
|
1765
2207
|
pendingPageRestores.clear()
|
|
2208
|
+
commandBodyKeys.clear()
|
|
2209
|
+
explicitTopCommand = null
|
|
2210
|
+
suspendedBodyKeys.clear()
|
|
2211
|
+
alignedBodyKeys.clear()
|
|
2212
|
+
transitionAlignedViews.clear()
|
|
2213
|
+
deferredRecyclerGeometry.clear()
|
|
2214
|
+
displayBodyOverrides.clear()
|
|
2215
|
+
displayRecyclerAnchors.clear()
|
|
1766
2216
|
ownedScrollerRestoreRequests.clear()
|
|
1767
2217
|
transitionHeaderOffsets?.let { headers ->
|
|
1768
2218
|
resumeTransitionSource = transitionSourcePage
|