@onekeyfe/react-native-pager-view 3.0.173 → 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 +737 -25
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerViewManager.kt +12 -0
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerView.kt +16 -0
- package/android/src/main/res/values/native_scroll_coordinator_ids.xml +2 -0
- package/android/src/test/java/com/reactnativepagerview/NativeScrollerPendingRestoreTest.kt +11 -0
- package/docs/SPEC.md +56 -1
- package/ios/RNCCollapsiblePagerViewComponentView.mm +492 -29
- package/ios/RNCNativeScrollerComponentView.h +1 -0
- package/ios/RNCNativeScrollerComponentView.mm +48 -0
- package/ios/tests/RNCCollapsiblePagerFirstFrameTests.mm +293 -0
- 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
|
|
@@ -123,6 +124,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
123
124
|
private val originalScrollerPadding = WeakHashMap<ScrollView, ScrollerPadding>()
|
|
124
125
|
private val restoredScrollerKeys = WeakHashMap<ScrollView, String>()
|
|
125
126
|
private val ownedScrollerKeys = WeakHashMap<NativeScrollerViewport, String>()
|
|
127
|
+
private val ownedScrollerRestoreRequests = HashMap<String, Pair<WeakReference<NativeScrollerViewport>, Int?>>()
|
|
126
128
|
private var foregroundRefreshIndicator: WeakReference<ImageView>? = null
|
|
127
129
|
private var refreshForegroundWasVisible = false
|
|
128
130
|
private val refreshForegroundMatrix = Matrix()
|
|
@@ -130,13 +132,74 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
130
132
|
private val refreshForegroundAncestors = ArrayList<View>()
|
|
131
133
|
private var refreshForegroundAncestorAlpha = 1f
|
|
132
134
|
private val pageOffsets = HashMap<String, Int>()
|
|
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>>()
|
|
156
|
+
private var transitionHeaderOffsets: Map<String, Int>? = null
|
|
157
|
+
private var transitionPageOffsets: Map<String, Int>? = null
|
|
158
|
+
private var transitionSourcePage = 0
|
|
159
|
+
private var transitionTargetPage: Int? = null
|
|
160
|
+
private var syntheticPageRestore = false
|
|
161
|
+
private var resumeTransitionSource: Int? = null
|
|
162
|
+
private val pendingPageRestores = HashSet<String>()
|
|
163
|
+
private class PageRestoreRequest(view: View, val pageIndex: Int, val generation: Int) {
|
|
164
|
+
val view = WeakReference(view)
|
|
165
|
+
}
|
|
166
|
+
private val pageRestoreRequests = HashMap<String, PageRestoreRequest>()
|
|
167
|
+
|
|
168
|
+
private fun registerPageRestore(key: String, view: View, pageIndex: Int): PageRestoreRequest? {
|
|
169
|
+
if (!nativeSmoothHeaderScrollEnabled) return null
|
|
170
|
+
val previous = pageRestoreRequests[key]?.view?.get()
|
|
171
|
+
if (previous is NativeScrollerViewport && previous !== view) previous.host.cancelPendingScrollRestore()
|
|
172
|
+
ownedScrollerRestoreRequests.remove(key)
|
|
173
|
+
return PageRestoreRequest(view, pageIndex, attachmentGeneration).also {
|
|
174
|
+
pageRestoreRequests[key] = it
|
|
175
|
+
pendingPageRestores.add(key)
|
|
176
|
+
offsetDiag("request-register", view, key, pageIndex, it)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private fun isCurrentPageRestore(key: String, request: PageRestoreRequest?): Boolean =
|
|
181
|
+
!nativeSmoothHeaderScrollEnabled || (request != null && pageRestoreRequests[key] === request &&
|
|
182
|
+
key in pendingPageRestores && request.generation == attachmentGeneration &&
|
|
183
|
+
pageKey(request.pageIndex) == key)
|
|
184
|
+
|
|
185
|
+
private fun clearPageRestoreForView(key: String, view: View, request: PageRestoreRequest? = null) {
|
|
186
|
+
if (request != null && pageRestoreRequests[key] !== request) return
|
|
187
|
+
if (pageRestoreRequests[key]?.view?.get() !== view) return
|
|
188
|
+
offsetDiag("request-clear-before", view, key, request = request ?: pageRestoreRequests[key])
|
|
189
|
+
pageRestoreRequests.remove(key)
|
|
190
|
+
pendingPageRestores.remove(key)
|
|
191
|
+
ownedScrollerRestoreRequests.remove(key)
|
|
192
|
+
offsetDiag("request-clear-after", view, key, request = request)
|
|
193
|
+
}
|
|
133
194
|
private var observedRecyclerView: RecyclerView? = null
|
|
195
|
+
private var observedRecyclerKey: String? = null
|
|
134
196
|
private var observedScrollListener: RecyclerView.OnScrollListener? = null
|
|
135
197
|
private var observedOwnedScroller: NativeScrollerViewport? = null
|
|
136
198
|
private var observedOwnedScrollerKey: String? = null
|
|
137
199
|
private var observedOwnedScrollerListener: ViewTreeObserver.OnScrollChangedListener? = null
|
|
138
200
|
private var observedOwnedScrollerTreeObserver: ViewTreeObserver? = null
|
|
139
201
|
private var observedNativeScroller: ScrollView? = null
|
|
202
|
+
private var observedNativeScrollerKey: String? = null
|
|
140
203
|
private var observedNativeScrollerListener: ViewTreeObserver.OnScrollChangedListener? = null
|
|
141
204
|
private var observedNativeScrollerTreeObserver: ViewTreeObserver? = null
|
|
142
205
|
private var observedPageRemoved = false
|
|
@@ -162,8 +225,14 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
162
225
|
// Apply its header insets before that list first draws.
|
|
163
226
|
prepareAdjacentPages()
|
|
164
227
|
attachPrimaryScrollObserver()
|
|
228
|
+
finishPagerTopIfSettled()
|
|
165
229
|
// OneKey patch: a filtered short page can reduce the existing collapse range.
|
|
166
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
|
+
}
|
|
167
236
|
true
|
|
168
237
|
}
|
|
169
238
|
|
|
@@ -189,6 +258,20 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
189
258
|
var headerOffsetPx = 0
|
|
190
259
|
private set
|
|
191
260
|
var pageKeys: List<String> = emptyList()
|
|
261
|
+
set(value) {
|
|
262
|
+
if (field == value) return
|
|
263
|
+
pageRestoreRequests.entries.toList().forEach { (key, request) ->
|
|
264
|
+
if ((value.getOrNull(request.pageIndex) ?: "page-${request.pageIndex}") != key) {
|
|
265
|
+
(request.view.get() as? NativeScrollerViewport)?.host?.cancelPendingScrollRestore()
|
|
266
|
+
pageRestoreRequests.remove(key)
|
|
267
|
+
pendingPageRestores.remove(key)
|
|
268
|
+
ownedScrollerRestoreRequests.remove(key)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
recyclerReadingAnchors.keys.retainAll(value.toSet())
|
|
272
|
+
displayRecyclerAnchors.keys.retainAll(value.toSet())
|
|
273
|
+
field = value
|
|
274
|
+
}
|
|
192
275
|
var retainedPages: String = "[]"
|
|
193
276
|
var onHeaderOffsetChanged: (() -> Unit)? = null
|
|
194
277
|
var nativeSmoothHeaderScrollEnabled = false
|
|
@@ -230,6 +313,51 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
230
313
|
OneKeyLog.debug("CollapsiblePager", "host=$hostIdentity $message")
|
|
231
314
|
}
|
|
232
315
|
|
|
316
|
+
private var offsetDiagSequence = 0L
|
|
317
|
+
private val offsetDiagNextDraw = WeakHashMap<View, Pair<String, Int>>()
|
|
318
|
+
|
|
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
|
|
334
|
+
val safeKey = if (key in setOf("portfolio", "perps", "defi", "nft", "history")) key else "other"
|
|
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)
|
|
343
|
+
OneKeyLog.debug("CollapsiblePager", "[HOME-OFFSET-DIAG] phase=$phase seq=${++offsetDiagSequence} " +
|
|
344
|
+
"wallMs=${System.currentTimeMillis()} monoMs=${SystemClock.uptimeMillis()} host=$hostIdentity " +
|
|
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} " +
|
|
350
|
+
"view=${view?.let { System.identityHashCode(it) }} tag=${view?.id} observed=${observed?.let { System.identityHashCode(it) }} " +
|
|
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}")
|
|
359
|
+
}
|
|
360
|
+
|
|
233
361
|
private fun outerPagerIndex(): Int {
|
|
234
362
|
var ancestor = parent as? View
|
|
235
363
|
while (ancestor != null) {
|
|
@@ -364,6 +492,19 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
364
492
|
fun removeAllReactChildren() {
|
|
365
493
|
releaseRefreshForeground()
|
|
366
494
|
detachPrimaryScrollObserver()
|
|
495
|
+
pageRestoreRequests.clear()
|
|
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()
|
|
505
|
+
ownedScrollerRestoreRequests.clear()
|
|
506
|
+
recyclerReadingAnchors.clear()
|
|
507
|
+
transitionSourceRecyclerAnchor = null
|
|
367
508
|
// Weak keys may disappear after size is read; Collection.toArray tolerates this.
|
|
368
509
|
for (recycler in originalRecyclerPadding.keys.toTypedArray()) {
|
|
369
510
|
restoreRecyclerInsets(recycler)
|
|
@@ -386,16 +527,90 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
386
527
|
|
|
387
528
|
fun reactChildAt(index: Int) = logicalChildren[index]
|
|
388
529
|
|
|
530
|
+
internal fun beginPageTransition(target: Int? = null) {
|
|
531
|
+
cancelPagerTop()
|
|
532
|
+
if (!nativeSmoothHeaderScrollEnabled || headerHeightPx <= 0) return
|
|
533
|
+
if (transitionHeaderOffsets == null) {
|
|
534
|
+
transitionSourceDisplayBody = primaryScrollableForPage(selectedPage)?.let(::verticalScrollOffset) ?: 0
|
|
535
|
+
transitionSourceRecyclerAnchor = (primaryScrollableForPage(selectedPage) as? RecyclerView)?.let(::captureRecyclerAnchor)
|
|
536
|
+
pageHeaderOffsets[currentPageKey()] = headerOffsetPx
|
|
537
|
+
detachPrimaryScrollObserver()
|
|
538
|
+
transitionSourcePage = selectedPage
|
|
539
|
+
transitionHeaderOffsets = pageHeaderOffsets.toMap()
|
|
540
|
+
transitionPageOffsets = pageOffsets.toMap()
|
|
541
|
+
}
|
|
542
|
+
if (target != null) {
|
|
543
|
+
transitionTargetPage = target
|
|
544
|
+
alignPageBody(target)
|
|
545
|
+
}
|
|
546
|
+
offsetDiag("transition-begin-source=$transitionSourcePage", primaryScrollableForPage(selectedPage), pageKey(target ?: selectedPage), target ?: selectedPage)
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
internal fun onPagerScrolled(position: Int, offset: Float) {
|
|
550
|
+
if (transitionHeaderOffsets == null) return
|
|
551
|
+
alignPageBody(position)
|
|
552
|
+
if (position + 1 < adapter.itemCount) alignPageBody(position + 1)
|
|
553
|
+
updateNativeTabProgress(position, offset)
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
internal fun onPagerScrollStateChanged(state: Int) {
|
|
557
|
+
if (state == ViewPager2.SCROLL_STATE_DRAGGING || state == ViewPager2.SCROLL_STATE_SETTLING) {
|
|
558
|
+
beginPageTransition()
|
|
559
|
+
} else if (state == ViewPager2.SCROLL_STATE_IDLE && transitionHeaderOffsets != null) {
|
|
560
|
+
finishPageTransition(pager.currentItem)
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
private fun finishPageTransition(position: Int) {
|
|
565
|
+
val headers = transitionHeaderOffsets ?: return
|
|
566
|
+
detachPrimaryScrollObserver(recordOffset = false)
|
|
567
|
+
releaseRefreshForeground()
|
|
568
|
+
selectedPage = position.coerceIn(0, adapter.itemCount - 1)
|
|
569
|
+
syntheticPageRestore = true
|
|
570
|
+
setHeaderOffset(headers[pageKey(transitionSourcePage)] ?: headerOffsetPx)
|
|
571
|
+
transitionHeaderOffsets = null
|
|
572
|
+
transitionPageOffsets = null
|
|
573
|
+
transitionTargetPage = null
|
|
574
|
+
syntheticPageRestore = false
|
|
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)
|
|
588
|
+
prepareAdjacentPages()
|
|
589
|
+
attachPrimaryScrollObserver()
|
|
590
|
+
}
|
|
591
|
+
|
|
389
592
|
fun selectPage(position: Int) {
|
|
593
|
+
offsetDiag("select-begin", observedOwnedScroller ?: observedNativeScroller ?: observedRecyclerView, pageKey(position), position)
|
|
390
594
|
pendingRefreshPull = null
|
|
391
595
|
if (adapter.itemCount == 0) return
|
|
596
|
+
if (nativeSmoothHeaderScrollEnabled && headerHeightPx > 0) {
|
|
597
|
+
if (transitionHeaderOffsets != null) return
|
|
598
|
+
beginPageTransition(position)
|
|
599
|
+
finishPageTransition(position)
|
|
600
|
+
return
|
|
601
|
+
}
|
|
392
602
|
detachPrimaryScrollObserver()
|
|
393
603
|
releaseRefreshForeground()
|
|
394
604
|
selectedPage = position.coerceIn(0, adapter.itemCount - 1)
|
|
395
605
|
val savedOffset = pageOffsets[currentPageKey()]
|
|
396
606
|
?: primaryScrollableForPage(selectedPage)?.let(::verticalScrollOffset)
|
|
397
607
|
?: 0
|
|
398
|
-
if (
|
|
608
|
+
offsetDiag("select-offset=$savedOffset-source=${if (pageOffsets[currentPageKey()] != null) "saved" else "live"}")
|
|
609
|
+
if (savedOffset > 0) {
|
|
610
|
+
offsetDiag("select-force-collapse-before")
|
|
611
|
+
setHeaderOffset(headerHeightPx)
|
|
612
|
+
offsetDiag("select-force-collapse-after")
|
|
613
|
+
}
|
|
399
614
|
postForCurrentAttachment {
|
|
400
615
|
prepareAdjacentPages()
|
|
401
616
|
attachPrimaryScrollObserver()
|
|
@@ -519,7 +734,287 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
519
734
|
}
|
|
520
735
|
}
|
|
521
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
|
+
|
|
522
1016
|
private fun maximumHeaderOffset(): Int {
|
|
1017
|
+
if (nativeSmoothHeaderScrollEnabled) return headerHeightPx
|
|
523
1018
|
val scrollable = observedRecyclerView
|
|
524
1019
|
?: observedOwnedScroller
|
|
525
1020
|
?: observedNativeScroller
|
|
@@ -537,9 +1032,14 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
537
1032
|
|
|
538
1033
|
private fun setHeaderOffset(offset: Int) {
|
|
539
1034
|
// val next = offset.coerceIn(0, headerHeightPx)
|
|
540
|
-
val
|
|
1035
|
+
val pending = nativeSmoothHeaderScrollEnabled && currentPageKey() in pendingPageRestores
|
|
1036
|
+
val maximum = if (transitionHeaderOffsets != null || pending || pagerTopRequest != null) headerHeightPx else maximumHeaderOffset()
|
|
1037
|
+
val next = offset.coerceIn(0, maximum)
|
|
541
1038
|
if (headerOffsetPx == next) return
|
|
542
1039
|
headerOffsetPx = next
|
|
1040
|
+
if (nativeSmoothHeaderScrollEnabled && transitionHeaderOffsets == null && !syntheticPageRestore && !pending) {
|
|
1041
|
+
pageHeaderOffsets[currentPageKey()] = next
|
|
1042
|
+
}
|
|
543
1043
|
applyHeaderOffset()
|
|
544
1044
|
}
|
|
545
1045
|
|
|
@@ -656,13 +1156,20 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
656
1156
|
}
|
|
657
1157
|
|
|
658
1158
|
private fun attachPrimaryScrollObserver() {
|
|
1159
|
+
if (transitionHeaderOffsets != null) return
|
|
659
1160
|
if (selectedPage !in 0 until adapter.itemCount) return
|
|
660
1161
|
val previous = observedRecyclerView ?: observedOwnedScroller ?: observedNativeScroller
|
|
661
1162
|
if (observedPageRemoved || previous != null && !isDescendant(previous, adapter.pageAt(selectedPage))) {
|
|
1163
|
+
offsetDiag("primary-replacement-before-removed=$observedPageRemoved", previous)
|
|
662
1164
|
observedPageRemoved = false
|
|
663
|
-
detachPrimaryScrollObserver()
|
|
1165
|
+
detachPrimaryScrollObserver(recordOffset = !nativeSmoothHeaderScrollEnabled)
|
|
664
1166
|
// A replacement scroller starts at the beginning while retaining header collapse.
|
|
665
|
-
|
|
1167
|
+
offsetDiag("primary-replacement-cache-before", previous)
|
|
1168
|
+
if (!nativeSmoothHeaderScrollEnabled) {
|
|
1169
|
+
offsetDiag("cache-write-replacement-new=0", previous)
|
|
1170
|
+
pageOffsets[currentPageKey()] = 0
|
|
1171
|
+
}
|
|
1172
|
+
offsetDiag("primary-replacement-cache-after", previous)
|
|
666
1173
|
}
|
|
667
1174
|
when (val scrollable = primaryScrollableForPage(selectedPage)) {
|
|
668
1175
|
is RecyclerView -> attachRecyclerObserver(scrollable)
|
|
@@ -672,24 +1179,45 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
672
1179
|
}
|
|
673
1180
|
|
|
674
1181
|
private fun attachRecyclerObserver(recycler: RecyclerView) {
|
|
675
|
-
if (observedRecyclerView === recycler) {
|
|
1182
|
+
if (observedRecyclerView === recycler && observedRecyclerKey == currentPageKey()) {
|
|
676
1183
|
applyRecyclerInsets(recycler, selectedPage)
|
|
677
1184
|
publishConsumedOffset(recycler)
|
|
678
1185
|
return
|
|
679
1186
|
}
|
|
680
1187
|
|
|
681
1188
|
detachPrimaryScrollObserver()
|
|
1189
|
+
offsetDiag("recycler-attach-before", recycler)
|
|
682
1190
|
observedRecyclerView = recycler
|
|
1191
|
+
observedRecyclerKey = currentPageKey()
|
|
1192
|
+
publishScrollCommandListener(recycler)
|
|
683
1193
|
applyRecyclerInsets(recycler, selectedPage)
|
|
684
1194
|
publishConsumedOffset(recycler)
|
|
1195
|
+
val key = currentPageKey()
|
|
685
1196
|
val listener = object : RecyclerView.OnScrollListener() {
|
|
1197
|
+
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
|
1198
|
+
if (newState == RecyclerView.SCROLL_STATE_IDLE) finishPagerTopIfSettled()
|
|
1199
|
+
}
|
|
686
1200
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
|
1201
|
+
if (observedScrollListener !== this || observedRecyclerKey != key ||
|
|
1202
|
+
key != currentPageKey() || !isAttachedToWindow ||
|
|
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
|
|
687
1211
|
val contentOffset = recyclerView.computeVerticalScrollOffset()
|
|
688
|
-
pageOffsets[
|
|
1212
|
+
if (pageOffsets[key] != contentOffset) offsetDiag("cache-write-recycler-observer-new=$contentOffset-dx=$dx-dy=$dy", recyclerView, key)
|
|
1213
|
+
pageOffsets[key] = contentOffset
|
|
1214
|
+
if (nativeSmoothHeaderScrollEnabled) saveRecyclerReadingAnchor(key, recyclerView)
|
|
689
1215
|
// Nested touch scrolling moves list content only after the header has
|
|
690
1216
|
// collapsed. Programmatic list jumps bypass those parent callbacks.
|
|
691
|
-
if (contentOffset > 0 && headerOffsetPx < headerHeightPx) {
|
|
1217
|
+
if (contentOffset > 0 && headerOffsetPx < headerHeightPx && !hasExplicitTopCommand(recyclerView, key) && !matchesPagerTop(recyclerView)) {
|
|
1218
|
+
offsetDiag("recycler-positive-collapse-before", recyclerView)
|
|
692
1219
|
setHeaderOffset(headerHeightPx)
|
|
1220
|
+
offsetDiag("recycler-positive-collapse-after", recyclerView)
|
|
693
1221
|
}
|
|
694
1222
|
}
|
|
695
1223
|
}
|
|
@@ -702,11 +1230,21 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
702
1230
|
}
|
|
703
1231
|
|
|
704
1232
|
private fun detachRecyclerObserver(recordOffset: Boolean) {
|
|
1233
|
+
cancelPagerTop()
|
|
1234
|
+
explicitTopCommand = null
|
|
705
1235
|
val recycler = observedRecyclerView
|
|
706
1236
|
val listener = observedScrollListener
|
|
707
1237
|
if (recycler != null) {
|
|
708
|
-
|
|
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
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
offsetDiag("recycler-detach-save-after", recycler, observedRecyclerKey ?: currentPageKey())
|
|
709
1246
|
publishConsumedOffset(recycler, remove = true)
|
|
1247
|
+
publishScrollCommandListener(recycler, remove = true)
|
|
710
1248
|
}
|
|
711
1249
|
if (recycler != null && listener != null) recycler.removeOnScrollListener(listener)
|
|
712
1250
|
if (recycler != null) {
|
|
@@ -716,28 +1254,38 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
716
1254
|
)
|
|
717
1255
|
}
|
|
718
1256
|
observedRecyclerView = null
|
|
1257
|
+
observedRecyclerKey = null
|
|
719
1258
|
observedScrollListener = null
|
|
720
1259
|
}
|
|
721
1260
|
|
|
722
1261
|
private fun attachNativeScrollerObserver(scroller: ScrollView) {
|
|
723
|
-
if (observedNativeScroller === scroller) {
|
|
1262
|
+
if (observedNativeScroller === scroller && observedNativeScrollerKey == currentPageKey()) {
|
|
724
1263
|
applyNativeScrollerInsets(scroller, selectedPage)
|
|
725
1264
|
return
|
|
726
1265
|
}
|
|
727
1266
|
|
|
728
1267
|
detachPrimaryScrollObserver()
|
|
1268
|
+
offsetDiag("scrollview-attach-before", scroller)
|
|
729
1269
|
observedNativeScroller = scroller
|
|
1270
|
+
observedNativeScrollerKey = currentPageKey()
|
|
730
1271
|
applyNativeScrollerInsets(scroller, selectedPage)
|
|
1272
|
+
val key = currentPageKey()
|
|
731
1273
|
val listener = object : ViewTreeObserver.OnScrollChangedListener {
|
|
732
1274
|
override fun onScrollChanged() {
|
|
733
1275
|
if (observedNativeScrollerListener !== this) {
|
|
734
1276
|
removeScrollChangedListener(null, scroller, this)
|
|
735
1277
|
return
|
|
736
1278
|
}
|
|
1279
|
+
if (key != currentPageKey() || !isAttachedToWindow ||
|
|
1280
|
+
transitionHeaderOffsets != null || syntheticPageRestore || key in pendingPageRestores || key in alignedBodyKeys) return
|
|
1281
|
+
if (nativeSmoothHeaderScrollEnabled && nestedScrollingParentHelper.nestedScrollAxes == 0 && key !in commandBodyKeys) return
|
|
737
1282
|
val contentOffset = scroller.scrollY
|
|
738
|
-
pageOffsets[
|
|
739
|
-
|
|
1283
|
+
if (pageOffsets[key] != contentOffset) offsetDiag("cache-write-scrollview-observer-new=$contentOffset", scroller, key)
|
|
1284
|
+
pageOffsets[key] = contentOffset
|
|
1285
|
+
if (contentOffset > 0 && headerOffsetPx < headerHeightPx && !matchesPagerTop(scroller)) {
|
|
1286
|
+
offsetDiag("scrollview-positive-collapse-before", scroller)
|
|
740
1287
|
setHeaderOffset(headerHeightPx)
|
|
1288
|
+
offsetDiag("scrollview-positive-collapse-after", scroller)
|
|
741
1289
|
}
|
|
742
1290
|
}
|
|
743
1291
|
}
|
|
@@ -752,9 +1300,17 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
752
1300
|
}
|
|
753
1301
|
|
|
754
1302
|
private fun detachNativeScrollerObserver(recordOffset: Boolean) {
|
|
1303
|
+
cancelPagerTop()
|
|
755
1304
|
val scroller = observedNativeScroller
|
|
756
1305
|
val listener = observedNativeScrollerListener
|
|
757
|
-
|
|
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
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
offsetDiag("scrollview-detach-save-after", scroller, observedNativeScrollerKey ?: currentPageKey())
|
|
758
1314
|
if (scroller != null && listener != null) {
|
|
759
1315
|
removeScrollChangedListener(observedNativeScrollerTreeObserver, scroller, listener)
|
|
760
1316
|
}
|
|
@@ -765,6 +1321,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
765
1321
|
)
|
|
766
1322
|
}
|
|
767
1323
|
observedNativeScroller = null
|
|
1324
|
+
observedNativeScrollerKey = null
|
|
768
1325
|
observedNativeScrollerListener = null
|
|
769
1326
|
observedNativeScrollerTreeObserver = null
|
|
770
1327
|
}
|
|
@@ -793,9 +1350,24 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
793
1350
|
val key = pageKey(pageIndex)
|
|
794
1351
|
if (ownedScrollerKeys[scroller] == key) return
|
|
795
1352
|
ownedScrollerKeys[scroller] = key
|
|
1353
|
+
val restoreRequest = registerPageRestore(key, scroller, pageIndex)
|
|
1354
|
+
if (nativeSmoothHeaderScrollEnabled) ownedScrollerRestoreRequests[key] = WeakReference(scroller) to null
|
|
1355
|
+
val generation = attachmentGeneration
|
|
1356
|
+
offsetDiag("owned-enqueue", scroller, key, pageIndex, restoreRequest)
|
|
796
1357
|
postForCurrentAttachment {
|
|
797
|
-
|
|
798
|
-
|
|
1358
|
+
offsetDiag("owned-execute", scroller, key, pageIndex, restoreRequest)
|
|
1359
|
+
if (isCurrentPageRestore(key, restoreRequest) && ownedScrollerKeys[scroller] == key && generation == attachmentGeneration &&
|
|
1360
|
+
scroller.isAttachedToWindow && (!nativeSmoothHeaderScrollEnabled ||
|
|
1361
|
+
(key in pendingPageRestores && ownedScrollerRestoreRequests[key]?.first?.get() === scroller))) {
|
|
1362
|
+
offsetDiag("owned-restore-before", scroller, key, pageIndex, restoreRequest)
|
|
1363
|
+
val saved = restorationBodyOffset(key)
|
|
1364
|
+
scroller.host.restoreScrollOffset(saved)
|
|
1365
|
+
if (nativeSmoothHeaderScrollEnabled) {
|
|
1366
|
+
ownedScrollerRestoreRequests[key] = WeakReference(scroller) to generation
|
|
1367
|
+
recordOwnedScrollerOffset(scroller, key)
|
|
1368
|
+
}
|
|
1369
|
+
offsetDiag("owned-restore-after-saved=$saved", scroller, key, pageIndex, restoreRequest)
|
|
1370
|
+
offsetDiagAfterDraw(scroller, key, pageIndex)
|
|
799
1371
|
}
|
|
800
1372
|
}
|
|
801
1373
|
}
|
|
@@ -803,7 +1375,10 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
803
1375
|
private fun restoreOwnedScrollerInsets(scroller: NativeScrollerViewport) {
|
|
804
1376
|
scroller.stopMotion()
|
|
805
1377
|
scroller.host.cancelPendingScrollRestore()
|
|
806
|
-
ownedScrollerKeys.remove(scroller)
|
|
1378
|
+
ownedScrollerKeys.remove(scroller)?.let { key ->
|
|
1379
|
+
clearPageRestoreForView(key, scroller)
|
|
1380
|
+
if (ownedScrollerRestoreRequests[key]?.first?.get() === scroller) ownedScrollerRestoreRequests.remove(key)
|
|
1381
|
+
}
|
|
807
1382
|
publishConsumedOffset(scroller, remove = true)
|
|
808
1383
|
scroller.host.setPagerInset(this, null)
|
|
809
1384
|
}
|
|
@@ -811,9 +1386,11 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
811
1386
|
private fun attachOwnedScrollerObserver(scroller: NativeScrollerViewport) {
|
|
812
1387
|
if (observedOwnedScroller === scroller && observedOwnedScrollerKey == currentPageKey()) {
|
|
813
1388
|
applyOwnedScrollerInsets(scroller, selectedPage)
|
|
1389
|
+
if (nativeSmoothHeaderScrollEnabled) recordOwnedScrollerOffset(scroller, currentPageKey())
|
|
814
1390
|
return
|
|
815
1391
|
}
|
|
816
1392
|
detachPrimaryScrollObserver()
|
|
1393
|
+
offsetDiag("owned-attach-before", scroller)
|
|
817
1394
|
observedOwnedScroller = scroller
|
|
818
1395
|
observedOwnedScrollerKey = currentPageKey()
|
|
819
1396
|
val key = currentPageKey()
|
|
@@ -824,9 +1401,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
824
1401
|
removeScrollChangedListener(null, scroller, this)
|
|
825
1402
|
return
|
|
826
1403
|
}
|
|
827
|
-
|
|
828
|
-
pageOffsets[key] = contentOffset
|
|
829
|
-
if (contentOffset > 0 && headerOffsetPx < headerHeightPx) setHeaderOffset(headerHeightPx)
|
|
1404
|
+
recordOwnedScrollerOffset(scroller, key)
|
|
830
1405
|
}
|
|
831
1406
|
}
|
|
832
1407
|
observedOwnedScrollerListener = listener
|
|
@@ -835,11 +1410,41 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
835
1410
|
}
|
|
836
1411
|
}
|
|
837
1412
|
|
|
1413
|
+
private fun recordOwnedScrollerOffset(scroller: NativeScrollerViewport, key: String) {
|
|
1414
|
+
if (ownedScrollerKeys[scroller] != key || !isAttachedToWindow || !scroller.isAttachedToWindow) return
|
|
1415
|
+
if (key in pendingPageRestores) {
|
|
1416
|
+
// A false native pending flag before our posted request executes is not cancellation.
|
|
1417
|
+
val request = ownedScrollerRestoreRequests[key]
|
|
1418
|
+
if (request == null || request.first.get() !== scroller || request.second != attachmentGeneration ||
|
|
1419
|
+
scroller.width <= 0 || scroller.height <= 0 || scroller.host.hasPendingScrollRestore()) return
|
|
1420
|
+
clearPageRestoreForView(key, scroller)
|
|
1421
|
+
}
|
|
1422
|
+
if (observedOwnedScroller !== scroller || observedOwnedScrollerKey != key ||
|
|
1423
|
+
key != currentPageKey() || transitionHeaderOffsets != null || syntheticPageRestore || key in alignedBodyKeys) return
|
|
1424
|
+
if (nativeSmoothHeaderScrollEnabled && !scroller.isReadingMotionActive() && key !in commandBodyKeys) return
|
|
1425
|
+
val contentOffset = scroller.scrollY
|
|
1426
|
+
if (pageOffsets[key] != contentOffset) offsetDiag("cache-write-owned-observer-new=$contentOffset", scroller, key)
|
|
1427
|
+
pageOffsets[key] = contentOffset
|
|
1428
|
+
if (contentOffset > 0 && headerOffsetPx < headerHeightPx && !matchesPagerTop(scroller)) {
|
|
1429
|
+
offsetDiag("owned-positive-collapse-before", scroller, key)
|
|
1430
|
+
setHeaderOffset(headerHeightPx)
|
|
1431
|
+
offsetDiag("owned-positive-collapse-after", scroller, key)
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
838
1435
|
private fun detachOwnedScrollerObserver(recordOffset: Boolean) {
|
|
1436
|
+
cancelPagerTop()
|
|
839
1437
|
val scroller = observedOwnedScroller ?: return
|
|
840
1438
|
scroller.stopMotion()
|
|
841
1439
|
publishConsumedOffset(scroller, remove = true)
|
|
842
|
-
|
|
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
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
offsetDiag("owned-detach-save-after", scroller, observedOwnedScrollerKey ?: currentPageKey())
|
|
843
1448
|
observedOwnedScrollerListener?.let {
|
|
844
1449
|
removeScrollChangedListener(observedOwnedScrollerTreeObserver, scroller, it)
|
|
845
1450
|
}
|
|
@@ -876,6 +1481,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
876
1481
|
val anchorOffset = if (first != RecyclerView.NO_POSITION) {
|
|
877
1482
|
manager?.findViewByPosition(first)?.let { manager.getDecoratedTop(it) - recycler.paddingTop }
|
|
878
1483
|
} else null
|
|
1484
|
+
offsetDiag("recycler-inset-before-nextTop=$top-anchorPosition=$first-anchorOffset=$anchorOffset", recycler, pageKey(pageIndex), pageIndex)
|
|
879
1485
|
recycler.setPadding(original.left, top, original.right, bottom)
|
|
880
1486
|
if (anchorOffset != null) manager?.scrollToPositionWithOffset(first, anchorOffset)
|
|
881
1487
|
// React Native can stop this nested requestLayout at its React-owned parent.
|
|
@@ -889,21 +1495,67 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
889
1495
|
recycler.layout(recycler.left, recycler.top, recycler.right, recycler.bottom)
|
|
890
1496
|
}
|
|
891
1497
|
}
|
|
1498
|
+
if (original.appliedTopInset != topInset) offsetDiag("recycler-inset-after-old=${original.appliedTopInset}-new=$topInset", recycler, pageKey(pageIndex), pageIndex)
|
|
892
1499
|
original.appliedTopInset = topInset
|
|
893
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
|
+
}
|
|
894
1512
|
if (restoredRecyclerKeys[recycler] != key) {
|
|
895
1513
|
restoredRecyclerKeys[recycler] = key
|
|
1514
|
+
val restoreRequest = registerPageRestore(key, recycler, pageIndex)
|
|
896
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
|
+
}
|
|
897
1521
|
recycler.post {
|
|
1522
|
+
offsetDiag("recycler-execute", recycler, key, pageIndex, restoreRequest)
|
|
1523
|
+
if (!isCurrentPageRestore(key, restoreRequest)) {
|
|
1524
|
+
offsetDiag("recycler-post-skip-cancelled", recycler, key, pageIndex, restoreRequest)
|
|
1525
|
+
return@post
|
|
1526
|
+
}
|
|
898
1527
|
if (!isAttachedToWindow || generation != attachmentGeneration ||
|
|
899
1528
|
restoredRecyclerKeys[recycler] != key ||
|
|
900
1529
|
originalRecyclerPadding[recycler] !== original) {
|
|
901
1530
|
if (restoredRecyclerKeys[recycler] == key) restoredRecyclerKeys.remove(recycler)
|
|
1531
|
+
clearPageRestoreForView(key, recycler, restoreRequest)
|
|
902
1532
|
return@post
|
|
903
1533
|
}
|
|
904
|
-
val saved =
|
|
1534
|
+
val saved = restorationBodyOffset(key)
|
|
905
1535
|
val current = recycler.computeVerticalScrollOffset()
|
|
906
|
-
|
|
1536
|
+
offsetDiag("recycler-restore-before-saved=$saved-current=${current}-requestedDelta=${saved - current}", recycler, key, pageIndex, restoreRequest)
|
|
1537
|
+
if (nativeSmoothHeaderScrollEnabled && (recycler.height <= 0 || recycler.childCount == 0)) {
|
|
1538
|
+
restoredRecyclerKeys.remove(recycler)
|
|
1539
|
+
if (pageRestoreRequests[key] === restoreRequest) pageRestoreRequests.remove(key)
|
|
1540
|
+
return@post
|
|
1541
|
+
}
|
|
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)
|
|
1555
|
+
if (nativeSmoothHeaderScrollEnabled) clearPageRestoreForView(key, recycler, restoreRequest)
|
|
1556
|
+
else pendingPageRestores.remove(key)
|
|
1557
|
+
offsetDiag("recycler-restore-after-saved=$saved", recycler, key, pageIndex, restoreRequest)
|
|
1558
|
+
offsetDiagAfterDraw(recycler, key, pageIndex)
|
|
907
1559
|
}
|
|
908
1560
|
}
|
|
909
1561
|
}
|
|
@@ -949,16 +1601,35 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
949
1601
|
val key = pageKey(pageIndex)
|
|
950
1602
|
if (restoredScrollerKeys[scroller] != key) {
|
|
951
1603
|
restoredScrollerKeys[scroller] = key
|
|
1604
|
+
val restoreRequest = registerPageRestore(key, scroller, pageIndex)
|
|
952
1605
|
val generation = attachmentGeneration
|
|
1606
|
+
offsetDiag("scrollview-enqueue", scroller, key, pageIndex, restoreRequest)
|
|
953
1607
|
scroller.post {
|
|
1608
|
+
offsetDiag("scrollview-execute", scroller, key, pageIndex, restoreRequest)
|
|
1609
|
+
if (!isCurrentPageRestore(key, restoreRequest)) {
|
|
1610
|
+
offsetDiag("scrollview-post-skip-cancelled", scroller, key, pageIndex, restoreRequest)
|
|
1611
|
+
return@post
|
|
1612
|
+
}
|
|
954
1613
|
if (!isAttachedToWindow || generation != attachmentGeneration ||
|
|
955
1614
|
restoredScrollerKeys[scroller] != key ||
|
|
956
1615
|
originalScrollerPadding[scroller] !== original) {
|
|
957
1616
|
if (restoredScrollerKeys[scroller] == key) restoredScrollerKeys.remove(scroller)
|
|
1617
|
+
clearPageRestoreForView(key, scroller, restoreRequest)
|
|
1618
|
+
return@post
|
|
1619
|
+
}
|
|
1620
|
+
val saved = restorationBodyOffset(key)
|
|
1621
|
+
offsetDiag("scrollview-restore-before-saved=$saved-current=${scroller.scrollY}-requestedDelta=${saved - scroller.scrollY}", scroller, key, pageIndex, restoreRequest)
|
|
1622
|
+
if (nativeSmoothHeaderScrollEnabled &&
|
|
1623
|
+
(scroller.height <= 0 || (scroller.getChildAt(0)?.height ?: 0) <= 0)) {
|
|
1624
|
+
restoredScrollerKeys.remove(scroller)
|
|
1625
|
+
if (pageRestoreRequests[key] === restoreRequest) pageRestoreRequests.remove(key)
|
|
958
1626
|
return@post
|
|
959
1627
|
}
|
|
960
|
-
val saved = pageOffsets[key] ?: 0
|
|
961
1628
|
if (scroller.scrollY != saved) scroller.scrollTo(scroller.scrollX, saved)
|
|
1629
|
+
if (nativeSmoothHeaderScrollEnabled) clearPageRestoreForView(key, scroller, restoreRequest)
|
|
1630
|
+
else pendingPageRestores.remove(key)
|
|
1631
|
+
offsetDiag("scrollview-restore-after-saved=$saved", scroller, key, pageIndex, restoreRequest)
|
|
1632
|
+
offsetDiagAfterDraw(scroller, key, pageIndex)
|
|
962
1633
|
}
|
|
963
1634
|
}
|
|
964
1635
|
}
|
|
@@ -1017,7 +1688,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1017
1688
|
publishConsumedOffset(recycler, remove = true)
|
|
1018
1689
|
val original = originalRecyclerPadding.remove(recycler) ?: return
|
|
1019
1690
|
updateRecyclerPaddingOwnership(recycler, original)
|
|
1020
|
-
restoredRecyclerKeys.remove(recycler)
|
|
1691
|
+
restoredRecyclerKeys.remove(recycler)?.let { clearPageRestoreForView(it, recycler) }
|
|
1021
1692
|
recycler.clipToPadding = original.clipToPadding
|
|
1022
1693
|
recycler.setPadding(original.left, original.top, original.right, original.bottom)
|
|
1023
1694
|
}
|
|
@@ -1025,7 +1696,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1025
1696
|
private fun restoreNativeScrollerInsets(scroller: ScrollView) {
|
|
1026
1697
|
val original = originalScrollerPadding.remove(scroller) ?: return
|
|
1027
1698
|
updateScrollerPaddingOwnership(scroller, original)
|
|
1028
|
-
restoredScrollerKeys.remove(scroller)
|
|
1699
|
+
restoredScrollerKeys.remove(scroller)?.let { clearPageRestoreForView(it, scroller) }
|
|
1029
1700
|
if (scroller is ReactScrollView) {
|
|
1030
1701
|
val state = scroller.reactScrollViewScrollState
|
|
1031
1702
|
scroller.setScrollAwayPaddingEnabledUnstable(
|
|
@@ -1399,6 +2070,15 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1399
2070
|
override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {
|
|
1400
2071
|
if (type == ViewCompat.TYPE_TOUCH) pendingRefreshPull = null
|
|
1401
2072
|
nestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes, type)
|
|
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) }
|
|
2077
|
+
pageRestoreRequests.remove(currentPageKey())
|
|
2078
|
+
pendingPageRestores.remove(currentPageKey())
|
|
2079
|
+
ownedScrollerRestoreRequests.remove(currentPageKey())
|
|
2080
|
+
offsetDiag("touch-cancel-after", target, request = request)
|
|
2081
|
+
}
|
|
1402
2082
|
}
|
|
1403
2083
|
|
|
1404
2084
|
override fun onStopNestedScroll(target: View, type: Int) {
|
|
@@ -1415,6 +2095,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1415
2095
|
// val used = min(dy, headerHeightPx - headerOffsetPx)
|
|
1416
2096
|
if (dy > 0 && headerOffsetPx < maximumOffset) {
|
|
1417
2097
|
val used = min(dy, maximumOffset - headerOffsetPx)
|
|
2098
|
+
if (type == ViewCompat.TYPE_TOUCH && used != 0) takeOverBodyForVerticalConsumption()
|
|
1418
2099
|
setHeaderOffset(headerOffsetPx + used)
|
|
1419
2100
|
consumed[1] += used
|
|
1420
2101
|
} else if (dy < 0 && headerOffsetPx > 0) {
|
|
@@ -1426,6 +2107,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1426
2107
|
}
|
|
1427
2108
|
if (!canScrollUp) {
|
|
1428
2109
|
val used = max(dy, -headerOffsetPx)
|
|
2110
|
+
if (type == ViewCompat.TYPE_TOUCH && used != 0) takeOverBodyForVerticalConsumption()
|
|
1429
2111
|
setHeaderOffset(headerOffsetPx + used)
|
|
1430
2112
|
consumed[1] += used
|
|
1431
2113
|
}
|
|
@@ -1441,8 +2123,11 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1441
2123
|
type: Int,
|
|
1442
2124
|
consumed: IntArray,
|
|
1443
2125
|
) {
|
|
1444
|
-
if (!isCurrentPageTarget(target)
|
|
2126
|
+
if (!isCurrentPageTarget(target)) return
|
|
2127
|
+
if (type == ViewCompat.TYPE_TOUCH && dyConsumed != 0) takeOverBodyForVerticalConsumption()
|
|
2128
|
+
if (dyUnconsumed >= 0) return
|
|
1445
2129
|
val used = max(dyUnconsumed, -headerOffsetPx)
|
|
2130
|
+
if (type == ViewCompat.TYPE_TOUCH && used != 0) takeOverBodyForVerticalConsumption()
|
|
1446
2131
|
setHeaderOffset(headerOffsetPx + used)
|
|
1447
2132
|
consumed[1] += used
|
|
1448
2133
|
if (type == ViewCompat.TYPE_TOUCH && dyUnconsumed < used &&
|
|
@@ -1492,6 +2177,13 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1492
2177
|
restoredRecyclerKeys.clear()
|
|
1493
2178
|
restoredScrollerKeys.clear()
|
|
1494
2179
|
viewTreeObserver.addOnPreDrawListener(pageContentLayoutListener)
|
|
2180
|
+
resumeTransitionSource?.let { source ->
|
|
2181
|
+
resumeTransitionSource = null
|
|
2182
|
+
if (source in 0 until adapter.itemCount) {
|
|
2183
|
+
pager.setCurrentItem(source, false)
|
|
2184
|
+
selectPage(source)
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
1495
2187
|
log(
|
|
1496
2188
|
"host-attach generation=$attachmentGeneration inner=$selectedPage " +
|
|
1497
2189
|
"outer=${outerPagerIndex()} nativePages=${adapter.itemCount}",
|
|
@@ -1511,6 +2203,26 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1511
2203
|
nestedNativeGestureStarted = false
|
|
1512
2204
|
viewTreeObserver.removeOnPreDrawListener(pageContentLayoutListener)
|
|
1513
2205
|
detachPrimaryScrollObserver()
|
|
2206
|
+
pageRestoreRequests.clear()
|
|
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()
|
|
2216
|
+
ownedScrollerRestoreRequests.clear()
|
|
2217
|
+
transitionHeaderOffsets?.let { headers ->
|
|
2218
|
+
resumeTransitionSource = transitionSourcePage
|
|
2219
|
+
syntheticPageRestore = true
|
|
2220
|
+
setHeaderOffset(headers[pageKey(transitionSourcePage)] ?: 0)
|
|
2221
|
+
syntheticPageRestore = false
|
|
2222
|
+
}
|
|
2223
|
+
transitionHeaderOffsets = null
|
|
2224
|
+
transitionPageOffsets = null
|
|
2225
|
+
transitionTargetPage = null
|
|
1514
2226
|
// Snapshot live keys before restoration removes their padding records.
|
|
1515
2227
|
for (recycler in originalRecyclerPadding.keys.toTypedArray()) {
|
|
1516
2228
|
restoreRecyclerInsets(recycler)
|