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