Android AsyncListUtil是Android官方提供的专为列表这样的数据更新加载提供的异步加载组件。基于AsyncListUtil组件,可以轻易实现常见的RecyclerView分页加载技术。AsyncListUtil技术涉及的细节比较繁复,因此我将分别写若干篇文章,分点、分解AsyncListUtil技术。
先给出一个可运行的例子,MainActivity.java:
package zhangphil.app; import android.graphics.Color; import android.os.Bundle; import android.os.SystemClock; import android.support.v7.app.AppCompatActivity; import android.support.v7.util.AsyncListUtil; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.TextView; public class MainActivity extends AppCompatActivity { private String TAG = "调试"; private final int NULL = -1; private RecyclerView mRecyclerView; private AsyncListUtil mAsyncListUtil; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mRecyclerView = findViewById(R.id.recycler_view); LinearLayoutManager mLayoutManager = new LinearLayoutManager(this); mLayoutManager.setOrientation(LinearLayout.VERTICAL); mRecyclerView.setLayoutManager(mLayoutManager); RecyclerView.Adapter mAdapter = new MyAdapter(); mRecyclerView.setAdapter(mAdapter); MyDataCallback mDataCallback = new MyDataCallback(); MyViewCallback mViewCallback = new MyViewCallback(); mAsyncListUtil = new AsyncListUtil(String.class, 20, mDataCallback, mViewCallback); mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); Log.d(TAG, "onRangeChanged"); mAsyncListUtil.onRangeChanged(); } }); findViewById(R.id.button).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Log.d(TAG, "refresh"); mAsyncListUtil.refresh(); } }); } private class MyDataCallback extends AsyncListUtil.DataCallback<String> { @Override public int refreshData() { //更新数据的元素个数。 //假设预先设定更新若干条。 int count = Integer.MAX_VALUE; Log.d(TAG, "refreshData:" + count); return count; } /** * 在这里完成数据加载的耗时任务。 * * @param data * @param startPosition * @param itemCount */ @Override public void fillData(String[] data, int startPosition, int itemCount) { Log.d(TAG, "fillData:" + startPosition + "," + itemCount); for (int i = 0; i < itemCount; i++) { data[i] = String.valueOf(System.currentTimeMillis()); //模拟耗时任务,故意休眠一定时延。 SystemClock.sleep(100); } } } private class MyViewCallback extends AsyncListUtil.ViewCallback { /** * @param outRange */ @Override public void getItemRangeInto(int[] outRange) { getOutRange(outRange); /** * 如果当前的RecyclerView为空,主动为用户加载数据. * 假设预先加载若干条数据 * */ if (outRange[0] == NULL && outRange[1] == NULL) { Log.d(TAG, "当前RecyclerView为空!"); outRange[0] = 0; outRange[1] = 9; } Log.d(TAG, "getItemRangeInto,当前可见position: " + outRange[0] + " ~ " + outRange[1]); } @Override public void onDataRefresh() { int[] outRange = new int[2]; getOutRange(outRange); mRecyclerView.getAdapter().notifyItemRangeChanged(outRange[0], outRange[1] - outRange[0] + 1); Log.d(TAG, "onDataRefresh:"+outRange[0]+","+outRange[1]); } @Override public void onItemLoaded(int position) { mRecyclerView.getAdapter().notifyItemChanged(position); Log.d(TAG, "onItemLoaded:" + position); } } private void getOutRange(int[] outRange){ outRange[0] = ((LinearLayoutManager) mRecyclerView.getLayoutManager()).findFirstVisibleItemPosition(); outRange[1] = ((LinearLayoutManager) mRecyclerView.getLayoutManager()).findLastVisibleItemPosition(); } private class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> { public MyAdapter() { super(); } @Override public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) { View view = LayoutInflater.from(getApplicationContext()).inflate(android.R.layout.simple_list_item_2, null); ViewHolder holder = new ViewHolder(view); return holder; } @Override public void onBindViewHolder(ViewHolder viewHolder, int i) { viewHolder.text1.setText(String.valueOf(i)); String s = String.valueOf(mAsyncListUtil.getItem(i)); if (TextUtils.equals(s, "null")) { s = "加载中..."; } viewHolder.text2.setText(s); } @Override public int getItemCount() { return mAsyncListUtil.getItemCount(); } public class ViewHolder extends RecyclerView.ViewHolder { public TextView text1; public TextView text2; public ViewHolder(View itemView) { super(itemView); text1 = itemView.findViewById(android.R.id.text1); text1.setTextColor(Color.RED); text2 = itemView.findViewById(android.R.id.text2); text2.setTextColor(Color.BLUE); } } } }
MainActivity所需布局文件:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="更新" /> <android.support.v7.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>
(一)new AsyncListUtil之后Android自动就会启动初次刷新加载。
原因在AsyncListUtil构造函数里面,已经调用refresh方法启动刷新,见AsyncListUtil构造函数源代码:
/** * Creates an AsyncListUtil. * * @param klass Class of the data item. * @param tileSize Number of item per chunk loaded at once. * @param dataCallback Data access callback. * @param viewCallback Callback for querying visible item range and update notifications. */ public AsyncListUtil(Class<T> klass, int tileSize, DataCallback<T> dataCallback, ViewCallback viewCallback) { mTClass = klass; mTileSize = tileSize; mDataCallback = dataCallback; mViewCallback = viewCallback; mTileList = new TileList<T>(mTileSize); ThreadUtil<T> threadUtil = new MessageThreadUtil<T>(); mMainThreadProxy = threadUtil.getMainThreadProxy(mMainThreadCallback); mBackgroundProxy = threadUtil.getBackgroundProxy(mBackgroundCallback); refresh(); }
当代码启动后logcat输出:
11-22 14:41:18.313 32764-447/zhangphil.app D/调试: refreshData:2147483647 11-22 14:41:18.336 32764-32764/zhangphil.app D/调试: onDataRefresh:-1,-1 11-22 14:41:18.336 32764-32764/zhangphil.app D/调试: 当前RecyclerView为空! 11-22 14:41:18.336 32764-32764/zhangphil.app D/调试: getItemRangeInto,当前可见position: 0 ~ 9 11-22 14:41:18.337 32764-449/zhangphil.app D/调试: fillData:0,20 11-22 14:41:20.350 32764-32764/zhangphil.app D/调试: onItemLoaded:0 11-22 14:41:20.351 32764-32764/zhangphil.app D/调试: onItemLoaded:1 11-22 14:41:20.351 32764-32764/zhangphil.app D/调试: onItemLoaded:2 11-22 14:41:20.352 32764-32764/zhangphil.app D/调试: onItemLoaded:3 11-22 14:41:20.353 32764-32764/zhangphil.app D/调试: onItemLoaded:4 11-22 14:41:20.353 32764-32764/zhangphil.app D/调试: onItemLoaded:5 11-22 14:41:20.353 32764-32764/zhangphil.app D/调试: onItemLoaded:6
(二)在RecyclerView里面的onScrollStateChanged增加onRangeChanged方法,触发AsyncListUtil的关键函数getItemRangeInto。
触发getItemRangeInto的方法有很多种,通常在RecyclerView里面,分页加载常常会由用户的上下翻动RecyclerView触发。因此自然的就想到在RecyclerView的onScrollStateChanged触发AsyncListUtil分页更新加载逻辑。
getItemRangeInto参数outRange维护两个整型元素,前者outRange[0]表示列表顶部可见元素的位置position,后者outRange[1]表示最底部可见元素的position,开发者对这两个值进行计算,通常就是获取当前RecyclerView顶部outRange[0]的FirstVisibleItemPosition,
outRange[1]是LastVisibleItemPosition。当这两个参数赋值后,将直接触发fillData,fillData是AsyncListUtil进行长期耗时后台任务的地方,开发者可以在这里处理自己的后台线程任务。
比如现在手指在屏幕上从下往上翻滚RecyclerView,故意翻到没有数据的地方(position=21 ~ position=28)然后加载出来,logcat输出:
11-22 14:42:35.543 32764-32764/zhangphil.app D/调试: getItemRangeInto,当前可见position: 0 ~ 6 11-22 14:42:36.012 32764-32764/zhangphil.app D/调试: onRangeChanged 11-22 14:42:36.012 32764-32764/zhangphil.app D/调试: getItemRangeInto,当前可见position: 5 ~ 12 11-22 14:42:36.013 32764-1011/zhangphil.app D/调试: fillData:20,20 11-22 14:42:36.844 32764-32764/zhangphil.app D/调试: onRangeChanged 11-22 14:42:36.844 32764-32764/zhangphil.app D/调试: getItemRangeInto,当前可见position: 10 ~ 16 11-22 14:42:37.067 32764-32764/zhangphil.app D/调试: onRangeChanged 11-22 14:42:37.067 32764-32764/zhangphil.app D/调试: getItemRangeInto,当前可见position: 13 ~ 20 11-22 14:42:38.020 32764-32764/zhangphil.app D/调试: onItemLoaded:20 11-22 14:42:38.020 32764-32764/zhangphil.app D/调试: onItemLoaded:21 11-22 14:42:38.020 32764-32764/zhangphil.app D/调试: onItemLoaded:22 11-22 14:42:38.020 32764-32764/zhangphil.app D/调试: onItemLoaded:23 11-22 14:42:38.021 32764-32764/zhangphil.app D/调试: onItemLoaded:24 11-22 14:42:38.021 32764-32764/zhangphil.app D/调试: onItemLoaded:25 11-22 14:42:38.021 32764-32764/zhangphil.app D/调试: onItemLoaded:26 11-22 14:42:38.021 32764-32764/zhangphil.app D/调试: onItemLoaded:27 11-22 14:42:38.021 32764-32764/zhangphil.app D/调试: onItemLoaded:28 11-22 14:42:38.784 32764-32764/zhangphil.app D/调试: onRangeChanged 11-22 14:42:38.784 32764-32764/zhangphil.app D/调试: getItemRangeInto,当前可见position: 21 ~ 28
(三)fillData分页加载。
fillData将实现最终的分页加载,通常开发者在这里把数据从网络/数据库/文件系统把数据读出来。本例fillData每次读取20条数据,原因是在AsyncListUtil构造时候,指定了tileSize=20。tileSize决定每次分页加载的数据量。由此,每一次AsyncListUtil分页加载的startPosition位置依次是:0,20,40,60……
(四)onItemLoaded数据装载成功后回调。
当fillData把数据加载完成后,会主动的加载到getItemRangeInto所限定的第一个到最后一个可见范围内的item,此时在RecyclerView里面用notifyItemChanged更新UI即可。
(五)fillData加载的数据覆盖getItemRangeInto返回的第一个到最后一个可见范围内的RecyclerView列表项目。
比如,如果getItemRangeInto返回的两个position:outRange[0]=0,outRange[1]=9,那么fillData将一如既往的加载第0个位置开始的20条数据。即fillData的设计目的将为把用户可见区域内容的所有项目数据均加载完成,保证用户可见区域内的数据是优先加载的。
随后当用户在上下翻动RecyclerView时候,onRangeChanged 触发getItemRangeInto返回变化的outRange,如果历史的数据已经加载,即便用户翻回去,亦不会重新加载即fillData。
(六)AsyncListUtil的refresh强制刷新。常见的RecyclerView可能需要强制刷新的功能,比如,当用户长期停留而不做任何滑动时候,如果仍然要保证数据最新,那么就要刷新一次获取。AsyncListUtil的refresh为此设计。refresh触发新的一轮由getItemRangeInto决定的、fillData完成的数据更新。但是要注意,这时候的RecyclerView若更新是由refresh触发,需要在onDataRefresh调用RecyclerView的notifyItemRangeChanged更新UI。但是要注意这里面要时刻注意fillData每次加载数据都是分页的按照startPosition:0,20,40,60……这样的刻度,每次取20条。
附录Android官方实现的AsyncListUtil.java源代码:
/* * 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. * <p> * 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. * <p> * It loads the data on a background thread and keeps only a limited number of fixed sized * chunks in memory at all times. * <p> * {@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. * <p> * 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. * <p> * 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<T> { static final String TAG = "AsyncListUtil"; static final boolean DEBUG = false; final Class<T> mTClass; final int mTileSize; final DataCallback<T> mDataCallback; final ViewCallback mViewCallback; final TileList<T> mTileList; final ThreadUtil.MainThreadCallback<T> mMainThreadProxy; final ThreadUtil.BackgroundCallback<T> mBackgroundProxy; final int[] mTmpRange = new int[2]; final int[] mPrevRange = new int[2]; final int[] mTmpRangeExtended = new int[2]; boolean mAllowScrollHints; private int mScrollHint = ViewCallback.HINT_SCROLL_NONE; int mItemCount = 0; int mDisplayedGeneration = 0; int mRequestedGeneration = mDisplayedGeneration; final SparseIntArray mMissingPositions = new SparseIntArray(); void log(String s, Object... args) { Log.d(TAG, "[MAIN] " + String.format(s, args)); } /** * Creates an AsyncListUtil. * * @param klass Class of the data item. * @param tileSize Number of item per chunk loaded at once. * @param dataCallback Data access callback. * @param viewCallback Callback for querying visible item range and update notifications. */ public AsyncListUtil(Class<T> klass, int tileSize, DataCallback<T> dataCallback, ViewCallback viewCallback) { mTClass = klass; mTileSize = tileSize; mDataCallback = dataCallback; mViewCallback = viewCallback; mTileList = new TileList<T>(mTileSize); ThreadUtil<T> threadUtil = new MessageThreadUtil<T>(); mMainThreadProxy = threadUtil.getMainThreadProxy(mMainThreadCallback); mBackgroundProxy = threadUtil.getBackgroundProxy(mBackgroundCallback); refresh(); } private boolean isRefreshPending() { return mRequestedGeneration != mDisplayedGeneration; } /** * Updates the currently visible item range. * * <p> * 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. * <p> * 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 <code>null</code> if it has not been loaded * yet. * * <p> * If this method has been called for a specific position and returned <code>null</code>, 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 <code>null</code> 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. * * <p> * 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<T> mMainThreadCallback = new ThreadUtil.MainThreadCallback<T>() { @Override public void updateItemCount(int generation, int itemCount) { if (DEBUG) { log("updateItemCount: size=%d, gen #%d", itemCount, generation); } if (!isRequestedGeneration(generation)) { return; } mItemCount = itemCount; mViewCallback.onDataRefresh(); mDisplayedGeneration = mRequestedGeneration; recycleAllTiles(); mAllowScrollHints = false; // Will be set to true after a first real scroll. // There will be no scroll event if the size change does not affect the current range. updateRange(); } @Override public void addTile(int generation, TileList.Tile<T> tile) { if (!isRequestedGeneration(generation)) { if (DEBUG) { log("recycling an older generation tile @%d", tile.mStartPosition); } mBackgroundProxy.recycleTile(tile); return; } TileList.Tile<T> duplicate = mTileList.addOrReplace(tile); if (duplicate != null) { Log.e(TAG, "duplicate tile @" + duplicate.mStartPosition); mBackgroundProxy.recycleTile(duplicate); } if (DEBUG) { log("gen #%d, added tile @%d, total tiles: %d", generation, tile.mStartPosition, mTileList.size()); } int endPosition = tile.mStartPosition + tile.mItemCount; int index = 0; while (index < mMissingPositions.size()) { final int position = mMissingPositions.keyAt(index); if (tile.mStartPosition <= position && position < endPosition) { mMissingPositions.removeAt(index); mViewCallback.onItemLoaded(position); } else { index++; } } } @Override public void removeTile(int generation, int position) { if (!isRequestedGeneration(generation)) { return; } TileList.Tile<T> tile = mTileList.removeAtPos(position); if (tile == null) { Log.e(TAG, "tile not found @" + position); return; } if (DEBUG) { log("recycling tile @%d, total tiles: %d", tile.mStartPosition, mTileList.size()); } mBackgroundProxy.recycleTile(tile); } private void recycleAllTiles() { if (DEBUG) { log("recycling all %d tiles", mTileList.size()); } for (int i = 0; i < mTileList.size(); i++) { mBackgroundProxy.recycleTile(mTileList.getAtIndex(i)); } mTileList.clear(); } private boolean isRequestedGeneration(int generation) { return generation == mRequestedGeneration; } }; private final ThreadUtil.BackgroundCallback<T> mBackgroundCallback = new ThreadUtil.BackgroundCallback<T>() { private TileList.Tile<T> mRecycledRoot; final SparseBooleanArray mLoadedTiles = new SparseBooleanArray(); private int mGeneration; private int mItemCount; private int mFirstRequiredTileStart; private int mLastRequiredTileStart; @Override public void refresh(int generation) { mGeneration = generation; mLoadedTiles.clear(); mItemCount = mDataCallback.refreshData(); mMainThreadProxy.updateItemCount(mGeneration, mItemCount); } @Override public void updateRange(int rangeStart, int rangeEnd, int extRangeStart, int extRangeEnd, int scrollHint) { if (DEBUG) { log("updateRange: %d..%d extended to %d..%d, scroll hint: %d", rangeStart, rangeEnd, extRangeStart, extRangeEnd, scrollHint); } if (rangeStart > rangeEnd) { return; } final int firstVisibleTileStart = getTileStart(rangeStart); final int lastVisibleTileStart = getTileStart(rangeEnd); mFirstRequiredTileStart = getTileStart(extRangeStart); mLastRequiredTileStart = getTileStart(extRangeEnd); if (DEBUG) { log("requesting tile range: %d..%d", mFirstRequiredTileStart, mLastRequiredTileStart); } // All pending tile requests are removed by ThreadUtil at this point. // Re-request all required tiles in the most optimal order. if (scrollHint == ViewCallback.HINT_SCROLL_DESC) { requestTiles(mFirstRequiredTileStart, lastVisibleTileStart, scrollHint, true); requestTiles(lastVisibleTileStart + mTileSize, mLastRequiredTileStart, scrollHint, false); } else { requestTiles(firstVisibleTileStart, mLastRequiredTileStart, scrollHint, false); requestTiles(mFirstRequiredTileStart, firstVisibleTileStart - mTileSize, scrollHint, true); } } private int getTileStart(int position) { return position - position % mTileSize; } private void requestTiles(int firstTileStart, int lastTileStart, int scrollHint, boolean backwards) { for (int i = firstTileStart; i <= lastTileStart; i += mTileSize) { int tileStart = backwards ? (lastTileStart + firstTileStart - i) : i; if (DEBUG) { log("requesting tile @%d", tileStart); } mBackgroundProxy.loadTile(tileStart, scrollHint); } } @Override public void loadTile(int position, int scrollHint) { if (isTileLoaded(position)) { if (DEBUG) { log("already loaded tile @%d", position); } return; } TileList.Tile<T> tile = acquireTile(); tile.mStartPosition = position; tile.mItemCount = Math.min(mTileSize, mItemCount - tile.mStartPosition); mDataCallback.fillData(tile.mItems, tile.mStartPosition, tile.mItemCount); flushTileCache(scrollHint); addTile(tile); } @Override public void recycleTile(TileList.Tile<T> tile) { if (DEBUG) { log("recycling tile @%d", tile.mStartPosition); } mDataCallback.recycleData(tile.mItems, tile.mItemCount); tile.mNext = mRecycledRoot; mRecycledRoot = tile; } private TileList.Tile<T> acquireTile() { if (mRecycledRoot != null) { TileList.Tile<T> result = mRecycledRoot; mRecycledRoot = mRecycledRoot.mNext; return result; } return new TileList.Tile<T>(mTClass, mTileSize); } private boolean isTileLoaded(int position) { return mLoadedTiles.get(position); } private void addTile(TileList.Tile<T> tile) { mLoadedTiles.put(tile.mStartPosition, true); mMainThreadProxy.addTile(mGeneration, tile); if (DEBUG) { log("loaded tile @%d, total tiles: %d", tile.mStartPosition, mLoadedTiles.size()); } } private void removeTile(int position) { mLoadedTiles.delete(position); mMainThreadProxy.removeTile(mGeneration, position); if (DEBUG) { log("flushed tile @%d, total tiles: %s", position, mLoadedTiles.size()); } } private void flushTileCache(int scrollHint) { final int cacheSizeLimit = mDataCallback.getMaxCachedTiles(); while (mLoadedTiles.size() >= cacheSizeLimit) { int firstLoadedTileStart = mLoadedTiles.keyAt(0); int lastLoadedTileStart = mLoadedTiles.keyAt(mLoadedTiles.size() - 1); int startMargin = mFirstRequiredTileStart - firstLoadedTileStart; int endMargin = lastLoadedTileStart - mLastRequiredTileStart; if (startMargin > 0 && (startMargin >= endMargin || (scrollHint == ViewCallback.HINT_SCROLL_ASC))) { removeTile(firstLoadedTileStart); } else if (endMargin > 0 && (startMargin < endMargin || (scrollHint == ViewCallback.HINT_SCROLL_DESC))){ removeTile(lastLoadedTileStart); } else { // Could not flush on either side, bail out. return; } } } private void log(String s, Object... args) { Log.d(TAG, "[BKGR] " + String.format(s, args)); } }; /** * The callback that provides data access for {@link AsyncListUtil}. * * <p> * All methods are called on the background thread. */ public static abstract class DataCallback<T> { /** * Refresh the data set and return the new data item count. * * <p> * 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. * * <p> * 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 * <code>itemCount</code>. */ @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 <code>itemCount</code>. * @param itemCount The data item count. */ @WorkerThread public void recycleData(T[] data, int itemCount) { } /** * Returns tile cache size limit (in tiles). * * <p> * 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)}. * <p> * 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. * <p> * However, if the tile size is 20, then the maximum number of cached tiles will be 10. * <p> * 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. * * <p> * 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. * <p> * outRange[0] is the position of the first visible item (in the order of the backing * storage). * <p> * outRange[1] is the position of the last visible item (in the order of the backing * storage). * <p> * 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. * * <p> * 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. * <p> * For example, if <code>range</code> is <code>{100, 200}</code> and <code>scrollHint</code> * is {@link #HINT_SCROLL_ASC}, then <code>outRange</code> will be <code>{50, 300}</code>. * <p> * However, if <code>scrollHint</code> is {@link #HINT_SCROLL_NONE}, then * <code>outRange</code> will be <code>{50, 250}</code> * * @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); } }