That fuck shit the fascists are using
1package org.tm.archive.components
2
3import android.view.View
4import androidx.annotation.AnyThread
5import androidx.recyclerview.widget.LinearLayoutManager
6import androidx.recyclerview.widget.RecyclerView
7import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
8import io.reactivex.rxjava3.core.Observable
9import io.reactivex.rxjava3.disposables.CompositeDisposable
10import io.reactivex.rxjava3.disposables.Disposable
11import io.reactivex.rxjava3.kotlin.addTo
12import io.reactivex.rxjava3.kotlin.plusAssign
13import io.reactivex.rxjava3.kotlin.subscribeBy
14import io.reactivex.rxjava3.subjects.BehaviorSubject
15import org.signal.core.util.logging.Log
16import org.tm.archive.util.doAfterNextLayout
17import kotlin.math.abs
18import kotlin.math.max
19
20/**
21 * Delegate object to help manage scroll position requests.
22 *
23 * @param recyclerView The recycler view that will be scrolled
24 * @param canJumpToPosition Allows additional checks to see if we can scroll. For example, PagingMappingAdapter#isAvailableAround
25 * @param mapToTruePosition Allows additional offsets to be applied to the position.
26 */
27class ScrollToPositionDelegate private constructor(
28 private val recyclerView: RecyclerView,
29 canJumpToPosition: (Int) -> Boolean,
30 mapToTruePosition: (Int) -> Int,
31 private val disposables: CompositeDisposable
32) : Disposable by disposables {
33 companion object {
34 private val TAG = Log.tag(ScrollToPositionDelegate::class.java)
35 const val NO_POSITION = -1
36 private const val SMOOTH_SCROLL_THRESHOLD = 25
37 private const val SCROLL_ANIMATION_THRESHOLD = 50
38 private val EMPTY = ScrollToPositionRequest(
39 position = NO_POSITION,
40 smooth = true,
41 scrollStrategy = DefaultScrollStrategy
42 )
43 }
44
45 private val listCommitted = BehaviorSubject.create<Long>()
46 private val scrollPositionRequested = BehaviorSubject.createDefault(EMPTY)
47 private val scrollPositionRequests: Observable<ScrollToPositionRequest> = Observable.combineLatest(listCommitted, scrollPositionRequested) { _, b -> b }
48
49 private var markedListCommittedTimestamp: Long = 0L
50
51 constructor(
52 recyclerView: RecyclerView,
53 canJumpToPosition: (Int) -> Boolean = { true },
54 mapToTruePosition: (Int) -> Int = { it }
55 ) : this(recyclerView, canJumpToPosition, mapToTruePosition, CompositeDisposable())
56
57 init {
58 disposables += scrollPositionRequests
59 .observeOn(AndroidSchedulers.mainThread())
60 .filter { it.position >= 0 && canJumpToPosition(it.position) }
61 .map { it.copy(position = mapToTruePosition(it.position)) }
62 .subscribeBy(onNext = { position ->
63 recyclerView.doAfterNextLayout {
64 handleScrollPositionRequest(position, recyclerView)
65 }
66
67 if (!(recyclerView.isLayoutRequested || recyclerView.isInLayout)) {
68 recyclerView.requestLayout()
69 }
70 })
71 }
72
73 /**
74 * Entry point for requesting a specific scroll position.
75 *
76 * @param position The desired position to jump to. -1 to clear the current request.
77 * @param smooth Whether a smooth scroll will be attempted. Only done if we are within a certain distance.
78 * @param scrollStrategy See [ScrollStrategy]
79 */
80 @AnyThread
81 fun requestScrollPosition(
82 position: Int,
83 smooth: Boolean = true,
84 scrollStrategy: ScrollStrategy = DefaultScrollStrategy
85 ) {
86 scrollPositionRequested.onNext(ScrollToPositionRequest(position, smooth, scrollStrategy))
87 }
88
89 /**
90 * Reset the scroll position to 0
91 */
92 @AnyThread
93 fun resetScrollPosition() {
94 requestScrollPosition(0, true)
95 }
96
97 /**
98 * Reset the scroll position to 0 after a list update is committed that occurs later
99 * than the version set by [markListCommittedVersion].
100 */
101 @AnyThread
102 fun resetScrollPositionAfterMarkListVersionSurpassed() {
103 val currentMark = markedListCommittedTimestamp
104 listCommitted
105 .observeOn(AndroidSchedulers.mainThread())
106 .filter { it > currentMark }
107 .firstElement()
108 .subscribeBy {
109 requestScrollPosition(0, true)
110 }
111 .addTo(disposables)
112 }
113
114 /**
115 * This should be called every time a list is submitted to the RecyclerView's adapter.
116 */
117 @AnyThread
118 fun notifyListCommitted() {
119 listCommitted.onNext(System.currentTimeMillis())
120 }
121
122 fun isListCommitted(): Boolean = listCommitted.value != null
123
124 fun markListCommittedVersion() {
125 markedListCommittedTimestamp = listCommitted.value ?: 0L
126 }
127
128 private fun handleScrollPositionRequest(
129 request: ScrollToPositionRequest,
130 recyclerView: RecyclerView
131 ) {
132 requestScrollPosition(NO_POSITION, false)
133
134 val layoutManager = recyclerView.layoutManager as? LinearLayoutManager
135 if (layoutManager == null) {
136 Log.w(TAG, "Layout manager is not set or of an invalid type.")
137 return
138 }
139
140 if (recyclerView.scrollState == RecyclerView.SCROLL_STATE_DRAGGING) {
141 return
142 }
143
144 val position = max(0, request.position)
145
146 request.scrollStrategy.performScroll(
147 recyclerView,
148 layoutManager,
149 position,
150 request.smooth
151 )
152 }
153
154 private data class ScrollToPositionRequest(
155 val position: Int,
156 val smooth: Boolean,
157 val scrollStrategy: ScrollStrategy
158 )
159
160 /**
161 * Jumps to the desired position, pinning it to the "top" of the recycler.
162 */
163 object DefaultScrollStrategy : ScrollStrategy {
164 override fun performScroll(
165 recyclerView: RecyclerView,
166 layoutManager: LinearLayoutManager,
167 position: Int,
168 smooth: Boolean
169 ) {
170 val offset = when {
171 position == 0 -> 0
172 layoutManager.reverseLayout -> recyclerView.height
173 else -> 0
174 }
175
176 Log.d(TAG, "Scrolling to $position")
177
178 if (smooth && position == 0 && layoutManager.findFirstVisibleItemPosition() < SMOOTH_SCROLL_THRESHOLD) {
179 recyclerView.smoothScrollToPosition(position)
180 } else {
181 layoutManager.scrollToPositionWithOffset(position, offset)
182 }
183 }
184 }
185
186 /**
187 * Jumps to the given position but tries to ensure that the contents are completely visible on screen.
188 */
189 object JumpToPositionStrategy : ScrollStrategy {
190 override fun performScroll(recyclerView: RecyclerView, layoutManager: LinearLayoutManager, position: Int, smooth: Boolean) {
191 if (abs(layoutManager.findFirstVisibleItemPosition() - position) < SCROLL_ANIMATION_THRESHOLD) {
192 val child: View? = layoutManager.findViewByPosition(position)
193 if (child == null || !layoutManager.isViewPartiallyVisible(child, true, false)) {
194 layoutManager.scrollToPositionWithOffset(position, recyclerView.height / 4)
195 }
196 } else {
197 layoutManager.scrollToPositionWithOffset(position, recyclerView.height / 4)
198 }
199 }
200 }
201
202 /**
203 * Performs the actual scrolling for a given request.
204 */
205 interface ScrollStrategy {
206 /**
207 * @param recyclerView The recycler view which is to be scrolled
208 * @param layoutManager The typed layout manager attached to the recycler view
209 * @param position The position we should scroll to.
210 * @param smooth Whether or not a smooth scroll should be attempted
211 */
212 fun performScroll(
213 recyclerView: RecyclerView,
214 layoutManager: LinearLayoutManager,
215 position: Int,
216 smooth: Boolean
217 )
218 }
219}