Home | History | Annotate | Download | only in widget
      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          */
     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 
     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 
    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 }
    174