一点点有助于巧用RecyclerView的小技巧

在RecyclerView问世之前,ListView可能是我们使用频率最高的系统控件之一了。而随着Android的发展,虽然ListView依旧重要,但确实越来越多的时候大家都开始选择使用RecyclerView了。当然这也是事物发展的必然,个人觉得最重要的原因就是RecyclerView相对来说,确实灵活性更高。

但是显然并不能说RecyclerView就优于ListView,二者各有优劣,我们应该根据不同的需求选择最合适的进行使用。这里的重点是:当我们已经用习惯了ListView,刚开始转向RecyclerView的时候,还是容易在很多小地方出现水土不服的。故此,在这里记录几个关于RecyclerView比较实用的小技巧。


添加Header/Footer

我们知道想要为ListView添加上一个Header或者Footer是非常容易的,因为ListView本身已经提供了相关的方法接口,我们只负责调用就可以了。
而在RecyclerView里我们是找不到类似于setHeaderView这样的方法的,但是这样的功能确实又还是比较常用的。所以这时应该如何做呢?

其实关于RecyclerView有一个非常有用的东西叫做viewType,而它究竟能起到什么作用呢?我们具体来看一看。假设我们先写一个最基本的Adapter类:

public class SimpleRecyclerAdapter extends RecyclerView.Adapter<SimpleRecyclerAdapter.ViewHolder> {

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return null;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {

    }

    @Override
    public int getItemCount() {
        return 0;
    }

    public class ViewHolder extends RecyclerView.ViewHolder {

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

以上就是一个最最基本的RecyclerView的Adapter类,我们可以看到一个命名非常能够说明其作用的方法叫做onCreateViewHolder

如果对ListView的使用已经有了了解,我们就知道ViewHolder实际上就是用来复用ItemView,从而大大提高效率的。所以onCreateViewHolder顾名思义就是在为RecyclerView的itemView创建ViewHolder时所调用的,我们在此需要注意到的是该方法有一个参数叫做viewType

实际上,从其命名我们就很容易联想到:它多半是与创建ViewHolder时,itemView的布局类型有关系的。那么,其作用究竟如何?其实我们可以到RecyclerView的源码去简单找找答案,简单来说,其逻辑可以归纳如下:

在RecyclerView开始初始化需要显示的item数据的时候,会通过方法getViewForPosition(int position)来获取对应的itemView。
而这个获取的过程,其实是含有一个缓存机制的。这里源码很长,我们没有那么多精力也没有必要去全部读的明明白白,就捡关键的几行代码看:

  • final int type = mAdapter.getItemViewType(offsetPosition);
  • holder = getRecycledViewPool().getRecycledView(type);
  • holder = mAdapter.createViewHolder(RecyclerView.this, type);

其实,分析一下以上我们提炼出的这几行代码。我们可以得知:

  • RecyclerView在获取itemView的时候,会首先通过getItemViewType方法去获取该position位置的viewType。
  • 当获取到了type就会根据它的值去RecycledViewPool这个缓存池中查找对应类型的ViewHolder来进行复用。
  • 但是,如果当前缓存池中还没有可以进行复用的ViewHolder怎么办呢?当然就是通过createViewHolder来创建全新的ViewHolder了。
  • 在createViewHolder方法中adapter里的onCreateViewHolder方法就被回调了,所以这也是为什么自定义的Adapter类必须覆写这个方法的原因。
  • 而创建出的ViewHolder在合适的时机就会被加入到缓存池,以便其他的item进行复用。

以上谈到的这个过程只要对于ListView使用ViewHolder的原理有所了解,相信就不难理解。

当然,除了getViewForPosition(int position)之外,还有另一个方法也很关键,即:bindViewToPosition(View view, int position)
这个方法的实现逻辑相对来说更简单一点,我们只需要明白这个方法的核心作用就是:将给定的视图绑定到指定位置(position)。其大致逻辑是:

  • 首先,会通过ViewHolder holder = getChildViewHolderInt(view)去获取ViewHolder。
  • 之后只要获取到的该itemView的ViewHolder不为空,那么就会通过mAdapter.bindViewHolder(holder, offsetPosition)进行视图的数据绑定。
  • 最后同理的,在该方法内Adapter的onBindViewHolder方法就会被回调,这当然也就是会什么我们必须覆写onBindViewHolder的原因了。

OK,那么有了以上的分析作为基础,我们对RecyclerView的工作流程会有一个大概的了解。如果想要更加深入,我们可以自己再继续到源码中去进行研究。这里至少记住一个关键点,那就是:RecyclerView在获取itemView的时候,其布局是与ViewType相关的。现在我们回到之前分析中的一行代码:

  • final int type = mAdapter.getItemViewType(offsetPosition);

也就是说,我们发现我们提到的ViewType这个东西,在源码中会通过Adapter类的getItemViewType方法来进行获取。但是回顾一下我们自定义的Adapter类,似乎并没有覆写这个方法。由此我们很容易可以推测出,在源码中这个方法肯定是有默认实现的:

        public int getItemViewType(int position) {
            return 0;
        }

由此我们知道,源码中该方法的实现很简单,就是固定的返回0。这意味着:只要我们不自己覆写该方法,那么itemView就永远只有固定的一种type。
但与此同时也代表着,我们可以自己覆写该方法添加额外的ViewType。那么,所谓的添加Header这种操作,不就很容易实现了吗?

public class SimpleRecyclerAdapter extends RecyclerView.Adapter<SimpleRecyclerAdapter.ViewHolder> {

    private List<String> data;

    private static final int TYPE_HEADER = 0;
    private static final int TYPE_CONTENT = 1;

    private View mHeaderView;

    public void setHeaderView(View headerView) {
        mHeaderView = headerView;
        notifyItemInserted(0);
    }

    public SimpleRecyclerAdapter(List<String> data) {
        this.data = data;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        ViewHolder holder = null;
        if (viewType == TYPE_HEADER) {
            holder = new ViewHolder(mHeaderView);
        } else if (viewType == TYPE_CONTENT) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false);
            holder = new ViewHolder(view);
        }
        return holder;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        if (getItemViewType(position) == TYPE_HEADER)
            return;

        holder.tvContent.setText(data.get(getRealPosition(holder)));
    }

    private int getRealPosition(ViewHolder holder) {
        return mHeaderView == null ? holder.getLayoutPosition() : holder.getLayoutPosition() - 1;
    }

    @Override
    public int getItemCount() {
        return mHeaderView == null ? data.size() : data.size() + 1;
    }

    @Override
    public int getItemViewType(int position) {
        if (mHeaderView == null)
            return TYPE_CONTENT;

        if (position == 0) {
            return TYPE_HEADER;
        } else {
            return TYPE_CONTENT;
        }
    }


    public class ViewHolder extends RecyclerView.ViewHolder {

        TextView tvContent;

        public ViewHolder(View itemView) {
            super(itemView);

            tvContent = (TextView) itemView.findViewById(R.id.tv_content);
        }
    }
}

以上就是我们实现的一个最基本的可以设置Header的Adapter。我们分析一下会发现逻辑其实非常简单,关键其实就在于:

  • 在设置Header的方法setHeaderView当中,我们通过notifyItemInserted(0)告诉RecyclerView在最前方插入了一个item。
  • 覆写getItemViewType方法,在这里判断该postion位置的itemView其viewType究竟是TYPE_HEADER还是TYPE_CONTENT。
  • 而添加了额外的ViewType之后,自然就需要在onCreateViewHolder中根据不同的ViewType创建不同类型的ViewHolder。
  • 那么,同样的道理,在onBindViewHolder我们自然也应该根据ViewType的不同做对应逻辑的数据绑定操作。
  • 最后,因为插入了一个新的item作为Header,但显然这是不计算进data的数量的。所以还需要对getItemCount和getPosition做额外的计算。

好了,现在运行一下程序,我们得到如下的效果:

这样我们就已经为RecyclerView成功的添加了一个Header了,当然这只是一个最最基本的例子,重在掌握其原理就行。实际上重中之重应该是掌握RecyclerView创建itemView的工作原理和viewType这个东西,因为灵活的运用viewType可以很方便的完成很多需求,比如我们接着要看的。

不同类型的item布局

对于实际开发来说,RecyclerView、ListView这类控件的使用肯定不会像我们学习Demo时那样简单规律。比如,很多时候一个列表中不同的item之间它们的布局样式也是不同的。举例来说,最近没事的时候自己在做一个练手的小项目,其中有一个界面是这样的:

要实现这种效果肯定有很多方法,但我们想做的是在一个RecyclerView中直接搞定它。与此同时,再比如说非常常见的聊天界面,聊天列表的布局也是会分为接受的消息和发出的消息两种样式。那么,我们又该怎么简单的实现它呢?有了之前的基础,我们其实很容易举一反三。ViewType这个东西用在这里实在是合适到不能再合适了。首先让我们分别定义好接受和发出的消息两种布局文件:

接着,当然就是根据我们这里的需求来定义这个RecyclerView的Adapter类了:

public class ChatRecyclerAdapter extends RecyclerView.Adapter<ChatRecyclerAdapter.ViewHolder> {

    public static final int TYPE_MSG_FROM = 0;
    public static final int TYPE_MSG_TO = 1;

    private List<ChatMessage> data;

    public ChatRecyclerAdapter(List<ChatMessage> data) {
        this.data = data;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        ViewHolder holder = null;
        if (viewType == TYPE_MSG_FROM) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_chat_from, parent, false);
            holder = new ViewHolder(view);
        } else if (viewType == TYPE_MSG_TO) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_chat_to, parent, false);
            holder = new ViewHolder(view);
        }
        return holder;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        holder.tvContent.setText(data.get(position).getMessageContent());
    }

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

    @Override
    public int getItemViewType(int position) {
        return data.get(position).getMsgType();
    }


    public class ViewHolder extends RecyclerView.ViewHolder {

        TextView tvContent;

        public ViewHolder(View itemView) {
            super(itemView);

            tvContent = (TextView) itemView.findViewById(R.id.tv_message_content);
        }
    }
}

瞄一眼代码,这种实现方式是不是还是挺优雅的呢?接下来简单的写下调用测试,然后看看效果吧:

setOnItemClickListener

要说RecyclerView最让人郁闷的就是居然没有setOnItemClickListener这样的东西,第一次用的时候我是懵逼的?哈哈,之所以这么说是因为实际使用中,可能说基本上百分之九十的列表都是要实现item的点击事件的。那么,既然RecyclerView自身没有提供的话,关于这种需求我们又要作何实现呢?这里记录一种自己比较喜欢的方式。

我们分析一下,其实所谓的setOnItemClickListener其实本质就是:给列表中的每个Item添加点击事件,所以显然我们应该在itemView上找切入点。幸运的是,有了之前的基础,我们能够记得在ViewHolder的构造器中,实际上是能够拿到itemView的。那么就很容易实现添加点击事件这种需求了。

首先,让我们自定义一个监听接口:

public interface RecyclerItemOnClickListener {
    void onItemClick(View view, int position);
}

接着,就可以开始编写Adapter类了:

public class ClickableRecyclerAdapter extends RecyclerView.Adapter<ClickableRecyclerAdapter.ViewHolder> {

    private RecyclerItemOnClickListener mListener;

    private List<String> data;

    public ClickableRecyclerAdapter(List<String> data) {
        this.data = data;
    }

    public void setOnItemClickListener(RecyclerItemOnClickListener listener) {
        this.mListener = listener;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        holder.tvContent.setText(data.get(position));
    }

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

    public class ViewHolder extends RecyclerView.ViewHolder{

        TextView tvContent;

        public ViewHolder(View itemView) {
            super(itemView);

            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if(mListener == null)
                        return;

                    mListener.onItemClick(v,getAdapterPosition());
                }
            });

            tvContent = (TextView) itemView.findViewById(R.id.tv_content);
        }
    }
}

最后,当然依旧是编写测试代码来看看效果:

好了,就总结到这里了,都是一些简单但也比较使用的小技巧。其实在积累使用经验的过程中,可以发现关于RecyclerView的使用技巧其实还是挺多的;但也可以发现大多数优雅的技巧都是建立在RecyclerView自身的工作原理上的。所以总的来说依旧是那样,越了解RecyclerView自身的机制,才能越得心应手的进行拓展。

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

推荐阅读更多精彩内容