使用RecyclerView,一句代碼就夠了

前言

RecyclerView出來(lái)有好幾年了,它的重要性不言而喻。然而RecyclerView只提供了基本的View復(fù)用功能,相關(guān)功能如刷新、點(diǎn)擊等都需要開(kāi)發(fā)者自己實(shí)現(xiàn),每個(gè)項(xiàng)目實(shí)現(xiàn)一遍RecyclerView功能集成又無(wú)必要,因此出現(xiàn)了許多RecyclerView封裝的“輪子”,Github上一搜多如牛毛。

簡(jiǎn)介

輪子雖多,各有特點(diǎn)。有時(shí)候還是自己造的最適合,OneRecyclerView這個(gè)輪子的特點(diǎn)如下:

  • 用很少的代碼(主要Java代碼300多行)實(shí)現(xiàn)了RecyclerView集成的大部分功能,包括下拉刷新、加載更多、多種ViewType、多列顯示、自定義HeaderView和空數(shù)據(jù)EmptyView
  • 實(shí)現(xiàn)多種ViewType的方式很巧妙,不需要復(fù)雜的映射關(guān)系,不需要注冊(cè)類型,不需要反射
  • 一句代碼即可調(diào)用,多種ViewType也是如此

效果圖

orv_base.gif
orv_types.gif
orv_columns.gif
orv_empty.gif

使用方法

  1. 在布局文件中添加OneRecyclerView
<?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">

    <cc.rome753.demo.onerecycler.OneRecyclerView
        android:id="@+id/orv"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </cc.rome753.demo.onerecycler.OneRecyclerView>
</LinearLayout>
  1. 實(shí)現(xiàn)自定義ViewHolder

    class UserInfoVH extends OneVH<UserInfo> {

        public UserInfoVH(ViewGroup parent) {//1.設(shè)置item布局文件
            super(parent, R.layout.item_user_simple);
        }

        @Override
        public void bindView(int position, final UserInfo o) {//2.處理點(diǎn)擊事件和設(shè)置數(shù)據(jù)
            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Toast.makeText(v.getContext(), o.getName(), Toast.LENGTH_SHORT).show();
                }
            });
            TextView tvName = itemView.findViewById(R.id.tv_name);
            tvName.setText(o.getName());
        }
    }

包括item的布局文件、給item設(shè)置數(shù)據(jù)和點(diǎn)擊事件

  1. 一句代碼使用OneRecyclerView
        mOneRecyclerView.init(
                new SwipeRefreshLayout.OnRefreshListener() {
                    @Override
                    public void onRefresh() {
                        requestData(false);
                    }
                },
                new OneLoadingLayout.OnLoadMoreListener() {
                    @Override
                    public void onLoadMore() {
                        requestData(true);
                    }
                },
                new OnCreateVHListener() {
                    @Override
                    public OneVH onCreateHolder(ViewGroup parent) {
                        return new UserInfoVH(parent);
                    }

                    @Override
                    public boolean isCreate(int position, Object o) {
                        return position % 3 > 0;
                    }
                }
        );

調(diào)用OneRecyclerView的init()方法,傳入下拉刷新監(jiān)聽(tīng)、加載更多監(jiān)聽(tīng)和創(chuàng)建ViewHolder監(jiān)聽(tīng)即可。

OneRecyclerView的init()方法最后一個(gè)參數(shù)是可變參數(shù),針對(duì)多種ItemType情況:
實(shí)現(xiàn)多個(gè)ViewHolder,用OnCreateVHListener包裝并傳入

  1. 添加自定義header
        View header = View.inflate(this, R.layout.layout_header, null);
        mOneRecyclerView.addHeader(header);

可添加多個(gè)header,多次調(diào)用addHader()方法即可,header的顯示完全由自身控制

  1. 設(shè)置多列顯示的列數(shù)
        mOneRecyclerView.setSpanCount(3);

不設(shè)置默認(rèn)是1列;多列顯示與多種ViewType一般不會(huì)同時(shí)用到,根據(jù)具體需求選擇其一

原理分析

下拉刷新

使用官方的SwipeRefreshLayout

    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/srl_wrapper"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <android.support.v7.widget.RecyclerView
            android:id="@+id/rv_wrapper"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
        </android.support.v7.widget.RecyclerView>
    </android.support.v4.widget.SwipeRefreshLayout>

布局文件中使用SwipeRefreshLayout,在里面放入RecyclerView,然后setOnRefreshListener設(shè)置刷新監(jiān)聽(tīng)

加載更多

給RecyclerView.Adapter的ItemCount + 1,并使用一個(gè)單獨(dú)的ViewType。當(dāng)劃到底部時(shí),顯示一個(gè)loading布局;獲取到數(shù)據(jù)后再隱藏這個(gè)布局

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <ProgressBar
        android:layout_gravity="center"
        android:layout_width="30dp"
        android:layout_height="48dp" />

</FrameLayout>

這里只放了一個(gè)ProgressBar,可以自定義

ViewHolder封裝

對(duì)于一個(gè)通用的庫(kù)來(lái)說(shuō):

  1. 外部調(diào)用者使用的數(shù)據(jù)類型
  2. RecyclerView的Item布局
  3. Item加載數(shù)據(jù)的方式
  4. Item點(diǎn)擊事件的處理

這幾點(diǎn)都是不確定的,第一點(diǎn)需要使用泛型,即由調(diào)用者定義數(shù)據(jù)類型;后面三點(diǎn)都可以在ViewHolder中處理。這里封裝一個(gè)繼承自RecyclerView.ViewHolder的抽象類OneVH<T>,具體由調(diào)用者實(shí)現(xiàn)

public abstract class OneVH<T> extends RecyclerView.ViewHolder {

    public OneVH(View itemView) {
        super(itemView);
    }

    public OneVH(ViewGroup parent, int layoutRes) {
        super(LayoutInflater.from(parent.getContext()).inflate(layoutRes, parent, false));
    }

    public abstract void bindView(int position, T t);
}

一個(gè)簡(jiǎn)單的OneVH<T>實(shí)現(xiàn)類如下

    class TextVH extends OneVH<UserInfo> {

        public TextVH(ViewGroup parent) {//1.設(shè)置item布局文件
            super(parent, android.R.layout.simple_list_item_1);
        }

        @Override
        public void bindView(int position, final UserInfo o) {//2.處理點(diǎn)擊事件和設(shè)置數(shù)據(jù)
            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Toast.makeText(v.getContext(), o.getName(), Toast.LENGTH_SHORT).show();
                }
            });
            TextView tvName = itemView.findViewById(android.R.id.text1);
            tvName.setText(o.getName());
            tvName.setBackgroundColor(Color.GREEN);
        }
    }

OnCreateVHListener封裝

封裝好ViewHolder之后,并不是直接創(chuàng)建ViewHolder對(duì)象傳入Adapter的,因?yàn)锳dapter中的onCreateViewHolder方法創(chuàng)建ViewHolder的時(shí)機(jī)和數(shù)量是不確定的,所以需要定義一個(gè)接口OnCreateVHListener

public interface OnCreateVHListener<S extends OneVH, T>{
        /**
         * 創(chuàng)建ViewHolder
         * @param parent RecyclerView
         * @return S extends OneVH
         */
        S onCreateHolder(ViewGroup parent);
}

Adapter中需要?jiǎng)?chuàng)建ViewHolder時(shí),調(diào)用OnCreateVHListener的onCreateHolder方法,返回一個(gè)自定義的OneVH實(shí)現(xiàn)對(duì)象

    @Override
    public S onCreateViewHolder(ViewGroup parent, int viewType) {
        ...
        return onCreateVHListener.onCreateHolder(parent);
    }

因?yàn)镺nCreateVHListener能返回具體OneVH<T>實(shí)現(xiàn)對(duì)象,所以Adapter只依賴OnCreateVHListener,不依賴OneVH。也就是說(shuō)外部調(diào)用者只需要傳一個(gè)OnCreateVHListener實(shí)現(xiàn)類給OneRecyclerView就行(OneRecyclerView傳給Adapter)


經(jīng)過(guò)以上步驟,已經(jīng)能很方便地使用單一ViewType的RecyclerView了。調(diào)用者只要實(shí)現(xiàn)自己的ViewHolder,不需要關(guān)心其他。下面介紹在此基礎(chǔ)上多種ViewType的實(shí)現(xiàn)

實(shí)現(xiàn)多種ViewType

首先分析一下ViewType的原理。Adapter中跟ViewType相關(guān)的主要是getItemViewType()onCreateViewHolder()方法

    @Override
    public int getItemViewType(int position) {
    }

    @Override
    public S onCreateViewHolder(ViewGroup parent, int viewType) {
    }

getItemViewType()方法用于確定當(dāng)前位置的item屬于哪種類型,返回一個(gè)表示類型viewType的int值
onCreateViewHolder()方法則是根據(jù)類型viewType獲取對(duì)應(yīng)的ViewHolder

常規(guī)思路思考,這里需要記錄兩種對(duì)應(yīng)關(guān)系:

  1. 位置position與viewType的對(duì)應(yīng)關(guān)系
  2. viewType與ViewHolder的對(duì)應(yīng)關(guān)系

那就需要兩個(gè)Map來(lái)保存,也許再加一個(gè)類管理它們。能否將這兩個(gè)Map合并成一個(gè)?或者根本不用Map?

先考慮第二個(gè)對(duì)應(yīng)關(guān)系,由于前面已經(jīng)將ViewHolder類型綁定到OnCreateVHListener接口上,不同的viewType也就對(duì)應(yīng)了不同的OnCreateVHListener對(duì)象。比如有3種Item類型,那么就有3個(gè)OnCreateVHListener對(duì)象,這3個(gè)viewType用3個(gè)不同int值分別對(duì)應(yīng)。

其實(shí)用Map是一種冗余,3個(gè)或更多int值完全可以用0,1,2...表示,那么3個(gè)OnCreateVHListener可以直接用List<OnCreateVHListener>保存,每個(gè)OnCreateVHListener在List中的序號(hào)就是它的viewType!

接著考慮第一個(gè)對(duì)應(yīng)關(guān)系,根據(jù)position獲取viewType值,在有了List<OnCreateVHListener>的基礎(chǔ)上,viewType就是OnCreateVHListener在List中的序號(hào)。直接獲取這個(gè)序號(hào)并不好實(shí)現(xiàn),能否先獲取OnCreateVHListener,再遍歷List<OnCreateVHListener>獲得它的位置?

根據(jù)position獲取OnCreateVHListener也不方便,這個(gè)對(duì)應(yīng)關(guān)系是調(diào)用者定義的,需要給外部提供一種很自然的定義方式,而不是注冊(cè)類型或定義一個(gè)Manager類。其實(shí)可以不用考慮對(duì)應(yīng)關(guān)系,每個(gè)OnCreateVHListener只需要知道對(duì)應(yīng)的position是不是自己就行了。

這樣在OnCreateVHListener接口中添加一個(gè)isCreate(int position, T t)方法,參數(shù)是位置position和對(duì)應(yīng)位置的數(shù)據(jù),調(diào)用者通過(guò)這兩個(gè)參數(shù)判斷該位置是不是對(duì)應(yīng)的ViewHolder

public interface OnCreateVHListener<S extends OneVH, T>{
        /**
         * 創(chuàng)建ViewHolder
         * @param parent RecyclerView
         * @return S extends OneVH
         */
        S onCreateHolder(ViewGroup parent);

        /**
         * 根據(jù)當(dāng)前位置或數(shù)據(jù)判斷是否創(chuàng)建S類型的ViewHolder
         * @param position
         * @param t
         * @return
         */
        boolean isCreate(int position, T t);
}

在Adapter的getItemViewType方法中遍歷List<OnCreateVHListener>,調(diào)用isCreate()方法,如果結(jié)果是true,就返回當(dāng)前序號(hào),這個(gè)序號(hào)就是viewType


    @Override
    public int getItemViewType(int position) {
        ...
        int pos = position - headerVHList.size();
        T t = data.get(pos);

        for(int i = 0; i < listeners.size(); i++){
            OnCreateVHListener<S,T> listener = listeners.get(i);
            if(listener.isCreate(pos, t)){
                return i;
            }
        }
        return TYPE_NORMAL_MIN;
    }

最終,position、viewType、ViewHolder、OnCreateVHListener就全部關(guān)聯(lián)起來(lái)了。代價(jià)只是在Adapter中添加一個(gè)List<OnCreateVHListener>(初始化時(shí)傳進(jìn)來(lái))、OnCreateVHListener接口中添加一個(gè)方法。

雖然在getItemViewType方法里進(jìn)行了遍歷操作,但是考慮到99%的列表Item類型是個(gè)位數(shù),而且判斷類型不是耗時(shí)操作,帶來(lái)的性能影響可以忽略不計(jì)

OnCreateVHListener里面定義的兩個(gè)方法,不僅關(guān)聯(lián)了ViewHolder類型,還關(guān)聯(lián)了與position的對(duì)應(yīng)關(guān)系。外部調(diào)用者使用幾種Item類型,傳入幾個(gè)OnCreateVHListener實(shí)現(xiàn)類就行了。實(shí)際使用如下

        mOneRecyclerView.init(
                new SwipeRefreshLayout.OnRefreshListener() {
                    @Override
                    public void onRefresh() {
                        requestData(false);
                    }
                },
                new OneLoadingLayout.OnLoadMoreListener() {
                    @Override
                    public void onLoadMore() {
                        requestData(true);
                    }
                },
                new OnCreateVHListener() {
                    @Override
                    public OneVH onCreateHolder(ViewGroup parent) {
                        return new UserInfoVH(parent);
                    }

                    @Override
                    public boolean isCreate(int position, Object o) {
                        return position % 3 > 0;
                    }
                },
                new OnCreateVHListener() {
                    @Override
                    public OneVH onCreateHolder(ViewGroup parent) {
                        return new TextVH(parent);
                    }

                    @Override
                    public boolean isCreate(int position, Object o) {
                        return position % 3 == 0;
                    }
                }
        );

OneRecyclerView.init()方法前面兩個(gè)參數(shù)分別是下拉刷新和加載更多的回調(diào),后面兩個(gè)參數(shù)給OneRecyclerView的初始化方法傳入了兩個(gè)OnCreateVHListener,分別對(duì)應(yīng)兩個(gè)OneVH子類:UserInfoVH和TextVH。前者的isCreate()position % 3 > 0時(shí)返回true,后者的isCreate()position % 3 == 0時(shí)返回true。也就是位置0,3,6,9...顯示TextVH對(duì)應(yīng)的布局,位置1,2,4,5,7,8...顯示UserInfoVH對(duì)應(yīng)的布局。這是一種交替顯示的效果(如最上面圖二orv_types.gif所示)。

總結(jié)

說(shuō)了這么多,其實(shí)實(shí)現(xiàn)代碼并不復(fù)雜,只用到常見(jiàn)的繼承、封裝、多態(tài)、接口、抽象類、泛型,數(shù)據(jù)結(jié)構(gòu)只用到List。原因一方面是軟件設(shè)計(jì)的高級(jí)技術(shù)自己還有待學(xué)習(xí);另一方面,的確,實(shí)現(xiàn)一個(gè)具備常見(jiàn)功能、簡(jiǎn)單易用的RecyclerView小框架,這些就夠了。自己實(shí)現(xiàn)一遍或者研究一遍代碼會(huì)對(duì)RecyclerView的原理和的Java基礎(chǔ)技術(shù)有較好的理解。

Github地址如下,歡迎forkstar

https://github.com/rome753/OneRecyclerView

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容