/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.support.v7.util; import android.support.annotation.UiThread; import android.support.annotation.WorkerThread; import android.util.Log; import android.util.SparseBooleanArray; import android.util.SparseIntArray; /** * A utility class that supports asynchronous content loading. *
* It can be used to load Cursor data in chunks without querying the Cursor on the UI Thread while * keeping UI and cache synchronous for better user experience. *
* It loads the data on a background thread and keeps only a limited number of fixed sized * chunks in memory at all times. *
* {@link AsyncListUtil} queries the currently visible range through {@link ViewCallback}, * loads the required data items in the background through {@link DataCallback}, and notifies a * {@link ViewCallback} when the data is loaded. It may load some extra items for smoother * scrolling. *
* Note that this class uses a single thread to load the data, so it suitable to load data from * secondary storage such as disk, but not from network. *
* This class is designed to work with {@link android.support.v7.widget.RecyclerView}, but it does
* not depend on it and can be used with other list views.
*
*/
public class AsyncListUtil
* Identifies the data items that have not been loaded yet and initiates loading them in the
* background. Should be called from the view's scroll listener (such as
* {@link android.support.v7.widget.RecyclerView.OnScrollListener#onScrolled}).
*/
public void onRangeChanged() {
if (isRefreshPending()) {
return; // Will update range will the refresh result arrives.
}
updateRange();
mAllowScrollHints = true;
}
/**
* Forces reloading the data.
*
* Discards all the cached data and reloads all required data items for the currently visible
* range. To be called when the data item count and/or contents has changed.
*/
public void refresh() {
mMissingPositions.clear();
mBackgroundProxy.refresh(++mRequestedGeneration);
}
/**
* Returns the data item at the given position or
* If this method has been called for a specific position and returned
* This is the number returned by a recent call to
* {@link DataCallback#refreshData()}.
*
* @return Number of items.
*/
public int getItemCount() {
return mItemCount;
}
void updateRange() {
mViewCallback.getItemRangeInto(mTmpRange);
if (mTmpRange[0] > mTmpRange[1] || mTmpRange[0] < 0) {
return;
}
if (mTmpRange[1] >= mItemCount) {
// Invalid range may arrive soon after the refresh.
return;
}
if (!mAllowScrollHints) {
mScrollHint = ViewCallback.HINT_SCROLL_NONE;
} else if (mTmpRange[0] > mPrevRange[1] || mPrevRange[0] > mTmpRange[1]) {
// Ranges do not intersect, long leap not a scroll.
mScrollHint = ViewCallback.HINT_SCROLL_NONE;
} else if (mTmpRange[0] < mPrevRange[0]) {
mScrollHint = ViewCallback.HINT_SCROLL_DESC;
} else if (mTmpRange[0] > mPrevRange[0]) {
mScrollHint = ViewCallback.HINT_SCROLL_ASC;
}
mPrevRange[0] = mTmpRange[0];
mPrevRange[1] = mTmpRange[1];
mViewCallback.extendRangeInto(mTmpRange, mTmpRangeExtended, mScrollHint);
mTmpRangeExtended[0] = Math.min(mTmpRange[0], Math.max(mTmpRangeExtended[0], 0));
mTmpRangeExtended[1] =
Math.max(mTmpRange[1], Math.min(mTmpRangeExtended[1], mItemCount - 1));
mBackgroundProxy.updateRange(mTmpRange[0], mTmpRange[1],
mTmpRangeExtended[0], mTmpRangeExtended[1], mScrollHint);
}
private final ThreadUtil.MainThreadCallback
* All methods are called on the background thread.
*/
public static abstract class DataCallback
* If the data is being accessed through {@link android.database.Cursor} this is where
* the new cursor should be created.
*
* @return Data item count.
*/
@WorkerThread
public abstract int refreshData();
/**
* Fill the given tile.
*
*
* The provided tile might be a recycled tile, in which case it will already have objects.
* It is suggested to re-use these objects if possible in your use case.
*
* @param startPosition The start position in the list.
* @param itemCount The data item count.
* @param data The data item array to fill into. Should not be accessed beyond
*
* The actual number of cached tiles will be the maximum of this value and the number of
* tiles that is required to cover the range returned by
* {@link ViewCallback#extendRangeInto(int[], int[], int)}.
*
* For example, if this method returns 10, and the most
* recent call to {@link ViewCallback#extendRangeInto(int[], int[], int)} returned
* {100, 179}, and the tile size is 5, then the maximum number of cached tiles will be 16.
*
* However, if the tile size is 20, then the maximum number of cached tiles will be 10.
*
* The default implementation returns 10.
*
* @return Maximum cache size.
*/
@WorkerThread
public int getMaxCachedTiles() {
return 10;
}
}
/**
* The callback that links {@link AsyncListUtil} with the list view.
*
*
* All methods are called on the main thread.
*/
public static abstract class ViewCallback {
/**
* No scroll direction hint available.
*/
public static final int HINT_SCROLL_NONE = 0;
/**
* Scrolling in descending order (from higher to lower positions in the order of the backing
* storage).
*/
public static final int HINT_SCROLL_DESC = 1;
/**
* Scrolling in ascending order (from lower to higher positions in the order of the backing
* storage).
*/
public static final int HINT_SCROLL_ASC = 2;
/**
* Compute the range of visible item positions.
*
* outRange[0] is the position of the first visible item (in the order of the backing
* storage).
*
* outRange[1] is the position of the last visible item (in the order of the backing
* storage).
*
* Negative positions and positions greater or equal to {@link #getItemCount} are invalid.
* If the returned range contains invalid positions it is ignored (no item will be loaded).
*
* @param outRange The visible item range.
*/
@UiThread
public abstract void getItemRangeInto(int[] outRange);
/**
* Compute a wider range of items that will be loaded for smoother scrolling.
*
*
* If there is no scroll hint, the default implementation extends the visible range by half
* its length in both directions. If there is a scroll hint, the range is extended by
* its full length in the scroll direction, and by half in the other direction.
*
* For example, if
* However, if null
if it has not been loaded
* yet.
*
* null
, then
* {@link ViewCallback#onItemLoaded(int)} will be called when it finally loads. Note that if
* this position stays outside of the cached item range (as defined by
* {@link ViewCallback#extendRangeInto} method), then the callback will never be called for
* this position.
*
* @param position Item position.
*
* @return The data item at the given position or null
if it has not been loaded
* yet.
*/
public T getItem(int position) {
if (position < 0 || position >= mItemCount) {
throw new IndexOutOfBoundsException(position + " is not within 0 and " + mItemCount);
}
T item = mTileList.getItemAt(position);
if (item == null && !isRefreshPending()) {
mMissingPositions.put(position, 0);
}
return item;
}
/**
* Returns the number of items in the data set.
*
* itemCount
.
*/
@WorkerThread
public abstract void fillData(T[] data, int startPosition, int itemCount);
/**
* Recycle the objects created in {@link #fillData} if necessary.
*
*
* @param data Array of data items. Should not be accessed beyond itemCount
.
* @param itemCount The data item count.
*/
@WorkerThread
public void recycleData(T[] data, int itemCount) {
}
/**
* Returns tile cache size limit (in tiles).
*
* range
is {100, 200}
and scrollHint
* is {@link #HINT_SCROLL_ASC}, then outRange
will be {50, 300}
.
* scrollHint
is {@link #HINT_SCROLL_NONE}, then
* outRange
will be {50, 250}
*
* @param range Visible item range.
* @param outRange Extended range.
* @param scrollHint The scroll direction hint.
*/
@UiThread
public void extendRangeInto(int[] range, int[] outRange, int scrollHint) {
final int fullRange = range[1] - range[0] + 1;
final int halfRange = fullRange / 2;
outRange[0] = range[0] - (scrollHint == HINT_SCROLL_DESC ? fullRange : halfRange);
outRange[1] = range[1] + (scrollHint == HINT_SCROLL_ASC ? fullRange : halfRange);
}
/**
* Called when the entire data set has changed.
*/
@UiThread
public abstract void onDataRefresh();
/**
* Called when an item at the given position is loaded.
* @param position Item position.
*/
@UiThread
public abstract void onItemLoaded(int position);
}
}