package com.bumptech.glide; import android.support.annotation.Nullable; import android.widget.AbsListView; import com.bumptech.glide.request.target.BaseTarget; import com.bumptech.glide.request.target.SizeReadyCallback; import com.bumptech.glide.request.transition.Transition; import com.bumptech.glide.util.Synthetic; import com.bumptech.glide.util.Util; import java.util.List; import java.util.Queue; /** * Loads a few resources ahead in the direction of scrolling in any {@link AbsListView} so that * images are in the memory cache just before the corresponding view in created in the list. Gives * the appearance of an infinitely large image cache, depending on scrolling speed, cpu speed, and * cache size. * * <p> Must be put using * {@link AbsListView#setOnScrollListener(android.widget.AbsListView.OnScrollListener)}, or have its * corresponding methods called from another {@link android.widget.AbsListView.OnScrollListener} to * function. </p> * * @param <T> The type of the model being displayed in the list. */ public class ListPreloader<T> implements AbsListView.OnScrollListener { private final int maxPreload; private final PreloadTargetQueue preloadTargetQueue; private final RequestManager requestManager; private final PreloadModelProvider<T> preloadModelProvider; private final PreloadSizeProvider<T> preloadDimensionProvider; private int lastEnd; private int lastStart; private int lastFirstVisible; private int totalItemCount; private boolean isIncreasing = true; /** * An implementation of PreloadModelProvider should provide all the models that should be * preloaded. * * @param <U> The type of the model being preloaded. */ public interface PreloadModelProvider<U> { /** * Returns a non null list of all models that need to be loaded for the list to display adapter * items in positions between {@code start} and {@code end}. * * <p> A list of any size can be returned so there can be multiple models per adapter position. * </p> * * @param position The adapter position. */ List<U> getPreloadItems(int position); /** * Returns a non null {@link RequestBuilder} for a given item. Must exactly match the request * used to load the resource in the list. * * <p> The target and context will be provided by the preloader. </p> * * @param item The model to load. */ RequestBuilder getPreloadRequestBuilder(U item); } /** * An implementation of PreloadSizeProvider should provide the size of the view in the list where * the resources will be displayed. * * @param <T> The type of the model the size should be provided for. */ public interface PreloadSizeProvider<T> { /** * Returns the size of the view in the list where the resources will be displayed in pixels in * the format [x, y], or {@code null} if no size is currently available. * * <p> Note - The dimensions returned here must precisely match those of the view in the list. * </p> * * @param item A model */ @Nullable int[] getPreloadSize(T item, int adapterPosition, int perItemPosition); } /** * Constructor for {@link com.bumptech.glide.ListPreloader} that accepts interfaces for providing * the dimensions of images to preload, the list of models to preload for a given position, and * the request to use to load images. * * @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. */ public ListPreloader(RequestManager requestManager, PreloadModelProvider<T> preloadModelProvider, PreloadSizeProvider<T> preloadDimensionProvider, int maxPreload) { this.requestManager = requestManager; this.preloadModelProvider = preloadModelProvider; this.preloadDimensionProvider = preloadDimensionProvider; this.maxPreload = maxPreload; preloadTargetQueue = new PreloadTargetQueue(maxPreload + 1); } @Override public void onScrollStateChanged(AbsListView absListView, int scrollState) { // Do nothing. } @Override public void onScroll(AbsListView absListView, int firstVisible, int visibleCount, int totalCount) { totalItemCount = totalCount; if (firstVisible > lastFirstVisible) { preload(firstVisible + visibleCount, true); } else if (firstVisible < lastFirstVisible) { preload(firstVisible, false); } lastFirstVisible = firstVisible; } private void preload(int start, boolean increasing) { if (isIncreasing != increasing) { isIncreasing = increasing; cancelAll(); } preload(start, start + (increasing ? maxPreload : -maxPreload)); } private void preload(int from, int to) { int start; int end; if (from < to) { start = Math.max(lastEnd, from); end = to; } else { start = to; end = Math.min(lastStart, from); } end = Math.min(totalItemCount, end); start = Math.min(totalItemCount, Math.max(0, start)); if (from < to) { // Increasing for (int i = start; i < end; i++) { preloadAdapterPosition(this.preloadModelProvider.getPreloadItems(i), i, true); } } else { // Decreasing for (int i = end - 1; i >= start; i--) { preloadAdapterPosition(this.preloadModelProvider.getPreloadItems(i), i, false); } } lastStart = start; lastEnd = end; } private void preloadAdapterPosition(List<T> items, int position, boolean isIncreasing) { final int numItems = items.size(); if (isIncreasing) { for (int i = 0; i < numItems; ++i) { preloadItem(items.get(i), position, i); } } else { for (int i = numItems - 1; i >= 0; --i) { preloadItem(items.get(i), position, i); } } } @SuppressWarnings("unchecked") private void preloadItem(T item, int position, int i) { final int[] dimensions = this.preloadDimensionProvider.getPreloadSize(item, position, i); if (dimensions != null) { RequestBuilder<Object> preloadRequestBuilder = this.preloadModelProvider.getPreloadRequestBuilder(item); preloadRequestBuilder.into(preloadTargetQueue.next(dimensions[0], dimensions[1])); } } private void cancelAll() { for (int i = 0; i < maxPreload; i++) { requestManager.clear(preloadTargetQueue.next(0, 0)); } } private static final class PreloadTargetQueue { private final Queue<PreloadTarget> queue; public PreloadTargetQueue(int size) { queue = Util.createQueue(size); for (int i = 0; i < size; i++) { queue.offer(new PreloadTarget()); } } public PreloadTarget next(int width, int height) { final PreloadTarget result = queue.poll(); queue.offer(result); result.photoWidth = width; result.photoHeight = height; return result; } } private static class PreloadTarget extends BaseTarget<Object> { @Synthetic int photoHeight; @Synthetic int photoWidth; @Synthetic PreloadTarget() { } @Override public void onResourceReady(Object resource, Transition<? super Object> transition) { // Do nothing. } @Override public void getSize(SizeReadyCallback cb) { cb.onSizeReady(photoWidth, photoHeight); } } }