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