美文网首页技术Android知识Android技术知识
一点点有助于巧用RecyclerView的小技巧

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

作者: Machivellia | 来源:发表于2016-10-26 09:24 被阅读739次

    在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自身的机制,才能越得心应手的进行拓展。

    相关文章

      网友评论

      本文标题:一点点有助于巧用RecyclerView的小技巧

      本文链接:https://www.haomeiwen.com/subject/iicduttx.html