diff --git a/integration/recyclerview/build.gradle.kts b/integration/recyclerview/build.gradle.kts
index d2bc779a08..2a0ef0ef61 100644
--- a/integration/recyclerview/build.gradle.kts
+++ b/integration/recyclerview/build.gradle.kts
@@ -21,6 +21,12 @@ dependencies {
implementation(project(":library"))
compileOnly(libs.androidx.recyclerview)
compileOnly(libs.androidx.fragment)
+
+ testImplementation(libs.androidx.recyclerview)
+ testImplementation(libs.androidx.test.core)
+ testImplementation(libs.junit)
+ testImplementation(libs.robolectric)
+ testImplementation(libs.truth)
}
-apply(from = "${rootProject.projectDir}/scripts/upload.gradle.kts")
\ No newline at end of file
+apply(from = "${rootProject.projectDir}/scripts/upload.gradle.kts")
diff --git a/integration/recyclerview/src/main/java/com/bumptech/glide/integration/recyclerview/RecyclerToListViewScrollListener.java b/integration/recyclerview/src/main/java/com/bumptech/glide/integration/recyclerview/RecyclerToListViewScrollListener.java
index 8039d677ad..dcd4a468a1 100644
--- a/integration/recyclerview/src/main/java/com/bumptech/glide/integration/recyclerview/RecyclerToListViewScrollListener.java
+++ b/integration/recyclerview/src/main/java/com/bumptech/glide/integration/recyclerview/RecyclerToListViewScrollListener.java
@@ -17,12 +17,20 @@
public final class RecyclerToListViewScrollListener extends RecyclerView.OnScrollListener {
public static final int UNKNOWN_SCROLL_STATE = Integer.MIN_VALUE;
private final AbsListView.OnScrollListener scrollListener;
+ private final RecyclerViewPositionProvider positionProvider;
private int lastFirstVisible = -1;
private int lastVisibleCount = -1;
private int lastItemCount = -1;
public RecyclerToListViewScrollListener(@NonNull AbsListView.OnScrollListener scrollListener) {
+ this(scrollListener, new LinearLayoutManagerPositionProvider());
+ }
+
+ public RecyclerToListViewScrollListener(
+ @NonNull AbsListView.OnScrollListener scrollListener,
+ @NonNull RecyclerViewPositionProvider positionProvider) {
this.scrollListener = scrollListener;
+ this.positionProvider = positionProvider;
}
@Override
@@ -47,10 +55,8 @@ public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
- LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
-
- int firstVisible = layoutManager.findFirstVisibleItemPosition();
- int visibleCount = Math.abs(firstVisible - layoutManager.findLastVisibleItemPosition());
+ int firstVisible = positionProvider.getFirstVisiblePosition(recyclerView);
+ int visibleCount = positionProvider.getVisibleItemCount(recyclerView);
int itemCount = recyclerView.getAdapter().getItemCount();
if (firstVisible != lastFirstVisible
@@ -62,4 +68,27 @@ public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
lastItemCount = itemCount;
}
}
+
+ private static final class LinearLayoutManagerPositionProvider
+ implements RecyclerViewPositionProvider {
+ @Override
+ public int getFirstVisiblePosition(@NonNull RecyclerView recyclerView) {
+ return getLayoutManager(recyclerView).findFirstVisibleItemPosition();
+ }
+
+ @Override
+ public int getVisibleItemCount(@NonNull RecyclerView recyclerView) {
+ LinearLayoutManager layoutManager = getLayoutManager(recyclerView);
+ int firstVisible = layoutManager.findFirstVisibleItemPosition();
+ int lastVisible = layoutManager.findLastVisibleItemPosition();
+ if (firstVisible == RecyclerView.NO_POSITION || lastVisible == RecyclerView.NO_POSITION) {
+ return 0;
+ }
+ return Math.abs(lastVisible - firstVisible) + 1;
+ }
+
+ private static LinearLayoutManager getLayoutManager(RecyclerView recyclerView) {
+ return (LinearLayoutManager) recyclerView.getLayoutManager();
+ }
+ }
}
diff --git a/integration/recyclerview/src/main/java/com/bumptech/glide/integration/recyclerview/RecyclerViewPositionProvider.java b/integration/recyclerview/src/main/java/com/bumptech/glide/integration/recyclerview/RecyclerViewPositionProvider.java
new file mode 100644
index 0000000000..1c42fa5e0a
--- /dev/null
+++ b/integration/recyclerview/src/main/java/com/bumptech/glide/integration/recyclerview/RecyclerViewPositionProvider.java
@@ -0,0 +1,17 @@
+package com.bumptech.glide.integration.recyclerview;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * Provides visible adapter position metadata from a {@link RecyclerView}.
+ *
+ *
Implement this interface to use {@link RecyclerViewPreloader} with custom {@link
+ * RecyclerView.LayoutManager} implementations that do not extend {@link
+ * androidx.recyclerview.widget.LinearLayoutManager}.
+ */
+public interface RecyclerViewPositionProvider {
+ int getFirstVisiblePosition(@NonNull RecyclerView recyclerView);
+
+ int getVisibleItemCount(@NonNull RecyclerView recyclerView);
+}
diff --git a/integration/recyclerview/src/main/java/com/bumptech/glide/integration/recyclerview/RecyclerViewPreloader.java b/integration/recyclerview/src/main/java/com/bumptech/glide/integration/recyclerview/RecyclerViewPreloader.java
index d1e0056d48..1d8780e889 100644
--- a/integration/recyclerview/src/main/java/com/bumptech/glide/integration/recyclerview/RecyclerViewPreloader.java
+++ b/integration/recyclerview/src/main/java/com/bumptech/glide/integration/recyclerview/RecyclerViewPreloader.java
@@ -22,8 +22,11 @@
* methods called from another {@link androidx.recyclerview.widget.RecyclerView.OnScrollListener} to
* function.
*
- *
This class only works with {@link androidx.recyclerview.widget.LinearLayoutManager} and
- * subclasses of {@link androidx.recyclerview.widget.LinearLayoutManager}.
+ *
By default this class only works with {@link
+ * androidx.recyclerview.widget.LinearLayoutManager} and subclasses of {@link
+ * androidx.recyclerview.widget.LinearLayoutManager}. To support another {@link
+ * RecyclerView.LayoutManager}, use the constructor that accepts a {@link
+ * RecyclerViewPositionProvider}.
*
* @param The type of the model being displayed in the {@link RecyclerView}.
*/
@@ -94,6 +97,29 @@ public RecyclerViewPreloader(
recyclerScrollListener = new RecyclerToListViewScrollListener(listPreloader);
}
+ /**
+ * Constructor that accepts a {@link RecyclerViewPositionProvider} for custom {@link
+ * RecyclerView.LayoutManager} implementations.
+ *
+ * @param preloadModelProvider Provides models to load and requests capable of loading them.
+ * @param preloadDimensionProvider Provides the dimensions of images to load.
+ * @param maxPreload Maximum number of items to preload.
+ * @param positionProvider Provides visible adapter position metadata.
+ */
+ public RecyclerViewPreloader(
+ @NonNull RequestManager requestManager,
+ @NonNull PreloadModelProvider preloadModelProvider,
+ @NonNull PreloadSizeProvider preloadDimensionProvider,
+ int maxPreload,
+ @NonNull RecyclerViewPositionProvider positionProvider) {
+
+ ListPreloader listPreloader =
+ new ListPreloader<>(
+ requestManager, preloadModelProvider, preloadDimensionProvider, maxPreload);
+ recyclerScrollListener =
+ new RecyclerToListViewScrollListener(listPreloader, positionProvider);
+ }
+
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
recyclerScrollListener.onScrolled(recyclerView, dx, dy);
diff --git a/integration/recyclerview/src/test/java/com/bumptech/glide/integration/recyclerview/RecyclerToListViewScrollListenerTest.java b/integration/recyclerview/src/test/java/com/bumptech/glide/integration/recyclerview/RecyclerToListViewScrollListenerTest.java
new file mode 100644
index 0000000000..041cc3dbcb
--- /dev/null
+++ b/integration/recyclerview/src/test/java/com/bumptech/glide/integration/recyclerview/RecyclerToListViewScrollListenerTest.java
@@ -0,0 +1,139 @@
+package com.bumptech.glide.integration.recyclerview;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.test.core.app.ApplicationProvider;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public final class RecyclerToListViewScrollListenerTest {
+ @Test
+ public void onScrolled_withCustomPositionProvider_forwardsScrollValues() {
+ RecyclerView recyclerView = newRecyclerViewWithAdapter(20);
+ RecordingOnScrollListener scrollListener = new RecordingOnScrollListener();
+ RecyclerToListViewScrollListener listener =
+ new RecyclerToListViewScrollListener(
+ scrollListener, new FixedPositionProvider(7, 3));
+
+ listener.onScrolled(recyclerView, /* dx= */ 0, /* dy= */ 5);
+
+ assertThat(scrollListener.callCount).isEqualTo(1);
+ assertThat(scrollListener.firstVisible).isEqualTo(7);
+ assertThat(scrollListener.visibleCount).isEqualTo(3);
+ assertThat(scrollListener.totalCount).isEqualTo(20);
+ }
+
+ @Test
+ public void onScrolled_withLinearLayoutManager_countsVisibleItems() {
+ Context context = ApplicationProvider.getApplicationContext();
+ RecyclerView recyclerView = newRecyclerViewWithAdapter(20);
+ recyclerView.setLayoutManager(new TestLinearLayoutManager(context, 2, 5));
+ RecordingOnScrollListener scrollListener = new RecordingOnScrollListener();
+ RecyclerToListViewScrollListener listener =
+ new RecyclerToListViewScrollListener(scrollListener);
+
+ listener.onScrolled(recyclerView, /* dx= */ 0, /* dy= */ 5);
+
+ assertThat(scrollListener.firstVisible).isEqualTo(2);
+ assertThat(scrollListener.visibleCount).isEqualTo(4);
+ assertThat(scrollListener.totalCount).isEqualTo(20);
+ }
+
+ private static RecyclerView newRecyclerViewWithAdapter(int itemCount) {
+ Context context = ApplicationProvider.getApplicationContext();
+ RecyclerView recyclerView = new RecyclerView(context);
+ recyclerView.setAdapter(new TestAdapter(itemCount));
+ return recyclerView;
+ }
+
+ private static final class RecordingOnScrollListener implements AbsListView.OnScrollListener {
+ int callCount;
+ int firstVisible;
+ int visibleCount;
+ int totalCount;
+
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {}
+
+ @Override
+ public void onScroll(
+ AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
+ callCount++;
+ firstVisible = firstVisibleItem;
+ visibleCount = visibleItemCount;
+ totalCount = totalItemCount;
+ }
+ }
+
+ private static final class FixedPositionProvider implements RecyclerViewPositionProvider {
+ private final int firstVisible;
+ private final int visibleCount;
+
+ FixedPositionProvider(int firstVisible, int visibleCount) {
+ this.firstVisible = firstVisible;
+ this.visibleCount = visibleCount;
+ }
+
+ @Override
+ public int getFirstVisiblePosition(@NonNull RecyclerView recyclerView) {
+ return firstVisible;
+ }
+
+ @Override
+ public int getVisibleItemCount(@NonNull RecyclerView recyclerView) {
+ return visibleCount;
+ }
+ }
+
+ private static final class TestLinearLayoutManager extends LinearLayoutManager {
+ private final int firstVisible;
+ private final int lastVisible;
+
+ TestLinearLayoutManager(Context context, int firstVisible, int lastVisible) {
+ super(context);
+ this.firstVisible = firstVisible;
+ this.lastVisible = lastVisible;
+ }
+
+ @Override
+ public int findFirstVisibleItemPosition() {
+ return firstVisible;
+ }
+
+ @Override
+ public int findLastVisibleItemPosition() {
+ return lastVisible;
+ }
+ }
+
+ private static final class TestAdapter extends RecyclerView.Adapter {
+ private final int itemCount;
+
+ TestAdapter(int itemCount) {
+ this.itemCount = itemCount;
+ }
+
+ @NonNull
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ return new RecyclerView.ViewHolder(new View(parent.getContext())) {};
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {}
+
+ @Override
+ public int getItemCount() {
+ return itemCount;
+ }
+ }
+}