基於Android官方AsyncListUtil優化改進RecyclerView分頁加載機制(一)

基於Android官方AsyncListUtil優化改進RecyclerView分頁加載機制(一)。

Android AsyncListUtil是Android官方提供的專為列表這樣的數據更新加載提供的異步加載組件。基於AsyncListUtil組件,可以輕易實現常見的RecyclerView分頁加載技術。AsyncListUtil技術涉及的細節比較繁復,因此我將分別寫若幹篇文章,分點、分解AsyncListUtil技術。

先給出一個可運行的例子,MainActivity.java:

packagezhangphil.app;

importandroid.graphics.Color;

importandroid.os.Bundle;

importandroid.os.SystemClock;

importandroid.support.v7.app.AppCompatActivity;

importandroid.support.v7.util.AsyncListUtil;

importandroid.support.v7.widget.LinearLayoutManager;

importandroid.support.v7.widget.RecyclerView;

importandroid.text.TextUtils;

importandroid.util.Log;

importandroid.view.LayoutInflater;

importandroid.view.View;

importandroid.view.ViewGroup;

importandroid.widget.LinearLayout;

importandroid.widget.TextView;

 

publicclassMainActivityextendsAppCompatActivity{

privateStringTAG=“調試”;

privatefinalintNULL=-1;

privateRecyclerViewmRecyclerView;

privateAsyncListUtilmAsyncListUtil;

 

@Override

protectedvoidonCreate(BundlesavedInstanceState){

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

mRecyclerView=findViewById(R.id.recycler_view);

LinearLayoutManagermLayoutManager=newLinearLayoutManager(this);

mLayoutManager.setOrientation(LinearLayout.VERTICAL);

mRecyclerView.setLayoutManager(mLayoutManager);

RecyclerView.AdaptermAdapter=newMyAdapter();

mRecyclerView.setAdapter(mAdapter);

 

MyDataCallbackmDataCallback=newMyDataCallback();

MyViewCallbackmViewCallback=newMyViewCallback();

mAsyncListUtil=newAsyncListUtil(String.class,20,mDataCallback,mViewCallback);

 

mRecyclerView.addOnScrollListener(newRecyclerView.OnScrollListener(){

@Override

publicvoidonScrollStateChanged(RecyclerViewrecyclerView,intnewState){

super.onScrollStateChanged(recyclerView,newState);

Log.d(TAG,”onRangeChanged”);

mAsyncListUtil.onRangeChanged();

}

});

 

findViewById(R.id.button).setOnClickListener(newView.OnClickListener(){

@Override

publicvoidonClick(Viewv){

Log.d(TAG,”refresh”);

mAsyncListUtil.refresh();

}

});

}

privateclassMyDataCallbackextendsAsyncListUtil.DataCallback{

@Override

publicintrefreshData(){

//更新數據的元素個數。

//假設預先設定更新若幹條。

intcount=Integer.MAX_VALUE;

Log.d(TAG,”refreshData:”+count);

returncount;

}

 

/**

*在這裡完成數據加載的耗時任務。

*

*@paramdata

*@paramstartPosition

*@paramitemCount

*/

@Override

publicvoidfillData(String[]data,intstartPosition,intitemCount){

Log.d(TAG,”fillData:”+startPosition+“,”+itemCount);

for(inti=0;i<itemcount;i++){

  • data[i]=String.valueOf(System.currentTimeMillis());</itemcount;i++){
  • //模擬耗時任務,故意休眠一定時延。

    SystemClock.sleep(100);

    }

    }

    }

    privateclassMyViewCallbackextendsAsyncListUtil.ViewCallback{

    /**

    *@paramoutRange

    */

    @Override

    publicvoidgetItemRangeInto(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

    publicvoidonDataRefresh(){

    int[]outRange=newint[2];

    getOutRange(outRange);

    mRecyclerView.getAdapter().notifyItemRangeChanged(outRange[0],outRange[1]-outRange[0]+1);

     

    Log.d(TAG,”onDataRefresh:”+outRange[0]+“,”+outRange[1]);

    }

    @Override

    publicvoidonItemLoaded(intposition){

    mRecyclerView.getAdapter().notifyItemChanged(position);

    Log.d(TAG,”onItemLoaded:”+position);

    }

    }

     

    privatevoidgetOutRange(int[]outRange){

    outRange[0]=((LinearLayoutManager)mRecyclerView.getLayoutManager()).findFirstVisibleItemPosition();

    outRange[1]=((LinearLayoutManager)mRecyclerView.getLayoutManager()).findLastVisibleItemPosition();

    }

    privateclassMyAdapterextendsRecyclerView.Adapter{

    publicMyAdapter(){

    super();

    }

     

    @Override

    publicViewHolderonCreateViewHolder(ViewGroupviewGroup,inti){

    Viewview=LayoutInflater.from(getApplicationContext()).inflate(android.R.layout.simple_list_item_2,null);

    ViewHolderholder=newViewHolder(view);

    returnholder;

    }

    @Override

    publicvoidonBindViewHolder(ViewHolderviewHolder,inti){

    viewHolder.text1.setText(String.valueOf(i));

    Strings=String.valueOf(mAsyncListUtil.getItem(i));

    if(TextUtils.equals(s,“null”)){

    s=”加載中…”;

    }

     

    viewHolder.text2.setText(s);

    }

    @Override

    publicintgetItemCount(){

    returnmAsyncListUtil.getItemCount();

    }

     

    publicclassViewHolderextendsRecyclerView.ViewHolder{

    publicTextViewtext1;

    publicTextViewtext2;

     

    publicViewHolder(ViewitemView){

    super(itemView);

    text1=itemView.findViewById(android.R.id.text1);

    text1.setTextColor(Color.RED);

     

    text2=itemView.findViewById(android.R.id.text2);

    text2.setTextColor(Color.BLUE);

    }

    }

    }

    }
     

    android:layout_width=“match_parent”
    android:layout_height=“match_parent”

    android:orientation=“vertical”>

    <button

  • android:id=“@+id/button”</button
  • android:layout_width=“wrap_content”

    android:layout_height=“wrap_content”

    android:text=“更新”/>

    android:id=“@+id/recycler_view”</android.support.v7.widget.recyclerview

  • android:layout_width=“match_parent”

    android:layout_height=“match_parent”/>

    
    

    (一)new AsyncListUtil之後Android自動就會啟動初次刷新加載。
    原因在AsyncListUtil構造函數裡面,已經調用refresh方法啟動刷新,見AsyncListUtil構造函數源代碼:

    /**

    *CreatesanAsyncListUtil.

    *

    *@paramklassClassofthedataitem.

    *@paramtileSizeNumberofitemperchunkloadedatonce.

    *@paramdataCallbackDataaccesscallback.

    *@paramviewCallbackCallbackforqueryingvisibleitemrangeandupdatenotifications.

    */

    publicAsyncListUtil(Classklass,inttileSize,DataCallbackdataCallback,

    ViewCallbackviewCallback){

    mTClass=klass;

    mTileSize=tileSize;

    mDataCallback=dataCallback;

    mViewCallback=viewCallback;

     

    mTileList=newTileList(mTileSize);

     

    ThreadUtilthreadUtil=newMessageThreadUtil();

    mMainThreadProxy=threadUtil.getMainThreadProxy(mMainThreadCallback);

    mBackgroundProxy=threadUtil.getBackgroundProxy(mBackgroundCallback);

     

    refresh();

    }
    當代碼啟動後logcat輸出:

    11-2214:41:18.31332764-447/zhangphil.appD/調試:refreshData:2147483647

    11-2214:41:18.33632764-32764/zhangphil.appD/調試:onDataRefresh:-1,-1

    11-2214:41:18.33632764-32764/zhangphil.appD/調試:當前RecyclerView為空!

    11-2214:41:18.33632764-32764/zhangphil.appD/調試:getItemRangeInto,當前可見position:0~9

    11-2214:41:18.33732764-449/zhangphil.appD/調試:fillData:0,20

    11-2214:41:20.35032764-32764/zhangphil.appD/調試:onItemLoaded:0

    11-2214:41:20.35132764-32764/zhangphil.appD/調試:onItemLoaded:1

    11-2214:41:20.35132764-32764/zhangphil.appD/調試:onItemLoaded:2

    11-2214:41:20.35232764-32764/zhangphil.appD/調試:onItemLoaded:3

    11-2214:41:20.35332764-32764/zhangphil.appD/調試:onItemLoaded:4

    11-2214:41:20.35332764-32764/zhangphil.appD/調試:onItemLoaded:5

    11-2214:41:20.35332764-32764/zhangphil.appD/調試:onItemLoaded:6

    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-2214:42:35.54332764-32764/zhangphil.appD/調試:getItemRangeInto,當前可見position:0~6

    11-2214:42:36.01232764-32764/zhangphil.appD/調試:onRangeChanged

    11-2214:42:36.01232764-32764/zhangphil.appD/調試:getItemRangeInto,當前可見position:5~12

    11-2214:42:36.01332764-1011/zhangphil.appD/調試:fillData:20,20

    11-2214:42:36.84432764-32764/zhangphil.appD/調試:onRangeChanged

    11-2214:42:36.84432764-32764/zhangphil.appD/調試:getItemRangeInto,當前可見position:10~16

    11-2214:42:37.06732764-32764/zhangphil.appD/調試:onRangeChanged

    11-2214:42:37.06732764-32764/zhangphil.appD/調試:getItemRangeInto,當前可見position:13~20

    11-2214:42:38.02032764-32764/zhangphil.appD/調試:onItemLoaded:20

    11-2214:42:38.02032764-32764/zhangphil.appD/調試:onItemLoaded:21

    11-2214:42:38.02032764-32764/zhangphil.appD/調試:onItemLoaded:22

    11-2214:42:38.02032764-32764/zhangphil.appD/調試:onItemLoaded:23

    11-2214:42:38.02132764-32764/zhangphil.appD/調試:onItemLoaded:24

    11-2214:42:38.02132764-32764/zhangphil.appD/調試:onItemLoaded:25

    11-2214:42:38.02132764-32764/zhangphil.appD/調試:onItemLoaded:26

    11-2214:42:38.02132764-32764/zhangphil.appD/調試:onItemLoaded:27

    11-2214:42:38.02132764-32764/zhangphil.appD/調試:onItemLoaded:28

    11-2214:42:38.78432764-32764/zhangphil.appD/調試:onRangeChanged

    11-2214:42:38.78432764-32764/zhangphil.appD/調試: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的設計目的將為把用戶可見區域內容的所有項目數據均加載完成,保證用戶可見區域內的數據是優先加載的。
    (六)AsyncListUtil的refresh強制刷新。

    /*

    *Copyright(C)2015TheAndroidOpenSourceProject

    *

    *LicensedundertheApacheLicense,Version2.0(the“License”);

    *youmaynotusethisfileexceptincompliancewiththeLicense.

    *YoumayobtainacopyoftheLicenseat

    *

    *http://www.apache.org/licenses/LICENSE-2.0

    *

    *Unlessrequiredbyapplicablelaworagreedtoinwriting,software

    *distributedundertheLicenseisdistributedonan“ASIS”BASIS,

    *WITHOUTWARRANTIESORCONDITIONSOFANYKIND,eitherexpressorimplied.

    *SeetheLicenseforthespecificlanguagegoverningpermissionsand

    *limitationsundertheLicense.

    */

    packageandroid.support.v7.util;

    importandroid.support.annotation.UiThread;

    importandroid.support.annotation.WorkerThread;

    importandroid.util.Log;

    importandroid.util.SparseBooleanArray;

    importandroid.util.SparseIntArray;

    /**

    *Autilityclassthatsupportsasynchronouscontentloading.

    *

    *ItcanbeusedtoloadCursordatainchunkswithoutqueryingtheCursorontheUIThreadwhile

    *keepingUIandcachesynchronousforbetteruserexperience.

    *

     

    *Itloadsthedataonabackgroundthreadandkeepsonlyalimitednumberoffixedsized

    *chunksinmemoryatalltimes.

    *

    *{@linkAsyncListUtil}queriesthecurrentlyvisiblerangethrough{@linkViewCallback},

    *loadstherequireddataitemsinthebackgroundthrough{@linkDataCallback},andnotifiesa

    *{@linkViewCallback}whenthedataisloaded.Itmayloadsomeextraitemsforsmoother

    *scrolling.

    *

     

    *Notethatthisclassusesasinglethreadtoloadthedata,soitsuitabletoloaddatafrom

    *secondarystoragesuchasdisk,butnotfromnetwork.

    *

    *Thisclassisdesignedtoworkwith{@linkandroid.support.v7.widget.RecyclerView},butitdoes

    *notdependonitandcanbeusedwithotherlistviews.

    *

    */

    publicclassAsyncListUtil{

    staticfinalStringTAG=“AsyncListUtil”;

    staticfinalbooleanDEBUG=false;

    finalClassmTClass;

    finalintmTileSize;

    finalDataCallbackmDataCallback;

    finalViewCallbackmViewCallback;

     

    finalTileListmTileList;

     

    finalThreadUtil.MainThreadCallbackmMainThreadProxy;

    finalThreadUtil.BackgroundCallbackmBackgroundProxy;

    finalint[]mTmpRange=newint[2];

    finalint[]mPrevRange=newint[2];

    finalint[]mTmpRangeExtended=newint[2];

    booleanmAllowScrollHints;

    privateintmScrollHint=ViewCallback.HINT_SCROLL_NONE;

     

    intmItemCount=0;

     

    intmDisplayedGeneration=0;

    intmRequestedGeneration=mDisplayedGeneration;

    finalSparseIntArraymMissingPositions=newSparseIntArray();

    voidlog(Strings,Object…args){

    Log.d(TAG,”[MAIN]”+String.format(s,args));

    }

    /**

    *CreatesanAsyncListUtil.

    *

    *@paramklassClassofthedataitem.

    *@paramtileSizeNumberofitemperchunkloadedatonce.

    *@paramdataCallbackDataaccesscallback.

    *@paramviewCallbackCallbackforqueryingvisibleitemrangeandupdatenotifications.

    */

    publicAsyncListUtil(Classklass,inttileSize,DataCallbackdataCallback,

    ViewCallbackviewCallback){

    mTClass=klass;

    mTileSize=tileSize;

    mDataCallback=dataCallback;

    mViewCallback=viewCallback;

     

    mTileList=newTileList(mTileSize);

     

    ThreadUtilthreadUtil=newMessageThreadUtil();

    mMainThreadProxy=threadUtil.getMainThreadProxy(mMainThreadCallback);

    mBackgroundProxy=threadUtil.getBackgroundProxy(mBackgroundCallback);

     

    refresh();

    }

    privatebooleanisRefreshPending(){

    returnmRequestedGeneration!=mDisplayedGeneration;

    }

    /**

    *Updatesthecurrentlyvisibleitemrange.

    *

    *

     

    *Identifiesthedataitemsthathavenotbeenloadedyetandinitiatesloadingtheminthe

    *background.Shouldbecalledfromtheview’sscrolllistener(suchas

    *{@linkandroid.support.v7.widget.RecyclerView.OnScrollListener#onScrolled}).

    */

    publicvoidonRangeChanged(){

    if(isRefreshPending()){

    return;//Willupdaterangewilltherefreshresultarrives.

    }

    updateRange();

    mAllowScrollHints=true;

    }

    /**

    *Forcesreloadingthedata.

    *

    *Discardsallthecacheddataandreloadsallrequireddataitemsforthecurrentlyvisible

    *range.Tobecalledwhenthedataitemcountand/orcontentshaschanged.

    */

    publicvoidrefresh(){

    mMissingPositions.clear();

    mBackgroundProxy.refresh(++mRequestedGeneration);

    }

     

    /**

    *Returnsthedataitematthegivenpositionornullifithasnotbeenloaded

    *yet.

    *

    *

     

    *Ifthismethodhasbeencalledforaspecificpositionandreturnednull,then

    *{@linkViewCallback#onItemLoaded(int)}willbecalledwhenitfinallyloads.Notethatif

    *thispositionstaysoutsideofthecacheditemrange(asdefinedby

    *{@linkViewCallback#extendRangeInto}method),thenthecallbackwillneverbecalledfor

    *thisposition.

    *

    *@parampositionItemposition.

    *

    *@returnThedataitematthegivenpositionornullifithasnotbeenloaded

    *yet.

    */

    publicTgetItem(intposition){

    if(position<0||position>=mItemCount){

    thrownewIndexOutOfBoundsException(position+“isnotwithin0and”+mItemCount);

    }

    Titem=mTileList.getItemAt(position);

    if(item==null&&!isRefreshPending()){

    mMissingPositions.put(position,0);

    }

    returnitem;

    }

    /**

    *Returnsthenumberofitemsinthedataset.

    *

    *

     

    *Thisisthenumberreturnedbyarecentcallto

    *{@linkDataCallback#refreshData()}.

    *

    *@returnNumberofitems.

    */

    publicintgetItemCount(){

    returnmItemCount;

    }

     

    voidupdateRange(){

    mViewCallback.getItemRangeInto(mTmpRange);

    if(mTmpRange[0]>mTmpRange[1]||mTmpRange[0]<0){

    return;

    }

    if(mTmpRange[1]>=mItemCount){

    //Invalidrangemayarrivesoonaftertherefresh.

    return;

    }

     

    if(!mAllowScrollHints){

    mScrollHint=ViewCallback.HINT_SCROLL_NONE;

    }elseif(mTmpRange[0]>mPrevRange[1]||mPrevRange[0]>mTmpRange[1]){

    //Rangesdonotintersect,longleapnotascroll.

    mScrollHint=ViewCallback.HINT_SCROLL_NONE;

    }elseif(mTmpRange[0]<mprevrange[0]){

  • mScrollHint=ViewCallback.HINT_SCROLL_DESC;</mprevrange[0]){
  • }elseif(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);

    }

    privatefinalThreadUtil.MainThreadCallback

    mMainThreadCallback=newThreadUtil.MainThreadCallback(){

    @Override

    publicvoidupdateItemCount(intgeneration,intitemCount){

    if(DEBUG){

    log(”updateItemCount:size=%d,gen#%d”,itemCount,generation);

    }

    if(!isRequestedGeneration(generation)){

    return;

    }

    mItemCount=itemCount;

    mViewCallback.onDataRefresh();

    mDisplayedGeneration=mRequestedGeneration;

    recycleAllTiles();

     

    mAllowScrollHints=false;//Willbesettotrueafterafirstrealscroll.

    //Therewillbenoscrolleventifthesizechangedoesnotaffectthecurrentrange.

    updateRange();

    }

    @Override

    publicvoidaddTile(intgeneration,TileList.Tiletile){

    if(!isRequestedGeneration(generation)){

    if(DEBUG){

    log(”recyclinganoldergenerationtile@%d”,tile.mStartPosition);

    }

    mBackgroundProxy.recycleTile(tile);

    return;

    }

    TileList.Tileduplicate=mTileList.addOrReplace(tile);

    if(duplicate!=null){

    Log.e(TAG,”duplicatetile@”+duplicate.mStartPosition);

    mBackgroundProxy.recycleTile(duplicate);

    }

    if(DEBUG){

    log(”gen#%d,addedtile@%d,totaltiles:%d”,

    generation,tile.mStartPosition,mTileList.size());

    }

    intendPosition=tile.mStartPosition+tile.mItemCount;

    intindex=0;

    while(index<mmissingpositions.size()){

  • finalintposition=mMissingPositions.keyAt(index);</mmissingpositions.size()){
  • if(tile.mStartPosition<=position&&position<endposition){

  • mMissingPositions.removeAt(index);</endposition){
  • mViewCallback.onItemLoaded(position);

    }else{

    index++;

    }

    }

    }

     

    @Override

    publicvoidremoveTile(intgeneration,intposition){

    if(!isRequestedGeneration(generation)){

    return;

    }

    TileList.Tiletile=mTileList.removeAtPos(position);

    if(tile==null){

    Log.e(TAG,”tilenotfound@”+position);

    return;

    }

    if(DEBUG){

    log(”recyclingtile@%d,totaltiles:%d”,tile.mStartPosition,mTileList.size());

    }

    mBackgroundProxy.recycleTile(tile);

    }

     

    privatevoidrecycleAllTiles(){

    if(DEBUG){

    log(”recyclingall%dtiles”,mTileList.size());

    }

    for(inti=0;i<mtilelist.size();i++){

  • mBackgroundProxy.recycleTile(mTileList.getAtIndex(i));</mtilelist.size();i++){

  • }

    mTileList.clear();

    }

     

    privatebooleanisRequestedGeneration(intgeneration){

    returngeneration==mRequestedGeneration;

    }

    };

    privatefinalThreadUtil.BackgroundCallback

    mBackgroundCallback=newThreadUtil.BackgroundCallback(){

     

    privateTileList.TilemRecycledRoot;

     

    finalSparseBooleanArraymLoadedTiles=newSparseBooleanArray();

     

    privateintmGeneration;

    privateintmItemCount;

    privateintmFirstRequiredTileStart;

    privateintmLastRequiredTileStart;

     

    @Override

    publicvoidrefresh(intgeneration){

    mGeneration=generation;

    mLoadedTiles.clear();

    mItemCount=mDataCallback.refreshData();

    mMainThreadProxy.updateItemCount(mGeneration,mItemCount);

    }

     

    @Override

    publicvoidupdateRange(intrangeStart,intrangeEnd,intextRangeStart,intextRangeEnd,

    intscrollHint){

    if(DEBUG){

    log(”updateRange:%d..%dextendedto%d..%d,scrollhint:%d”,

    rangeStart,rangeEnd,extRangeStart,extRangeEnd,scrollHint);

    }

     

    if(rangeStart>rangeEnd){

    return;

    }

     

    finalintfirstVisibleTileStart=getTileStart(rangeStart);

    finalintlastVisibleTileStart=getTileStart(rangeEnd);

    mFirstRequiredTileStart=getTileStart(extRangeStart);

    mLastRequiredTileStart=getTileStart(extRangeEnd);

    if(DEBUG){

    log(”requestingtilerange:%d..%d”,

    mFirstRequiredTileStart,mLastRequiredTileStart);

    }

     

    //AllpendingtilerequestsareremovedbyThreadUtilatthispoint.

    //Re-requestallrequiredtilesinthemostoptimalorder.

    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);

    }

    }

    privateintgetTileStart(intposition){

    returnposition-position%mTileSize;

    }

    privatevoidrequestTiles(intfirstTileStart,intlastTileStart,intscrollHint,

    booleanbackwards){

    for(inti=firstTileStart;i<=lastTileStart;i+=mTileSize){

    inttileStart=backwards?(lastTileStart+firstTileStart-i):i;

    if(DEBUG){

    log(”requestingtile@%d”,tileStart);

    }

    mBackgroundProxy.loadTile(tileStart,scrollHint);

    }

    }

     

    @Override

    publicvoidloadTile(intposition,intscrollHint){

    if(isTileLoaded(position)){

    if(DEBUG){

    log(”alreadyloadedtile@%d”,position);

    }

    return;

    }

    TileList.Tiletile=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

    publicvoidrecycleTile(TileList.Tiletile){

    if(DEBUG){

    log(”recyclingtile@%d”,tile.mStartPosition);

    }

    mDataCallback.recycleData(tile.mItems,tile.mItemCount);

    tile.mNext=mRecycledRoot;

    mRecycledRoot=tile;

    }

    privateTileList.TileacquireTile(){

    if(mRecycledRoot!=null){

    TileList.Tileresult=mRecycledRoot;

    mRecycledRoot=mRecycledRoot.mNext;

    returnresult;

    }

    returnnewTileList.Tile(mTClass,mTileSize);

    }

     

    privatebooleanisTileLoaded(intposition){

    returnmLoadedTiles.get(position);

    }

     

    privatevoidaddTile(TileList.Tiletile){

    mLoadedTiles.put(tile.mStartPosition,true);

    mMainThreadProxy.addTile(mGeneration,tile);

    if(DEBUG){

    log(”loadedtile@%d,totaltiles:%d”,tile.mStartPosition,mLoadedTiles.size());

    }

    }

     

    privatevoidremoveTile(intposition){

    mLoadedTiles.delete(position);

    mMainThreadProxy.removeTile(mGeneration,position);

    if(DEBUG){

    log(”flushedtile@%d,totaltiles:%s”,position,mLoadedTiles.size());

    }

    }

     

    privatevoidflushTileCache(intscrollHint){

    finalintcacheSizeLimit=mDataCallback.getMaxCachedTiles();

    while(mLoadedTiles.size()>=cacheSizeLimit){

    intfirstLoadedTileStart=mLoadedTiles.keyAt(0);

    intlastLoadedTileStart=mLoadedTiles.keyAt(mLoadedTiles.size()-1);

    intstartMargin=mFirstRequiredTileStart-firstLoadedTileStart;

    intendMargin=lastLoadedTileStart-mLastRequiredTileStart;

    if(startMargin>0&&(startMargin>=endMargin||

    (scrollHint==ViewCallback.HINT_SCROLL_ASC))){

    removeTile(firstLoadedTileStart);

    }elseif(endMargin>0&&(startMargin<endmargin||

  • (scrollHint==ViewCallback.HINT_SCROLL_DESC))){</endmargin||

  • removeTile(lastLoadedTileStart);

    }else{

    //Couldnotflushoneitherside,bailout.

    return;

    }

    }

    }

     

    privatevoidlog(Strings,Object…args){

    Log.d(TAG,”[BKGR]”+String.format(s,args));

    }

    };

    /**

    *Thecallbackthatprovidesdataaccessfor{@linkAsyncListUtil}.

    *

    *

     

    *Allmethodsarecalledonthebackgroundthread.

    */

    publicstaticabstractclassDataCallback{

    /**

    *Refreshthedatasetandreturnthenewdataitemcount.

    *

    *

     

    *Ifthedataisbeingaccessedthrough{@linkandroid.database.Cursor}thisiswhere

    *thenewcursorshouldbecreated.

    *

    *@returnDataitemcount.

    */

    @WorkerThread

    publicabstractintrefreshData();

    /**

    *Fillthegiventile.

    *

    *

     

    *Theprovidedtilemightbearecycledtile,inwhichcaseitwillalreadyhaveobjects.

    *Itissuggestedtore-usetheseobjectsifpossibleinyourusecase.

    *

    *@paramstartPositionThestartpositioninthelist.

    *@paramitemCountThedataitemcount.

    *@paramdataThedataitemarraytofillinto.Shouldnotbeaccessedbeyond

    *itemCount.

    */

    @WorkerThread

    publicabstractvoidfillData(T[]data,intstartPosition,intitemCount);

     

    /**

    *Recycletheobjectscreatedin{@link#fillData}ifnecessary.

    *

    *

    *@paramdataArrayofdataitems.ShouldnotbeaccessedbeyonditemCount.

    *@paramitemCountThedataitemcount.

    */

    @WorkerThread

    publicvoidrecycleData(T[]data,intitemCount){

    }

    /**

    *Returnstilecachesizelimit(intiles).

    *

    *

     

    *Theactualnumberofcachedtileswillbethemaximumofthisvalueandthenumberof

    *tilesthatisrequiredtocovertherangereturnedby

    *{@linkViewCallback#extendRangeInto(int[],int[],int)}.

    *

     

    *Forexample,ifthismethodreturns10,andthemost

    *recentcallto{@linkViewCallback#extendRangeInto(int[],int[],int)}returned

    *{100,179},andthetilesizeis5,thenthemaximumnumberofcachedtileswillbe16.

    *

     

    *However,ifthetilesizeis20,thenthemaximumnumberofcachedtileswillbe10.

    *

     

    *Thedefaultimplementationreturns10.

    *

    *@returnMaximumcachesize.

    */

    @WorkerThread

    publicintgetMaxCachedTiles(){

    return10;

    }

    }

    /**

    *Thecallbackthatlinks{@linkAsyncListUtil}withthelistview.

    *

    *

     

    *Allmethodsarecalledonthemainthread.

    */

    publicstaticabstractclassViewCallback{

    /**

    *Noscrolldirectionhintavailable.

    */

    publicstaticfinalintHINT_SCROLL_NONE=0;

     

    /**

    *Scrollingindescendingorder(fromhighertolowerpositionsintheorderofthebacking

    *storage).

    */

    publicstaticfinalintHINT_SCROLL_DESC=1;

     

    /**

    *Scrollinginascendingorder(fromlowertohigherpositionsintheorderofthebacking

    *storage).

    */

    publicstaticfinalintHINT_SCROLL_ASC=2;

     

    /**

    *Computetherangeofvisibleitempositions.

    *

     

    *outRange[0]isthepositionofthefirstvisibleitem(intheorderofthebacking

    *storage).

    *

    *outRange[1]isthepositionofthelastvisibleitem(intheorderofthebacking

    *storage).

    *

     

    *Negativepositionsandpositionsgreaterorequalto{@link#getItemCount}areinvalid.

    *Ifthereturnedrangecontainsinvalidpositionsitisignored(noitemwillbeloaded).

    *

    *@paramoutRangeThevisibleitemrange.

    */

    @UiThread

    publicabstractvoidgetItemRangeInto(int[]outRange);

    /**

    *Computeawiderrangeofitemsthatwillbeloadedforsmootherscrolling.

    *

    *

     

    *Ifthereisnoscrollhint,thedefaultimplementationextendsthevisiblerangebyhalf

    *itslengthinbothdirections.Ifthereisascrollhint,therangeisextendedby

    *itsfulllengthinthescrolldirection,andbyhalfintheotherdirection.

    *

     

    *Forexample,ifrangeis{100,200}andscrollHint

    *is{@link#HINT_SCROLL_ASC},thenoutRangewillbe{50,300}.

    *

    *However,ifscrollHintis{@link#HINT_SCROLL_NONE},then

    *outRangewillbe{50,250}

    *

    *@paramrangeVisibleitemrange.

    *@paramoutRangeExtendedrange.

    *@paramscrollHintThescrolldirectionhint.

    */

    @UiThread

    publicvoidextendRangeInto(int[]range,int[]outRange,intscrollHint){

    finalintfullRange=range[1]-range[0]+1;

    finalinthalfRange=fullRange/2;

    outRange[0]=range[0]-(scrollHint==HINT_SCROLL_DESC?fullRange:halfRange);

    outRange[1]=range[1]+(scrollHint==HINT_SCROLL_ASC?fullRange:halfRange);

    }

    /**

    *Calledwhentheentiredatasethaschanged.

    */

    @UiThread

    publicabstractvoidonDataRefresh();

    /**

    *Calledwhenanitematthegivenpositionisloaded.

    *@parampositionItemposition.

    */

    @UiThread

    publicabstractvoidonItemLoaded(intposition);

    }

    }

    /*
     * 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 { static final String TAG = "AsyncListUtil"; static final boolean DEBUG = false; final Class mTClass; final int mTileSize; final DataCallback mDataCallback; final ViewCallback mViewCallback; final TileList mTileList; final ThreadUtil.MainThreadCallback mMainThreadProxy; final ThreadUtil.BackgroundCallback 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 klass, int tileSize, DataCallback dataCallback, ViewCallback viewCallback) { mTClass = klass; mTileSize = tileSize; mDataCallback = dataCallback; mViewCallback = viewCallback; mTileList = new TileList(mTileSize); ThreadUtil threadUtil = new MessageThreadUtil(); mMainThreadProxy = threadUtil.getMainThreadProxy(mMainThreadCallback); mBackgroundProxy = threadUtil.getBackgroundProxy(mBackgroundCallback); refresh(); } private boolean isRefreshPending() { return mRequestedGeneration != mDisplayedGeneration; } /** * Updates the currently visible item range. * *

    * 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 null if it has not been loaded * yet. * *

    * If this method has been called for a specific position and returned 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. * *

    * 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 mMainThreadCallback = new ThreadUtil.MainThreadCallback() { @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 tile) { if (!isRequestedGeneration(generation)) { if (DEBUG) { log("recycling an older generation tile @%d", tile.mStartPosition); } mBackgroundProxy.recycleTile(tile); return; } TileList.Tile 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 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 mBackgroundCallback = new ThreadUtil.BackgroundCallback() { private TileList.Tile 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 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 tile) { if (DEBUG) { log("recycling tile @%d", tile.mStartPosition); } mDataCallback.recycleData(tile.mItems, tile.mItemCount); tile.mNext = mRecycledRoot; mRecycledRoot = tile; } private TileList.Tile acquireTile() { if (mRecycledRoot != null) { TileList.Tile result = mRecycledRoot; mRecycledRoot = mRecycledRoot.mNext; return result; } return new TileList.Tile(mTClass, mTileSize); } private boolean isTileLoaded(int position) { return mLoadedTiles.get(position); } private void addTile(TileList.Tile 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}. * *

    * All methods are called on the background thread. */ public static abstract class DataCallback { /** * Refresh the data set and return the new data item count. * *

    * 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 * 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). * *

    * 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 range is {100, 200} and scrollHint * is {@link #HINT_SCROLL_ASC}, then outRange will be {50, 300}. *

    * However, if 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); } }

  • 發佈留言

    發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *