package com.android.camera.widget; import android.widget.AbsListView; import com.android.camera.debug.Log; import java.util.Collections; import java.util.List; import java.util.Queue; import java.util.concurrent.LinkedBlockingQueue; /** * Responsible for controlling preloading logic. Intended usage is for ListViews that * benefit from initiating a load before the row appear on screen. * @param The type of items this class preload. * @param The type of load tokens that can be used to cancel loads for the items this class * preloads. */ public class Preloader implements AbsListView.OnScrollListener { private static final Log.Tag TAG = new Log.Tag("Preloader"); /** * Implemented by the source for items that should be preloaded. */ public interface ItemSource { /** * Returns the objects in the range [startPosition; endPosition). */ public List getItemsInRange(int startPosition, int endPosition); /** * Returns the total number of items in the source. */ public int getCount(); } /** * Responsible for the loading of items. */ public interface ItemLoader { /** * Initiates a load for the specified items and returns a list of 0 or more load tokens that * can be used to cancel the loads for the given items. Should preload the items in the list * order,preloading the 0th item in the list fist. */ public List preloadItems(List items); /** * Cancels all of the loads represented by the given load tokens. */ public void cancelItems(List loadTokens); } private final int mMaxConcurrentPreloads; /** * Keep track of the largest/smallest item we requested (depending on scroll direction) so * we don't preload the same items repeatedly. Without this var, scrolling down we preload * 0-5, then 1-6 etc. Using this we instead preload 0-5, then 5-6, 6-7 etc. */ private int mLastEnd = -1; private int mLastStart; private final int mLoadAheadItems; private ItemSource mItemSource; private ItemLoader mItemLoader; private Queue> mItemLoadTokens = new LinkedBlockingQueue>(); private int mLastVisibleItem; private boolean mScrollingDown = false; public Preloader(int loadAheadItems, ItemSource itemSource, ItemLoader itemLoader) { mItemSource = itemSource; mItemLoader = itemLoader; mLoadAheadItems = loadAheadItems; // Add an additional item so we don't cancel a preload before we start a real load. mMaxConcurrentPreloads = loadAheadItems + 1; } /** * Initiates a pre load. * * @param first The source position to load from * @param increasing The direction we're going in (increasing -> source positions are * increasing -> we're scrolling down the list) */ private void preload(int first, boolean increasing) { final int start; final int end; if (increasing) { start = Math.max(first, mLastEnd); end = Math.min(first + mLoadAheadItems, mItemSource.getCount()); } else { start = Math.max(0, first - mLoadAheadItems); end = Math.min(first, mLastStart); } Log.v(TAG, "preload first=" + first + " increasing=" + increasing + " start=" + start + " end=" + end); mLastEnd = end; mLastStart = start; if (start == 0 && end == 0) { return; } final List items = mItemSource.getItemsInRange(start, end); if (!increasing) { Collections.reverse(items); } registerLoadTokens(mItemLoader.preloadItems(items)); } private void registerLoadTokens(List loadTokens) { mItemLoadTokens.offer(loadTokens); // We pretend that one batch of load tokens corresponds to one item in the list. This isn't // strictly true because we may batch preload multiple items at once when we first start // scrolling in the list or change the direction we're scrolling in. In those cases, we will // have a single large batch of load tokens for multiple items, and then go back to getting // one batch per item as we continue to scroll. This means we may not cancel as many // preloads as we expect when we change direction, but we can at least be sure we won't // cancel preloads for items we still care about. We can't be more precise here because // there is no guarantee that there is a one to one relationship between load tokens // and list items. if (mItemLoadTokens.size() > mMaxConcurrentPreloads) { final List loadTokensToCancel = mItemLoadTokens.poll(); mItemLoader.cancelItems(loadTokensToCancel); } } public void cancelAllLoads() { for (List loadTokens : mItemLoadTokens) { mItemLoader.cancelItems(loadTokens); } mItemLoadTokens.clear(); } @Override public void onScrollStateChanged(AbsListView absListView, int i) { // Do nothing. } @Override public void onScroll(AbsListView absListView, int firstVisible, int visibleItemCount, int totalItemCount) { boolean wasScrollingDown = mScrollingDown; int preloadStart = -1; if (firstVisible > mLastVisibleItem) { // Scrolling list down mScrollingDown = true; preloadStart = firstVisible + visibleItemCount; } else if (firstVisible < mLastVisibleItem) { // Scrolling list Up mScrollingDown = false; preloadStart = firstVisible; } if (wasScrollingDown != mScrollingDown) { // If we've changed directions, we don't care about any of our old preloads, so cancel // all of them. cancelAllLoads(); } // onScroll can be called multiple times with the same arguments, so we only want to preload // if we've actually scrolled at least an item in either direction. if (preloadStart != -1) { preload(preloadStart, mScrollingDown); } mLastVisibleItem = firstVisible; } }