1 package com.android.camera.widget;
2 
3 import android.widget.AbsListView;
4 
5 import com.android.camera.debug.Log;
6 
7 import java.util.Collections;
8 import java.util.List;
9 import java.util.Queue;
10 import java.util.concurrent.LinkedBlockingQueue;
11 
12 /**
13  * Responsible for controlling preloading logic. Intended usage is for ListViews that
14  * benefit from initiating a load before the row appear on screen.
15  * @param <T> The type of items this class preload.
16  * @param <Y> The type of load tokens that can be used to cancel loads for the items this class
17  *           preloads.
18  */
19 public class Preloader<T, Y> implements AbsListView.OnScrollListener {
20     private static final Log.Tag TAG = new Log.Tag("Preloader");
21 
22     /**
23      * Implemented by the source for items that should be preloaded.
24      */
25     public interface ItemSource<T> {
26         /**
27          * Returns the objects in the range [startPosition; endPosition).
28          */
getItemsInRange(int startPosition, int endPosition)29         public List<T> getItemsInRange(int startPosition, int endPosition);
30 
31         /**
32          * Returns the total number of items in the source.
33          */
getCount()34         public int getCount();
35     }
36 
37     /**
38      * Responsible for the loading of items.
39      */
40     public interface ItemLoader<T, Y> {
41         /**
42          * Initiates a load for the specified items and returns a list of 0 or more load tokens that
43          * can be used to cancel the loads for the given items. Should preload the items in the list
44          * order,preloading the 0th item in the list fist.
45          */
preloadItems(List<T> items)46         public List<Y> preloadItems(List<T> items);
47 
48         /**
49          * Cancels all of the loads represented by the given load tokens.
50          */
cancelItems(List<Y> loadTokens)51         public void cancelItems(List<Y> loadTokens);
52     }
53 
54     private final int mMaxConcurrentPreloads;
55 
56     /**
57      * Keep track of the largest/smallest item we requested (depending on scroll direction) so
58      *  we don't preload the same items repeatedly. Without this var, scrolling down we preload
59      *  0-5, then 1-6 etc. Using this we instead preload 0-5, then 5-6, 6-7 etc.
60      */
61     private int mLastEnd = -1;
62     private int mLastStart;
63 
64     private final int mLoadAheadItems;
65     private ItemSource<T> mItemSource;
66     private ItemLoader<T, Y> mItemLoader;
67     private Queue<List<Y>> mItemLoadTokens = new LinkedBlockingQueue<List<Y>>();
68 
69     private int mLastVisibleItem;
70     private boolean mScrollingDown = false;
71 
Preloader(int loadAheadItems, ItemSource<T> itemSource, ItemLoader<T, Y> itemLoader)72     public Preloader(int loadAheadItems, ItemSource<T> itemSource, ItemLoader<T, Y> itemLoader) {
73         mItemSource = itemSource;
74         mItemLoader = itemLoader;
75         mLoadAheadItems = loadAheadItems;
76         // Add an additional item so we don't cancel a preload before we start a real load.
77         mMaxConcurrentPreloads = loadAheadItems + 1;
78     }
79 
80     /**
81      * Initiates a pre load.
82      *
83      * @param first The source position to load from
84      * @param increasing The direction we're going in (increasing -> source positions are
85      *                   increasing -> we're scrolling down the list)
86      */
preload(int first, boolean increasing)87     private void preload(int first, boolean increasing) {
88         final int start;
89         final int end;
90         if (increasing) {
91             start = Math.max(first, mLastEnd);
92             end = Math.min(first + mLoadAheadItems, mItemSource.getCount());
93         } else {
94             start = Math.max(0, first - mLoadAheadItems);
95             end = Math.min(first, mLastStart);
96         }
97 
98         Log.v(TAG, "preload first=" + first + " increasing=" + increasing + " start=" + start +
99                 " end=" + end);
100 
101         mLastEnd = end;
102         mLastStart = start;
103 
104         if (start == 0 && end == 0) {
105             return;
106         }
107 
108         final List<T> items = mItemSource.getItemsInRange(start, end);
109         if (!increasing) {
110             Collections.reverse(items);
111         }
112         registerLoadTokens(mItemLoader.preloadItems(items));
113     }
114 
registerLoadTokens(List<Y> loadTokens)115     private void registerLoadTokens(List<Y> loadTokens) {
116         mItemLoadTokens.offer(loadTokens);
117         // We pretend that one batch of load tokens corresponds to one item in the list. This isn't
118         // strictly true because we may batch preload multiple items at once when we first start
119         // scrolling in the list or change the direction we're scrolling in. In those cases, we will
120         // have a single large batch of load tokens for multiple items, and then go back to getting
121         // one batch per item as we continue to scroll. This means we may not cancel as many
122         // preloads as we expect when we change direction, but we can at least be sure we won't
123         // cancel preloads for items we still care about. We can't be more precise here because
124         // there is no guarantee that there is a one to one relationship between load tokens
125         // and list items.
126         if (mItemLoadTokens.size() > mMaxConcurrentPreloads) {
127             final List<Y> loadTokensToCancel = mItemLoadTokens.poll();
128             mItemLoader.cancelItems(loadTokensToCancel);
129         }
130     }
131 
cancelAllLoads()132     public void cancelAllLoads() {
133         for (List<Y> loadTokens : mItemLoadTokens) {
134             mItemLoader.cancelItems(loadTokens);
135         }
136         mItemLoadTokens.clear();
137     }
138 
139     @Override
onScrollStateChanged(AbsListView absListView, int i)140     public void onScrollStateChanged(AbsListView absListView, int i) {
141         // Do nothing.
142     }
143 
144     @Override
onScroll(AbsListView absListView, int firstVisible, int visibleItemCount, int totalItemCount)145     public void onScroll(AbsListView absListView, int firstVisible, int visibleItemCount,
146             int totalItemCount) {
147         boolean wasScrollingDown = mScrollingDown;
148         int preloadStart = -1;
149         if (firstVisible > mLastVisibleItem) {
150             // Scrolling list down
151             mScrollingDown = true;
152             preloadStart = firstVisible + visibleItemCount;
153         } else if (firstVisible < mLastVisibleItem) {
154             // Scrolling list Up
155             mScrollingDown = false;
156             preloadStart = firstVisible;
157         }
158 
159         if (wasScrollingDown != mScrollingDown) {
160             // If we've changed directions, we don't care about any of our old preloads, so cancel
161             // all of them.
162             cancelAllLoads();
163         }
164 
165         // onScroll can be called multiple times with the same arguments, so we only want to preload
166         // if we've actually scrolled at least an item in either direction.
167         if (preloadStart != -1) {
168             preload(preloadStart, mScrollingDown);
169         }
170 
171         mLastVisibleItem = firstVisible;
172     }
173 }
174