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