ronnie
2022-10-14 1504bb53e29d3d46222c0b3ea994fc494b48e153
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
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 <T> The type of items this class preload.
 * @param <Y> The type of load tokens that can be used to cancel loads for the items this class
 *           preloads.
 */
public class Preloader<T, Y> 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<T> {
        /**
         * Returns the objects in the range [startPosition; endPosition).
         */
        public List<T> 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<T, Y> {
        /**
         * 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<Y> preloadItems(List<T> items);
 
        /**
         * Cancels all of the loads represented by the given load tokens.
         */
        public void cancelItems(List<Y> 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<T> mItemSource;
    private ItemLoader<T, Y> mItemLoader;
    private Queue<List<Y>> mItemLoadTokens = new LinkedBlockingQueue<List<Y>>();
 
    private int mLastVisibleItem;
    private boolean mScrollingDown = false;
 
    public Preloader(int loadAheadItems, ItemSource<T> itemSource, ItemLoader<T, Y> 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<T> items = mItemSource.getItemsInRange(start, end);
        if (!increasing) {
            Collections.reverse(items);
        }
        registerLoadTokens(mItemLoader.preloadItems(items));
    }
 
    private void registerLoadTokens(List<Y> 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<Y> loadTokensToCancel = mItemLoadTokens.poll();
            mItemLoader.cancelItems(loadTokensToCancel);
        }
    }
 
    public void cancelAllLoads() {
        for (List<Y> 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;
    }
}