That fuck shit the fascists are using
1package org.tm.archive.util;
2
3import androidx.annotation.CheckResult;
4import androidx.annotation.NonNull;
5import androidx.annotation.Nullable;
6import androidx.recyclerview.widget.LinearLayoutManager;
7import androidx.recyclerview.widget.RecyclerView;
8
9import org.signal.core.util.logging.Log;
10
11import java.util.Objects;
12
13/**
14 * Helper class to scroll to the top of a RecyclerView when new data is inserted.
15 * This works for both newly inserted data and moved data. It applies the following rules:
16 *
17 * <ul>
18 * <li>If the user is currently scrolled to some position, then we will not snap.</li>
19 * <li>If the user is currently dragging, then we will not snap.</li>
20 * <li>If the user has requested a scroll position, then we will only snap to that position.</li>
21 * </ul>
22 */
23public class SnapToTopDataObserver extends RecyclerView.AdapterDataObserver {
24
25 private static final String TAG = Log.tag(SnapToTopDataObserver.class);
26
27 private final RecyclerView recyclerView;
28 private final LinearLayoutManager layoutManager;
29 private final Deferred deferred;
30 private final ScrollRequestValidator scrollRequestValidator;
31 private final ScrollToTop scrollToTop;
32
33 public SnapToTopDataObserver(@NonNull RecyclerView recyclerView) {
34 this(recyclerView, null, null);
35 }
36
37 public SnapToTopDataObserver(@NonNull RecyclerView recyclerView,
38 @Nullable ScrollRequestValidator scrollRequestValidator,
39 @Nullable ScrollToTop scrollToTop)
40 {
41 this.recyclerView = recyclerView;
42 this.layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
43 this.deferred = new Deferred();
44 this.scrollRequestValidator = scrollRequestValidator;
45 this.scrollToTop = scrollToTop == null ? () -> layoutManager.scrollToPosition(0)
46 : scrollToTop;
47 }
48
49 /**
50 * Requests a scroll to a specific position. This call will defer until the position is loaded or
51 * becomes invalid.
52 *
53 * @param position The position to scroll to.
54 */
55 public void requestScrollPosition(int position) {
56 buildScrollPosition(position).submit();
57 }
58
59 /**
60 * Creates a ScrollRequestBuilder which can be used to customize a particular scroll request with
61 * different callbacks. Don't forget to call `submit()`!
62 *
63 * @param position The position to scroll to.
64 * @return A ScrollRequestBuilder that must be submitted once you are satisfied with it.
65 */
66 @CheckResult(suggest = "#requestScrollPosition(int)")
67 public ScrollRequestBuilder buildScrollPosition(int position) {
68 return new ScrollRequestBuilder(position);
69 }
70
71 /**
72 * Requests that instead of snapping to top, we should scroll to a specific position in the adapter.
73 * It is up to the caller to ensure that the adapter will load the appropriate data, either by
74 * invalidating and restarting the page load at the appropriate position or by utilizing
75 * PagedList#loadAround(int).
76 *
77 * @param position The position to scroll to.
78 * @param onPerformScroll Callback allowing the caller to perform the scroll themselves.
79 * @param onScrollRequestComplete Notification that the scroll has completed successfully.
80 * @param onInvalidPosition Notification that the requested position has become invalid.
81 */
82 private void requestScrollPositionInternal(int position,
83 @NonNull OnPerformScroll onPerformScroll,
84 @NonNull Runnable onScrollRequestComplete,
85 @NonNull Runnable onInvalidPosition)
86 {
87 Objects.requireNonNull(scrollRequestValidator, "Cannot request positions when SnapToTopObserver was initialized without a validator.");
88
89 if (!scrollRequestValidator.isPositionStillValid(position)) {
90 Log.d(TAG, "requestScrollPositionInternal(" + position + ") Invalid");
91 onInvalidPosition.run();
92 } else if (scrollRequestValidator.isItemAtPositionLoaded(position)) {
93 Log.d(TAG, "requestScrollPositionInternal(" + position + ") Scrolling");
94 onPerformScroll.onPerformScroll(layoutManager, position);
95 onScrollRequestComplete.run();
96 } else {
97 Log.d(TAG, "requestScrollPositionInternal(" + position + ") Deferring");
98 deferred.setDeferred(true);
99 deferred.defer(() -> {
100 Log.d(TAG, "requestScrollPositionInternal(" + position + ") Executing deferred");
101 requestScrollPositionInternal(position, onPerformScroll, onScrollRequestComplete, onInvalidPosition);
102 });
103 }
104 }
105
106 @Override
107 public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
108 snapToTopIfNecessary(toPosition);
109 }
110
111 @Override
112 public void onItemRangeInserted(int positionStart, int itemCount) {
113 snapToTopIfNecessary(positionStart);
114 }
115
116 private void snapToTopIfNecessary(int newItemPosition) {
117 if (deferred.isDeferred()) {
118 deferred.setDeferred(false);
119 return;
120 }
121
122 if (newItemPosition != 0 ||
123 recyclerView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE ||
124 recyclerView.canScrollVertically(layoutManager.getReverseLayout() ? 1 : -1))
125 {
126 return;
127 }
128
129 if (layoutManager.findFirstVisibleItemPosition() == 0) {
130 Log.d(TAG, "Scrolling to top.");
131 scrollToTop.scrollToTop();
132 }
133 }
134
135 public interface ScrollRequestValidator {
136 /**
137 * This method is responsible for determining whether a given position is still a valid jump target.
138 * @param position The position to validate
139 * @return Whether the position is valid
140 */
141 boolean isPositionStillValid(int position);
142
143 /**
144 * This method is responsible for checking whether the desired position is available to be jumped to.
145 * In the case of a PagedListAdapter, it is whether getItem returns a non-null value.
146 * @param position The position to check for.
147 * @return Whether or not the data for the given position is loaded.
148 */
149 boolean isItemAtPositionLoaded(int position);
150 }
151
152 public interface OnPerformScroll {
153 /**
154 * This method is responsible for actually performing the requested scroll. It is always called
155 * immediately before the onScrollRequestComplete callback, and is always run via recyclerView.post(...)
156 * so you don't have to do this yourself.
157 *
158 * By default, SnapToTopDataObserver will utilize layoutManager.scrollToPosition. This lets you modify that
159 * behavior, and also gives you a chance to perform actions just before scrolling occurs.
160 *
161 * @param layoutManager The layoutManager containing your items.
162 * @param position The position to scroll to.
163 */
164 void onPerformScroll(@NonNull LinearLayoutManager layoutManager, int position);
165 }
166
167 /**
168 * Method Object for scrolling to the top of a view, in case special handling is desired.
169 */
170 public interface ScrollToTop {
171 void scrollToTop();
172 }
173
174 public final class ScrollRequestBuilder {
175 private final int position;
176
177 private OnPerformScroll onPerformScroll = LinearLayoutManager::scrollToPosition;
178 private Runnable onScrollRequestComplete = () -> {};
179 private Runnable onInvalidPosition = () -> {};
180
181 public ScrollRequestBuilder(int position) {
182 this.position = position;
183 }
184
185 @CheckResult
186 public ScrollRequestBuilder withOnPerformScroll(@NonNull OnPerformScroll onPerformScroll) {
187 this.onPerformScroll = onPerformScroll;
188 return this;
189 }
190
191 @CheckResult
192 public ScrollRequestBuilder withOnScrollRequestComplete(@NonNull Runnable onScrollRequestComplete) {
193 this.onScrollRequestComplete = onScrollRequestComplete;
194 return this;
195 }
196
197 @CheckResult
198 public ScrollRequestBuilder withOnInvalidPosition(@NonNull Runnable onInvalidPosition) {
199 this.onInvalidPosition = onInvalidPosition;
200 return this;
201 }
202
203 public void submit() {
204 requestScrollPositionInternal(position, onPerformScroll, onScrollRequestComplete, onInvalidPosition);
205 }
206 }
207}