使用RecyclerView,一句代码就够了

前言

RecyclerView出来有好几年了,它的重要性不言而喻。然而RecyclerView只提供了基本的View复用功能,相关功能如刷新、点击等都需要开发者自己实现,每个项目实现一遍RecyclerView功能集成又无必要,因此出现了许多RecyclerView封装的“轮子”,Github上一搜多如牛毛。

简介

轮子虽多,各有特点。有时候还是自己造的最适合,OneRecyclerView这个轮子的特点如下:

  • 用很少的代码(主要Java代码300多行)实现了RecyclerView集成的大部分功能,包括下拉刷新、加载更多、多种ViewType、多列显示、自定义HeaderView和空数据EmptyView
  • 实现多种ViewType的方式很巧妙,不需要复杂的映射关系,不需要注册类型,不需要反射
  • 一句代码即可调用,多种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. 实现自定义ViewHolder

    class UserInfoVH extends OneVH<UserInfo> {

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

        @Override
        public void bindView(int position, final UserInfo o) {//2.处理点击事件和设置数据
            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设置数据和点击事件

  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;
                    }
                }
        );

调用OneRecyclerView的init()方法,传入下拉刷新监听、加载更多监听和创建ViewHolder监听即可。

OneRecyclerView的init()方法最后一个参数是可变参数,针对多种ItemType情况:
实现多个ViewHolder,用OnCreateVHListener包装并传入

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

可添加多个header,多次调用addHader()方法即可,header的显示完全由自身控制

  1. 设置多列显示的列数
        mOneRecyclerView.setSpanCount(3);

不设置默认是1列;多列显示与多种ViewType一般不会同时用到,根据具体需求选择其一

原理分析

下拉刷新

使用官方的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设置刷新监听

加载更多

给RecyclerView.Adapter的ItemCount + 1,并使用一个单独的ViewType。当划到底部时,显示一个loading布局;获取到数据后再隐藏这个布局

<?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>

这里只放了一个ProgressBar,可以自定义

ViewHolder封装

对于一个通用的库来说:

  1. 外部调用者使用的数据类型
  2. RecyclerView的Item布局
  3. Item加载数据的方式
  4. Item点击事件的处理

这几点都是不确定的,第一点需要使用泛型,即由调用者定义数据类型;后面三点都可以在ViewHolder中处理。这里封装一个继承自RecyclerView.ViewHolder的抽象类OneVH<T>,具体由调用者实现

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);
}

一个简单的OneVH<T>实现类如下

    class TextVH extends OneVH<UserInfo> {

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

        @Override
        public void bindView(int position, final UserInfo o) {//2.处理点击事件和设置数据
            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之后,并不是直接创建ViewHolder对象传入Adapter的,因为Adapter中的onCreateViewHolder方法创建ViewHolder的时机和数量是不确定的,所以需要定义一个接口OnCreateVHListener

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

Adapter中需要创建ViewHolder时,调用OnCreateVHListener的onCreateHolder方法,返回一个自定义的OneVH实现对象

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

因为OnCreateVHListener能返回具体OneVH<T>实现对象,所以Adapter只依赖OnCreateVHListener,不依赖OneVH。也就是说外部调用者只需要传一个OnCreateVHListener实现类给OneRecyclerView就行(OneRecyclerView传给Adapter)


经过以上步骤,已经能很方便地使用单一ViewType的RecyclerView了。调用者只要实现自己的ViewHolder,不需要关心其他。下面介绍在此基础上多种ViewType的实现

实现多种ViewType

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

    @Override
    public int getItemViewType(int position) {
    }

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

getItemViewType()方法用于确定当前位置的item属于哪种类型,返回一个表示类型viewType的int值
onCreateViewHolder()方法则是根据类型viewType获取对应的ViewHolder

常规思路思考,这里需要记录两种对应关系:

  1. 位置position与viewType的对应关系
  2. viewType与ViewHolder的对应关系

那就需要两个Map来保存,也许再加一个类管理它们。能否将这两个Map合并成一个?或者根本不用Map?

先考虑第二个对应关系,由于前面已经将ViewHolder类型绑定到OnCreateVHListener接口上,不同的viewType也就对应了不同的OnCreateVHListener对象。比如有3种Item类型,那么就有3个OnCreateVHListener对象,这3个viewType用3个不同int值分别对应。

其实用Map是一种冗余,3个或更多int值完全可以用0,1,2...表示,那么3个OnCreateVHListener可以直接用List<OnCreateVHListener>保存,每个OnCreateVHListener在List中的序号就是它的viewType!

接着考虑第一个对应关系,根据position获取viewType值,在有了List<OnCreateVHListener>的基础上,viewType就是OnCreateVHListener在List中的序号。直接获取这个序号并不好实现,能否先获取OnCreateVHListener,再遍历List<OnCreateVHListener>获得它的位置?

根据position获取OnCreateVHListener也不方便,这个对应关系是调用者定义的,需要给外部提供一种很自然的定义方式,而不是注册类型或定义一个Manager类。其实可以不用考虑对应关系,每个OnCreateVHListener只需要知道对应的position是不是自己就行了。

这样在OnCreateVHListener接口中添加一个isCreate(int position, T t)方法,参数是位置position和对应位置的数据,调用者通过这两个参数判断该位置是不是对应的ViewHolder

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

        /**
         * 根据当前位置或数据判断是否创建S类型的ViewHolder
         * @param position
         * @param t
         * @return
         */
        boolean isCreate(int position, T t);
}

在Adapter的getItemViewType方法中遍历List<OnCreateVHListener>,调用isCreate()方法,如果结果是true,就返回当前序号,这个序号就是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就全部关联起来了。代价只是在Adapter中添加一个List<OnCreateVHListener>(初始化时传进来)、OnCreateVHListener接口中添加一个方法。

虽然在getItemViewType方法里进行了遍历操作,但是考虑到99%的列表Item类型是个位数,而且判断类型不是耗时操作,带来的性能影响可以忽略不计

OnCreateVHListener里面定义的两个方法,不仅关联了ViewHolder类型,还关联了与position的对应关系。外部调用者使用几种Item类型,传入几个OnCreateVHListener实现类就行了。实际使用如下

        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()方法前面两个参数分别是下拉刷新和加载更多的回调,后面两个参数给OneRecyclerView的初始化方法传入了两个OnCreateVHListener,分别对应两个OneVH子类:UserInfoVH和TextVH。前者的isCreate()position % 3 > 0时返回true,后者的isCreate()position % 3 == 0时返回true。也就是位置0,3,6,9...显示TextVH对应的布局,位置1,2,4,5,7,8...显示UserInfoVH对应的布局。这是一种交替显示的效果(如最上面图二orv_types.gif所示)。

总结

说了这么多,其实实现代码并不复杂,只用到常见的继承、封装、多态、接口、抽象类、泛型,数据结构只用到List。原因一方面是软件设计的高级技术自己还有待学习;另一方面,的确,实现一个具备常见功能、简单易用的RecyclerView小框架,这些就够了。自己实现一遍或者研究一遍代码会对RecyclerView的原理和的Java基础技术有较好的理解。

Github地址如下,欢迎forkstar

https://github.com/rome753/OneRecyclerView

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容