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