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

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


Android AsyncListUtil是Android官方提供的专为列表这样的数据更新加载提供的异步加载组件。基于AsyncListUtil组件,可以轻易实现常见的RecyclerView分页加载技术。AsyncListUtil技术涉及的细节比较繁复,因此我将分别写若干篇文章,分点、分解AsyncListUtil技术。

先给出一个可运行的例子,MainActivity.java:

  1. package zhangphil.app;  
  2.   
  3. import android.graphics.Color;  
  4. import android.os.Bundle;  
  5. import android.os.SystemClock;  
  6. import android.support.v7.app.AppCompatActivity;  
  7. import android.support.v7.util.AsyncListUtil;  
  8. import android.support.v7.widget.LinearLayoutManager;  
  9. import android.support.v7.widget.RecyclerView;  
  10. import android.text.TextUtils;  
  11. import android.util.Log;  
  12. import android.view.LayoutInflater;  
  13. import android.view.View;  
  14. import android.view.ViewGroup;  
  15. import android.widget.LinearLayout;  
  16. import android.widget.TextView;  
  17.   
  18. public class MainActivity extends AppCompatActivity {  
  19.     private String TAG = “调试”;  
  20.   
  21.     private final int NULL = -1;  
  22.   
  23.     private RecyclerView mRecyclerView;  
  24.     private AsyncListUtil mAsyncListUtil;  
  25.   
  26.     @Override  
  27.     protected void onCreate(Bundle savedInstanceState) {  
  28.         super.onCreate(savedInstanceState);  
  29.         setContentView(R.layout.activity_main);  
  30.   
  31.         mRecyclerView = findViewById(R.id.recycler_view);  
  32.   
  33.         LinearLayoutManager mLayoutManager = new LinearLayoutManager(this);  
  34.         mLayoutManager.setOrientation(LinearLayout.VERTICAL);  
  35.         mRecyclerView.setLayoutManager(mLayoutManager);  
  36.   
  37.         RecyclerView.Adapter mAdapter = new MyAdapter();  
  38.         mRecyclerView.setAdapter(mAdapter);  
  39.   
  40.         MyDataCallback mDataCallback = new MyDataCallback();  
  41.         MyViewCallback mViewCallback = new MyViewCallback();  
  42.         mAsyncListUtil = new AsyncListUtil(String.class20, mDataCallback, mViewCallback);  
  43.   
  44.         mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {  
  45.             @Override  
  46.             public void onScrollStateChanged(RecyclerView recyclerView, int newState) {  
  47.                 super.onScrollStateChanged(recyclerView, newState);  
  48.   
  49.                 Log.d(TAG, ”onRangeChanged”);  
  50.                 mAsyncListUtil.onRangeChanged();  
  51.             }  
  52.         });  
  53.   
  54.         findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {  
  55.             @Override  
  56.             public void onClick(View v) {  
  57.                 Log.d(TAG, ”refresh”);  
  58.                 mAsyncListUtil.refresh();  
  59.             }  
  60.         });  
  61.     }  
  62.   
  63.     private class MyDataCallback extends AsyncListUtil.DataCallback<String> {  
  64.   
  65.         @Override  
  66.         public int refreshData() {  
  67.             //更新数据的元素个数。  
  68.             //假设预先设定更新若干条。  
  69.             int count = Integer.MAX_VALUE;  
  70.             Log.d(TAG, ”refreshData:” + count);  
  71.             return count;  
  72.         }  
  73.   
  74.         /** 
  75.          * 在这里完成数据加载的耗时任务。 
  76.          * 
  77.          * @param data 
  78.          * @param startPosition 
  79.          * @param itemCount 
  80.          */  
  81.         @Override  
  82.         public void fillData(String[] data, int startPosition, int itemCount) {  
  83.             Log.d(TAG, ”fillData:” + startPosition + “,” + itemCount);  
  84.             for (int i = 0; i < itemCount; i++) {  
  85.                 data[i] = String.valueOf(System.currentTimeMillis());  
  86.   
  87.                 //模拟耗时任务,故意休眠一定时延。  
  88.                 SystemClock.sleep(100);  
  89.             }  
  90.         }  
  91.     }  
  92.   
  93.     private class MyViewCallback extends AsyncListUtil.ViewCallback {  
  94.   
  95.         /** 
  96.          * @param outRange 
  97.          */  
  98.         @Override  
  99.         public void getItemRangeInto(int[] outRange) {  
  100.             getOutRange(outRange);  
  101.   
  102.             /** 
  103.              * 如果当前的RecyclerView为空,主动为用户加载数据. 
  104.              * 假设预先加载若干条数据 
  105.              * 
  106.              */  
  107.             if (outRange[0] == NULL && outRange[1] == NULL) {  
  108.                 Log.d(TAG, ”当前RecyclerView为空!”);  
  109.                 outRange[0] = 0;  
  110.                 outRange[1] = 9;  
  111.             }  
  112.   
  113.             Log.d(TAG, ”getItemRangeInto,当前可见position: ” + outRange[0] + “ ~ ” + outRange[1]);  
  114.         }  
  115.   
  116.         @Override  
  117.         public void onDataRefresh() {  
  118.             int[] outRange = new int[2];  
  119.             getOutRange(outRange);  
  120.             mRecyclerView.getAdapter().notifyItemRangeChanged(outRange[0], outRange[1] - outRange[0] + 1);  
  121.   
  122.             Log.d(TAG, ”onDataRefresh:”+outRange[0]+“,”+outRange[1]);  
  123.         }  
  124.   
  125.         @Override  
  126.         public void onItemLoaded(int position) {  
  127.             mRecyclerView.getAdapter().notifyItemChanged(position);  
  128.             Log.d(TAG, ”onItemLoaded:” + position);  
  129.         }  
  130.     }  
  131.   
  132.     private void    getOutRange(int[] outRange){  
  133.         outRange[0] = ((LinearLayoutManager) mRecyclerView.getLayoutManager()).findFirstVisibleItemPosition();  
  134.         outRange[1] = ((LinearLayoutManager) mRecyclerView.getLayoutManager()).findLastVisibleItemPosition();  
  135.     }  
  136.   
  137.     private class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {  
  138.         public MyAdapter() {  
  139.             super();  
  140.         }  
  141.   
  142.         @Override  
  143.         public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {  
  144.             View view = LayoutInflater.from(getApplicationContext()).inflate(android.R.layout.simple_list_item_2, null);  
  145.             ViewHolder holder = new ViewHolder(view);  
  146.             return holder;  
  147.         }  
  148.   
  149.         @Override  
  150.         public void onBindViewHolder(ViewHolder viewHolder, int i) {  
  151.             viewHolder.text1.setText(String.valueOf(i));  
  152.   
  153.             String s = String.valueOf(mAsyncListUtil.getItem(i));  
  154.             if (TextUtils.equals(s, “null”)) {  
  155.                 s = ”加载中…”;  
  156.             }  
  157.   
  158.             viewHolder.text2.setText(s);  
  159.         }  
  160.   
  161.         @Override  
  162.         public int getItemCount() {  
  163.             return mAsyncListUtil.getItemCount();  
  164.         }  
  165.   
  166.         public class ViewHolder extends RecyclerView.ViewHolder {  
  167.             public TextView text1;  
  168.             public TextView text2;  
  169.   
  170.             public ViewHolder(View itemView) {  
  171.                 super(itemView);  
  172.   
  173.                 text1 = itemView.findViewById(android.R.id.text1);  
  174.                 text1.setTextColor(Color.RED);  
  175.   
  176.                 text2 = itemView.findViewById(android.R.id.text2);  
  177.                 text2.setTextColor(Color.BLUE);  
  178.             }  
  179.         }  
  180.     }  
  181. }  
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所需布局文件:

  1. <?xml version=“1.0” encoding=“utf-8”?>  
  2. <LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”  
  3.     android:layout_width=“match_parent”  
  4.     android:layout_height=“match_parent”  
  5.     android:orientation=“vertical”>  
  6.   
  7.     <Button  
  8.         android:id=“@+id/button”  
  9.         android:layout_width=“wrap_content”  
  10.         android:layout_height=“wrap_content”  
  11.         android:text=“更新” />  
  12.   
  13.     <android.support.v7.widget.RecyclerView  
  14.         android:id=“@+id/recycler_view”  
  15.         android:layout_width=“match_parent”  
  16.         android:layout_height=“match_parent” />  
  17. </LinearLayout>  
<?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构造函数源代码:
  1. /** 
  2.      * Creates an AsyncListUtil. 
  3.      * 
  4.      * @param klass Class of the data item. 
  5.      * @param tileSize Number of item per chunk loaded at once. 
  6.      * @param dataCallback Data access callback. 
  7.      * @param viewCallback Callback for querying visible item range and update notifications. 
  8.      */  
  9.     public AsyncListUtil(Class<T> klass, int tileSize, DataCallback<T> dataCallback,  
  10.                          ViewCallback viewCallback) {  
  11.         mTClass = klass;  
  12.         mTileSize = tileSize;  
  13.         mDataCallback = dataCallback;  
  14.         mViewCallback = viewCallback;  
  15.   
  16.         mTileList = new TileList<T>(mTileSize);  
  17.   
  18.         ThreadUtil<T> threadUtil = new MessageThreadUtil<T>();  
  19.         mMainThreadProxy = threadUtil.getMainThreadProxy(mMainThreadCallback);  
  20.         mBackgroundProxy = threadUtil.getBackgroundProxy(mBackgroundCallback);  
  21.   
  22.         refresh();  
  23.     }  
/**
     * 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输出:
  1. 11-22 14:41:18.313 32764-447/zhangphil.app D/调试: refreshData:2147483647  
  2. 11-22 14:41:18.336 32764-32764/zhangphil.app D/调试: onDataRefresh:-1,-1  
  3. 11-22 14:41:18.336 32764-32764/zhangphil.app D/调试: 当前RecyclerView为空!  
  4. 11-22 14:41:18.336 32764-32764/zhangphil.app D/调试: getItemRangeInto,当前可见position: 0 ~ 9  
  5. 11-22 14:41:18.337 32764-449/zhangphil.app D/调试: fillData:0,20  
  6. 11-22 14:41:20.350 32764-32764/zhangphil.app D/调试: onItemLoaded:0  
  7. 11-22 14:41:20.351 32764-32764/zhangphil.app D/调试: onItemLoaded:1  
  8. 11-22 14:41:20.351 32764-32764/zhangphil.app D/调试: onItemLoaded:2  
  9. 11-22 14:41:20.352 32764-32764/zhangphil.app D/调试: onItemLoaded:3  
  10. 11-22 14:41:20.353 32764-32764/zhangphil.app D/调试: onItemLoaded:4  
  11. 11-22 14:41:20.353 32764-32764/zhangphil.app D/调试: onItemLoaded:5  
  12. 11-22 14:41:20.353 32764-32764/zhangphil.app D/调试: 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输出:

  1. 11-22 14:42:35.543 32764-32764/zhangphil.app D/调试: getItemRangeInto,当前可见position: 0 ~ 6  
  2. 11-22 14:42:36.012 32764-32764/zhangphil.app D/调试: onRangeChanged  
  3. 11-22 14:42:36.012 32764-32764/zhangphil.app D/调试: getItemRangeInto,当前可见position: 5 ~ 12  
  4. 11-22 14:42:36.013 32764-1011/zhangphil.app D/调试: fillData:20,20  
  5. 11-22 14:42:36.844 32764-32764/zhangphil.app D/调试: onRangeChanged  
  6. 11-22 14:42:36.844 32764-32764/zhangphil.app D/调试: getItemRangeInto,当前可见position: 10 ~ 16  
  7. 11-22 14:42:37.067 32764-32764/zhangphil.app D/调试: onRangeChanged  
  8. 11-22 14:42:37.067 32764-32764/zhangphil.app D/调试: getItemRangeInto,当前可见position: 13 ~ 20  
  9. 11-22 14:42:38.020 32764-32764/zhangphil.app D/调试: onItemLoaded:20  
  10. 11-22 14:42:38.020 32764-32764/zhangphil.app D/调试: onItemLoaded:21  
  11. 11-22 14:42:38.020 32764-32764/zhangphil.app D/调试: onItemLoaded:22  
  12. 11-22 14:42:38.020 32764-32764/zhangphil.app D/调试: onItemLoaded:23  
  13. 11-22 14:42:38.021 32764-32764/zhangphil.app D/调试: onItemLoaded:24  
  14. 11-22 14:42:38.021 32764-32764/zhangphil.app D/调试: onItemLoaded:25  
  15. 11-22 14:42:38.021 32764-32764/zhangphil.app D/调试: onItemLoaded:26  
  16. 11-22 14:42:38.021 32764-32764/zhangphil.app D/调试: onItemLoaded:27  
  17. 11-22 14:42:38.021 32764-32764/zhangphil.app D/调试: onItemLoaded:28  
  18. 11-22 14:42:38.784 32764-32764/zhangphil.app D/调试: onRangeChanged  
  19. 11-22 14:42:38.784 32764-32764/zhangphil.app D/调试: getItemRangeInto,当前可见position: 21 ~ 28  
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源代码:

  1. /* 
  2.  * Copyright (C) 2015 The Android Open Source Project 
  3.  * 
  4.  * Licensed under the Apache License, Version 2.0 (the “License”); 
  5.  * you may not use this file except in compliance with the License. 
  6.  * You may obtain a copy of the License at 
  7.  * 
  8.  *      http://www.apache.org/licenses/LICENSE-2.0 
  9.  * 
  10.  * Unless required by applicable law or agreed to in writing, software 
  11.  * distributed under the License is distributed on an “AS IS” BASIS, 
  12.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
  13.  * See the License for the specific language governing permissions and 
  14.  * limitations under the License. 
  15.  */  
  16.   
  17. package android.support.v7.util;  
  18.   
  19. import android.support.annotation.UiThread;  
  20. import android.support.annotation.WorkerThread;  
  21. import android.util.Log;  
  22. import android.util.SparseBooleanArray;  
  23. import android.util.SparseIntArray;  
  24.   
  25. /** 
  26.  * A utility class that supports asynchronous content loading. 
  27.  * <p> 
  28.  * It can be used to load Cursor data in chunks without querying the Cursor on the UI Thread while 
  29.  * keeping UI and cache synchronous for better user experience. 
  30.  * <p> 
  31.  * It loads the data on a background thread and keeps only a limited number of fixed sized 
  32.  * chunks in memory at all times. 
  33.  * <p> 
  34.  * {@link AsyncListUtil} queries the currently visible range through {@link ViewCallback}, 
  35.  * loads the required data items in the background through {@link DataCallback}, and notifies a 
  36.  * {@link ViewCallback} when the data is loaded. It may load some extra items for smoother 
  37.  * scrolling. 
  38.  * <p> 
  39.  * Note that this class uses a single thread to load the data, so it suitable to load data from 
  40.  * secondary storage such as disk, but not from network. 
  41.  * <p> 
  42.  * This class is designed to work with {@link android.support.v7.widget.RecyclerView}, but it does 
  43.  * not depend on it and can be used with other list views. 
  44.  * 
  45.  */  
  46. public class AsyncListUtil<T> {  
  47.     static final String TAG = “AsyncListUtil”;  
  48.   
  49.     static final boolean DEBUG = false;  
  50.   
  51.     final Class<T> mTClass;  
  52.     final int mTileSize;  
  53.     final DataCallback<T> mDataCallback;  
  54.     final ViewCallback mViewCallback;  
  55.   
  56.     final TileList<T> mTileList;  
  57.   
  58.     final ThreadUtil.MainThreadCallback<T> mMainThreadProxy;  
  59.     final ThreadUtil.BackgroundCallback<T> mBackgroundProxy;  
  60.   
  61.     final int[] mTmpRange = new int[2];  
  62.     final int[] mPrevRange = new int[2];  
  63.     final int[] mTmpRangeExtended = new int[2];  
  64.   
  65.     boolean mAllowScrollHints;  
  66.     private int mScrollHint = ViewCallback.HINT_SCROLL_NONE;  
  67.   
  68.     int mItemCount = 0;  
  69.   
  70.     int mDisplayedGeneration = 0;  
  71.     int mRequestedGeneration = mDisplayedGeneration;  
  72.   
  73.     final SparseIntArray mMissingPositions = new SparseIntArray();  
  74.   
  75.     void log(String s, Object… args) {  
  76.         Log.d(TAG, ”[MAIN] ” + String.format(s, args));  
  77.     }  
  78.   
  79.     /** 
  80.      * Creates an AsyncListUtil. 
  81.      * 
  82.      * @param klass Class of the data item. 
  83.      * @param tileSize Number of item per chunk loaded at once. 
  84.      * @param dataCallback Data access callback. 
  85.      * @param viewCallback Callback for querying visible item range and update notifications. 
  86.      */  
  87.     public AsyncListUtil(Class<T> klass, int tileSize, DataCallback<T> dataCallback,  
  88.                          ViewCallback viewCallback) {  
  89.         mTClass = klass;  
  90.         mTileSize = tileSize;  
  91.         mDataCallback = dataCallback;  
  92.         mViewCallback = viewCallback;  
  93.   
  94.         mTileList = new TileList<T>(mTileSize);  
  95.   
  96.         ThreadUtil<T> threadUtil = new MessageThreadUtil<T>();  
  97.         mMainThreadProxy = threadUtil.getMainThreadProxy(mMainThreadCallback);  
  98.         mBackgroundProxy = threadUtil.getBackgroundProxy(mBackgroundCallback);  
  99.   
  100.         refresh();  
  101.     }  
  102.   
  103.     private boolean isRefreshPending() {  
  104.         return mRequestedGeneration != mDisplayedGeneration;  
  105.     }  
  106.   
  107.     /** 
  108.      * Updates the currently visible item range. 
  109.      * 
  110.      * <p> 
  111.      * Identifies the data items that have not been loaded yet and initiates loading them in the 
  112.      * background. Should be called from the view’s scroll listener (such as 
  113.      * {@link android.support.v7.widget.RecyclerView.OnScrollListener#onScrolled}). 
  114.      */  
  115.     public void onRangeChanged() {  
  116.         if (isRefreshPending()) {  
  117.             return;  // Will update range will the refresh result arrives.  
  118.         }  
  119.         updateRange();  
  120.         mAllowScrollHints = true;  
  121.     }  
  122.   
  123.     /** 
  124.      * Forces reloading the data. 
  125.      * <p> 
  126.      * Discards all the cached data and reloads all required data items for the currently visible 
  127.      * range. To be called when the data item count and/or contents has changed. 
  128.      */  
  129.     public void refresh() {  
  130.         mMissingPositions.clear();  
  131.         mBackgroundProxy.refresh(++mRequestedGeneration);  
  132.     }  
  133.   
  134.     /** 
  135.      * Returns the data item at the given position or <code>null</code> if it has not been loaded 
  136.      * yet. 
  137.      * 
  138.      * <p> 
  139.      * If this method has been called for a specific position and returned <code>null</code>, then 
  140.      * {@link ViewCallback#onItemLoaded(int)} will be called when it finally loads. Note that if 
  141.      * this position stays outside of the cached item range (as defined by 
  142.      * {@link ViewCallback#extendRangeInto} method), then the callback will never be called for 
  143.      * this position. 
  144.      * 
  145.      * @param position Item position. 
  146.      * 
  147.      * @return The data item at the given position or <code>null</code> if it has not been loaded 
  148.      *         yet. 
  149.      */  
  150.     public T getItem(int position) {  
  151.         if (position < 0 || position >= mItemCount) {  
  152.             throw new IndexOutOfBoundsException(position + “ is not within 0 and ” + mItemCount);  
  153.         }  
  154.         T item = mTileList.getItemAt(position);  
  155.         if (item == null && !isRefreshPending()) {  
  156.             mMissingPositions.put(position, 0);  
  157.         }  
  158.         return item;  
  159.     }  
  160.   
  161.     /** 
  162.      * Returns the number of items in the data set. 
  163.      * 
  164.      * <p> 
  165.      * This is the number returned by a recent call to 
  166.      * {@link DataCallback#refreshData()}. 
  167.      * 
  168.      * @return Number of items. 
  169.      */  
  170.     public int getItemCount() {  
  171.         return mItemCount;  
  172.     }  
  173.   
  174.     void updateRange() {  
  175.         mViewCallback.getItemRangeInto(mTmpRange);  
  176.         if (mTmpRange[0] > mTmpRange[1] || mTmpRange[0] < 0) {  
  177.             return;  
  178.         }  
  179.         if (mTmpRange[1] >= mItemCount) {  
  180.             // Invalid range may arrive soon after the refresh.  
  181.             return;  
  182.         }  
  183.   
  184.         if (!mAllowScrollHints) {  
  185.             mScrollHint = ViewCallback.HINT_SCROLL_NONE;  
  186.         } else if (mTmpRange[0] > mPrevRange[1] || mPrevRange[0] > mTmpRange[1]) {  
  187.             // Ranges do not intersect, long leap not a scroll.  
  188.             mScrollHint = ViewCallback.HINT_SCROLL_NONE;  
  189.         } else if (mTmpRange[0] < mPrevRange[0]) {  
  190.             mScrollHint = ViewCallback.HINT_SCROLL_DESC;  
  191.         } else if (mTmpRange[0] > mPrevRange[0]) {  
  192.             mScrollHint = ViewCallback.HINT_SCROLL_ASC;  
  193.         }  
  194.   
  195.         mPrevRange[0] = mTmpRange[0];  
  196.         mPrevRange[1] = mTmpRange[1];  
  197.   
  198.         mViewCallback.extendRangeInto(mTmpRange, mTmpRangeExtended, mScrollHint);  
  199.         mTmpRangeExtended[0] = Math.min(mTmpRange[0], Math.max(mTmpRangeExtended[0], 0));  
  200.         mTmpRangeExtended[1] =  
  201.                 Math.max(mTmpRange[1], Math.min(mTmpRangeExtended[1], mItemCount - 1));  
  202.   
  203.         mBackgroundProxy.updateRange(mTmpRange[0], mTmpRange[1],  
  204.                 mTmpRangeExtended[0], mTmpRangeExtended[1], mScrollHint);  
  205.     }  
  206.   
  207.     private final ThreadUtil.MainThreadCallback<T>  
  208.             mMainThreadCallback = new ThreadUtil.MainThreadCallback<T>() {  
  209.         @Override  
  210.         public void updateItemCount(int generation, int itemCount) {  
  211.             if (DEBUG) {  
  212.                 log(”updateItemCount: size=%d, gen #%d”, itemCount, generation);  
  213.             }  
  214.             if (!isRequestedGeneration(generation)) {  
  215.                 return;  
  216.             }  
  217.             mItemCount = itemCount;  
  218.             mViewCallback.onDataRefresh();  
  219.             mDisplayedGeneration = mRequestedGeneration;  
  220.             recycleAllTiles();  
  221.   
  222.             mAllowScrollHints = false;  // Will be set to true after a first real scroll.  
  223.             // There will be no scroll event if the size change does not affect the current range.  
  224.             updateRange();  
  225.         }  
  226.   
  227.         @Override  
  228.         public void addTile(int generation, TileList.Tile<T> tile) {  
  229.             if (!isRequestedGeneration(generation)) {  
  230.                 if (DEBUG) {  
  231.                     log(”recycling an older generation tile @%d”, tile.mStartPosition);  
  232.                 }  
  233.                 mBackgroundProxy.recycleTile(tile);  
  234.                 return;  
  235.             }  
  236.             TileList.Tile<T> duplicate = mTileList.addOrReplace(tile);  
  237.             if (duplicate != null) {  
  238.                 Log.e(TAG, ”duplicate tile @” + duplicate.mStartPosition);  
  239.                 mBackgroundProxy.recycleTile(duplicate);  
  240.             }  
  241.             if (DEBUG) {  
  242.                 log(”gen #%d, added tile @%d, total tiles: %d”,  
  243.                         generation, tile.mStartPosition, mTileList.size());  
  244.             }  
  245.             int endPosition = tile.mStartPosition + tile.mItemCount;  
  246.             int index = 0;  
  247.             while (index < mMissingPositions.size()) {  
  248.                 final int position = mMissingPositions.keyAt(index);  
  249.                 if (tile.mStartPosition <= position && position < endPosition) {  
  250.                     mMissingPositions.removeAt(index);  
  251.                     mViewCallback.onItemLoaded(position);  
  252.                 } else {  
  253.                     index++;  
  254.                 }  
  255.             }  
  256.         }  
  257.   
  258.         @Override  
  259.         public void removeTile(int generation, int position) {  
  260.             if (!isRequestedGeneration(generation)) {  
  261.                 return;  
  262.             }  
  263.             TileList.Tile<T> tile = mTileList.removeAtPos(position);  
  264.             if (tile == null) {  
  265.                 Log.e(TAG, ”tile not found @” + position);  
  266.                 return;  
  267.             }  
  268.             if (DEBUG) {  
  269.                 log(”recycling tile @%d, total tiles: %d”, tile.mStartPosition, mTileList.size());  
  270.             }  
  271.             mBackgroundProxy.recycleTile(tile);  
  272.         }  
  273.   
  274.         private void recycleAllTiles() {  
  275.             if (DEBUG) {  
  276.                 log(”recycling all %d tiles”, mTileList.size());  
  277.             }  
  278.             for (int i = 0; i < mTileList.size(); i++) {  
  279.                 mBackgroundProxy.recycleTile(mTileList.getAtIndex(i));  
  280.             }  
  281.             mTileList.clear();  
  282.         }  
  283.   
  284.         private boolean isRequestedGeneration(int generation) {  
  285.             return generation == mRequestedGeneration;  
  286.         }  
  287.     };  
  288.   
  289.     private final ThreadUtil.BackgroundCallback<T>  
  290.             mBackgroundCallback = new ThreadUtil.BackgroundCallback<T>() {  
  291.   
  292.         private TileList.Tile<T> mRecycledRoot;  
  293.   
  294.         final SparseBooleanArray mLoadedTiles = new SparseBooleanArray();  
  295.   
  296.         private int mGeneration;  
  297.         private int mItemCount;  
  298.   
  299.         private int mFirstRequiredTileStart;  
  300.         private int mLastRequiredTileStart;  
  301.   
  302.         @Override  
  303.         public void refresh(int generation) {  
  304.             mGeneration = generation;  
  305.             mLoadedTiles.clear();  
  306.             mItemCount = mDataCallback.refreshData();  
  307.             mMainThreadProxy.updateItemCount(mGeneration, mItemCount);  
  308.         }  
  309.   
  310.         @Override  
  311.         public void updateRange(int rangeStart, int rangeEnd, int extRangeStart, int extRangeEnd,  
  312.                 int scrollHint) {  
  313.             if (DEBUG) {  
  314.                 log(”updateRange: %d..%d extended to %d..%d, scroll hint: %d”,  
  315.                         rangeStart, rangeEnd, extRangeStart, extRangeEnd, scrollHint);  
  316.             }  
  317.   
  318.             if (rangeStart > rangeEnd) {  
  319.                 return;  
  320.             }  
  321.   
  322.             final int firstVisibleTileStart = getTileStart(rangeStart);  
  323.             final int lastVisibleTileStart = getTileStart(rangeEnd);  
  324.   
  325.             mFirstRequiredTileStart = getTileStart(extRangeStart);  
  326.             mLastRequiredTileStart = getTileStart(extRangeEnd);  
  327.             if (DEBUG) {  
  328.                 log(”requesting tile range: %d..%d”,  
  329.                         mFirstRequiredTileStart, mLastRequiredTileStart);  
  330.             }  
  331.   
  332.             // All pending tile requests are removed by ThreadUtil at this point.  
  333.             // Re-request all required tiles in the most optimal order.  
  334.             if (scrollHint == ViewCallback.HINT_SCROLL_DESC) {  
  335.                 requestTiles(mFirstRequiredTileStart, lastVisibleTileStart, scrollHint, true);  
  336.                 requestTiles(lastVisibleTileStart + mTileSize, mLastRequiredTileStart, scrollHint,  
  337.                         false);  
  338.             } else {  
  339.                 requestTiles(firstVisibleTileStart, mLastRequiredTileStart, scrollHint, false);  
  340.                 requestTiles(mFirstRequiredTileStart, firstVisibleTileStart - mTileSize, scrollHint,  
  341.                         true);  
  342.             }  
  343.         }  
  344.   
  345.         private int getTileStart(int position) {  
  346.             return position - position % mTileSize;  
  347.         }  
  348.   
  349.         private void requestTiles(int firstTileStart, int lastTileStart, int scrollHint,  
  350.                                   boolean backwards) {  
  351.             for (int i = firstTileStart; i <= lastTileStart; i += mTileSize) {  
  352.                 int tileStart = backwards ? (lastTileStart + firstTileStart - i) : i;  
  353.                 if (DEBUG) {  
  354.                     log(”requesting tile @%d”, tileStart);  
  355.                 }  
  356.                 mBackgroundProxy.loadTile(tileStart, scrollHint);  
  357.             }  
  358.         }  
  359.   
  360.         @Override  
  361.         public void loadTile(int position, int scrollHint) {  
  362.             if (isTileLoaded(position)) {  
  363.                 if (DEBUG) {  
  364.                     log(”already loaded tile @%d”, position);  
  365.                 }  
  366.                 return;  
  367.             }  
  368.             TileList.Tile<T> tile = acquireTile();  
  369.             tile.mStartPosition = position;  
  370.             tile.mItemCount = Math.min(mTileSize, mItemCount - tile.mStartPosition);  
  371.             mDataCallback.fillData(tile.mItems, tile.mStartPosition, tile.mItemCount);  
  372.             flushTileCache(scrollHint);  
  373.             addTile(tile);  
  374.         }  
  375.   
  376.         @Override  
  377.         public void recycleTile(TileList.Tile<T> tile) {  
  378.             if (DEBUG) {  
  379.                 log(”recycling tile @%d”, tile.mStartPosition);  
  380.             }  
  381.             mDataCallback.recycleData(tile.mItems, tile.mItemCount);  
  382.   
  383.             tile.mNext = mRecycledRoot;  
  384.             mRecycledRoot = tile;  
  385.         }  
  386.   
  387.         private TileList.Tile<T> acquireTile() {  
  388.             if (mRecycledRoot != null) {  
  389.                 TileList.Tile<T> result = mRecycledRoot;  
  390.                 mRecycledRoot = mRecycledRoot.mNext;  
  391.                 return result;  
  392.             }  
  393.             return new TileList.Tile<T>(mTClass, mTileSize);  
  394.         }  
  395.   
  396.         private boolean isTileLoaded(int position) {  
  397.             return mLoadedTiles.get(position);  
  398.         }  
  399.   
  400.         private void addTile(TileList.Tile<T> tile) {  
  401.             mLoadedTiles.put(tile.mStartPosition, true);  
  402.             mMainThreadProxy.addTile(mGeneration, tile);  
  403.             if (DEBUG) {  
  404.                 log(”loaded tile @%d, total tiles: %d”, tile.mStartPosition, mLoadedTiles.size());  
  405.             }  
  406.         }  
  407.   
  408.         private void removeTile(int position) {  
  409.             mLoadedTiles.delete(position);  
  410.             mMainThreadProxy.removeTile(mGeneration, position);  
  411.             if (DEBUG) {  
  412.                 log(”flushed tile @%d, total tiles: %s”, position, mLoadedTiles.size());  
  413.             }  
  414.         }  
  415.   
  416.         private void flushTileCache(int scrollHint) {  
  417.             final int cacheSizeLimit = mDataCallback.getMaxCachedTiles();  
  418.             while (mLoadedTiles.size() >= cacheSizeLimit) {  
  419.                 int firstLoadedTileStart = mLoadedTiles.keyAt(0);  
  420.                 int lastLoadedTileStart = mLoadedTiles.keyAt(mLoadedTiles.size() - 1);  
  421.                 int startMargin = mFirstRequiredTileStart - firstLoadedTileStart;  
  422.                 int endMargin = lastLoadedTileStart - mLastRequiredTileStart;  
  423.                 if (startMargin > 0 && (startMargin >= endMargin ||  
  424.                         (scrollHint == ViewCallback.HINT_SCROLL_ASC))) {  
  425.                     removeTile(firstLoadedTileStart);  
  426.                 } else if (endMargin > 0 && (startMargin < endMargin ||  
  427.                         (scrollHint == ViewCallback.HINT_SCROLL_DESC))){  
  428.                     removeTile(lastLoadedTileStart);  
  429.                 } else {  
  430.                     // Could not flush on either side, bail out.  
  431.                     return;  
  432.                 }  
  433.             }  
  434.         }  
  435.   
  436.         private void log(String s, Object… args) {  
  437.             Log.d(TAG, ”[BKGR] ” + String.format(s, args));  
  438.         }  
  439.     };  
  440.   
  441.     /** 
  442.      * The callback that provides data access for {@link AsyncListUtil}. 
  443.      * 
  444.      * <p> 
  445.      * All methods are called on the background thread. 
  446.      */  
  447.     public static abstract class DataCallback<T> {  
  448.   
  449.         /** 
  450.          * Refresh the data set and return the new data item count. 
  451.          * 
  452.          * <p> 
  453.          * If the data is being accessed through {@link android.database.Cursor} this is where 
  454.          * the new cursor should be created. 
  455.          * 
  456.          * @return Data item count. 
  457.          */  
  458.         @WorkerThread  
  459.         public abstract int refreshData();  
  460.   
  461.         /** 
  462.          * Fill the given tile. 
  463.          * 
  464.          * <p> 
  465.          * The provided tile might be a recycled tile, in which case it will already have objects. 
  466.          * It is suggested to re-use these objects if possible in your use case. 
  467.          * 
  468.          * @param startPosition The start position in the list. 
  469.          * @param itemCount The data item count. 
  470.          * @param data The data item array to fill into. Should not be accessed beyond 
  471.          *             <code>itemCount</code>. 
  472.          */  
  473.         @WorkerThread  
  474.         public abstract void fillData(T[] data, int startPosition, int itemCount);  
  475.   
  476.         /** 
  477.          * Recycle the objects created in {@link #fillData} if necessary. 
  478.          * 
  479.          * 
  480.          * @param data Array of data items. Should not be accessed beyond <code>itemCount</code>. 
  481.          * @param itemCount The data item count. 
  482.          */  
  483.         @WorkerThread  
  484.         public void recycleData(T[] data, int itemCount) {  
  485.         }  
  486.   
  487.         /** 
  488.          * Returns tile cache size limit (in tiles). 
  489.          * 
  490.          * <p> 
  491.          * The actual number of cached tiles will be the maximum of this value and the number of 
  492.          * tiles that is required to cover the range returned by 
  493.          * {@link ViewCallback#extendRangeInto(int[], int[], int)}. 
  494.          * <p> 
  495.          * For example, if this method returns 10, and the most 
  496.          * recent call to {@link ViewCallback#extendRangeInto(int[], int[], int)} returned 
  497.          * {100, 179}, and the tile size is 5, then the maximum number of cached tiles will be 16. 
  498.          * <p> 
  499.          * However, if the tile size is 20, then the maximum number of cached tiles will be 10. 
  500.          * <p> 
  501.          * The default implementation returns 10. 
  502.          * 
  503.          * @return Maximum cache size. 
  504.          */  
  505.         @WorkerThread  
  506.         public int getMaxCachedTiles() {  
  507.             return 10;  
  508.         }  
  509.     }  
  510.   
  511.     /** 
  512.      * The callback that links {@link AsyncListUtil} with the list view. 
  513.      * 
  514.      * <p> 
  515.      * All methods are called on the main thread. 
  516.           */  
  517.     public static abstract class ViewCallback {  
  518.   
  519.         /** 
  520.          * No scroll direction hint available. 
  521.          */  
  522.         public static final int HINT_SCROLL_NONE = 0;  
  523.   
  524.         /** 
  525.          * Scrolling in descending order (from higher to lower positions in the order of the backing 
  526.          * storage). 
  527.          */  
  528.         public static final int HINT_SCROLL_DESC = 1;  
  529.   
  530.         /** 
  531.          * Scrolling in ascending order (from lower to higher positions in the order of the backing 
  532.          * storage). 
  533.          */  
  534.         public static final int HINT_SCROLL_ASC = 2;  
  535.   
  536.         /** 
  537.          * Compute the range of visible item positions. 
  538.          * <p> 
  539.          * outRange[0] is the position of the first visible item (in the order of the backing 
  540.          * storage). 
  541.          * <p> 
  542.          * outRange[1] is the position of the last visible item (in the order of the backing 
  543.          * storage). 
  544.          * <p> 
  545.          * Negative positions and positions greater or equal to {@link #getItemCount} are invalid. 
  546.          * If the returned range contains invalid positions it is ignored (no item will be loaded). 
  547.          * 
  548.          * @param outRange The visible item range. 
  549.          */  
  550.         @UiThread  
  551.         public abstract void getItemRangeInto(int[] outRange);  
  552.   
  553.         /** 
  554.          * Compute a wider range of items that will be loaded for smoother scrolling. 
  555.          * 
  556.          * <p> 
  557.          * If there is no scroll hint, the default implementation extends the visible range by half 
  558.          * its length in both directions. If there is a scroll hint, the range is extended by 
  559.          * its full length in the scroll direction, and by half in the other direction. 
  560.          * <p> 
  561.          * For example, if <code>range</code> is <code>{100, 200}</code> and <code>scrollHint</code> 
  562.          * is {@link #HINT_SCROLL_ASC}, then <code>outRange</code> will be <code>{50, 300}</code>. 
  563.          * <p> 
  564.          * However, if <code>scrollHint</code> is {@link #HINT_SCROLL_NONE}, then 
  565.          * <code>outRange</code> will be <code>{50, 250}</code> 
  566.          * 
  567.          * @param range Visible item range. 
  568.          * @param outRange Extended range. 
  569.          * @param scrollHint The scroll direction hint. 
  570.          */  
  571.         @UiThread  
  572.         public void extendRangeInto(int[] range, int[] outRange, int scrollHint) {  
  573.             final int fullRange = range[1] - range[0] + 1;  
  574.             final int halfRange = fullRange / 2;  
  575.             outRange[0] = range[0] - (scrollHint == HINT_SCROLL_DESC ? fullRange : halfRange);  
  576.             outRange[1] = range[1] + (scrollHint == HINT_SCROLL_ASC ? fullRange : halfRange);  
  577.         }  
  578.   
  579.         /** 
  580.          * Called when the entire data set has changed. 
  581.          */  
  582.         @UiThread  
  583.         public abstract void onDataRefresh();  
  584.   
  585.         /** 
  586.          * Called when an item at the given position is loaded. 
  587.          * @param position Item position. 
  588.          */  
  589.         @UiThread  
  590.         public abstract void onItemLoaded(int position);  
  591.     }  
  592. }  
/*
 * 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);
    }
}

转自:http://blog.csdn.net/zhangphil/article/details/78603499

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值