欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

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

程序员文章站 2022-05-31 08:17:30
基于android官方asynclistutil优化改进recyclerview分页加载机制(一)。 android asynclistutil是android官方提供的专为...

基于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
  • data[i]=string.valueof(system.currenttimemillis());



  • //模拟耗时任务,故意休眠一定时延。

    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”
    =“http:>
    android:layout_height=“match_parent”

    android:orientation=“vertical”>



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

    android:layout_height=“wrap_content”

    android:text=“更新”/>



  • android:id=“@+id/recycler_view”
  • 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.

    *

    *

     

    *ifthismethodhasbeencalledforecificpositionandreturnednull,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]

  • mscrollhint=viewcallback.hint_scroll_desc;
  • }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

  • finalintposition=mmissingpositions.keyat(index);
  • if(tile.mstartposition<=position&&position

  • mmissingpositions.removeat(index);
  • 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
  • mbackgroundproxy.recycletile(mtilelist.getatindex(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
  • (scrollhint==viewcallback.hint_scroll_desc))){

  • 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-usethebjectsifpossibleinyourusecase.

    *

    *@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, extrang