您的位置:首页 > 移动开发 > Android开发

基于Android官方AsyncListUtil优化改进RecyclerView分页加载机制(一)

2017-11-22 14:49 357 查看
基于Android官方AsyncListUtil优化改进RecyclerView分页加载机制(一)

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);
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: