一点点有助于巧用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自身的机制,才能越得心应手的进行拓展。

推荐阅读更多精彩内容