Migrate ConversationList to paging library and apply abstractions to conversation.

This commit is contained in:
Alex Hart
2020-06-12 15:11:36 -03:00
committed by Greyson Parrelli
parent ce940235b0
commit 49f75d7036
25 changed files with 1212 additions and 622 deletions

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}