• Android 5.X新特性之RecyclerView基本解析及无限复用


    说到RecyclerView,相信大家都不陌生,它是我们经典级ListView的升级版,升级后的RecyclerView展现了极大的灵活性。同时内部直接封装了ViewHolder,不用我们自己定义ViewHolder就能实现item的回收和复用功能。当然它肯定不止这些好处,比如我们可以自定义分割线,可以更加方便的实现列表的布局方式等等。虽说我们自己在第一次使用时,会比使用listView和gridView稍微的复杂一些,需要自定义的也多了一点,但是它却更好的体现了灵活性,可以随自己的喜好来随便的定义,当然最主要的是能更好的复用,只需一次的定义,却可随处的复用。

    下面,我们来好好的学习下它的使用。

    首先,我们要是用RecyclerView必须引入support-V7包,拿android studio来举例:

    先打开File->选择Project Structure,之后在左边Modules选择你的项目,然后在点击右边的Dependencies,然后点击绿色的+号选择添加Library,然后找到recyclerview-v7双击加入到依赖库中。然后可以在build.gradle中查看:

    dependencies {
        compile fileTree(include: ['*.jar'], dir: 'libs')
        testCompile 'junit:junit:4.12'
        compile 'com.android.support:appcompat-v7:23.3.0'
        compile 'com.android.support:recyclerview-v7:23.3.0'
    }
    

    可以找dependencies 中找到对recyclerview的支持,则说明添加成功,如果还没有的,可以clean下自己的工程。

    接下来,我们就进入Recyclerview的学习。RecyclerView的学习目标就是以下四个方法,把以下四个方法完全的掌握了,也就真正的掌握了RecyclerView。

    RecyclerView.setAdapter:用来设置adapter,显示数据
    RecyclerView.setLayoutManager :用来设置显示布局的,目前系统给出三种布局,分别是垂直,水平和瀑布流式布局
    RecyclerView.setItemAnimator :用来设置显示动画的
    RecyclerView.addItemDecoration :用来设置列表分割线的

    接下来我们就学习怎么使用以上四个方法来真正掌握Recyclerview的使用。要使用Recyclerview,我们必须先定义一个类(CustomRecyclerAdapter)并继承Recyclerview.Adapter,且实现它里面的方法,代码如下:

    public class CustomRecyclerAdapter extends RecyclerView.Adapter<CustomRecyclerAdapter.ViewHolderHelper>{
    
        @Override
        public ViewHolderHelper onCreateViewHolder(ViewGroup parent, int viewType) {
            return null;
        }
        @Override
        public void onBindViewHolder(ViewHolderHelper holder, int position) {
    
        }
        @Override
        public int getItemCount() {
            return 0;
        }
    
        public class ViewHolderHelper extends RecyclerView.ViewHolder{
    
            public ViewHolderHelper(View itemView) {
                super(itemView);
            }
        }
    }
    

    在我们还没正式开始使用之前,先大体上了解下上面三个方法是做什么的:

    A. onCreateViewHolder()方法:该方法就是将布局文件转化为View并传递给RecyclerView封装好的ViewHolder。

    B. onBindViewHolder()方法:该方法将会在固定的位置上把ViewHolder里的itemView数据映射在item中。

    C. getItemCount()方法:该方法和listView中的getCount()一样,都是返回Item的个数。

    了解了这三个方法,我们来先实现最简单的应用,把我们的数据显示在app中。

    首先,我们创建一个布局文件recycler_view.xml,如下

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
        </android.support.v7.widget.RecyclerView>
    
    </LinearLayout>
    

    创建RecycerActivity用来加载布局文件:

    public class RecycerActivity extends Activity {
        private RecyclerView mRecyclerView;
        private List<String> mData;
        private CustomRecyclerAdapter mCustomRecyclerAdapter;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.recycer_view);
            mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
            initData();
            //线性布局管理器
            recyclerViewLayoutManager = new LinearLayoutManager(this);
            //设置布局管理器
    	    mRecyclerView.setLayoutManager(recyclerViewLayoutManager);
            //设置显示动画
            mRecyclerView.setItemAnimator(new DefaultItemAnimator());
    		//设置adapter
            mCustomRecyclerAdapter = new CustomRecyclerAdapter(mData);
            mRecyclerView.setAdapter(mCustomRecyclerAdapter);
    
        }
        private void initData() {
            mData = new ArrayList<String>();
            for(int i = 0; i < 10; i++){
                mData.add("第"+i+"item");
            }
        }
    }
    

    经过修改的CustomRecyclerAdapter 如下,

    public class CustomRecyclerAdapter extends RecyclerView.Adapter<CustomRecyclerAdapter.ViewHolderHelper>{
    
        private List<String> mData;
        public CustomRecyclerAdapter(List<String> data) {
            mData = data;
        }
    
        @Override
        public ViewHolderHelper onCreateViewHolder(ViewGroup parent, int viewType) {
            //onCreateViewHolder方法就是将布局文件转化为View并传递给RecyclerView封装好的ViewHolder
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_view, parent, false);
            return new ViewHolderHelper(view);
        }
    
        @Override
        public void onBindViewHolder(ViewHolderHelper holder, int position) {
            holder.textView.setText(mData.get(position));
        }
    
        @Override
        public int getItemCount() {
            return mData.size();
        }
    }
    

    item_view.xml布局文件

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
        <TextView
            android:id="@+id/tv_item"
            android:layout_toRightOf="@+id/iv_item"
            android:layout_width="match_parent"
            android:layout_height="30dp"
            android:text="I am item view"/>
    
    </RelativeLayout>
    

    好,基础工作已做足,我们先来看看效果吧

    这里写图片描述

    已经显示出来了,不过,大家看着是不是很别扭呢,连个分割线也没有,还不如listView呢,别急,我们在上面也提到过,RecyclerView给了我们最大的发挥自由度,它本身并没有给定列表的分割线,这是需要我们自己定义的。由此我们来定义自己的分割线。自定义分割线是需要我们继承RecyclerView.ItemDecoration类,并实现它的onDraw()方法。请看代码:

    public class DividerItemDecoration extends RecyclerView.ItemDecoration{
    
        public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
        public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
        private int mOrientation;
        private Context mContext;
        private TextPaint mTextPaint;
        private float listDividerSize = 2;
        private int listDividerColor;
        public DividerItemDecoration(Context context,int orientation){
            mContext = context;
            mTextPaint = new TextPaint();
            mTextPaint.setColor(Color.RED);
            setOrientation(orientation);
        }
        public void setOrientation(int orientation) {
            if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
                throw new IllegalArgumentException("invalid orientation");
            }
            mOrientation = orientation;
        }
        @Override
        public void onDraw(Canvas c, RecyclerView parent) {
            super.onDraw(c, parent);
            if(mOrientation == HORIZONTAL_LIST){
                drawHorizontal(c, parent);
            }else{
                drawVertical(c, parent);
            }
        }
        /**
         * 绘制垂直分割线
         * @param c
         * @param parent
         */
        private void drawVertical(Canvas c, RecyclerView parent) {
            //分割线的左边界 = 子View的左padding值
            int rectLeft = parent.getPaddingLeft();
            //分割线的右边界 = 子View的宽度 - 子View的右padding值
            int rectRight = parent.getWidth() - parent.getPaddingRight();
            int childCount = parent.getChildCount();
            for(int i = 0; i < childCount; i ++){
                View child = parent.getChildAt(i);
                RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams();
                // 分割线的top = 子View的底部 + 子View的margin值
                int rectTop = child.getBottom() + layoutParams.bottomMargin;
                // 分割线的bottom = 分割线的top + 分割线的高度
                float rectBottom = rectTop + listDividerSize;
                c.drawRect(rectLeft,rectTop,rectRight,rectBottom,mTextPaint);
            }
        }
    
        /**
         * 绘制水平分割线
         * @param c
         * @param parent
         */
        private void drawHorizontal(Canvas c, RecyclerView parent) {
            //分割线的上边界 = 子View的上padding值
            int rectTop = parent.getPaddingTop();
            //分割线的下边界 = 子View的高度 - 子View的底部padding值
            int rectBottom = parent.getHeight() - parent.getPaddingBottom();
            int childCount = parent.getChildCount();
            for(int i = 0; i < childCount; i ++){
                View child = parent.getChildAt(i);
                RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams();
                //分割线的Left = 子View的右边界 + 子View的左margin值
                int rectLeft = child.getRight() + layoutParams.rightMargin;
                //分割线的right = 分割线的Left + 分割线的宽度
                float rectRight = rectLeft + listDividerSize;
                c.drawRect(rectLeft,rectTop,rectRight,rectBottom,mTextPaint);
            }
        }
    
        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
            super.getItemOffsets(outRect, view, parent, state);
            if(mOrientation == VERTICAL_LIST){
                outRect.set(0,0,0,(int)listDividerSize);
            } else{
                outRect.set(0,0,(int)listDividerSize,0);
            }
        }
    }
    

    代码很好理解,这里考虑了两个情况,分别是垂直和水平的布局,然后再ondraw()里面计算出四角边值,最后直接绘制一个矩形即可。

    在RecycerActivity 中的onCreate中添加上一句
    mRecyclerView.addItemDecoration(new DividerItemDecoration(this,DividerItemDecoration.VERTICAL_LIST));
    现在再来看看效果。
    这里写图片描述

    现在已显示出来了,分割线也出来了,在这里只列出了垂直方向的布局,就不再列出其他样式的布局代码了,伙伴们可自行写写看。

    或许有经验的小伙伴们已经知道我们的RecyclerView自己是没有实现点击事件的,这里需要我们来根据业务的需求自己来实现。这里我们利用事件回调机制来完成事件的触发。

    首先我们需要在CustomRecyclerAdapter中定义一个接口,并在其中定义两个可用的事件方法,如下:

    public interface OnItemClickListener{
            void onItemClickListener();
            void onLongItemClickListener();
        }
    

    这里提供了用于点击和长按的事件方法,接下来我们需要对外暴露该接口用于被调用

     public void setOnClickItemListener(OnItemClickListener onItemClickListener){
            mOnItemClickListener = onItemClickListener;
     }
    

    然后我们可以在ViewHolderHelper 做如下的修改:

    public class ViewHolderHelper extends RecyclerView.ViewHolder{
    
            private TextView textView;
            public ViewHolderHelper(View itemView) {
                super(itemView);
                textView = (TextView)itemView.findViewById(R.id.tv_item);
                textView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        mOnItemClickListener.onItemClickListener();
                    }
                });
                textView.setOnLongClickListener(new View.OnLongClickListener() {
                    @Override
                    public boolean onLongClick(View v) {
                        mOnItemClickListener.onLongItemClickListener();
                        return false;
                    }
                });
            }
        }
    

    首先得到我们itemView中的textView对应的Id,然后为textView添加事件,但是只是添加却并不实现,它的用意是谁调用谁实现。

    最后在我们的RecycerActivity中添加如下代码,请看:

    mCustomRecyclerAdapter.setOnClickItemListener(new CustomRecyclerAdapter.OnItemClickListener() {
                @Override
                public void onItemClickListener() {
                    Toast.makeText(getContext(),"单击",Toast.LENGTH_SHORT).show();
                }
    
                @Override
                public void onLongItemClickListener() {
                    Toast.makeText(getContext(),"长单击",Toast.LENGTH_SHORT).show();
                }
            });
    

    下面在来看看是否可以实现事件的触发了呢?

    这里写图片描述

    ok,学到这里大家至少对RecylerView有了一个初步的认识,但是我们一想,这样写的话肯定达不到我们标题所说的无限复用,甚至连复用也遥不可及,是的,这样写是不可能完成复用的,接下来我们一步一步的慢慢调整,让它可以支持一次编写N次复用,达到极大多数的重复使用,即使不符合需求,我们也只需要修改丁点即可满足需求,这是我们的目标,接下来一步一步的实现。

    首先,我们先整理下,看看可以调整哪些目标能逐步的实现重复使用的目标:

    1. 数据类型:我们在使用List集合时,是无法固定类型的,有可能是String,int等等类型,所以我们不应该固定为哪一种类型。

    2. 在onCreateViewHolder方法中,它需要映射一个布局文件并转化为View或是一个自定义View传递给RecyclerView封装好的ViewHolder,为了可以达到复用,所以我们就不可以在此直接映射布局文件。

    3. 在onBindViewHolder方法中也不应该直接为itemView设置属性,如上面的:holder.textView.setText(mData.get(position));

    4. 我们不应该在ViewHolder的构造方法中直接获取我们的itemView,并给它添加触发事件

    以上几个是我们能很直观的得到的能重构的问题所在,至于其他的不容易想到的我们再重构的时候慢慢讲解。现在我们逐一的解决上面的问题,使我们能更达到重复使用的目的。

    1,针对数据类型的不一致,我们可以根据具体的使用场景利用泛型进行传递到Adapter中,比如:我们再定义CustomRecyclerAdapter时使用泛型,让调用者传递过来它所拥有的类型,这样我们就可以不用考虑类型的不一致了。请看下面片段代码

    public class CustomRecyclerAdapter<T> extends RecyclerView.Adapter<CustomViewHolderHelper>{
        private Context mContext;
        private List<T> mData;
        private CustomOnItemClickListener mOnItemClickListener;
        public CustomRecyclerAdapter(Context context, List<T> data) {
            mContext = context;
            mData = data;
        }
     }
     ...
    

    RecycerActivity中在调用时可以这样使用:
    mCustomRecyclerAdapter = new CustomRecyclerAdapter(this,mData);

    这样就可以把类型给确定下来了,同时也解决了问题1的复用。

    2 , 在onCreateViewHolder方法中,和问题一的解决方案是一样的,我们把需要的itemView给传递过去而不是固定写死在方法中

    public class CustomRecyclerAdapter<T> extends RecyclerView.Adapter<CustomViewHolderHelper>{
        private Context mContext;
        private List<T> mData;
        protected int mLayoutResId;
        private CustomOnItemClickListener mOnItemClickListener;
        public CustomRecyclerAdapter(Context context, int layoutResId, List<T> data) {
            mContext = context;
            mLayoutResId = layoutResId;
            mData = data;
        }
    
        /**
         * Called when RecyclerView needs a new ViewHolder of the given type to represent
         * an item.
         * 当 RecyclerView 依据给出的类型需要一个新的 ViewHolder 去展示一个 item 时,该方法将会被调用
         *
         * 这个给出的类型是在 getItemViewType返回的,默认返回 0 。
         *
         * @param parent
         * @param viewType
         * @return
         */
        @Override
        public CustomViewHolderHelper onCreateViewHolder(ViewGroup parent, int viewType) {
            //onCreateViewHolder方法就是将布局文件转化为View并传递给RecyclerView封装好的ViewHolder
            View view = LayoutInflater.from(parent.getContext()).inflate(mLayoutResId, parent, false);
            return new CustomViewHolderHelper(view);
        }
    }
    

    RecycerActivity中在调用时可以这样使用:
    mCustomRecyclerAdapter = new CustomRecyclerAdapter(this,R.layout.item_view,mData);

    3 , 在onBindViewHolder方法中也不应该直接为itemView设置属性,我们可以在这里记录itemView的position,并给它设置监听事件,更重要的我们这这里可以创建一个抽象方法,让调用者自己去实现业务逻辑。请看代码:

    public abstract class CustomRecyclerAdapter<T> extends RecyclerView.Adapter<CustomViewHolderHelper>
                                            implements View.OnClickListener,View.OnLongClickListener{
        private Context mContext;
        private List<T> mData;
        protected int mLayoutResId;
        private CustomOnItemClickListener mOnItemClickListener;
        public CustomRecyclerAdapter(Context context, int layoutResId, List<T> data) {
            mContext = context;
            mLayoutResId = layoutResId;
            mData = data;
        }
    
        /**
         * Called when RecyclerView needs a new ViewHolder of the given type to represent
         * an item.
         * 当 RecyclerView 依据给出的类型需要一个新的 ViewHolder 去展示一个 item 时,该方法将会被调用
         *
         * 这个给出的类型是在 getItemViewType返回的,默认返回 0 。
         *
         * @param parent
         * @param viewType
         * @return
         */
        @Override
        public CustomViewHolderHelper onCreateViewHolder(ViewGroup parent, int viewType) {
            //onCreateViewHolder方法就是将布局文件转化为View并传递给RecyclerView封装好的ViewHolder
            View view = LayoutInflater.from(parent.getContext()).inflate(mLayoutResId, parent, false);
            return new CustomViewHolderHelper(view);
        }
    
        /**
         * Called by RecyclerView to display the data at the specified position. This method should
         * update the contents of the ViewHolder#itemView to reflect the item at the given position.
         *
         * RecyclerView 将要在特殊的位置上显示数据时,该方法将被调用。该方法将会在固定的位置上
         *  把ViewHolder里的itemView数据映射在item中。
         * @param holder
         * @param position
         */
        @Override
        public void onBindViewHolder(CustomViewHolderHelper holder, int position) {
            //把每一个itemView设置一个标签,方便以后根据标签获取到该itemView以便做其他事项,比如点击事件
            holder.itemView.setTag(position);
            holder.itemView.setOnClickListener(this);
            holder.itemView.setOnLongClickListener(this);
            T itemData = mData.get(position);
            displayContents(holder,itemData);
        }
    
        /**
         *用来在holder中设置每个ItemView的显示数据
         * 设定为抽象方法,是为:自己本身并不实现,谁使用谁设置
         * @param holder
         * @param itemData
         */
        protected abstract void displayContents(CustomViewHolderHelper holder, T itemData);
    }
    

    上面注解也写的很清楚,相信大家一看就明白,至于为什么可以直接使用holder.itemView来获取每个itemView是因为在onCreateViewHolder()方法中,我们返回了一个新的对象引用,这个对象的构造方法中使用super(itemView);把我们的itemView传递到了ViewHolder中,请看它的源码构造方法:

     public ViewHolder(View itemView) {
    	 if (itemView == null) {
    	     throw new IllegalArgumentException("itemView may not be null");
         }
         this.itemView = itemView;
     }
    

    因此我们可以在onBindViewHolder()方法中,直接使用holder.itemView来获取itemView。那么接下来,在RecycerActivity中,我们这样使用:

     mCustomRecyclerAdapter = new CustomRecyclerAdapter<String>(this, R.layout.item_view, mData) {
                @Override
                protected void displayContents(CustomViewHolderHelper holder, String itemData) {
                    holder.setText(R.id.tv_item,itemData);
                }
     };
    

    当然你必须也得在ViewHolder中定义相应的方法,如:

    public class CustomViewHolderHelper extends RecyclerView.ViewHolder{
    
        private SparseArray<View> views;
        public CustomViewHolderHelper(View itemView) {
            super(itemView);
            views = new SparseArray<View>();
        }
        private <T extends View> T converToViewFromId(int resId) {
            View view = views.get(resId);
            if(view == null){
                view = itemView.findViewById(resId);
            }
            views.put(resId,view);
            return (T)view;
        }
    
        public CustomViewHolderHelper setText(int resId, String value){
            TextView itemView = converToViewFromId(resId);
            if (TextUtils.isEmpty(value)) {
                itemView.setText("");
            } else {
                itemView.setText(value);
            }
            return  this;
        }
    }
    

    ok,这样我们连第四个问题也一并解决了,看下效果吧,完全的一样,这样我们就实现的重复使用,但是有人会有疑问,这里也就只能使用TextView啊,其实已经在ViewHolder中给出了答案,大家只需要在添加相对应的方法即可,比如

    public CustomViewHolderHelper setImageResource(int viewId, int imageResId) {
            ImageView view = converToViewFromId(viewId);
            view.setImageResource(imageResId);
            return this;
    }
    public CustomViewHolderHelper setOnClickListener(int viewId, View.OnClickListener listener) {
            View view = converToViewFromId(viewId);
            view.setOnClickListener(listener);
            return this;
    }
    

    上面我又添加了两个方法,用于点击事件和加载图片的ImageView,下面我们再把itemView布局文件修改下:

    item_view.xml布局文件:

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
    
        <ImageView
            android:id="@+id/iv_item"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
        <TextView
            android:id="@+id/tv_item"
            android:layout_toRightOf="@+id/iv_item"
            android:layout_width="match_parent"
            android:layout_height="30dp"
            android:text="I am item view"/>
        <Button
            android:id="@+id/btn_item"
            android:layout_toRightOf="@+id/iv_item"
            android:layout_below="@+id/tv_item"
            android:layout_width="wrap_content"
            android:text="点我"
            android:layout_height="wrap_content" />
    </RelativeLayout>
    

    RecycerActivity中在调用时可以这样使用:

    mCustomRecyclerAdapter = new CustomRecyclerAdapter<String>(this, R.layout.item_view, mData) {
                @Override
                protected void displayContents(CustomViewHolderHelper holder, String itemData) {
                    holder.setText(R.id.tv_item,itemData)
                          .setImageResource(R.id.iv_item,R.mipmap.ic_launcher)
                          .setOnClickListener(R.id.btn_item, new View.OnClickListener() {
                              @Override
                              public void onClick(View v) {
                                  Toast.makeText(RecycerActivity.this,"您单击了按钮",Toast.LENGTH_SHORT).show();
                              }
                          });
                }
            };
    

    ok,来看下效果吧
    这里写图片描述

    是不是可以了呢,这样我们就可以完全的一次定义N次复用了,每次使用只需要更换不同的布局文件即可而不需要再次编写代码,学会了吧。

    其实我们这节课主要讲解的RecyclerView.setAdapter的内容,其他的三个我们并没有详细的介入,我们会再以后的博文中陆续的讲解。

    好了,今天就讲到这里吧,祝大家学习愉快。

    更多资讯请关注微信平台,有博客更新会及时通知。爱学习爱技术。

    这里写图片描述

  • 相关阅读:
    springboot2 整合雪花算法,并兼容分布式部署
    docker 在 linux 搭建私有仓库
    jdbc 几种关系型数据库的连接 和 driver_class,以及简单的使用
    springboot2 整合发送邮件的功能
    oracle 唯一新约束 和 逻辑删除的 冲突处理办法
    oracle 一些常见操作方法
    spring-cloud-stream 整合 rabbitmq
    springboot2 整合 rabbitmq
    docker 安装 rabbitmq 消息队列
    网络统计学目录
  • 原文地址:https://www.cnblogs.com/guanmanman/p/6088056.html
Copyright © 2020-2023  润新知