Sam Judd | de3e9ab | 2014-03-17 13:07:22 -0700 | [diff] [blame] | 1 | package com.android.camera.widget; |
| 2 | |
Sam Judd | de3e9ab | 2014-03-17 13:07:22 -0700 | [diff] [blame] | 3 | import android.widget.AbsListView; |
| 4 | |
Angus Kong | 2bca210 | 2014-03-11 16:27:30 -0700 | [diff] [blame^] | 5 | import com.android.camera.debug.Log; |
| 6 | |
Sam Judd | de3e9ab | 2014-03-17 13:07:22 -0700 | [diff] [blame] | 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 { |
Angus Kong | 2bca210 | 2014-03-11 16:27:30 -0700 | [diff] [blame^] | 20 | private static final Log.Tag TAG = new Log.Tag("Preloader"); |
Sam Judd | de3e9ab | 2014-03-17 13:07:22 -0700 | [diff] [blame] | 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 | */ |
| 29 | public List<T> getItemsInRange(int startPosition, int endPosition); |
| 30 | |
| 31 | /** |
| 32 | * Returns the total number of items in the source. |
| 33 | */ |
| 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 | */ |
| 46 | public List<Y> preloadItems(List<T> items); |
| 47 | |
| 48 | /** |
| 49 | * Cancels all of the loads represented by the given load tokens. |
| 50 | */ |
| 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 | |
| 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 | */ |
| 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 | |
Angus Kong | 2bca210 | 2014-03-11 16:27:30 -0700 | [diff] [blame^] | 98 | Log.v(TAG, "preload first=" + first + " increasing=" + increasing + " start=" + start + |
| 99 | " end=" + end); |
Sam Judd | de3e9ab | 2014-03-17 13:07:22 -0700 | [diff] [blame] | 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 | |
| 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 | |
| 132 | public void cancelAllLoads() { |
| 133 | for (List<Y> loadTokens : mItemLoadTokens) { |
| 134 | mItemLoader.cancelItems(loadTokens); |
| 135 | } |
| 136 | mItemLoadTokens.clear(); |
| 137 | } |
| 138 | |
| 139 | @Override |
| 140 | public void onScrollStateChanged(AbsListView absListView, int i) { |
| 141 | // Do nothing. |
| 142 | } |
| 143 | |
| 144 | @Override |
| 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 | } |