使用RecyclerView溜徙,一句代碼就夠了

前言

RecyclerView出來(lái)有好幾年了湃缎,它的重要性不言而喻。然而RecyclerView只提供了基本的View復(fù)用功能蠢壹,相關(guān)功能如刷新嗓违、點(diǎn)擊等都需要開發(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)聽藻三、加載更多監(jiān)聽和創(chuàng)建ViewHolder監(jiān)聽即可洪橘。

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

加載更多

給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ù)雜契邀,只用到常見的繼承、封裝失暴、多態(tài)坯门、接口微饥、抽象類、泛型古戴,數(shù)據(jù)結(jié)構(gòu)只用到List欠橘。原因一方面是軟件設(shè)計(jì)的高級(jí)技術(shù)自己還有待學(xué)習(xí);另一方面现恼,的確肃续,實(shí)現(xiàn)一個(gè)具備常見功能、簡(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)系作者
  • 序言:七十年代末疼蛾,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子艺配,更是在濱河造成了極大的恐慌察郁,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件转唉,死亡現(xiàn)場(chǎng)離奇詭異皮钠,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)赠法,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門麦轰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人砖织,你說(shuō)我怎么就攤上這事款侵。” “怎么了侧纯?”我有些...
    開封第一講書人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵新锈,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我眶熬,道長(zhǎng)妹笆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任娜氏,我火速辦了婚禮拳缠,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘贸弥。我一直安慰自己窟坐,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著狸涌,像睡著了一般切省。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上帕胆,一...
    開封第一講書人閱讀 49,036評(píng)論 1 285
  • 那天朝捆,我揣著相機(jī)與錄音,去河邊找鬼懒豹。 笑死芙盘,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的脸秽。 我是一名探鬼主播儒老,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼记餐!你這毒婦竟也來(lái)了驮樊?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤片酝,失蹤者是張志新(化名)和其女友劉穎囚衔,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體雕沿,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡练湿,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了审轮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片肥哎。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖疾渣,靈堂內(nèi)的尸體忽然破棺而出篡诽,到底是詐尸還是另有隱情,我是刑警寧澤榴捡,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布霞捡,位于F島的核電站,受9級(jí)特大地震影響薄疚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜赊琳,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一街夭、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧躏筏,春花似錦板丽、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)猖辫。三九已至,卻和暖如春砚殿,著一層夾襖步出監(jiān)牢的瞬間啃憎,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工似炎, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留辛萍,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓羡藐,卻偏偏與公主長(zhǎng)得像贩毕,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子仆嗦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

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