1package com.bumptech.glide;
2
3import android.widget.AbsListView;
4
5import com.bumptech.glide.request.animation.GlideAnimation;
6import com.bumptech.glide.request.target.BaseTarget;
7import com.bumptech.glide.request.target.SizeReadyCallback;
8import com.bumptech.glide.util.Util;
9
10import java.util.List;
11import java.util.Queue;
12
13/**
14 * Loads a few resources ahead in the direction of scrolling in any {@link AbsListView} so that images are in the memory
15 * cache just before the corresponding view in created in the list. Gives the appearance of an infinitely large image
16 * cache, depending on scrolling speed, cpu speed, and cache size.
17 *
18 * <p>
19 *  Must be set using {@link AbsListView#setOnScrollListener(android.widget.AbsListView.OnScrollListener)}, or have its
20 *  corresponding methods called from another {@link android.widget.AbsListView.OnScrollListener} to function.
21 * </p>
22 *
23 * @param <T> The type of the model being displayed in the list.
24 */
25public abstract class ListPreloader<T> implements AbsListView.OnScrollListener {
26    private final int maxPreload;
27    private final PreloadTargetQueue preloadTargetQueue;
28
29    private int lastEnd;
30    private int lastStart;
31    private int lastFirstVisible;
32    private int totalItemCount;
33
34    private boolean isIncreasing = true;
35
36    /**
37     * Constructor for the preloader.
38     *
39     * @param maxPreload The maximum number of items in the list to load ahead (corresponds to adapter positions).
40     */
41    public ListPreloader(int maxPreload) {
42        this.maxPreload = maxPreload;
43        preloadTargetQueue = new PreloadTargetQueue(maxPreload + 1);
44    }
45
46    @Override
47    public void onScrollStateChanged(AbsListView absListView, int scrollState) {
48        // Do nothing.
49    }
50
51    @Override
52    public void onScroll(AbsListView absListView, int firstVisible, int visibleCount, int totalCount) {
53        totalItemCount = totalCount;
54        if (firstVisible > lastFirstVisible) {
55            preload(firstVisible + visibleCount, true);
56        } else if (firstVisible < lastFirstVisible) {
57            preload(firstVisible, false);
58        }
59        lastFirstVisible = firstVisible;
60    }
61
62    /**
63     * Returns the dimensions of the view in the list where the resources will be displayed.
64     * <p>
65     *     Note - The dimensions returned here must precisely match those of the view in the list.
66     * </p>
67     * @param item A model
68     * @return The dimensions of the view where the item will be displayed
69     */
70    protected abstract int[] getDimensions(T item);
71
72    /**
73     * Returns a list of all models that need to be loaded for the list to display adapter items {@code start - end}.
74     * A list of any size can be returned so there can be multiple models per adapter position.
75     *
76     * @param start The smallest adapter position. Will be {@code >= 0 && < adapter.getCount() && <= end}
77     * @param end The largest adapter position. Will be {@code >= 0 && < adapter.getCount && >= start}
78     * @return A non null list of all models for adapter positions between {@code start} and {@code end}.
79     */
80    protected abstract List<T> getItems(int start, int end);
81
82    /**
83     * Returns a glide request for a given item. Must exactly match the request used to load the resource in the list.
84     * The target and context will be provided by the preloader.
85     *
86     * @param item The model to load.
87     * @return A non null {@link BitmapRequestBuilder}.
88     */
89    @SuppressWarnings("rawtypes")
90    protected abstract GenericRequestBuilder getRequestBuilder(T item);
91
92    private void preload(int start, boolean increasing) {
93        if (isIncreasing != increasing) {
94            isIncreasing = increasing;
95            cancelAll();
96        }
97        preload(start, start + (increasing ? maxPreload : -maxPreload));
98    }
99
100    private void preload(int from, int to) {
101        int start;
102        int end;
103        if (from < to) {
104            start = Math.max(lastEnd, from);
105            end = to;
106        } else {
107            start = to;
108            end = Math.min(lastStart, from);
109        }
110        end = Math.min(totalItemCount, end);
111        start = Math.min(totalItemCount, Math.max(0, start));
112        List<T> items = getItems(start, end);
113
114        if (from < to) {
115            // Increasing
116            final int numItems = items.size();
117            for (int i = 0; i < numItems; i++) {
118                preloadItem(items, i);
119            }
120        } else {
121            // Decreasing
122            for (int i = items.size() - 1; i >= 0; i--) {
123                preloadItem(items, i);
124            }
125        }
126
127        lastStart = start;
128        lastEnd = end;
129    }
130
131    @SuppressWarnings("unchecked")
132    private void preloadItem(List<T> items, int position) {
133        final T item = items.get(position);
134        final int[] dimensions = getDimensions(item);
135        if (dimensions != null) {
136            getRequestBuilder(item).into(preloadTargetQueue.next(dimensions[0], dimensions[1]));
137        }
138    }
139
140    private void cancelAll() {
141        for (int i = 0; i < maxPreload; i++) {
142            Glide.clear(preloadTargetQueue.next(0, 0));
143        }
144    }
145
146    private static final class PreloadTargetQueue {
147        private final Queue<PreloadTarget> queue;
148
149        public PreloadTargetQueue(int size) {
150            queue = Util.createQueue(size);
151
152            for (int i = 0; i < size; i++) {
153                queue.offer(new PreloadTarget());
154            }
155        }
156
157        public PreloadTarget next(int width, int height) {
158            final PreloadTarget result = queue.poll();
159            queue.offer(result);
160            result.photoWidth = width;
161            result.photoHeight = height;
162            return result;
163        }
164    }
165
166    private static class PreloadTarget extends BaseTarget<Object> {
167        private int photoHeight;
168        private int photoWidth;
169
170        @Override
171        public void onResourceReady(Object resource, GlideAnimation<? super Object> glideAnimation) {
172            // Do nothing.
173        }
174
175        @Override
176        public void getSize(SizeReadyCallback cb) {
177            cb.onSizeReady(photoWidth, photoHeight);
178        }
179    }
180}
181