Android 實現(xiàn)自己的RecyclerView加載更多

很多時候撬碟,項目中都會有列表加載更多的場景图甜,這次我們讓RecyclerView輕松擁有加載更多的功能。雖然已有許多類似的輪子囚灼,但有的功能過于復(fù)雜骆膝,其實很多都用不到,所以不妨打造更適合自己的輪子灶体。

我們的RecyclerView加載更多是通過其Adapter子類實現(xiàn)的阅签,接下來我們一步步的構(gòu)建Adapter吧!

1赃春、編寫通用的Adapter愉择、ViewHolder

一般情況下使用Adapter都要為其創(chuàng)建一個ViewHolder,既然要編寫通用的Adapter织中,首先要有一個通用的ViewHolder:

public class ViewHolder extends RecyclerView.ViewHolder {
    private SparseArray<View> mViews;
    private View mConvertView;

    private ViewHolder(View itemView) {
        super(itemView);
        mConvertView = itemView;
        mViews = new SparseArray<>();
    }

    public static ViewHolder create(Context context, int layoutId, ViewGroup parent) {
        View itemView = LayoutInflater.from(context).inflate(layoutId, parent, false);
        return new ViewHolder(itemView);
    }

    public static ViewHolder create(View itemView) {
        return new ViewHolder(itemView);
    }

    public <T extends View> T getView(int viewId) {
        View view = mViews.get(viewId);
        if (view == null) {
            view = mConvertView.findViewById(viewId);
            mViews.put(viewId, view);
        }
        return (T) view;
    }

    public View getConvertView() {
        return mConvertView;
    }

    public void setText(int viewId, String text) {
        TextView textView = getView(viewId);
        textView.setText(text);
    }
    .......省略其它輔助方法.........
}

我們自定義的ViewHolder類可以根據(jù)布局文件的id或具體的itemView返回一個ViewHolder對象锥涕,并用SparseArray來緩存我們itemView中的子View,避免每次都要去解析子View狭吼,同時提供相關(guān)輔助方法設(shè)置itemView的內(nèi)容层坠。有了ViewHolder,接下來編寫Adapter就簡單了:

public abstract class BaseAdapter<T> extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    public static final int TYPE_COMMON_VIEW = 100001;
  
    private OnItemClickListeners<T> mItemClickListener;

    protected Context mContext;
    protected List<T> mDatas;

    protected abstract void convert(ViewHolder holder, T data);

    protected abstract int getItemLayoutId();

    public BaseAdapter(Context context, List<T> datas) {
        mContext = context;
        mDatas = datas == null ? new ArrayList<T>() : datas;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        ViewHolder viewHolder = null;
        switch (viewType) {
            case TYPE_COMMON_VIEW:
                viewHolder = ViewHolder.create(mContext, getItemLayoutId(), parent);
                break;
        }
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        switch (holder.getItemViewType()) {
            case TYPE_COMMON_VIEW:
                bindCommonItem(holder, position);
                break;
        }
    }

    private void bindCommonItem(RecyclerView.ViewHolder holder, final int position) {
        final ViewHolder viewHolder = (ViewHolder) holder;
        convert(viewHolder, mDatas.get(position));
        viewHolder.getConvertView().setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                mItemClickListener.onItemClick(viewHolder, mDatas.get(position), position);
            }
        });
    }

    @Override
    public int getItemCount() {
        return mDatas.size();
    }

    @Override
    public int getItemViewType(int position) {
        return TYPE_COMMON_VIEW;
    }

    public T getItem(int position) {
        if (mDatas.isEmpty()) {
            return null;
        }
        return mDatas.get(position);
    }

    public void setOnItemClickListener(OnItemClickListeners<T> itemClickListener) {
        mItemClickListener = itemClickListener;
    }
}

很簡單刁笙,繼承RecyclerView.Adapter破花,重寫相關(guān)方法谦趣,提供了getItemLayoutId()convert()兩個抽象方法供BaseAdapter的子類實現(xiàn)座每,來初始化item的布局id前鹅,以及item內(nèi)容,同時通過OnItemClickListeners接口為item綁定點擊事件峭梳。

編寫好了Adapter舰绘,我們在其構(gòu)造方法中添加一個參數(shù)isOpenLoadMore,來表示是否開啟加載更多:

public BaseAdapter(Context context, List<T> datas, boolean isOpenLoadMore) {
        mContext = context;
        mDatas = datas == null ? new ArrayList<T>() : datas;
        mOpenLoadMore = isOpenLoadMore;
    }

這樣初級版本的Adapter就完成了葱椭。

2捂寿、添加Footer View

接下來就要添加Footer View,這樣才能有加載更多的視覺效果么孵运。其實很簡單秦陋,如果當(dāng)前item的position滿足如下條件:

private boolean isFooterView(int position) {
        return mOpenLoadMore && position >= getItemCount() - 1;
    }

即已經(jīng)開啟加載更多、當(dāng)前position在列表的尾部治笨,則在getItemViewType()返回

@Override
    public int getItemViewType(int position) {
        if (isFooterView(position)) {
            return TYPE_FOOTER_VIEW;
        }
    }

之后會創(chuàng)建Footer View對應(yīng)的ViewHolder:

@Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        ViewHolder viewHolder = null;
        switch (viewType) {
            case TYPE_FOOTER_VIEW:
                if (mFooterLayout == null) {
                    mFooterLayout = new RelativeLayout(mContext);
                }
                viewHolder = ViewHolder.create(mFooterLayout);
                break;
        }
        return viewHolder;
    }

可以看到mFooterLayout是一個空的Container驳概,因為要根據(jù)加載更多對應(yīng)的狀態(tài)來更新mFooterLayout,這個稍后再說大磺。

這樣Footer View就添加完了嗎抡句?當(dāng)然沒有探膊,我們需要針對StaggeredGridLayoutManager杠愧、GridLayoutManager模式分別重寫onViewAttachedToWindow()onAttachedToRecyclerView()方法逞壁,否則會出現(xiàn)Footer View不能在列表底部占據(jù)一行的問題:

@Override
    public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
        super.onViewAttachedToWindow(holder);
        if (isFooterView(holder.getLayoutPosition())) {
            ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();

            if (lp != null && lp instanceof StaggeredGridLayoutManager.LayoutParams) {
                StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp;
                p.setFullSpan(true);
            }
        }
    }

    @Override
    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
        super.onAttachedToRecyclerView(recyclerView);
        final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
        if (layoutManager instanceof GridLayoutManager) {
            final GridLayoutManager gridManager = ((GridLayoutManager) layoutManager);
            gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                @Override
                public int getSpanSize(int position) {
                    if (isFooterView(position)) {
                        return gridManager.getSpanCount();
                    }
                    return 1;
                }
            });
        }
    }

到此無論是那種形式的列表都能正常添加Footer View了流济。

3、判斷列表是否滾動到了底部

按照常理腌闯,只有滑動到列表的底部才會觸發(fā)加載更多的操作绳瘟,之前提到了onAttachedToRecyclerView()方法,通過該方法可以得到Adapter所綁定的RecyclerView姿骏,這樣就能監(jiān)聽RecyclerView的滾動事件糖声,進(jìn)而判斷列表是否滾動了底部:

private void startLoadMore(RecyclerView recyclerView, final RecyclerView.LayoutManager layoutManager) {
        if (!mOpenLoadMore || mLoadMoreListener == null) {
            return;
        }

        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                    if (!isAutoLoadMore && findLastVisibleItemPosition(layoutManager) + 1 == getItemCount()) {
                        scrollLoadMore();
                    }
                }
            }

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                if (isAutoLoadMore && findLastVisibleItemPosition(layoutManager) + 1 == getItemCount()) {
                    scrollLoadMore();
                } else if (isAutoLoadMore) {
                    isAutoLoadMore = false;
                }
            }
        });
    }

我們單獨封裝了startLoadMore()方法,當(dāng)列表滾動狀態(tài)改變會回調(diào)onScrollStateChanged()方法分瘦,如果狀態(tài)為SCROLL_STATE_IDLE蘸泻,并且當(dāng)前可見的item位置為列表最后一項,則開始加載更多數(shù)據(jù)嘲玫。這里還重寫了onScrolled()方法悦施,當(dāng)列表滾動結(jié)束后會回調(diào),重寫該方法有什么用呢去团?如果初始item不滿一屏幕抡诞,則可在該方法中加載更多數(shù)據(jù)穷蛹,直到item占滿一屏幕,也就自動加載更多昼汗。我們用isAutoLoadMore來區(qū)分這種情況肴熏,如果isAutoLoadMore為true,則Footer View可見則自動加載更多顷窒。

再看一下scrollLoadMore()方法:

private void scrollLoadMore() {
        if (mFooterLayout.getChildAt(0) == mLoadingView) {
            mLoadMoreListener.onLoadMore(false);
        }
    }

如果當(dāng)前的Footer View 是正在加載的狀態(tài)扮超,則調(diào)用OnLoadMoreListener接口的onLoadMore()方法進(jìn)行具體的加載操作,該方法有一個boolean類型的參數(shù)蹋肮,表示是否重新加載出刷,因為存在加載失敗的情況,這樣可方便使用坯辩。

4馁龟、更新Footer View布局樣式

到這里,我們已經(jīng)明確了加載更多操作的觸發(fā)時機(jī)漆魔,接下來就是在加載更多的時候來更新Footer View坷檩,我們定義了三種狀態(tài):加載中、加載失敗改抡、加載結(jié)束矢炼,通過如下方法將對應(yīng)狀態(tài)的View或布局id添加到Footer View中:

public void setLoadingView(int loadingId) {
        setLoadingView(Util.inflate(mContext, loadingId));
    }

public void setLoadFailedView(int loadFailedId) {
        setLoadFailedView(Util.inflate(mContext, loadFailedId));
    }

public void setLoadEndView(int loadEndId) {
        setLoadEndView(Util.inflate(mContext, loadEndId));
    }

這三個方法時是通過布局id來給Footer View設(shè)置新樣式,當(dāng)然還有通過View來設(shè)置的重載方法阿纤。在初始化Adapter時可以調(diào)用setLoadingView()來設(shè)置加載中的Footer View樣式句灌,如果加載失敗了可調(diào)用setLoadFailedView()、如果加載結(jié)束沒有更多數(shù)據(jù)則可以調(diào)用setLoadEndView()設(shè)對應(yīng)的布局樣式欠拾。其實就是先移除mFooterLayout的子View胰锌,然后將新的布局添加進(jìn)去。

5藐窄、添加EmptyView

考慮一種情況资昧,如果初始化時,需要先從網(wǎng)絡(luò)請求數(shù)據(jù)荆忍,然后再更新列表格带,則一般需要有一個加載提示,所以我們有必要將這個小功能也封裝到Adapter中刹枉,這樣就省去了修改界面布局或者手動顯示叽唱、隱藏加載提示的步驟。
實現(xiàn)也很簡單嘶卧,先看如下代碼:

@Override
    public int getItemCount() {
        if (mDatas.isEmpty() && mEmptyView != null) {
            return 1;
        }
    }

如果mData為空尔觉,且設(shè)置了EmptyView則getItemCount()直接返回1。同理返回的item類型為TYPE_EMPTY_VIEW芥吟,代表EmptyView:

@Override
    public int getItemViewType(int position) {
        if (mDatas.isEmpty() && mEmptyView != null) {
            return TYPE_EMPTY_VIEW;
        }
    }

onCreateViewHolder()方法中會創(chuàng)建對應(yīng)的ViewHolder侦铜。

@Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        ViewHolder viewHolder = null;
        switch (viewType) {
            case TYPE_EMPTY_VIEW:
                viewHolder = ViewHolder.create(mEmptyView);
                break;
        }
        return viewHolder;
    }

同時提供方法在初始化Adapter時設(shè)置EmptyView:

public void setEmptyView(View emptyView) {
        mEmptyView = emptyView;
    }

6专甩、具體使用

完成了封裝,來看看具體的使用钉稍,首先創(chuàng)建一個RefreshAdapter繼承我們的BaseAdapter:

public class RefreshAdapter extends BaseAdapter<String> {

    public RefreshAdapter(Context context, List<String> datas, boolean isLoadMore) {
        super(context, datas, isLoadMore);
    }

    @Override
    protected void convert(ViewHolder holder, final String data) {
        holder.setText(R.id.item_title, data);
        holder.setOnClickListener(R.id.item_btn, new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(mContext, "我是" + data + "的button", Toast.LENGTH_SHORT).show();
            }
        });
    }

    @Override
    protected int getItemLayoutId() {
        return R.layout.item_layout;
    }
}

getItemLayoutId()中返回item布局id涤躲,在convert()中初始化item的內(nèi)容。有了RefreshAdapter贡未,接下來看Activity的操作:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mRecyclerView = (RecyclerView) findViewById(R.id.recyclerview);

        //初始化adapter
        mAdapter = new RefreshAdapter(this, null, true);

        //初始化EmptyView
        View emptyView = LayoutInflater.from(this).inflate(R.layout.empty_layout, (ViewGroup) mRecyclerView.getParent(), false);
        mAdapter.setEmptyView(emptyView);

        //初始化 開始加載更多的loading View
        mAdapter.setLoadingView(R.layout.load_loading_layout);

        //設(shè)置加載更多觸發(fā)的事件監(jiān)聽
        mAdapter.setOnLoadMoreListener(new OnLoadMoreListener() {
            @Override
            public void onLoadMore(boolean isReload) {
                loadMore();
            }
        });

        //設(shè)置item點擊事件監(jiān)聽
        mAdapter.setOnItemClickListener(new OnItemClickListeners<String>() {

            @Override
            public void onItemClick(ViewHolder viewHolder, String data, int position) {
                Toast.makeText(MainActivity.this, data, Toast.LENGTH_SHORT).show();
            }
        });

        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
        mRecyclerView.setLayoutManager(layoutManager);

        mRecyclerView.setAdapter(mAdapter);


        //延時3s刷新列表
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                List<String> data = new ArrayList<>();
                for (int i = 0; i < 12; i++) {
                    data.add("item--" + i);
                }
                //刷新數(shù)據(jù)
                mAdapter.setNewData(data);
            }
        }, 3000);
    }

注釋已經(jīng)很詳細(xì)了种樱,就不多說了。其中loadMore()方法如下:

private void loadMore() {

        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {

                if (mAdapter.getItemCount() > 15 && isFailed) {
                    isFailed = false;
                    //加載失敗俊卤,更新footer view提示
                    mAdapter.setLoadFailedView(R.layout.load_failed_layout);
                } else if (mAdapter.getItemCount() > 17) {
                    //加載完成嫩挤,更新footer view提示
                    mAdapter.setLoadEndView(R.layout.load_end_layout);
                } else {
                    final List<String> data = new ArrayList<>();
                    for (int i = 0; i < 2; i++) {
                        data.add("item--" + (mAdapter.getItemCount() + i - 1));
                    }
                    //刷新數(shù)據(jù)
                    mAdapter.setLoadMoreData(data);
                }
            }
        }, 2000);
    }

就是延時2s更新列表數(shù)據(jù),同時人為模擬加載失敗和結(jié)束的情況消恍。

7岂昭、效果

運行后,看具體的效果:

EmptyView
loading
load_failed
load_end
auto_load

PS:更新

(1)重構(gòu)基類繼承關(guān)系
(2)支持多種類型的Item View


創(chuàng)建只有一種類型的Item View的Adapter時狠怨,直接繼承CommonBaseAdapter類即可约啊,其它操作不變。

創(chuàng)建有多種類型的Item View的Adapter時時佣赖,繼承MultiBaseAdapter即可恰矩,實例如下:

public class MultiRefreshAdapter extends MultiBaseAdapter<String> {

    public MultiRefreshAdapter(Context context, List<String> datas, boolean isOpenLoadMore) {
        super(context, datas, isOpenLoadMore);
    }

    @Override
    protected void convert(ViewHolder holder, final String data, int viewType) {
        if (viewType == 0) {
            holder.setText(R.id.item_title, data);
            holder.setOnClickListener(R.id.item_btn, new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    Toast.makeText(mContext, "我是" + data + "的button", Toast.LENGTH_SHORT).show();
                }
            });
        } else {
            holder.setText(R.id.item_title1, data);
        }
    }

    @Override
    protected int getItemLayoutId(int viewType) {
        if (viewType == 0) {
            return R.layout.item_layout;
        }
        return R.layout.item_layout1;
    }

    @Override
    protected int getViewType(int position, String data) {
        if (position % 2 == 0) {
            return 0;
        }
        return 1;
    }
}

設(shè)置Item點擊事件時,通過如下方法:

mAdapter.setOnMultiItemClickListener(new OnMultiItemClickListeners<String>() {
            @Override
            public void onItemClick(ViewHolder viewHolder, String data, int position, int viewType) {
                
            }
        });

其它的操作不變憎蛤。效果就不貼了外傅,可通過源碼查看。

2016.12.6更新

使用EmptyView時蹂午,初始加載無數(shù)據(jù)可移除EmptyView栏豺,或添加新ReloadView以便進(jìn)行重新加載彬碱、提示等操作豆胸。

2017.7.4更新

支持Adapter重置、完善使用方式

2017.12.22更新

  1. 支持給RecyclerView添加HeaderView
  2. 自動判斷是否正在加載更多巷疼,避免重復(fù)加載

更多詳情可參考源碼晚胡,不合理的地方還求反饋!
?源碼戳這里

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末嚼沿,一起剝皮案震驚了整個濱河市估盘,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌骡尽,老刑警劉巖遣妥,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異攀细,居然都是意外死亡箫踩,警方通過查閱死者的電腦和手機(jī)爱态,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來境钟,“玉大人锦担,你說我怎么就攤上這事】鳎” “怎么了洞渔?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長缚态。 經(jīng)常有香客問我磁椒,道長,這世上最難降的妖魔是什么玫芦? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任衷快,我火速辦了婚禮,結(jié)果婚禮上姨俩,老公的妹妹穿的比我還像新娘蘸拔。我一直安慰自己,他們只是感情好环葵,可當(dāng)我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布调窍。 她就那樣靜靜地躺著,像睡著了一般张遭。 火紅的嫁衣襯著肌膚如雪邓萨。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天菊卷,我揣著相機(jī)與錄音缔恳,去河邊找鬼。 笑死洁闰,一個胖子當(dāng)著我的面吹牛歉甚,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播扑眉,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼纸泄,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了腰素?” 一聲冷哼從身側(cè)響起聘裁,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎弓千,沒想到半個月后衡便,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年镣陕,在試婚紗的時候發(fā)現(xiàn)自己被綠了征唬。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡茁彭,死狀恐怖总寒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情理肺,我是刑警寧澤摄闸,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站妹萨,受9級特大地震影響年枕,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜乎完,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一熏兄、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧树姨,春花似錦摩桶、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至转晰,卻和暖如春芦拿,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背查邢。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工蔗崎, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人扰藕。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓缓苛,卻偏偏與公主長得像,于是被迫代替她去往敵國和親实胸。 傳聞我的和親對象是個殘疾皇子他嫡,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容