@onekeyfe/react-native-pager-view 3.0.173 → 3.0.175
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 +860 -25
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerViewManager.kt +17 -0
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerView.kt +16 -0
- package/android/src/main/java/com/reactnativepagerview/event/CollapsibleStateChangedEvent.kt +4 -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 +65 -1
- package/ios/RNCCollapsiblePagerViewComponentView.mm +616 -30
- package/ios/RNCNativeScrollerComponentView.h +1 -0
- package/ios/RNCNativeScrollerComponentView.mm +48 -0
- package/ios/tests/RNCCollapsiblePagerFirstFrameTests.mm +332 -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 +8 -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 +3 -0
- package/package.json +2 -2
- package/src/CollapsiblePagerView.tsx +18 -0
- package/src/CollapsiblePagerView.web.tsx +170 -36
- package/src/CollapsiblePagerViewNativeComponent.ts +8 -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,17 +132,90 @@ 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
|
|
143
206
|
private var attachmentGeneration = 0
|
|
207
|
+
internal var headerSettledAtTop = false
|
|
208
|
+
private set
|
|
209
|
+
var onHeaderSettledStateChanged: (() -> Unit)? = null
|
|
210
|
+
private var headerSettledOwner: WeakReference<View>? = null
|
|
211
|
+
private var headerSettledKey: String? = null
|
|
212
|
+
private var headerSettledGeneration = -1
|
|
213
|
+
private var headerSettledGeometry: String? = null
|
|
214
|
+
private var headerSettledSince = 0L
|
|
215
|
+
private var headerSettledWasBusy = true
|
|
216
|
+
private var headerSettledCheck: Runnable? = null
|
|
217
|
+
private var headerCommandOwner: WeakReference<View>? = null
|
|
218
|
+
|
|
144
219
|
private val touchSlopPx = ViewConfiguration.get(context).scaledTouchSlop
|
|
145
220
|
private val hostIdentity = System.identityHashCode(this)
|
|
146
221
|
private var headerView: View? = null
|
|
@@ -162,8 +237,15 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
162
237
|
// Apply its header insets before that list first draws.
|
|
163
238
|
prepareAdjacentPages()
|
|
164
239
|
attachPrimaryScrollObserver()
|
|
240
|
+
finishPagerTopIfSettled()
|
|
165
241
|
// OneKey patch: a filtered short page can reduce the existing collapse range.
|
|
166
242
|
setHeaderOffset(headerOffsetPx)
|
|
243
|
+
updateHeaderSettledState()
|
|
244
|
+
if (offsetDiagEnabled() && offsetDiagNextDraw.isNotEmpty()) {
|
|
245
|
+
val samples = offsetDiagNextDraw.entries.map { it.key to it.value }
|
|
246
|
+
offsetDiagNextDraw.clear()
|
|
247
|
+
samples.forEach { (view, owner) -> offsetDiag("restore-next-predraw", view, owner.first, owner.second) }
|
|
248
|
+
}
|
|
167
249
|
true
|
|
168
250
|
}
|
|
169
251
|
|
|
@@ -189,6 +271,20 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
189
271
|
var headerOffsetPx = 0
|
|
190
272
|
private set
|
|
191
273
|
var pageKeys: List<String> = emptyList()
|
|
274
|
+
set(value) {
|
|
275
|
+
if (field == value) return
|
|
276
|
+
pageRestoreRequests.entries.toList().forEach { (key, request) ->
|
|
277
|
+
if ((value.getOrNull(request.pageIndex) ?: "page-${request.pageIndex}") != key) {
|
|
278
|
+
(request.view.get() as? NativeScrollerViewport)?.host?.cancelPendingScrollRestore()
|
|
279
|
+
pageRestoreRequests.remove(key)
|
|
280
|
+
pendingPageRestores.remove(key)
|
|
281
|
+
ownedScrollerRestoreRequests.remove(key)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
recyclerReadingAnchors.keys.retainAll(value.toSet())
|
|
285
|
+
displayRecyclerAnchors.keys.retainAll(value.toSet())
|
|
286
|
+
field = value
|
|
287
|
+
}
|
|
192
288
|
var retainedPages: String = "[]"
|
|
193
289
|
var onHeaderOffsetChanged: (() -> Unit)? = null
|
|
194
290
|
var nativeSmoothHeaderScrollEnabled = false
|
|
@@ -230,6 +326,51 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
230
326
|
OneKeyLog.debug("CollapsiblePager", "host=$hostIdentity $message")
|
|
231
327
|
}
|
|
232
328
|
|
|
329
|
+
private var offsetDiagSequence = 0L
|
|
330
|
+
private val offsetDiagNextDraw = WeakHashMap<View, Pair<String, Int>>()
|
|
331
|
+
|
|
332
|
+
private fun offsetDiagEnabled(): Boolean =
|
|
333
|
+
(context.applicationInfo.flags and android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE) != 0
|
|
334
|
+
|
|
335
|
+
private fun offsetDiagAfterDraw(view: View, key: String, pageIndex: Int) {
|
|
336
|
+
if (offsetDiagEnabled()) offsetDiagNextDraw[view] = key to pageIndex
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private fun offsetDiag(
|
|
340
|
+
phase: String,
|
|
341
|
+
view: View? = null,
|
|
342
|
+
key: String = currentPageKey(),
|
|
343
|
+
pageIndex: Int = pageKeys.indexOf(key),
|
|
344
|
+
request: PageRestoreRequest? = pageRestoreRequests[key],
|
|
345
|
+
) {
|
|
346
|
+
if (!offsetDiagEnabled()) return
|
|
347
|
+
val safeKey = if (key in setOf("portfolio", "perps", "defi", "nft", "history")) key else "other"
|
|
348
|
+
val observed = observedOwnedScroller ?: observedNativeScroller ?: observedRecyclerView
|
|
349
|
+
val observedKey = observedOwnedScrollerKey ?: observedNativeScrollerKey ?: observedRecyclerKey
|
|
350
|
+
val recycler = view as? RecyclerView
|
|
351
|
+
val manager = recycler?.layoutManager as? LinearLayoutManager
|
|
352
|
+
val first = manager?.findFirstVisibleItemPosition()
|
|
353
|
+
val last = manager?.findLastVisibleItemPosition()
|
|
354
|
+
val anchor = first?.takeIf { it != RecyclerView.NO_POSITION }?.let { manager?.findViewByPosition(it) }
|
|
355
|
+
val child = (view as? ViewGroup)?.getChildAt(0)
|
|
356
|
+
OneKeyLog.debug("CollapsiblePager", "[HOME-OFFSET-DIAG] phase=$phase seq=${++offsetDiagSequence} " +
|
|
357
|
+
"wallMs=${System.currentTimeMillis()} monoMs=${SystemClock.uptimeMillis()} host=$hostIdentity " +
|
|
358
|
+
"generation=$attachmentGeneration selectedPage=$selectedPage pagerPage=${pager.currentItem} targetPage=$pageIndex key=$safeKey " +
|
|
359
|
+
"ownerPage=${pageKeys.indexOf(observedKey)} header=$headerOffsetPx headerHeight=$headerHeightPx stickyHeight=$stickyHeaderHeightPx " +
|
|
360
|
+
"transition=${transitionHeaderOffsets != null} synthetic=$syntheticPageRestore pending=${key in pendingPageRestores} " +
|
|
361
|
+
"requestId=${request?.let { System.identityHashCode(it) }} requestGeneration=${request?.generation} " +
|
|
362
|
+
"requestPage=${request?.pageIndex} requestCurrent=${request != null && pageRestoreRequests[key] === request} " +
|
|
363
|
+
"view=${view?.let { System.identityHashCode(it) }} tag=${view?.id} observed=${observed?.let { System.identityHashCode(it) }} " +
|
|
364
|
+
"attached=${view?.isAttachedToWindow} estimatedOffset=${view?.let(::verticalScrollOffset)} " +
|
|
365
|
+
"cachePresent=${pageOffsets.containsKey(key)} cache=${pageOffsets[key]} snapshot=${transitionPageOffsets?.get(key)} " +
|
|
366
|
+
"boundsHeight=${view?.height} measuredHeight=${view?.measuredHeight} childHeight=${child?.height} childTop=${child?.top} " +
|
|
367
|
+
"paddingTop=${view?.paddingTop} paddingBottom=${view?.paddingBottom} childCount=${(view as? ViewGroup)?.childCount} " +
|
|
368
|
+
"itemCount=${recycler?.adapter?.itemCount} first=$first last=$last anchorTag=${anchor?.id} " +
|
|
369
|
+
"anchorTop=${anchor?.let { manager?.getDecoratedTop(it) }} anchorRelative=${anchor?.let { manager?.getDecoratedTop(it)?.minus(recycler?.paddingTop ?: 0) }} " +
|
|
370
|
+
"range=${recycler?.computeVerticalScrollRange()} extent=${recycler?.computeVerticalScrollExtent()} " +
|
|
371
|
+
"canUp=${view?.canScrollVertically(-1)} canDown=${view?.canScrollVertically(1)} scrollState=${recycler?.scrollState} computing=${recycler?.isComputingLayout}")
|
|
372
|
+
}
|
|
373
|
+
|
|
233
374
|
private fun outerPagerIndex(): Int {
|
|
234
375
|
var ancestor = parent as? View
|
|
235
376
|
while (ancestor != null) {
|
|
@@ -364,6 +505,19 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
364
505
|
fun removeAllReactChildren() {
|
|
365
506
|
releaseRefreshForeground()
|
|
366
507
|
detachPrimaryScrollObserver()
|
|
508
|
+
pageRestoreRequests.clear()
|
|
509
|
+
pendingPageRestores.clear()
|
|
510
|
+
commandBodyKeys.clear()
|
|
511
|
+
explicitTopCommand = null
|
|
512
|
+
suspendedBodyKeys.clear()
|
|
513
|
+
alignedBodyKeys.clear()
|
|
514
|
+
transitionAlignedViews.clear()
|
|
515
|
+
deferredRecyclerGeometry.clear()
|
|
516
|
+
displayBodyOverrides.clear()
|
|
517
|
+
displayRecyclerAnchors.clear()
|
|
518
|
+
ownedScrollerRestoreRequests.clear()
|
|
519
|
+
recyclerReadingAnchors.clear()
|
|
520
|
+
transitionSourceRecyclerAnchor = null
|
|
367
521
|
// Weak keys may disappear after size is read; Collection.toArray tolerates this.
|
|
368
522
|
for (recycler in originalRecyclerPadding.keys.toTypedArray()) {
|
|
369
523
|
restoreRecyclerInsets(recycler)
|
|
@@ -386,16 +540,91 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
386
540
|
|
|
387
541
|
fun reactChildAt(index: Int) = logicalChildren[index]
|
|
388
542
|
|
|
543
|
+
internal fun beginPageTransition(target: Int? = null) {
|
|
544
|
+
cancelPagerTop()
|
|
545
|
+
if (!nativeSmoothHeaderScrollEnabled || headerHeightPx <= 0) return
|
|
546
|
+
if (transitionHeaderOffsets == null) {
|
|
547
|
+
transitionSourceDisplayBody = primaryScrollableForPage(selectedPage)?.let(::verticalScrollOffset) ?: 0
|
|
548
|
+
transitionSourceRecyclerAnchor = (primaryScrollableForPage(selectedPage) as? RecyclerView)?.let(::captureRecyclerAnchor)
|
|
549
|
+
pageHeaderOffsets[currentPageKey()] = headerOffsetPx
|
|
550
|
+
detachPrimaryScrollObserver()
|
|
551
|
+
transitionSourcePage = selectedPage
|
|
552
|
+
transitionHeaderOffsets = pageHeaderOffsets.toMap()
|
|
553
|
+
transitionPageOffsets = pageOffsets.toMap()
|
|
554
|
+
}
|
|
555
|
+
if (target != null) {
|
|
556
|
+
transitionTargetPage = target
|
|
557
|
+
alignPageBody(target)
|
|
558
|
+
}
|
|
559
|
+
offsetDiag("transition-begin-source=$transitionSourcePage", primaryScrollableForPage(selectedPage), pageKey(target ?: selectedPage), target ?: selectedPage)
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
internal fun onPagerScrolled(position: Int, offset: Float) {
|
|
563
|
+
if (transitionHeaderOffsets == null) return
|
|
564
|
+
alignPageBody(position)
|
|
565
|
+
if (position + 1 < adapter.itemCount) alignPageBody(position + 1)
|
|
566
|
+
updateNativeTabProgress(position, offset)
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
internal fun onPagerScrollStateChanged(state: Int) {
|
|
570
|
+
updateHeaderSettledState(forceBusy = state != ViewPager2.SCROLL_STATE_IDLE)
|
|
571
|
+
if (state == ViewPager2.SCROLL_STATE_DRAGGING || state == ViewPager2.SCROLL_STATE_SETTLING) {
|
|
572
|
+
beginPageTransition()
|
|
573
|
+
} else if (state == ViewPager2.SCROLL_STATE_IDLE && transitionHeaderOffsets != null) {
|
|
574
|
+
finishPageTransition(pager.currentItem)
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
private fun finishPageTransition(position: Int) {
|
|
579
|
+
val headers = transitionHeaderOffsets ?: return
|
|
580
|
+
detachPrimaryScrollObserver(recordOffset = false)
|
|
581
|
+
releaseRefreshForeground()
|
|
582
|
+
selectedPage = position.coerceIn(0, adapter.itemCount - 1)
|
|
583
|
+
syntheticPageRestore = true
|
|
584
|
+
setHeaderOffset(headers[pageKey(transitionSourcePage)] ?: headerOffsetPx)
|
|
585
|
+
transitionHeaderOffsets = null
|
|
586
|
+
transitionPageOffsets = null
|
|
587
|
+
transitionTargetPage = null
|
|
588
|
+
syntheticPageRestore = false
|
|
589
|
+
transitionAlignedViews.clear()
|
|
590
|
+
if (selectedPage == transitionSourcePage) {
|
|
591
|
+
displayBodyOverrides[currentPageKey()] = transitionSourceDisplayBody
|
|
592
|
+
transitionSourceRecyclerAnchor?.let { displayRecyclerAnchors[currentPageKey()] = it }
|
|
593
|
+
} else {
|
|
594
|
+
displayBodyOverrides.remove(currentPageKey())
|
|
595
|
+
displayRecyclerAnchors.remove(currentPageKey())
|
|
596
|
+
}
|
|
597
|
+
transitionSourceRecyclerAnchor = null
|
|
598
|
+
if (headerOffsetPx == headerHeightPx) suspendedBodyKeys.remove(currentPageKey())
|
|
599
|
+
else if ((pageOffsets[currentPageKey()] ?: 0) > 0) suspendedBodyKeys.add(currentPageKey())
|
|
600
|
+
alignPageBody(selectedPage, force = true)
|
|
601
|
+
offsetDiag("transition-finish", primaryScrollableForPage(selectedPage), currentPageKey(), selectedPage)
|
|
602
|
+
prepareAdjacentPages()
|
|
603
|
+
attachPrimaryScrollObserver()
|
|
604
|
+
}
|
|
605
|
+
|
|
389
606
|
fun selectPage(position: Int) {
|
|
607
|
+
offsetDiag("select-begin", observedOwnedScroller ?: observedNativeScroller ?: observedRecyclerView, pageKey(position), position)
|
|
390
608
|
pendingRefreshPull = null
|
|
391
609
|
if (adapter.itemCount == 0) return
|
|
610
|
+
if (nativeSmoothHeaderScrollEnabled && headerHeightPx > 0) {
|
|
611
|
+
if (transitionHeaderOffsets != null) return
|
|
612
|
+
beginPageTransition(position)
|
|
613
|
+
finishPageTransition(position)
|
|
614
|
+
return
|
|
615
|
+
}
|
|
392
616
|
detachPrimaryScrollObserver()
|
|
393
617
|
releaseRefreshForeground()
|
|
394
618
|
selectedPage = position.coerceIn(0, adapter.itemCount - 1)
|
|
395
619
|
val savedOffset = pageOffsets[currentPageKey()]
|
|
396
620
|
?: primaryScrollableForPage(selectedPage)?.let(::verticalScrollOffset)
|
|
397
621
|
?: 0
|
|
398
|
-
if (
|
|
622
|
+
offsetDiag("select-offset=$savedOffset-source=${if (pageOffsets[currentPageKey()] != null) "saved" else "live"}")
|
|
623
|
+
if (savedOffset > 0) {
|
|
624
|
+
offsetDiag("select-force-collapse-before")
|
|
625
|
+
setHeaderOffset(headerHeightPx)
|
|
626
|
+
offsetDiag("select-force-collapse-after")
|
|
627
|
+
}
|
|
399
628
|
postForCurrentAttachment {
|
|
400
629
|
prepareAdjacentPages()
|
|
401
630
|
attachPrimaryScrollObserver()
|
|
@@ -519,7 +748,295 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
519
748
|
}
|
|
520
749
|
}
|
|
521
750
|
|
|
751
|
+
private fun restorationBodyOffset(key: String): Int {
|
|
752
|
+
val saved = if (transitionPageOffsets != null && key == pageKey(transitionSourcePage)) transitionSourceDisplayBody
|
|
753
|
+
else displayBodyOverrides[key] ?: transitionPageOffsets?.get(key) ?: pageOffsets[key] ?: 0
|
|
754
|
+
return if (nativeSmoothHeaderScrollEnabled && headerOffsetPx < headerHeightPx) 0 else saved
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
private fun captureRecyclerAnchor(view: RecyclerView): RecyclerReadingAnchor? {
|
|
758
|
+
val manager = view.layoutManager as? LinearLayoutManager ?: return null
|
|
759
|
+
if (manager.orientation != RecyclerView.VERTICAL) return null
|
|
760
|
+
val position = manager.findFirstVisibleItemPosition()
|
|
761
|
+
val child = manager.findViewByPosition(position) ?: return null
|
|
762
|
+
val adapter = view.adapter ?: return null
|
|
763
|
+
return RecyclerReadingAnchor(view, adapter, position, manager.getDecoratedTop(child) - view.paddingTop)
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
private fun saveRecyclerReadingAnchor(key: String, view: RecyclerView) {
|
|
767
|
+
captureRecyclerAnchor(view)?.let { recyclerReadingAnchors[key] = it }
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
private fun restoreRecyclerAnchor(view: RecyclerView, key: String, index: Int, request: PageRestoreRequest?): Boolean {
|
|
771
|
+
if (!nativeSmoothHeaderScrollEnabled || pageKey(index) != key ||
|
|
772
|
+
primaryScrollableForPage(index) !== view || !isCurrentPageRestore(key, request) ||
|
|
773
|
+
!isAttachedToWindow || !view.isAttachedToWindow || view.width <= 0 || view.height <= 0 ||
|
|
774
|
+
isInLayout || view.isInLayout || view.isComputingLayout) return false
|
|
775
|
+
val manager = view.layoutManager as? LinearLayoutManager
|
|
776
|
+
val reading = recyclerReadingAnchors[key]?.takeIf { it.view.get() === view && it.adapter === view.adapter }
|
|
777
|
+
val display = (if (transitionPageOffsets != null && index == transitionSourcePage) transitionSourceRecyclerAnchor
|
|
778
|
+
else displayRecyclerAnchors[key])?.takeIf { it.view.get() === view && it.adapter === view.adapter }
|
|
779
|
+
if (manager == null || manager.orientation != RecyclerView.VERTICAL ||
|
|
780
|
+
(headerOffsetPx == headerHeightPx && reading == null && display == null && restorationBodyOffset(key) > 0)) {
|
|
781
|
+
val saved = restorationBodyOffset(key)
|
|
782
|
+
val current = view.computeVerticalScrollOffset()
|
|
783
|
+
val previousSynthetic = syntheticPageRestore
|
|
784
|
+
syntheticPageRestore = true
|
|
785
|
+
try { if (saved != current) view.scrollBy(0, saved - current) }
|
|
786
|
+
finally { syntheticPageRestore = previousSynthetic }
|
|
787
|
+
return saved <= view.computeVerticalScrollOffset() || view.canScrollVertically(1)
|
|
788
|
+
}
|
|
789
|
+
val target = if (headerOffsetPx < headerHeightPx) null else display ?: reading
|
|
790
|
+
val position = target?.position ?: 0
|
|
791
|
+
val top = target?.top ?: 0
|
|
792
|
+
if (position !in 0 until (view.adapter?.itemCount ?: 0)) return false
|
|
793
|
+
fun reached(): Boolean {
|
|
794
|
+
val actual = captureRecyclerAnchor(view) ?: return false
|
|
795
|
+
return actual.position == position && actual.top == top
|
|
796
|
+
}
|
|
797
|
+
if (!reached()) {
|
|
798
|
+
if (!isAttachedToWindow || !view.isAttachedToWindow || width <= 0 || height <= 0 ||
|
|
799
|
+
view.width <= 0 || view.height <= 0 || isInLayout || view.isInLayout || view.isComputingLayout ||
|
|
800
|
+
pageKey(index) != key || primaryScrollableForPage(index) !== view || !isCurrentPageRestore(key, request)) return false
|
|
801
|
+
val previousSynthetic = syntheticPageRestore
|
|
802
|
+
syntheticPageRestore = true
|
|
803
|
+
try {
|
|
804
|
+
manager.scrollToPositionWithOffset(position, top)
|
|
805
|
+
// Use the existing inset commit boundary once, before the target's first frame.
|
|
806
|
+
view.forceLayout()
|
|
807
|
+
view.measure(MeasureSpec.makeMeasureSpec(view.width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(view.height, MeasureSpec.EXACTLY))
|
|
808
|
+
view.layout(view.left, view.top, view.right, view.bottom)
|
|
809
|
+
} finally {
|
|
810
|
+
syntheticPageRestore = previousSynthetic
|
|
811
|
+
}
|
|
812
|
+
offsetDiag("recycler-anchor-restore-position=$position-top=$top", view, key, index, request)
|
|
813
|
+
}
|
|
814
|
+
val readingStillUnreachable = display != null && reading != null &&
|
|
815
|
+
(reading.position != position || reading.top != top) && !view.canScrollVertically(1)
|
|
816
|
+
return reached() && !readingStillUnreachable
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
private fun alignPageBody(index: Int, force: Boolean = false) {
|
|
820
|
+
if (!nativeSmoothHeaderScrollEnabled || index !in 0 until adapter.itemCount) return
|
|
821
|
+
val view = primaryScrollableForPage(index) ?: return
|
|
822
|
+
val key = pageKey(index)
|
|
823
|
+
if (!force && transitionAlignedViews[view] == key) return
|
|
824
|
+
transitionAlignedViews[view] = key
|
|
825
|
+
if (transitionPageOffsets != null && index != transitionSourcePage) {
|
|
826
|
+
displayBodyOverrides.remove(key)
|
|
827
|
+
displayRecyclerAnchors.remove(key)
|
|
828
|
+
}
|
|
829
|
+
alignedBodyKeys.add(key)
|
|
830
|
+
if (headerOffsetPx < headerHeightPx && (pageOffsets[key] ?: 0) > 0) suspendedBodyKeys.add(key)
|
|
831
|
+
val previousSynthetic = syntheticPageRestore
|
|
832
|
+
syntheticPageRestore = true
|
|
833
|
+
when (view) {
|
|
834
|
+
is NativeScrollerViewport -> {
|
|
835
|
+
ownedScrollerKeys.remove(view)
|
|
836
|
+
applyOwnedScrollerInsets(view, index)
|
|
837
|
+
view.host.restoreScrollOffset(restorationBodyOffset(key))
|
|
838
|
+
}
|
|
839
|
+
is RecyclerView -> {
|
|
840
|
+
restoredRecyclerKeys.remove(view)
|
|
841
|
+
applyRecyclerInsets(view, index)
|
|
842
|
+
restoreRecyclerAnchor(view, key, index, pageRestoreRequests[key])
|
|
843
|
+
}
|
|
844
|
+
is ScrollView -> {
|
|
845
|
+
restoredScrollerKeys.remove(view)
|
|
846
|
+
applyNativeScrollerInsets(view, index)
|
|
847
|
+
if (view.height > 0 && view.childCount > 0) view.scrollTo(view.scrollX, restorationBodyOffset(key))
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
syntheticPageRestore = previousSynthetic
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
private fun takeOverBodyForVerticalConsumption() {
|
|
854
|
+
cancelPagerTop()
|
|
855
|
+
if (!nativeSmoothHeaderScrollEnabled || transitionHeaderOffsets != null || syntheticPageRestore) return
|
|
856
|
+
val key = currentPageKey()
|
|
857
|
+
commandBodyKeys.remove(key)
|
|
858
|
+
explicitTopCommand = null
|
|
859
|
+
suspendedBodyKeys.remove(key)
|
|
860
|
+
alignedBodyKeys.remove(key)
|
|
861
|
+
displayBodyOverrides.remove(key)
|
|
862
|
+
displayRecyclerAnchors.remove(key)
|
|
863
|
+
pageOffsets[key] = primaryScrollableForPage(selectedPage)?.let(::verticalScrollOffset) ?: 0
|
|
864
|
+
(primaryScrollableForPage(selectedPage) as? RecyclerView)?.let { saveRecyclerReadingAnchor(key, it) }
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
internal fun acceptObservedScrollCommand(view: View, explicitTop: Boolean, phase: Int = 0) {
|
|
868
|
+
if (dispatchingPagerTop && matchesPagerTop(view)) return
|
|
869
|
+
if (phase == 0 && view === (observedRecyclerView ?: observedOwnedScroller ?: observedNativeScroller)) cancelPagerTop()
|
|
870
|
+
if (phase != 0 && matchesPagerTop(view)) { finishPagerTopIfSettled(); return }
|
|
871
|
+
val owner = observedRecyclerView ?: observedOwnedScroller ?: observedNativeScroller
|
|
872
|
+
val stateOwner = observedRecyclerView ?: observedOwnedScroller ?: observedNativeScroller
|
|
873
|
+
val stateKey = observedRecyclerKey ?: observedOwnedScrollerKey ?: observedNativeScrollerKey
|
|
874
|
+
if (view === stateOwner && stateKey == currentPageKey() && view.isAttachedToWindow && isAttachedToWindow && transitionHeaderOffsets == null && !syntheticPageRestore) {
|
|
875
|
+
updateHeaderSettledState()
|
|
876
|
+
if (phase == 1) headerCommandOwner = WeakReference(view)
|
|
877
|
+
else if (phase == 0 || phase == 2) headerCommandOwner = null
|
|
878
|
+
updateHeaderSettledState(forceBusy = phase != 2)
|
|
879
|
+
}
|
|
880
|
+
if (!nativeSmoothHeaderScrollEnabled || view !== owner || transitionHeaderOffsets != null || syntheticPageRestore ||
|
|
881
|
+
!isAttachedToWindow || !view.isAttachedToWindow) return
|
|
882
|
+
val key = currentPageKey()
|
|
883
|
+
val observedKey = observedRecyclerKey ?: observedOwnedScrollerKey ?: observedNativeScrollerKey
|
|
884
|
+
if (observedKey != key) return
|
|
885
|
+
if (phase == 1) { commandBodyKeys.add(key); return }
|
|
886
|
+
if (phase == 2) {
|
|
887
|
+
if (!commandBodyKeys.remove(key)) return
|
|
888
|
+
val completingExplicitTop = hasExplicitTopCommand(view, key)
|
|
889
|
+
val manager = (view as? RecyclerView)?.layoutManager as? LinearLayoutManager
|
|
890
|
+
val first = manager?.findViewByPosition(0)
|
|
891
|
+
val atExplicitTop = completingExplicitTop && manager != null && manager.orientation == RecyclerView.VERTICAL &&
|
|
892
|
+
manager.findFirstVisibleItemPosition() == 0 && first != null &&
|
|
893
|
+
manager.getDecoratedTop(first) - view.paddingTop == 0
|
|
894
|
+
explicitTopCommand = null
|
|
895
|
+
pageOffsets[key] = if (atExplicitTop) 0 else verticalScrollOffset(view)
|
|
896
|
+
if (view is RecyclerView) saveRecyclerReadingAnchor(key, view)
|
|
897
|
+
if (atExplicitTop) setHeaderOffset(0)
|
|
898
|
+
else if ((pageOffsets[key] ?: 0) > 0) setHeaderOffset(headerHeightPx)
|
|
899
|
+
offsetDiag("command-result-complete", view, key)
|
|
900
|
+
return
|
|
901
|
+
}
|
|
902
|
+
commandBodyKeys.remove(key)
|
|
903
|
+
explicitTopCommand = if (explicitTop && view is RecyclerView) {
|
|
904
|
+
key to PageRestoreRequest(view, selectedPage, attachmentGeneration)
|
|
905
|
+
} else null
|
|
906
|
+
pageRestoreRequests.remove(key)
|
|
907
|
+
pendingPageRestores.remove(key)
|
|
908
|
+
ownedScrollerRestoreRequests.remove(key)
|
|
909
|
+
if (view is RecyclerView) deferredRecyclerGeometry.remove(view)
|
|
910
|
+
(view as? NativeScrollerViewport)?.host?.cancelPendingScrollRestore()
|
|
911
|
+
suspendedBodyKeys.remove(key)
|
|
912
|
+
alignedBodyKeys.remove(key)
|
|
913
|
+
displayBodyOverrides.remove(key)
|
|
914
|
+
displayRecyclerAnchors.remove(key)
|
|
915
|
+
if (view is RecyclerView) saveRecyclerReadingAnchor(key, view)
|
|
916
|
+
pageOffsets[key] = if (explicitTop) 0 else verticalScrollOffset(view)
|
|
917
|
+
if (explicitTop) setHeaderOffset(0)
|
|
918
|
+
offsetDiag("command-body-takeover-top=$explicitTop", view, key)
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
private fun matchesPagerTop(view: View): Boolean {
|
|
922
|
+
val request = pagerTopRequest ?: return false
|
|
923
|
+
val owner = observedRecyclerView ?: observedOwnedScroller ?: observedNativeScroller
|
|
924
|
+
val observedKey = observedRecyclerKey ?: observedOwnedScrollerKey ?: observedNativeScrollerKey
|
|
925
|
+
return request.view.get() === view && owner === view && observedKey == request.key &&
|
|
926
|
+
request.key == currentPageKey() && request.pageIndex == selectedPage &&
|
|
927
|
+
request.generation == attachmentGeneration && view.isAttachedToWindow && isAttachedToWindow
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
private fun cancelPagerTop() {
|
|
931
|
+
pagerTopRequest?.let { commandBodyKeys.remove(it.key) }
|
|
932
|
+
pagerTopRequest = null
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
private fun recyclerIsAtBodyTop(view: RecyclerView): Boolean {
|
|
936
|
+
val manager = view.layoutManager as? LinearLayoutManager ?: return !view.canScrollVertically(-1)
|
|
937
|
+
val first = manager.findViewByPosition(0) ?: return view.adapter?.itemCount == 0
|
|
938
|
+
return manager.orientation == RecyclerView.VERTICAL && manager.findFirstVisibleItemPosition() == 0 &&
|
|
939
|
+
manager.getDecoratedTop(first) - view.paddingTop == 0
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
private fun finishPagerTopIfSettled() {
|
|
943
|
+
val request = pagerTopRequest ?: return
|
|
944
|
+
val view = request.view.get() ?: run { cancelPagerTop(); return }
|
|
945
|
+
if (!matchesPagerTop(view)) { cancelPagerTop(); return }
|
|
946
|
+
if (dispatchingPagerTop) return
|
|
947
|
+
val settled = when (view) {
|
|
948
|
+
is RecyclerView -> !view.isComputingLayout && view.scrollState == RecyclerView.SCROLL_STATE_IDLE &&
|
|
949
|
+
view.layoutManager?.isSmoothScrolling != true && recyclerIsAtBodyTop(view)
|
|
950
|
+
is NativeScrollerViewport -> !view.isReadingMotionActive() && view.scrollY == 0
|
|
951
|
+
is ScrollView -> view.scrollY == 0
|
|
952
|
+
else -> false
|
|
953
|
+
}
|
|
954
|
+
if (!settled) return
|
|
955
|
+
pageOffsets[request.key] = 0
|
|
956
|
+
if (view is RecyclerView) saveRecyclerReadingAnchor(request.key, view)
|
|
957
|
+
setHeaderOffset(if (request.target == 1) headerHeightPx else 0)
|
|
958
|
+
offsetDiag("pager-top-complete-target=${request.target}-token=${request.token}", view, request.key)
|
|
959
|
+
cancelPagerTop()
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
internal fun scrollToTop(target: Int, animated: Boolean) {
|
|
963
|
+
if (target !in 0..1 || pager.scrollState != ViewPager2.SCROLL_STATE_IDLE || transitionHeaderOffsets != null ||
|
|
964
|
+
syntheticPageRestore || selectedPage !in 0 until adapter.itemCount || !isAttachedToWindow) return
|
|
965
|
+
val view = observedRecyclerView ?: observedOwnedScroller ?: observedNativeScroller ?: return
|
|
966
|
+
val key = currentPageKey()
|
|
967
|
+
val ownerKey = observedRecyclerKey ?: observedOwnedScrollerKey ?: observedNativeScrollerKey
|
|
968
|
+
if (ownerKey != key || !view.isAttachedToWindow || primaryScrollableForPage(selectedPage) !== view) return
|
|
969
|
+
// Finish old child notifications before installing the new Pager-owned target.
|
|
970
|
+
cancelPagerTop()
|
|
971
|
+
if (view is RecyclerView) {
|
|
972
|
+
(view.getTag(R.id.onekey_native_scroll_coordinator_command_cancel_listener) as? Runnable)?.run()
|
|
973
|
+
view.stopScroll()
|
|
974
|
+
} else if (view is NativeScrollerViewport) {
|
|
975
|
+
view.stopMotion()
|
|
976
|
+
view.host.cancelPendingScrollRestore()
|
|
977
|
+
}
|
|
978
|
+
explicitTopCommand = null
|
|
979
|
+
pageRestoreRequests.remove(key)
|
|
980
|
+
pendingPageRestores.remove(key)
|
|
981
|
+
ownedScrollerRestoreRequests.remove(key)
|
|
982
|
+
suspendedBodyKeys.remove(key)
|
|
983
|
+
alignedBodyKeys.remove(key)
|
|
984
|
+
displayBodyOverrides.remove(key)
|
|
985
|
+
displayRecyclerAnchors.remove(key)
|
|
986
|
+
recyclerReadingAnchors.remove(key)
|
|
987
|
+
if (view is RecyclerView) deferredRecyclerGeometry.remove(view)
|
|
988
|
+
pageOffsets[key] = 0
|
|
989
|
+
val request = PagerTopRequest(view, key, selectedPage, attachmentGeneration, target, ++pagerTopToken)
|
|
990
|
+
pagerTopRequest = request
|
|
991
|
+
commandBodyKeys.add(key)
|
|
992
|
+
setHeaderOffset(if (target == 1) headerHeightPx else 0)
|
|
993
|
+
offsetDiag("pager-top-begin-target=$target-animated=$animated-token=${request.token}", view, key)
|
|
994
|
+
dispatchingPagerTop = true
|
|
995
|
+
try {
|
|
996
|
+
when (view) {
|
|
997
|
+
is RecyclerView -> {
|
|
998
|
+
val manager = view.layoutManager as? LinearLayoutManager
|
|
999
|
+
if (animated && !recyclerIsAtBodyTop(view)) view.smoothScrollToPosition(0)
|
|
1000
|
+
else if (manager != null && manager.orientation == RecyclerView.VERTICAL) {
|
|
1001
|
+
manager.scrollToPositionWithOffset(0, 0)
|
|
1002
|
+
if (view.width > 0 && view.height > 0 && !isInLayout && !view.isInLayout && !view.isComputingLayout) {
|
|
1003
|
+
view.measure(MeasureSpec.makeMeasureSpec(view.width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(view.height, MeasureSpec.EXACTLY))
|
|
1004
|
+
view.layout(view.left, view.top, view.right, view.bottom)
|
|
1005
|
+
}
|
|
1006
|
+
} else view.scrollToPosition(0)
|
|
1007
|
+
}
|
|
1008
|
+
is NativeScrollerViewport -> view.host.scrollToPosition(0, 0, animated)
|
|
1009
|
+
is ScrollView -> if (animated) view.smoothScrollTo(0, 0) else view.scrollTo(0, 0)
|
|
1010
|
+
}
|
|
1011
|
+
} finally { dispatchingPagerTop = false }
|
|
1012
|
+
finishPagerTopIfSettled()
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
private fun hasExplicitTopCommand(view: View, key: String): Boolean {
|
|
1016
|
+
val command = explicitTopCommand ?: return false
|
|
1017
|
+
val request = command.second
|
|
1018
|
+
return view is RecyclerView && observedRecyclerView === view && observedRecyclerKey == key &&
|
|
1019
|
+
key == currentPageKey() && command.first == key && request.view.get() === view &&
|
|
1020
|
+
request.pageIndex == selectedPage && request.generation == attachmentGeneration
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
private fun publishScrollCommandListener(recycler: RecyclerView, remove: Boolean = false) {
|
|
1024
|
+
val tag = R.id.onekey_native_scroll_coordinator_command_listener
|
|
1025
|
+
@Suppress("UNCHECKED_CAST")
|
|
1026
|
+
val existing = recycler.getTag(tag) as? MutableMap<Any, BiConsumer<Boolean, Int>>
|
|
1027
|
+
val listeners = existing ?: if (remove) return else mutableMapOf<Any, BiConsumer<Boolean, Int>>().also { recycler.setTag(tag, it) }
|
|
1028
|
+
if (remove) listeners.remove(this)
|
|
1029
|
+
else {
|
|
1030
|
+
val generation = attachmentGeneration
|
|
1031
|
+
val key = currentPageKey()
|
|
1032
|
+
listeners[this] = BiConsumer { explicitTop, phase ->
|
|
1033
|
+
if (generation == attachmentGeneration && key == currentPageKey()) acceptObservedScrollCommand(recycler, explicitTop, phase)
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
522
1038
|
private fun maximumHeaderOffset(): Int {
|
|
1039
|
+
if (nativeSmoothHeaderScrollEnabled) return headerHeightPx
|
|
523
1040
|
val scrollable = observedRecyclerView
|
|
524
1041
|
?: observedOwnedScroller
|
|
525
1042
|
?: observedNativeScroller
|
|
@@ -535,11 +1052,105 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
535
1052
|
return (contentHeight.roundToInt() - height).coerceIn(0, headerHeightPx)
|
|
536
1053
|
}
|
|
537
1054
|
|
|
1055
|
+
internal fun headerStatePageKey(): String = currentPageKey()
|
|
1056
|
+
|
|
1057
|
+
private fun publishHeaderSettledState(settled: Boolean, identityChanged: Boolean = false) {
|
|
1058
|
+
if (headerSettledAtTop == settled && !identityChanged) return
|
|
1059
|
+
headerSettledAtTop = settled
|
|
1060
|
+
log("header-settled-state page=$selectedPage generation=$attachmentGeneration settled=$settled")
|
|
1061
|
+
onHeaderSettledStateChanged?.invoke()
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
private fun cancelHeaderSettledCheck() {
|
|
1065
|
+
headerSettledCheck?.let(::removeCallbacks)
|
|
1066
|
+
headerSettledCheck = null
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
private fun updateHeaderSettledState(forceBusy: Boolean = false) {
|
|
1070
|
+
val owner = observedRecyclerView ?: observedOwnedScroller ?: observedNativeScroller
|
|
1071
|
+
val ownerKey = observedRecyclerKey ?: observedOwnedScrollerKey ?: observedNativeScrollerKey
|
|
1072
|
+
val valid = isAttachedToWindow && width > 0 && height > 0 && headerHeightPx > 0 &&
|
|
1073
|
+
headerView?.isAttachedToWindow == true && owner != null && owner.isAttachedToWindow &&
|
|
1074
|
+
owner.width > 0 && owner.height > 0 && ownerKey == currentPageKey() &&
|
|
1075
|
+
selectedPage in 0 until adapter.itemCount && isDescendant(owner, adapter.pageAt(selectedPage))
|
|
1076
|
+
val identityChanged = headerSettledOwner?.get() !== owner || headerSettledKey != currentPageKey() ||
|
|
1077
|
+
headerSettledGeneration != attachmentGeneration
|
|
1078
|
+
if (identityChanged) {
|
|
1079
|
+
cancelHeaderSettledCheck()
|
|
1080
|
+
headerCommandOwner = null
|
|
1081
|
+
headerSettledOwner = owner?.let(::WeakReference)
|
|
1082
|
+
headerSettledKey = currentPageKey()
|
|
1083
|
+
headerSettledGeneration = attachmentGeneration
|
|
1084
|
+
headerSettledGeometry = null
|
|
1085
|
+
headerSettledWasBusy = true
|
|
1086
|
+
publishHeaderSettledState(false, true)
|
|
1087
|
+
}
|
|
1088
|
+
if (!valid || owner == null || headerOffsetPx != 0) {
|
|
1089
|
+
cancelHeaderSettledCheck()
|
|
1090
|
+
headerSettledGeometry = null
|
|
1091
|
+
headerSettledWasBusy = true
|
|
1092
|
+
publishHeaderSettledState(false)
|
|
1093
|
+
return
|
|
1094
|
+
}
|
|
1095
|
+
val windowPoint = IntArray(2)
|
|
1096
|
+
owner.getLocationInWindow(windowPoint)
|
|
1097
|
+
val recycler = owner as? RecyclerView
|
|
1098
|
+
val manager = recycler?.layoutManager as? LinearLayoutManager
|
|
1099
|
+
val first = manager?.findFirstVisibleItemPosition() ?: -1
|
|
1100
|
+
val firstTop = manager?.let { layoutManager ->
|
|
1101
|
+
layoutManager.findViewByPosition(first)?.let(layoutManager::getDecoratedTop)
|
|
1102
|
+
} ?: 0
|
|
1103
|
+
val content = (owner as? ViewGroup)?.getChildAt(0)
|
|
1104
|
+
val geometry = listOf(headerOffsetPx, headerHeightPx, stickyHeaderHeightPx, width, height,
|
|
1105
|
+
owner.width, owner.height, owner.paddingTop, owner.paddingBottom, owner.scrollY,
|
|
1106
|
+
windowPoint[0], windowPoint[1], first, firstTop, recycler?.adapter?.itemCount ?: 0,
|
|
1107
|
+
content?.height ?: 0, headerView?.height ?: 0).joinToString(":")
|
|
1108
|
+
val busy = forceBusy || pager.isFakeDragging || pager.scrollState != ViewPager2.SCROLL_STATE_IDLE ||
|
|
1109
|
+
transitionHeaderOffsets != null || syntheticPageRestore || pagerTopRequest != null ||
|
|
1110
|
+
headerCommandOwner?.get() === owner ||
|
|
1111
|
+
nestedScrollingParentHelper.nestedScrollAxes and ViewCompat.SCROLL_AXIS_VERTICAL != 0 ||
|
|
1112
|
+
(recycler != null && (recycler.scrollState != RecyclerView.SCROLL_STATE_IDLE ||
|
|
1113
|
+
recycler.isComputingLayout || recycler.layoutManager?.isSmoothScrolling == true)) ||
|
|
1114
|
+
(owner is NativeScrollerViewport && owner.isReadingMotionActive())
|
|
1115
|
+
val changed = geometry != headerSettledGeometry
|
|
1116
|
+
val now = SystemClock.uptimeMillis()
|
|
1117
|
+
if (changed || busy || headerSettledWasBusy) {
|
|
1118
|
+
headerSettledSince = now
|
|
1119
|
+
headerSettledGeometry = geometry
|
|
1120
|
+
cancelHeaderSettledCheck()
|
|
1121
|
+
publishHeaderSettledState(false)
|
|
1122
|
+
}
|
|
1123
|
+
headerSettledWasBusy = busy
|
|
1124
|
+
if (!busy && now - headerSettledSince >= 150L) {
|
|
1125
|
+
cancelHeaderSettledCheck()
|
|
1126
|
+
publishHeaderSettledState(true)
|
|
1127
|
+
return
|
|
1128
|
+
}
|
|
1129
|
+
if (headerSettledCheck == null) {
|
|
1130
|
+
val weakOwner = WeakReference(owner)
|
|
1131
|
+
val key = currentPageKey()
|
|
1132
|
+
val generation = attachmentGeneration
|
|
1133
|
+
val check = Runnable {
|
|
1134
|
+
headerSettledCheck = null
|
|
1135
|
+
if (generation == attachmentGeneration && key == currentPageKey() && weakOwner.get() ===
|
|
1136
|
+
(observedRecyclerView ?: observedOwnedScroller ?: observedNativeScroller)) updateHeaderSettledState()
|
|
1137
|
+
}
|
|
1138
|
+
headerSettledCheck = check
|
|
1139
|
+
postDelayed(check, 150L)
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
538
1143
|
private fun setHeaderOffset(offset: Int) {
|
|
539
1144
|
// val next = offset.coerceIn(0, headerHeightPx)
|
|
540
|
-
val
|
|
1145
|
+
val pending = nativeSmoothHeaderScrollEnabled && currentPageKey() in pendingPageRestores
|
|
1146
|
+
val maximum = if (transitionHeaderOffsets != null || pending || pagerTopRequest != null) headerHeightPx else maximumHeaderOffset()
|
|
1147
|
+
val next = offset.coerceIn(0, maximum)
|
|
541
1148
|
if (headerOffsetPx == next) return
|
|
542
1149
|
headerOffsetPx = next
|
|
1150
|
+
updateHeaderSettledState(forceBusy = true)
|
|
1151
|
+
if (nativeSmoothHeaderScrollEnabled && transitionHeaderOffsets == null && !syntheticPageRestore && !pending) {
|
|
1152
|
+
pageHeaderOffsets[currentPageKey()] = next
|
|
1153
|
+
}
|
|
543
1154
|
applyHeaderOffset()
|
|
544
1155
|
}
|
|
545
1156
|
|
|
@@ -656,13 +1267,20 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
656
1267
|
}
|
|
657
1268
|
|
|
658
1269
|
private fun attachPrimaryScrollObserver() {
|
|
1270
|
+
if (transitionHeaderOffsets != null) return
|
|
659
1271
|
if (selectedPage !in 0 until adapter.itemCount) return
|
|
660
1272
|
val previous = observedRecyclerView ?: observedOwnedScroller ?: observedNativeScroller
|
|
661
1273
|
if (observedPageRemoved || previous != null && !isDescendant(previous, adapter.pageAt(selectedPage))) {
|
|
1274
|
+
offsetDiag("primary-replacement-before-removed=$observedPageRemoved", previous)
|
|
662
1275
|
observedPageRemoved = false
|
|
663
|
-
detachPrimaryScrollObserver()
|
|
1276
|
+
detachPrimaryScrollObserver(recordOffset = !nativeSmoothHeaderScrollEnabled)
|
|
664
1277
|
// A replacement scroller starts at the beginning while retaining header collapse.
|
|
665
|
-
|
|
1278
|
+
offsetDiag("primary-replacement-cache-before", previous)
|
|
1279
|
+
if (!nativeSmoothHeaderScrollEnabled) {
|
|
1280
|
+
offsetDiag("cache-write-replacement-new=0", previous)
|
|
1281
|
+
pageOffsets[currentPageKey()] = 0
|
|
1282
|
+
}
|
|
1283
|
+
offsetDiag("primary-replacement-cache-after", previous)
|
|
666
1284
|
}
|
|
667
1285
|
when (val scrollable = primaryScrollableForPage(selectedPage)) {
|
|
668
1286
|
is RecyclerView -> attachRecyclerObserver(scrollable)
|
|
@@ -672,24 +1290,47 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
672
1290
|
}
|
|
673
1291
|
|
|
674
1292
|
private fun attachRecyclerObserver(recycler: RecyclerView) {
|
|
675
|
-
if (observedRecyclerView === recycler) {
|
|
1293
|
+
if (observedRecyclerView === recycler && observedRecyclerKey == currentPageKey()) {
|
|
676
1294
|
applyRecyclerInsets(recycler, selectedPage)
|
|
677
1295
|
publishConsumedOffset(recycler)
|
|
678
1296
|
return
|
|
679
1297
|
}
|
|
680
1298
|
|
|
681
1299
|
detachPrimaryScrollObserver()
|
|
1300
|
+
offsetDiag("recycler-attach-before", recycler)
|
|
682
1301
|
observedRecyclerView = recycler
|
|
1302
|
+
observedRecyclerKey = currentPageKey()
|
|
1303
|
+
publishScrollCommandListener(recycler)
|
|
683
1304
|
applyRecyclerInsets(recycler, selectedPage)
|
|
684
1305
|
publishConsumedOffset(recycler)
|
|
1306
|
+
val key = currentPageKey()
|
|
685
1307
|
val listener = object : RecyclerView.OnScrollListener() {
|
|
1308
|
+
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
|
1309
|
+
if (observedRecyclerView === recyclerView) updateHeaderSettledState(forceBusy = newState != RecyclerView.SCROLL_STATE_IDLE)
|
|
1310
|
+
if (newState == RecyclerView.SCROLL_STATE_IDLE) finishPagerTopIfSettled()
|
|
1311
|
+
}
|
|
686
1312
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
|
1313
|
+
if (observedRecyclerView === recyclerView && (dx != 0 || dy != 0)) updateHeaderSettledState(forceBusy = true)
|
|
1314
|
+
if (observedScrollListener !== this || observedRecyclerKey != key ||
|
|
1315
|
+
key != currentPageKey() || !isAttachedToWindow ||
|
|
1316
|
+
transitionHeaderOffsets != null || syntheticPageRestore || key in pendingPageRestores || key in alignedBodyKeys) {
|
|
1317
|
+
if (offsetDiagEnabled() && (dx != 0 || dy != 0 || pageOffsets[key] != recyclerView.computeVerticalScrollOffset())) {
|
|
1318
|
+
offsetDiag("recycler-observer-skip-dx=$dx-dy=$dy-listenerCurrent=${observedScrollListener === this}", recyclerView, key)
|
|
1319
|
+
}
|
|
1320
|
+
return
|
|
1321
|
+
}
|
|
1322
|
+
if (nativeSmoothHeaderScrollEnabled && key !in commandBodyKeys &&
|
|
1323
|
+
(dy == 0 || recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE)) return
|
|
687
1324
|
val contentOffset = recyclerView.computeVerticalScrollOffset()
|
|
688
|
-
pageOffsets[
|
|
1325
|
+
if (pageOffsets[key] != contentOffset) offsetDiag("cache-write-recycler-observer-new=$contentOffset-dx=$dx-dy=$dy", recyclerView, key)
|
|
1326
|
+
pageOffsets[key] = contentOffset
|
|
1327
|
+
if (nativeSmoothHeaderScrollEnabled) saveRecyclerReadingAnchor(key, recyclerView)
|
|
689
1328
|
// Nested touch scrolling moves list content only after the header has
|
|
690
1329
|
// collapsed. Programmatic list jumps bypass those parent callbacks.
|
|
691
|
-
if (contentOffset > 0 && headerOffsetPx < headerHeightPx) {
|
|
1330
|
+
if (contentOffset > 0 && headerOffsetPx < headerHeightPx && !hasExplicitTopCommand(recyclerView, key) && !matchesPagerTop(recyclerView)) {
|
|
1331
|
+
offsetDiag("recycler-positive-collapse-before", recyclerView)
|
|
692
1332
|
setHeaderOffset(headerHeightPx)
|
|
1333
|
+
offsetDiag("recycler-positive-collapse-after", recyclerView)
|
|
693
1334
|
}
|
|
694
1335
|
}
|
|
695
1336
|
}
|
|
@@ -702,11 +1343,21 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
702
1343
|
}
|
|
703
1344
|
|
|
704
1345
|
private fun detachRecyclerObserver(recordOffset: Boolean) {
|
|
1346
|
+
cancelPagerTop()
|
|
1347
|
+
explicitTopCommand = null
|
|
705
1348
|
val recycler = observedRecyclerView
|
|
706
1349
|
val listener = observedScrollListener
|
|
707
1350
|
if (recycler != null) {
|
|
708
|
-
|
|
1351
|
+
offsetDiag("recycler-detach-save-before-record=$recordOffset", recycler, observedRecyclerKey ?: currentPageKey())
|
|
1352
|
+
if (recordOffset && !nativeSmoothHeaderScrollEnabled && transitionPageOffsets == null) observedRecyclerKey?.let {
|
|
1353
|
+
if (it !in pendingPageRestores) {
|
|
1354
|
+
offsetDiag("cache-write-recycler-detach-new=${recycler.computeVerticalScrollOffset()}", recycler, it)
|
|
1355
|
+
pageOffsets[it] = recycler.computeVerticalScrollOffset()
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
offsetDiag("recycler-detach-save-after", recycler, observedRecyclerKey ?: currentPageKey())
|
|
709
1359
|
publishConsumedOffset(recycler, remove = true)
|
|
1360
|
+
publishScrollCommandListener(recycler, remove = true)
|
|
710
1361
|
}
|
|
711
1362
|
if (recycler != null && listener != null) recycler.removeOnScrollListener(listener)
|
|
712
1363
|
if (recycler != null) {
|
|
@@ -716,28 +1367,38 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
716
1367
|
)
|
|
717
1368
|
}
|
|
718
1369
|
observedRecyclerView = null
|
|
1370
|
+
observedRecyclerKey = null
|
|
719
1371
|
observedScrollListener = null
|
|
720
1372
|
}
|
|
721
1373
|
|
|
722
1374
|
private fun attachNativeScrollerObserver(scroller: ScrollView) {
|
|
723
|
-
if (observedNativeScroller === scroller) {
|
|
1375
|
+
if (observedNativeScroller === scroller && observedNativeScrollerKey == currentPageKey()) {
|
|
724
1376
|
applyNativeScrollerInsets(scroller, selectedPage)
|
|
725
1377
|
return
|
|
726
1378
|
}
|
|
727
1379
|
|
|
728
1380
|
detachPrimaryScrollObserver()
|
|
1381
|
+
offsetDiag("scrollview-attach-before", scroller)
|
|
729
1382
|
observedNativeScroller = scroller
|
|
1383
|
+
observedNativeScrollerKey = currentPageKey()
|
|
730
1384
|
applyNativeScrollerInsets(scroller, selectedPage)
|
|
1385
|
+
val key = currentPageKey()
|
|
731
1386
|
val listener = object : ViewTreeObserver.OnScrollChangedListener {
|
|
732
1387
|
override fun onScrollChanged() {
|
|
733
1388
|
if (observedNativeScrollerListener !== this) {
|
|
734
1389
|
removeScrollChangedListener(null, scroller, this)
|
|
735
1390
|
return
|
|
736
1391
|
}
|
|
1392
|
+
if (key != currentPageKey() || !isAttachedToWindow ||
|
|
1393
|
+
transitionHeaderOffsets != null || syntheticPageRestore || key in pendingPageRestores || key in alignedBodyKeys) return
|
|
1394
|
+
if (nativeSmoothHeaderScrollEnabled && nestedScrollingParentHelper.nestedScrollAxes == 0 && key !in commandBodyKeys) return
|
|
737
1395
|
val contentOffset = scroller.scrollY
|
|
738
|
-
pageOffsets[
|
|
739
|
-
|
|
1396
|
+
if (pageOffsets[key] != contentOffset) offsetDiag("cache-write-scrollview-observer-new=$contentOffset", scroller, key)
|
|
1397
|
+
pageOffsets[key] = contentOffset
|
|
1398
|
+
if (contentOffset > 0 && headerOffsetPx < headerHeightPx && !matchesPagerTop(scroller)) {
|
|
1399
|
+
offsetDiag("scrollview-positive-collapse-before", scroller)
|
|
740
1400
|
setHeaderOffset(headerHeightPx)
|
|
1401
|
+
offsetDiag("scrollview-positive-collapse-after", scroller)
|
|
741
1402
|
}
|
|
742
1403
|
}
|
|
743
1404
|
}
|
|
@@ -752,9 +1413,17 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
752
1413
|
}
|
|
753
1414
|
|
|
754
1415
|
private fun detachNativeScrollerObserver(recordOffset: Boolean) {
|
|
1416
|
+
cancelPagerTop()
|
|
755
1417
|
val scroller = observedNativeScroller
|
|
756
1418
|
val listener = observedNativeScrollerListener
|
|
757
|
-
|
|
1419
|
+
offsetDiag("scrollview-detach-save-before-record=$recordOffset", scroller, observedNativeScrollerKey ?: currentPageKey())
|
|
1420
|
+
if (scroller != null && recordOffset && !nativeSmoothHeaderScrollEnabled && transitionPageOffsets == null) observedNativeScrollerKey?.let {
|
|
1421
|
+
if (it !in pendingPageRestores) {
|
|
1422
|
+
offsetDiag("cache-write-scrollview-detach-new=${scroller.scrollY}", scroller, it)
|
|
1423
|
+
pageOffsets[it] = scroller.scrollY
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
offsetDiag("scrollview-detach-save-after", scroller, observedNativeScrollerKey ?: currentPageKey())
|
|
758
1427
|
if (scroller != null && listener != null) {
|
|
759
1428
|
removeScrollChangedListener(observedNativeScrollerTreeObserver, scroller, listener)
|
|
760
1429
|
}
|
|
@@ -765,11 +1434,17 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
765
1434
|
)
|
|
766
1435
|
}
|
|
767
1436
|
observedNativeScroller = null
|
|
1437
|
+
observedNativeScrollerKey = null
|
|
768
1438
|
observedNativeScrollerListener = null
|
|
769
1439
|
observedNativeScrollerTreeObserver = null
|
|
770
1440
|
}
|
|
771
1441
|
|
|
772
1442
|
private fun detachPrimaryScrollObserver(recordOffset: Boolean = true) {
|
|
1443
|
+
cancelHeaderSettledCheck()
|
|
1444
|
+
headerCommandOwner = null
|
|
1445
|
+
headerSettledGeometry = null
|
|
1446
|
+
headerSettledWasBusy = true
|
|
1447
|
+
publishHeaderSettledState(false)
|
|
773
1448
|
detachOwnedScrollerObserver(recordOffset)
|
|
774
1449
|
detachRecyclerObserver(recordOffset)
|
|
775
1450
|
detachNativeScrollerObserver(recordOffset)
|
|
@@ -793,9 +1468,24 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
793
1468
|
val key = pageKey(pageIndex)
|
|
794
1469
|
if (ownedScrollerKeys[scroller] == key) return
|
|
795
1470
|
ownedScrollerKeys[scroller] = key
|
|
1471
|
+
val restoreRequest = registerPageRestore(key, scroller, pageIndex)
|
|
1472
|
+
if (nativeSmoothHeaderScrollEnabled) ownedScrollerRestoreRequests[key] = WeakReference(scroller) to null
|
|
1473
|
+
val generation = attachmentGeneration
|
|
1474
|
+
offsetDiag("owned-enqueue", scroller, key, pageIndex, restoreRequest)
|
|
796
1475
|
postForCurrentAttachment {
|
|
797
|
-
|
|
798
|
-
|
|
1476
|
+
offsetDiag("owned-execute", scroller, key, pageIndex, restoreRequest)
|
|
1477
|
+
if (isCurrentPageRestore(key, restoreRequest) && ownedScrollerKeys[scroller] == key && generation == attachmentGeneration &&
|
|
1478
|
+
scroller.isAttachedToWindow && (!nativeSmoothHeaderScrollEnabled ||
|
|
1479
|
+
(key in pendingPageRestores && ownedScrollerRestoreRequests[key]?.first?.get() === scroller))) {
|
|
1480
|
+
offsetDiag("owned-restore-before", scroller, key, pageIndex, restoreRequest)
|
|
1481
|
+
val saved = restorationBodyOffset(key)
|
|
1482
|
+
scroller.host.restoreScrollOffset(saved)
|
|
1483
|
+
if (nativeSmoothHeaderScrollEnabled) {
|
|
1484
|
+
ownedScrollerRestoreRequests[key] = WeakReference(scroller) to generation
|
|
1485
|
+
recordOwnedScrollerOffset(scroller, key)
|
|
1486
|
+
}
|
|
1487
|
+
offsetDiag("owned-restore-after-saved=$saved", scroller, key, pageIndex, restoreRequest)
|
|
1488
|
+
offsetDiagAfterDraw(scroller, key, pageIndex)
|
|
799
1489
|
}
|
|
800
1490
|
}
|
|
801
1491
|
}
|
|
@@ -803,7 +1493,10 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
803
1493
|
private fun restoreOwnedScrollerInsets(scroller: NativeScrollerViewport) {
|
|
804
1494
|
scroller.stopMotion()
|
|
805
1495
|
scroller.host.cancelPendingScrollRestore()
|
|
806
|
-
ownedScrollerKeys.remove(scroller)
|
|
1496
|
+
ownedScrollerKeys.remove(scroller)?.let { key ->
|
|
1497
|
+
clearPageRestoreForView(key, scroller)
|
|
1498
|
+
if (ownedScrollerRestoreRequests[key]?.first?.get() === scroller) ownedScrollerRestoreRequests.remove(key)
|
|
1499
|
+
}
|
|
807
1500
|
publishConsumedOffset(scroller, remove = true)
|
|
808
1501
|
scroller.host.setPagerInset(this, null)
|
|
809
1502
|
}
|
|
@@ -811,9 +1504,11 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
811
1504
|
private fun attachOwnedScrollerObserver(scroller: NativeScrollerViewport) {
|
|
812
1505
|
if (observedOwnedScroller === scroller && observedOwnedScrollerKey == currentPageKey()) {
|
|
813
1506
|
applyOwnedScrollerInsets(scroller, selectedPage)
|
|
1507
|
+
if (nativeSmoothHeaderScrollEnabled) recordOwnedScrollerOffset(scroller, currentPageKey())
|
|
814
1508
|
return
|
|
815
1509
|
}
|
|
816
1510
|
detachPrimaryScrollObserver()
|
|
1511
|
+
offsetDiag("owned-attach-before", scroller)
|
|
817
1512
|
observedOwnedScroller = scroller
|
|
818
1513
|
observedOwnedScrollerKey = currentPageKey()
|
|
819
1514
|
val key = currentPageKey()
|
|
@@ -824,9 +1519,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
824
1519
|
removeScrollChangedListener(null, scroller, this)
|
|
825
1520
|
return
|
|
826
1521
|
}
|
|
827
|
-
|
|
828
|
-
pageOffsets[key] = contentOffset
|
|
829
|
-
if (contentOffset > 0 && headerOffsetPx < headerHeightPx) setHeaderOffset(headerHeightPx)
|
|
1522
|
+
recordOwnedScrollerOffset(scroller, key)
|
|
830
1523
|
}
|
|
831
1524
|
}
|
|
832
1525
|
observedOwnedScrollerListener = listener
|
|
@@ -835,11 +1528,41 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
835
1528
|
}
|
|
836
1529
|
}
|
|
837
1530
|
|
|
1531
|
+
private fun recordOwnedScrollerOffset(scroller: NativeScrollerViewport, key: String) {
|
|
1532
|
+
if (ownedScrollerKeys[scroller] != key || !isAttachedToWindow || !scroller.isAttachedToWindow) return
|
|
1533
|
+
if (key in pendingPageRestores) {
|
|
1534
|
+
// A false native pending flag before our posted request executes is not cancellation.
|
|
1535
|
+
val request = ownedScrollerRestoreRequests[key]
|
|
1536
|
+
if (request == null || request.first.get() !== scroller || request.second != attachmentGeneration ||
|
|
1537
|
+
scroller.width <= 0 || scroller.height <= 0 || scroller.host.hasPendingScrollRestore()) return
|
|
1538
|
+
clearPageRestoreForView(key, scroller)
|
|
1539
|
+
}
|
|
1540
|
+
if (observedOwnedScroller !== scroller || observedOwnedScrollerKey != key ||
|
|
1541
|
+
key != currentPageKey() || transitionHeaderOffsets != null || syntheticPageRestore || key in alignedBodyKeys) return
|
|
1542
|
+
if (nativeSmoothHeaderScrollEnabled && !scroller.isReadingMotionActive() && key !in commandBodyKeys) return
|
|
1543
|
+
val contentOffset = scroller.scrollY
|
|
1544
|
+
if (pageOffsets[key] != contentOffset) offsetDiag("cache-write-owned-observer-new=$contentOffset", scroller, key)
|
|
1545
|
+
pageOffsets[key] = contentOffset
|
|
1546
|
+
if (contentOffset > 0 && headerOffsetPx < headerHeightPx && !matchesPagerTop(scroller)) {
|
|
1547
|
+
offsetDiag("owned-positive-collapse-before", scroller, key)
|
|
1548
|
+
setHeaderOffset(headerHeightPx)
|
|
1549
|
+
offsetDiag("owned-positive-collapse-after", scroller, key)
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
|
|
838
1553
|
private fun detachOwnedScrollerObserver(recordOffset: Boolean) {
|
|
1554
|
+
cancelPagerTop()
|
|
839
1555
|
val scroller = observedOwnedScroller ?: return
|
|
840
1556
|
scroller.stopMotion()
|
|
841
1557
|
publishConsumedOffset(scroller, remove = true)
|
|
842
|
-
|
|
1558
|
+
offsetDiag("owned-detach-save-before-record=$recordOffset", scroller, observedOwnedScrollerKey ?: currentPageKey())
|
|
1559
|
+
if (recordOffset && !nativeSmoothHeaderScrollEnabled && transitionPageOffsets == null) observedOwnedScrollerKey?.let {
|
|
1560
|
+
if (it !in pendingPageRestores) {
|
|
1561
|
+
offsetDiag("cache-write-owned-detach-new=${scroller.scrollY}", scroller, it)
|
|
1562
|
+
pageOffsets[it] = scroller.scrollY
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
offsetDiag("owned-detach-save-after", scroller, observedOwnedScrollerKey ?: currentPageKey())
|
|
843
1566
|
observedOwnedScrollerListener?.let {
|
|
844
1567
|
removeScrollChangedListener(observedOwnedScrollerTreeObserver, scroller, it)
|
|
845
1568
|
}
|
|
@@ -876,6 +1599,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
876
1599
|
val anchorOffset = if (first != RecyclerView.NO_POSITION) {
|
|
877
1600
|
manager?.findViewByPosition(first)?.let { manager.getDecoratedTop(it) - recycler.paddingTop }
|
|
878
1601
|
} else null
|
|
1602
|
+
offsetDiag("recycler-inset-before-nextTop=$top-anchorPosition=$first-anchorOffset=$anchorOffset", recycler, pageKey(pageIndex), pageIndex)
|
|
879
1603
|
recycler.setPadding(original.left, top, original.right, bottom)
|
|
880
1604
|
if (anchorOffset != null) manager?.scrollToPositionWithOffset(first, anchorOffset)
|
|
881
1605
|
// React Native can stop this nested requestLayout at its React-owned parent.
|
|
@@ -889,21 +1613,67 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
889
1613
|
recycler.layout(recycler.left, recycler.top, recycler.right, recycler.bottom)
|
|
890
1614
|
}
|
|
891
1615
|
}
|
|
1616
|
+
if (original.appliedTopInset != topInset) offsetDiag("recycler-inset-after-old=${original.appliedTopInset}-new=$topInset", recycler, pageKey(pageIndex), pageIndex)
|
|
892
1617
|
original.appliedTopInset = topInset
|
|
893
1618
|
val key = pageKey(pageIndex)
|
|
1619
|
+
val geometry = Triple(recycler.computeVerticalScrollRange(), recycler.computeVerticalScrollExtent(), recycler.childCount)
|
|
1620
|
+
deferredRecyclerGeometry[recycler]?.let { deferred ->
|
|
1621
|
+
if (geometry != deferred) {
|
|
1622
|
+
restoredRecyclerKeys.remove(recycler)
|
|
1623
|
+
deferredRecyclerGeometry.remove(recycler)
|
|
1624
|
+
if (nativeSmoothHeaderScrollEnabled && headerOffsetPx == headerHeightPx) {
|
|
1625
|
+
displayBodyOverrides.remove(key)
|
|
1626
|
+
displayRecyclerAnchors.remove(key)
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
894
1630
|
if (restoredRecyclerKeys[recycler] != key) {
|
|
895
1631
|
restoredRecyclerKeys[recycler] = key
|
|
1632
|
+
val restoreRequest = registerPageRestore(key, recycler, pageIndex)
|
|
896
1633
|
val generation = attachmentGeneration
|
|
1634
|
+
offsetDiag("recycler-enqueue", recycler, key, pageIndex, restoreRequest)
|
|
1635
|
+
if (nativeSmoothHeaderScrollEnabled && recycler.height > 0 && recycler.childCount > 0) {
|
|
1636
|
+
// Consume an available reading anchor before this pre-draw reaches the first frame.
|
|
1637
|
+
restoreRecyclerAnchor(recycler, key, pageIndex, restoreRequest)
|
|
1638
|
+
}
|
|
897
1639
|
recycler.post {
|
|
1640
|
+
offsetDiag("recycler-execute", recycler, key, pageIndex, restoreRequest)
|
|
1641
|
+
if (!isCurrentPageRestore(key, restoreRequest)) {
|
|
1642
|
+
offsetDiag("recycler-post-skip-cancelled", recycler, key, pageIndex, restoreRequest)
|
|
1643
|
+
return@post
|
|
1644
|
+
}
|
|
898
1645
|
if (!isAttachedToWindow || generation != attachmentGeneration ||
|
|
899
1646
|
restoredRecyclerKeys[recycler] != key ||
|
|
900
1647
|
originalRecyclerPadding[recycler] !== original) {
|
|
901
1648
|
if (restoredRecyclerKeys[recycler] == key) restoredRecyclerKeys.remove(recycler)
|
|
1649
|
+
clearPageRestoreForView(key, recycler, restoreRequest)
|
|
902
1650
|
return@post
|
|
903
1651
|
}
|
|
904
|
-
val saved =
|
|
1652
|
+
val saved = restorationBodyOffset(key)
|
|
905
1653
|
val current = recycler.computeVerticalScrollOffset()
|
|
906
|
-
|
|
1654
|
+
offsetDiag("recycler-restore-before-saved=$saved-current=${current}-requestedDelta=${saved - current}", recycler, key, pageIndex, restoreRequest)
|
|
1655
|
+
if (nativeSmoothHeaderScrollEnabled && (recycler.height <= 0 || recycler.childCount == 0)) {
|
|
1656
|
+
restoredRecyclerKeys.remove(recycler)
|
|
1657
|
+
if (pageRestoreRequests[key] === restoreRequest) pageRestoreRequests.remove(key)
|
|
1658
|
+
return@post
|
|
1659
|
+
}
|
|
1660
|
+
if (nativeSmoothHeaderScrollEnabled) {
|
|
1661
|
+
if (!restoreRecyclerAnchor(recycler, key, pageIndex, restoreRequest)) {
|
|
1662
|
+
if (isInLayout || recycler.isInLayout || recycler.isComputingLayout) {
|
|
1663
|
+
restoredRecyclerKeys.remove(recycler)
|
|
1664
|
+
if (pageRestoreRequests[key] === restoreRequest) pageRestoreRequests.remove(key)
|
|
1665
|
+
return@post
|
|
1666
|
+
}
|
|
1667
|
+
deferredRecyclerGeometry[recycler] = Triple(recycler.computeVerticalScrollRange(), recycler.computeVerticalScrollExtent(), recycler.childCount)
|
|
1668
|
+
offsetDiag("recycler-anchor-pending-saved=$saved", recycler, key, pageIndex, restoreRequest)
|
|
1669
|
+
return@post
|
|
1670
|
+
}
|
|
1671
|
+
} else if (saved != current) recycler.scrollBy(0, saved - current)
|
|
1672
|
+
deferredRecyclerGeometry.remove(recycler)
|
|
1673
|
+
if (nativeSmoothHeaderScrollEnabled) clearPageRestoreForView(key, recycler, restoreRequest)
|
|
1674
|
+
else pendingPageRestores.remove(key)
|
|
1675
|
+
offsetDiag("recycler-restore-after-saved=$saved", recycler, key, pageIndex, restoreRequest)
|
|
1676
|
+
offsetDiagAfterDraw(recycler, key, pageIndex)
|
|
907
1677
|
}
|
|
908
1678
|
}
|
|
909
1679
|
}
|
|
@@ -949,16 +1719,35 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
949
1719
|
val key = pageKey(pageIndex)
|
|
950
1720
|
if (restoredScrollerKeys[scroller] != key) {
|
|
951
1721
|
restoredScrollerKeys[scroller] = key
|
|
1722
|
+
val restoreRequest = registerPageRestore(key, scroller, pageIndex)
|
|
952
1723
|
val generation = attachmentGeneration
|
|
1724
|
+
offsetDiag("scrollview-enqueue", scroller, key, pageIndex, restoreRequest)
|
|
953
1725
|
scroller.post {
|
|
1726
|
+
offsetDiag("scrollview-execute", scroller, key, pageIndex, restoreRequest)
|
|
1727
|
+
if (!isCurrentPageRestore(key, restoreRequest)) {
|
|
1728
|
+
offsetDiag("scrollview-post-skip-cancelled", scroller, key, pageIndex, restoreRequest)
|
|
1729
|
+
return@post
|
|
1730
|
+
}
|
|
954
1731
|
if (!isAttachedToWindow || generation != attachmentGeneration ||
|
|
955
1732
|
restoredScrollerKeys[scroller] != key ||
|
|
956
1733
|
originalScrollerPadding[scroller] !== original) {
|
|
957
1734
|
if (restoredScrollerKeys[scroller] == key) restoredScrollerKeys.remove(scroller)
|
|
1735
|
+
clearPageRestoreForView(key, scroller, restoreRequest)
|
|
1736
|
+
return@post
|
|
1737
|
+
}
|
|
1738
|
+
val saved = restorationBodyOffset(key)
|
|
1739
|
+
offsetDiag("scrollview-restore-before-saved=$saved-current=${scroller.scrollY}-requestedDelta=${saved - scroller.scrollY}", scroller, key, pageIndex, restoreRequest)
|
|
1740
|
+
if (nativeSmoothHeaderScrollEnabled &&
|
|
1741
|
+
(scroller.height <= 0 || (scroller.getChildAt(0)?.height ?: 0) <= 0)) {
|
|
1742
|
+
restoredScrollerKeys.remove(scroller)
|
|
1743
|
+
if (pageRestoreRequests[key] === restoreRequest) pageRestoreRequests.remove(key)
|
|
958
1744
|
return@post
|
|
959
1745
|
}
|
|
960
|
-
val saved = pageOffsets[key] ?: 0
|
|
961
1746
|
if (scroller.scrollY != saved) scroller.scrollTo(scroller.scrollX, saved)
|
|
1747
|
+
if (nativeSmoothHeaderScrollEnabled) clearPageRestoreForView(key, scroller, restoreRequest)
|
|
1748
|
+
else pendingPageRestores.remove(key)
|
|
1749
|
+
offsetDiag("scrollview-restore-after-saved=$saved", scroller, key, pageIndex, restoreRequest)
|
|
1750
|
+
offsetDiagAfterDraw(scroller, key, pageIndex)
|
|
962
1751
|
}
|
|
963
1752
|
}
|
|
964
1753
|
}
|
|
@@ -1017,7 +1806,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1017
1806
|
publishConsumedOffset(recycler, remove = true)
|
|
1018
1807
|
val original = originalRecyclerPadding.remove(recycler) ?: return
|
|
1019
1808
|
updateRecyclerPaddingOwnership(recycler, original)
|
|
1020
|
-
restoredRecyclerKeys.remove(recycler)
|
|
1809
|
+
restoredRecyclerKeys.remove(recycler)?.let { clearPageRestoreForView(it, recycler) }
|
|
1021
1810
|
recycler.clipToPadding = original.clipToPadding
|
|
1022
1811
|
recycler.setPadding(original.left, original.top, original.right, original.bottom)
|
|
1023
1812
|
}
|
|
@@ -1025,7 +1814,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1025
1814
|
private fun restoreNativeScrollerInsets(scroller: ScrollView) {
|
|
1026
1815
|
val original = originalScrollerPadding.remove(scroller) ?: return
|
|
1027
1816
|
updateScrollerPaddingOwnership(scroller, original)
|
|
1028
|
-
restoredScrollerKeys.remove(scroller)
|
|
1817
|
+
restoredScrollerKeys.remove(scroller)?.let { clearPageRestoreForView(it, scroller) }
|
|
1029
1818
|
if (scroller is ReactScrollView) {
|
|
1030
1819
|
val state = scroller.reactScrollViewScrollState
|
|
1031
1820
|
scroller.setScrollAwayPaddingEnabledUnstable(
|
|
@@ -1399,11 +2188,22 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1399
2188
|
override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {
|
|
1400
2189
|
if (type == ViewCompat.TYPE_TOUCH) pendingRefreshPull = null
|
|
1401
2190
|
nestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes, type)
|
|
2191
|
+
updateHeaderSettledState(forceBusy = true)
|
|
2192
|
+
if (type == ViewCompat.TYPE_TOUCH) {
|
|
2193
|
+
val request = pageRestoreRequests[currentPageKey()]
|
|
2194
|
+
offsetDiag("touch-cancel-before", target, request = request)
|
|
2195
|
+
observedRecyclerView?.let { deferredRecyclerGeometry.remove(it) }
|
|
2196
|
+
pageRestoreRequests.remove(currentPageKey())
|
|
2197
|
+
pendingPageRestores.remove(currentPageKey())
|
|
2198
|
+
ownedScrollerRestoreRequests.remove(currentPageKey())
|
|
2199
|
+
offsetDiag("touch-cancel-after", target, request = request)
|
|
2200
|
+
}
|
|
1402
2201
|
}
|
|
1403
2202
|
|
|
1404
2203
|
override fun onStopNestedScroll(target: View, type: Int) {
|
|
1405
2204
|
if (pendingRefreshPull?.get() === target) pendingRefreshPull = null
|
|
1406
2205
|
nestedScrollingParentHelper.onStopNestedScroll(target, type)
|
|
2206
|
+
updateHeaderSettledState()
|
|
1407
2207
|
onHeaderOffsetChanged?.invoke()
|
|
1408
2208
|
}
|
|
1409
2209
|
|
|
@@ -1415,6 +2215,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1415
2215
|
// val used = min(dy, headerHeightPx - headerOffsetPx)
|
|
1416
2216
|
if (dy > 0 && headerOffsetPx < maximumOffset) {
|
|
1417
2217
|
val used = min(dy, maximumOffset - headerOffsetPx)
|
|
2218
|
+
if (type == ViewCompat.TYPE_TOUCH && used != 0) takeOverBodyForVerticalConsumption()
|
|
1418
2219
|
setHeaderOffset(headerOffsetPx + used)
|
|
1419
2220
|
consumed[1] += used
|
|
1420
2221
|
} else if (dy < 0 && headerOffsetPx > 0) {
|
|
@@ -1426,6 +2227,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1426
2227
|
}
|
|
1427
2228
|
if (!canScrollUp) {
|
|
1428
2229
|
val used = max(dy, -headerOffsetPx)
|
|
2230
|
+
if (type == ViewCompat.TYPE_TOUCH && used != 0) takeOverBodyForVerticalConsumption()
|
|
1429
2231
|
setHeaderOffset(headerOffsetPx + used)
|
|
1430
2232
|
consumed[1] += used
|
|
1431
2233
|
}
|
|
@@ -1441,8 +2243,11 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1441
2243
|
type: Int,
|
|
1442
2244
|
consumed: IntArray,
|
|
1443
2245
|
) {
|
|
1444
|
-
if (!isCurrentPageTarget(target)
|
|
2246
|
+
if (!isCurrentPageTarget(target)) return
|
|
2247
|
+
if (type == ViewCompat.TYPE_TOUCH && dyConsumed != 0) takeOverBodyForVerticalConsumption()
|
|
2248
|
+
if (dyUnconsumed >= 0) return
|
|
1445
2249
|
val used = max(dyUnconsumed, -headerOffsetPx)
|
|
2250
|
+
if (type == ViewCompat.TYPE_TOUCH && used != 0) takeOverBodyForVerticalConsumption()
|
|
1446
2251
|
setHeaderOffset(headerOffsetPx + used)
|
|
1447
2252
|
consumed[1] += used
|
|
1448
2253
|
if (type == ViewCompat.TYPE_TOUCH && dyUnconsumed < used &&
|
|
@@ -1492,6 +2297,13 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1492
2297
|
restoredRecyclerKeys.clear()
|
|
1493
2298
|
restoredScrollerKeys.clear()
|
|
1494
2299
|
viewTreeObserver.addOnPreDrawListener(pageContentLayoutListener)
|
|
2300
|
+
resumeTransitionSource?.let { source ->
|
|
2301
|
+
resumeTransitionSource = null
|
|
2302
|
+
if (source in 0 until adapter.itemCount) {
|
|
2303
|
+
pager.setCurrentItem(source, false)
|
|
2304
|
+
selectPage(source)
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
1495
2307
|
log(
|
|
1496
2308
|
"host-attach generation=$attachmentGeneration inner=$selectedPage " +
|
|
1497
2309
|
"outer=${outerPagerIndex()} nativePages=${adapter.itemCount}",
|
|
@@ -1499,6 +2311,9 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1499
2311
|
}
|
|
1500
2312
|
|
|
1501
2313
|
override fun onDetachedFromWindow() {
|
|
2314
|
+
cancelHeaderSettledCheck()
|
|
2315
|
+
headerCommandOwner = null
|
|
2316
|
+
publishHeaderSettledState(false)
|
|
1502
2317
|
pendingRefreshPull = null
|
|
1503
2318
|
releaseRefreshForeground()
|
|
1504
2319
|
attachmentGeneration += 1
|
|
@@ -1511,6 +2326,26 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1511
2326
|
nestedNativeGestureStarted = false
|
|
1512
2327
|
viewTreeObserver.removeOnPreDrawListener(pageContentLayoutListener)
|
|
1513
2328
|
detachPrimaryScrollObserver()
|
|
2329
|
+
pageRestoreRequests.clear()
|
|
2330
|
+
pendingPageRestores.clear()
|
|
2331
|
+
commandBodyKeys.clear()
|
|
2332
|
+
explicitTopCommand = null
|
|
2333
|
+
suspendedBodyKeys.clear()
|
|
2334
|
+
alignedBodyKeys.clear()
|
|
2335
|
+
transitionAlignedViews.clear()
|
|
2336
|
+
deferredRecyclerGeometry.clear()
|
|
2337
|
+
displayBodyOverrides.clear()
|
|
2338
|
+
displayRecyclerAnchors.clear()
|
|
2339
|
+
ownedScrollerRestoreRequests.clear()
|
|
2340
|
+
transitionHeaderOffsets?.let { headers ->
|
|
2341
|
+
resumeTransitionSource = transitionSourcePage
|
|
2342
|
+
syntheticPageRestore = true
|
|
2343
|
+
setHeaderOffset(headers[pageKey(transitionSourcePage)] ?: 0)
|
|
2344
|
+
syntheticPageRestore = false
|
|
2345
|
+
}
|
|
2346
|
+
transitionHeaderOffsets = null
|
|
2347
|
+
transitionPageOffsets = null
|
|
2348
|
+
transitionTargetPage = null
|
|
1514
2349
|
// Snapshot live keys before restoration removes their padding records.
|
|
1515
2350
|
for (recycler in originalRecyclerPadding.keys.toTypedArray()) {
|
|
1516
2351
|
restoreRecyclerInsets(recycler)
|