mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-20 08:39:22 +01:00
Migrate ConversationList to paging library and apply abstractions to conversation.
This commit is contained in:
committed by
Greyson Parrelli
parent
ce940235b0
commit
49f75d7036
@@ -0,0 +1,33 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class Deferred {
|
||||
|
||||
private Runnable deferred;
|
||||
private boolean isDeferred = true;
|
||||
|
||||
public void defer(@Nullable Runnable deferred) {
|
||||
this.deferred = deferred;
|
||||
executeIfNecessary();
|
||||
}
|
||||
|
||||
public void setDeferred(boolean isDeferred) {
|
||||
this.isDeferred = isDeferred;
|
||||
executeIfNecessary();
|
||||
}
|
||||
|
||||
public boolean isDeferred() {
|
||||
return isDeferred;
|
||||
}
|
||||
|
||||
private void executeIfNecessary() {
|
||||
if (deferred != null && !isDeferred) {
|
||||
Runnable local = deferred;
|
||||
|
||||
deferred = null;
|
||||
|
||||
local.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import androidx.annotation.CheckResult;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Helper class to scroll to the top of a RecyclerView when new data is inserted.
|
||||
* This works for both newly inserted data and moved data. It applies the following rules:
|
||||
*
|
||||
* <ul>
|
||||
* <li>If the user is currently scrolled to some position, then we will not snap.</li>
|
||||
* <li>If the user is currently dragging, then we will not snap.</li>
|
||||
* <li>If the user has requested a scroll position, then we will only snap to that position.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class SnapToTopDataObserver extends RecyclerView.AdapterDataObserver {
|
||||
|
||||
private final RecyclerView recyclerView;
|
||||
private final LinearLayoutManager layoutManager;
|
||||
private final Deferred deferred;
|
||||
private final ScrollRequestValidator scrollRequestValidator;
|
||||
|
||||
public SnapToTopDataObserver(@NonNull RecyclerView recyclerView,
|
||||
@Nullable ScrollRequestValidator scrollRequestValidator)
|
||||
{
|
||||
this.recyclerView = recyclerView;
|
||||
this.layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
|
||||
this.deferred = new Deferred();
|
||||
this.scrollRequestValidator = scrollRequestValidator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests a scroll to a specific position. This call will defer until the position is loaded or
|
||||
* becomes invalid.
|
||||
*
|
||||
* @param position The position to scroll to.
|
||||
*/
|
||||
public void requestScrollPosition(int position) {
|
||||
buildScrollPosition(position).submit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a ScrollRequestBuilder which can be used to customize a particular scroll request with
|
||||
* different callbacks. Don't forget to call `submit()`!
|
||||
*
|
||||
* @param position The position to scroll to.
|
||||
* @return A ScrollRequestBuilder that must be submitted once you are satisfied with it.
|
||||
*/
|
||||
@CheckResult(suggest = "#requestScrollPosition(int)")
|
||||
public ScrollRequestBuilder buildScrollPosition(int position) {
|
||||
return new ScrollRequestBuilder(position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests that instead of snapping to top, we should scroll to a specific position in the adapter.
|
||||
* It is up to the caller to ensure that the adapter will load the appropriate data, either by
|
||||
* invalidating and restarting the page load at the appropriate position or by utilizing
|
||||
* PagedList#loadAround(int).
|
||||
*
|
||||
* @param position The position to scroll to.
|
||||
* @param onPerformScroll Callback allowing the caller to perform the scroll themselves.
|
||||
* @param onScrollRequestComplete Notification that the scroll has completed successfully.
|
||||
* @param onInvalidPosition Notification that the requested position has become invalid.
|
||||
*/
|
||||
private void requestScrollPositionInternal(int position,
|
||||
@NonNull OnPerformScroll onPerformScroll,
|
||||
@NonNull Runnable onScrollRequestComplete,
|
||||
@NonNull Runnable onInvalidPosition)
|
||||
{
|
||||
Objects.requireNonNull(scrollRequestValidator, "Cannot request positions when SnapToTopObserver was initialized without a validator.");
|
||||
|
||||
if (!scrollRequestValidator.isPositionStillValid(position)) {
|
||||
onInvalidPosition.run();
|
||||
} else if (scrollRequestValidator.isItemAtPositionLoaded(position)) {
|
||||
recyclerView.post(() -> {
|
||||
onPerformScroll.onPerformScroll(layoutManager, position);
|
||||
onScrollRequestComplete.run();
|
||||
});
|
||||
} else {
|
||||
deferred.setDeferred(true);
|
||||
deferred.defer(() -> requestScrollPositionInternal(position, onPerformScroll, onScrollRequestComplete, onInvalidPosition));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
|
||||
snapToTopIfNecessary(toPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeInserted(int positionStart, int itemCount) {
|
||||
snapToTopIfNecessary(positionStart);
|
||||
}
|
||||
|
||||
private void snapToTopIfNecessary(int newItemPosition) {
|
||||
if (deferred.isDeferred()) {
|
||||
deferred.setDeferred(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newItemPosition != 0 ||
|
||||
recyclerView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE ||
|
||||
recyclerView.canScrollVertically(layoutManager.getReverseLayout() ? 1 : -1)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (layoutManager.findFirstVisibleItemPosition() == 0) {
|
||||
layoutManager.scrollToPosition(0);
|
||||
}
|
||||
}
|
||||
|
||||
public interface ScrollRequestValidator {
|
||||
/**
|
||||
* This method is responsible for determining whether a given position is still a valid jump target.
|
||||
* @param position The position to validate
|
||||
* @return Whether the position is valid
|
||||
*/
|
||||
boolean isPositionStillValid(int position);
|
||||
|
||||
/**
|
||||
* This method is responsible for checking whether the desired position is available to be jumped to.
|
||||
* In the case of a PagedListAdapter, it is whether getItem returns a non-null value.
|
||||
* @param position The position to check for.
|
||||
* @return Whether or not the data for the given position is loaded.
|
||||
*/
|
||||
boolean isItemAtPositionLoaded(int position);
|
||||
}
|
||||
|
||||
public interface OnPerformScroll {
|
||||
/**
|
||||
* This method is responsible for actually performing the requested scroll. It is always called
|
||||
* immediately before the onScrollRequestComplete callback, and is always run via recyclerView.post(...)
|
||||
* so you don't have to do this yourself.
|
||||
*
|
||||
* By default, SnapToTopDataObserver will utilize layoutManager.scrollToPosition. This lets you modify that
|
||||
* behavior, and also gives you a chance to perform actions just before scrolling occurs.
|
||||
*
|
||||
* @param layoutManager The layoutManager containing your items.
|
||||
* @param position The position to scroll to.
|
||||
*/
|
||||
void onPerformScroll(@NonNull LinearLayoutManager layoutManager, int position);
|
||||
}
|
||||
|
||||
public final class ScrollRequestBuilder {
|
||||
private final int position;
|
||||
|
||||
private OnPerformScroll onPerformScroll = LinearLayoutManager::scrollToPosition;
|
||||
private Runnable onScrollRequestComplete = () -> {};
|
||||
private Runnable onInvalidPosition = () -> {};
|
||||
|
||||
public ScrollRequestBuilder(int position) {
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
@CheckResult
|
||||
public ScrollRequestBuilder withOnPerformScroll(@NonNull OnPerformScroll onPerformScroll) {
|
||||
this.onPerformScroll = onPerformScroll;
|
||||
return this;
|
||||
}
|
||||
|
||||
@CheckResult
|
||||
public ScrollRequestBuilder withOnScrollRequestComplete(@NonNull Runnable onScrollRequestComplete) {
|
||||
this.onScrollRequestComplete = onScrollRequestComplete;
|
||||
return this;
|
||||
}
|
||||
|
||||
@CheckResult
|
||||
public ScrollRequestBuilder withOnInvalidPosition(@NonNull Runnable onInvalidPosition) {
|
||||
this.onInvalidPosition = onInvalidPosition;
|
||||
return this;
|
||||
}
|
||||
|
||||
public void submit() {
|
||||
requestScrollPositionInternal(position, onPerformScroll, onScrollRequestComplete, onInvalidPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.thoughtcrime.securesms.util.paging;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class Invalidator {
|
||||
private Runnable callback;
|
||||
|
||||
public synchronized void invalidate() {
|
||||
if (callback != null) {
|
||||
callback.run();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void observe(@NonNull Runnable callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.thoughtcrime.securesms.util.paging;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SizeFixResult<T> {
|
||||
|
||||
private static final String TAG = Log.tag(SizeFixResult.class);
|
||||
|
||||
final List<T> items;
|
||||
final int total;
|
||||
|
||||
private SizeFixResult(@NonNull List<T> items, int total) {
|
||||
this.items = items;
|
||||
this.total = total;
|
||||
}
|
||||
|
||||
public List<T> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
public int getTotal() {
|
||||
return total;
|
||||
}
|
||||
|
||||
public static @NonNull <T> SizeFixResult<T> ensureMultipleOfPageSize(@NonNull List<T> records,
|
||||
int startPosition,
|
||||
int pageSize,
|
||||
int total)
|
||||
{
|
||||
if (records.size() + startPosition == total || (records.size() != 0 && records.size() % pageSize == 0)) {
|
||||
return new SizeFixResult<>(records, total);
|
||||
}
|
||||
|
||||
if (records.size() < pageSize) {
|
||||
Log.w(TAG, "Hit a miscalculation where we don't have the full dataset, but it's smaller than a page size. records: " + records.size() + ", startPosition: " + startPosition + ", pageSize: " + pageSize + ", total: " + total);
|
||||
return new SizeFixResult<>(records, records.size() + startPosition);
|
||||
}
|
||||
|
||||
Log.w(TAG, "Hit a miscalculation where our data size isn't a multiple of the page size. records: " + records.size() + ", startPosition: " + startPosition + ", pageSize: " + pageSize + ", total: " + total);
|
||||
int overflow = records.size() % pageSize;
|
||||
|
||||
return new SizeFixResult<>(records.subList(0, records.size() - overflow), total);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user