That fuck shit the fascists are using
at master 219 lines 7.2 kB view raw
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}