美文网首页
RecyclerView的使用,一篇文章就够了。

RecyclerView的使用,一篇文章就够了。

作者: 怪咖大蜀 | 来源:发表于2018-08-02 13:16 被阅读0次

    RecyclerView是support v7中提供的一个控件,可以说是listView和GridView的增强版,提供了一种插拔式的体验。提供了三个设置方法来供我们快速做出一些我们想要的效果效果。

    • LayoutManager:控制的是RecyclerView的显示方式。
    • ItemDecoration:绘制Item之间的间隔,并在特定时间绘制一些我们想要的东西。
    • ItemAnimator:控制Item增删的动画。

    今天我们来学习一下RecyclerView的基本使用方法。我们想要使用它,首先我们要在app/build.gradle文件中添加依赖。

        implementation 'com.android.support:recyclerview-v7:27.1.1'
    
    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <android.support.v7.widget.RecyclerView
            android:id="@+id/rcl_main"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </android.support.constraint.ConstraintLayout>
    

    添加好布局之后,和我们的listView一样,要创建Adapter和ViewHolder。和listView不同Recycler提供了内部抽象类为我们来继承,RecyclerView.Adapter<ViewHolder> 和 RecyclerView.ViewHolder。我们先来看一个例子。

        class MyAdapter  extends RecyclerView.Adapter<MyAdapter.ViewHolder>{
            private List<User> userList;
    
            public MyAdapter(List<User> userList) {
                this.userList = userList;
            }
    
            @NonNull
            @Override
            public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
                View view  = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_main,parent,false);
                ViewHolder viewHolder = new ViewHolder(view);
                return viewHolder;
            }
    
            @Override
            public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
                holder.tvName.setText("Name:"+userList.get(position).getName());         holder.tvAge.setText("Age:"+userList.get(position).getAge());
            }
            @Override
            public int getItemCount() {
                return userList.size();
            }
            class ViewHolder extends RecyclerView.ViewHolder{
                private TextView tvName ;
                private TextView tvAge;
    
                public ViewHolder(View itemView) {
                    super(itemView);
                    tvName = itemView.findViewById(R.id.tv_name);
                    tvAge = itemView.findViewById(R.id.tv_age);
                }
            }
        }
    

    我们可以看到 我们的MyAdapter继承自RecyclerView.Adapter,重写了三个方法。

    • onCreateViewHolder():用来创建ViewHolder实例,我们在这个方法中将item布局加载进来,并创建一个ViewHolder实例,将我们的item布局传入到ViewHolder的构造方法中,并将ViewHolder返回。
    • onBindViewHolder():通过ViewHolder拿到的item布局中的View进行赋值,写过listView的你对这一步一定不会陌生。
    • getItemCount():;返回数据集的个数,也就是我们RecyclerView的item的个数。

    ViewHolder的实现,我们继承RecyclerView.ViewHolder,重写了构造方法。

    • 在构造方法中我们拿到Item的View。进行findViewById查找到我们索要用的控件。方便我们进行使用。

    接下来我们在Activity中找到我们到RecyclerView进行使用。

    //创建一个存储User数据的集合
    List<User> userList = new ArrayList<>();
    //在布局中找到我们的RecyclerView
    rclMain = findViewById(R.id.rcl_main);
    //增加一些User假数据,放在我们的集合当中
    for (int i=0 ;i<=30 ;i++){
         userList.add(new User("张"+i,20+i));
    }
    //为我们的RecyclerView设置Adapter
    rclMain.setAdapter(new MyAdapter(userList));
    //设置我们的LayoutManager为线性布局竖直方向的
    rclMain.setLayoutManager(new LinearLayoutManager(this));
    //设置我们的Item之间的间隔为DividerItemDecoration为竖直方向。如需修改其默认样式,
    //可以修改 <!-- Application theme. -->中名字为android:listDivider的属性。
    rclMain.addItemDecoration(new DividerItemDecoration(this,DividerItemDecoration.VERTICAL));
    

    以上的效果和listView完全相同,并且Item之间的间隔是一个白色的一条线。大家可以看一下1-1的图。


    1-1

    LayoutManager

    RecyclerView 中设置LayoutManager方法的参数是RecyclerView.LayoutManager吧,这是一个抽象类,好在系统提供了3个实现类:

    • LinearLayoutManager 现行管理器,支持横向、纵向。(默认)
    • GridLayoutManager 网格布局管理器。
    • StaggeredGridLayoutManager 瀑布就式布局管理器。
      如果我们想做一个横向listView的效果也非常简单,稍微修改一下Item布局,将我们设置的LayoutManager设置成横向的LinearLayoutManager就可以了
     rclMain.setLayoutManager(new LinearLayoutManager(this,LinearLayoutManager.HORIZONTAL,false));
    
    1-2

    GridLayoutManager 实现纵向的网格布局。

    //设置列数为3的纵向网格布局,注意 一下网格中间的间隔我这里设置的是item的margin实现的。我们上边Demo中使用的DividerItemDecoration,在这里是无法使用的,因为网格布局的间隔是纵向与横向都要的,而DividerItemDecoration只能设置纵向或者横向。无法同时设置
     rclMain.setLayoutManager(new GridLayoutManager(this,3,GridLayoutManager.VERTICAL,false));
    
    1-3

    GridLayoutManager 实现横向的网格布局。

    //设置列数为3的纵向网格布局,注意 一下网格中间的间隔我这里设置的是item的margin实现的。我们上边Demo中使用的DividerItemDecoration,在这里是无法使用的,因为网格布局的间隔是纵向与横向都要的,而DividerItemDecoration只能设置纵向或者横向。无法同时设置
     rclMain.setLayoutManager(new GridLayoutManager(this,3,GridLayoutManager.HORIZONTAL,false));
    
    1-4

    StaggeredGridLayoutManager实现瀑布流,已经看过上面的实现网格布局和线性list布局,我相信实现网格布局对你来说已经非常之简单了。如何随机设置item的高度保证item每个都不一样高。这里就不贴出来了。相信一定难不倒你的。

    //设置数量和方向,我们设置的是3列纵向布局。
     rclMain.setLayoutManager(new StaggeredGridLayoutManager(3,StaggeredGridLayoutManager.VERTICAL));
    
    1-5

    只要修改一行代码就能实现三种不同的布局方式,Recycler的强大你是不是已经体会到了。反正我早已经被他震撼到了。

    ItemDecoration

    我们可以使用addItemDecoration(Recycler.ItemDecoration)添加item之间要绘制的东西。RecyclerView.ItemDecoration是抽象类,Android只提供了DividerItemDecoration一个实现类用来给使用LinearLayoutManager的RecyclerView绘制Item之间的间隙。我们可以修改APPTheme 中的android:listDivider来修改我们想要的间隙样式

    values/styles.xml
        <!-- Base application theme. -->
        <style name="AppBaseTheme" parent="Theme.AppCompat.Light.DarkActionBar">
            <!-- Customize your theme here. -->
            <item name="colorPrimary">@color/colorPrimary</item>
            <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
            <item name="colorAccent">@color/colorAccent</item>
        </style>
        <style name="AppTheme" parent="AppBaseTheme">
            <item name="android:listDivider">@drawable/divider_bg</item>
        </style>
    
    divider_bg.xml
    <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="line" >
        <stroke android:color="@android:color/white"/>
        <size android:height="4dp" />
    </shape>
    

    而Android提供的这个不能满足我们所有的需求,比如我们想给我们的网格布局添加item之间的间隔就不能使用DividerItemDecoration,我们只能自己来实现了。继承RecyclerView.ItemDecoration。首先我们来了解一下RecyclerView.ItemDecoration中的几个方法的含义

    • getItemOffests:可以通过outRect.set(l,t,r,b)设置指定itemview的paddingLeft,paddingTop, paddingRight, paddingBottom
    • onDraw:可以通过一系列c.drawXXX()方法在绘制itemView之前绘制我们需要的内容。可以理解为在Item下层 像BackGround
    • onDrawOver:与onDraw类似,只不过是在绘制itemView之后绘制,具体表现形式,就是绘制的内容在itemview上层。

    我们只设置item间隔的话 是不需要进行onDraw和onDrawOver操作的,除非你想在你要设置的这个间隔中绘制一些想要的东西。而想要设置Item之间的间隔,只要通过计算我们就可以通过getItemOffests设置item的padding就能够实现我们的效果。首先我们来看一下我们具体的实现思路。

    1、首先我们要初步确定我们没个item条目的padding计算规则。为了让我们的网格布局平分宽高所以我们每个item的padding值是不一样的,我们来看下面的一张演示图。 2-1无边距
    2-2有边距

    当orientation为vertical时,我们需要在getItemOffsets方法中计算每个Item的PaddingLeft,以及PaddingRight,保证每个Item的paddingLeft+paddingRight相等,这样才能达到均分的目的。
    这里我们可以用数字的带入来计算我们每个item的padding规律,从而推导出我们要的公式。这里我也是查看的大神的博客,如果想知道具体规则可在本章底部找到答案。

    @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
            RecyclerView.LayoutManager manager = parent.getLayoutManager();
            int childPosition = parent.getChildAdapterPosition(view);
            int itemCount = parent.getAdapter().getItemCount();
            if (manager != null) {
                if (manager instanceof GridLayoutManager) {
                    // manager为GridLayoutManager时
                    setGridOffset(((GridLayoutManager) manager).getOrientation(), ((GridLayoutManager) manager).getSpanCount(), outRect, childPosition, itemCount);
                }
            }
        }
    
    /**
         * 设置GridLayoutManager 类型的 offest
         *
         * @param orientation   方向
         * @param spanCount     个数
         * @param outRect       padding
         * @param childPosition 在 list 中的 postion
         * @param itemCount     list size
         */
        private void setGridOffset(int orientation, int spanCount, Rect outRect, int childPosition, int itemCount) {
            float totalSpace = mSpace * (spanCount - 1) + mEdgeSpace * 2; // 总共的padding值
            float eachSpace = totalSpace / spanCount; // 分配给每个item的padding值
            int column = childPosition % spanCount; // 列数
            int row = childPosition / spanCount;// 行数
            float left;
            float right;
            float top;
            float bottom;
            if (orientation == GridLayoutManager.VERTICAL) {
                top = 0; // 默认 top为0
                bottom = mSpace; // 默认bottom为间距值
                if (mEdgeSpace == 0) {
                    left = column * eachSpace / (spanCount - 1);
                    right = eachSpace - left;
                    // 无边距的话  只有最后一行bottom为0
                    if (itemCount / spanCount == row) {
                        bottom = 0;
                    }
                } else {
                    if (childPosition < spanCount) {
                        // 有边距的话 第一行top为边距值
                        top = mEdgeSpace;
                    } else if (itemCount / spanCount == row) {
                        // 有边距的话 最后一行bottom为边距值
                        bottom = mEdgeSpace;
                    }
                    left = column * (eachSpace - mEdgeSpace - mEdgeSpace) / (spanCount - 1) + mEdgeSpace;
                    right = eachSpace - left;
                }
            } else {
                // orientation == GridLayoutManager.HORIZONTAL 跟上面的大同小异, 将top,bottom替换为left,right即可
                left = 0;
                right = mSpace;
                if (mEdgeSpace == 0) {
                    top = column * eachSpace / (spanCount - 1);
                    bottom = eachSpace - top;
                    if (itemCount / spanCount == row) {
                        right = 0;
                    }
                } else {
                    if (childPosition < spanCount) {
                        left = mEdgeSpace;
                    } else if (itemCount / spanCount == row) {
                        right = mEdgeSpace;
                    }
                    top = column * (eachSpace - mEdgeSpace - mEdgeSpace) / (spanCount - 1) + mEdgeSpace;
                    bottom = eachSpace - top;
                }
            }
            outRect.set((int) left, (int) top, (int) right, (int) bottom);
        }
    

    接下来是onDraw和onDrawOver的用法。这里我制作简单的介绍,如果你想要做出一些不一样的内容。文章末尾我会给出几个比较好的博客,供大家欣赏。
    首先我们要知道onDraw和onDrawOver的执行时间,一下是RecyclerView绘制流程的源码

    /**
         * RecyclerView的draw方法
         * @param c
         */
        @Override
        public void draw(Canvas c) {
            // 调用父类也就是View的draw方法
            super.draw(c);
            final int count = mItemDecorations.size();
            for (int i = 0; i < count; i++) {
                // 执行ItemDecorations的onDrawOver方法
                mItemDecorations.get(i).onDrawOver(c, this, mState);
            }
        }
    
        /**
         *  View的draw方法
         * @param canvas
         */
        @CallSuper
        public void draw(Canvas canvas) {
            ....
            // View会继续调用onDraw
            if (!dirtyOpaque) onDraw(canvas);
            ....
        }
    
        /**
         * RecyclerView的onDraw方法
         * @param c
         */
        @Override
        public void onDraw(Canvas c) {
            super.onDraw(c);
            final int count = mItemDecorations.size();
            for (int i = 0; i < count; i++) {
                // 执行ItemDecorations的onDraw方法
                mItemDecorations.get(i).onDraw(c, this, mState);
            }
        }
    

    View先会调用draw方法,在draw中又会调用onDraw方法。 而在RecyclerView的draw方法中会先通过super.draw() 调用父类也就是View的draw方法,进而继续调用RecyclerView的OnDraw方法,ItemDecorations的onDraw方法就在此时会被调用,RecyclerView执行完super.draw()之后,ItemDecorations的onDrawOver方法也被调用,这也就解释了为什么说onDraw会绘制在itemview之前,表现形式是在最底层(抽象的说法,最底层应该是background),onDrawOver是在itemview绘制之后,表现形式在最上层。知道了onDraw和onDrawOver的具体执行时间与含义。我们这里再来看Android提供给我们的DividerItemDecoration实现我们listView的Divider效果是如何做到的

       @Override
        public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
            if (parent.getLayoutManager() == null || mDivider == null) {
                return;
            }
            if (mOrientation == VERTICAL) {
                drawVertical(c, parent);
            } else {
                drawHorizontal(c, parent);
            }
        }
    
       private void drawVertical(Canvas canvas, RecyclerView parent) {
            canvas.save();
            final int left;
            final int right;
            //noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.
            if (parent.getClipToPadding()) {
                left = parent.getPaddingLeft();
                right = parent.getWidth() - parent.getPaddingRight();
                canvas.clipRect(left, parent.getPaddingTop(), right,
                        parent.getHeight() - parent.getPaddingBottom());
            } else {
                left = 0;
                right = parent.getWidth();
            }
    
            final int childCount = parent.getChildCount();
            for (int i = 0; i < childCount; i++) {
                final View child = parent.getChildAt(i);
                parent.getDecoratedBoundsWithMargins(child, mBounds);
                final int bottom = mBounds.bottom + Math.round(child.getTranslationY());
                final int top = bottom - mDivider.getIntrinsicHeight();
                mDivider.setBounds(left, top, right, bottom);
                mDivider.draw(canvas);
            }
            canvas.restore();
        }
    
        private void drawHorizontal(Canvas canvas, RecyclerView parent) {
            canvas.save();
            final int top;
            final int bottom;
            //noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.
            if (parent.getClipToPadding()) {
                top = parent.getPaddingTop();
                bottom = parent.getHeight() - parent.getPaddingBottom();
                canvas.clipRect(parent.getPaddingLeft(), top,
                        parent.getWidth() - parent.getPaddingRight(), bottom);
            } else {
                top = 0;
                bottom = parent.getHeight();
            }
    
      @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
                RecyclerView.State state) {
            if (mDivider == null) {
                outRect.set(0, 0, 0, 0);
                return;
            }
            if (mOrientation == VERTICAL) {
                outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
            } else {
                outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
            }
        }
    

    原理很简单,总结起来就是通过getItemOffsets方法来设置我们item的paddingBottom或paddingRight,然后在我们onDraw方法中遍历我们的item并在我们需要的区域内绘制我们想要的绘制的内容。

    setItemAnimator

    设置我们的Item动画效果。Android提供了一个默认的实现。DefaultItemAnimator,添加以下代码我们就可以了。

    rclMain.setItemAnimator(new DefaultItemAnimator());
    

    添加了这行代码我们再运行我们的代码发现,完全木有效果啊,什么鬼。
    这里我忘记和大家说了。如果我们想要看到效果,就在我们的数据集合中添加删除数据。并执行notifyItemInserted(position)或notifyItemRemoved(position) ,就可以看到效果啦。当然这一种效果怎么可能满足的了我们产品的需求呢,幸好我们有大神。大神们已经开源了很多有关的代码。这就要大家自己去挖掘了。

    点击事件

    因为RecyclerView没有给我们提供onItemClickListener的方法。所以这个只能我们自己来实现,具体实现比较简单,和我们ListView,Item中设置点击按钮的实现方式是一样的。我们只需要自己在RecyclerView中写一个onclickListener监听器,并提供出去,在BindViewHolder的时候设置ItemView的点击事件,调用我们监听器的方法回调给我们的使用者就可以了。

    class HomeAdapter extends RecyclerView.Adapter<HomeAdapter.MyViewHolder>
    {
    
    //...
        public interface OnItemClickLitener
        {
            void onItemClick(View view, int position);
            void onItemLongClick(View view , int position);
        }
    
        private OnItemClickLitener mOnItemClickLitener;
    
        public void setOnItemClickLitener(OnItemClickLitener mOnItemClickLitener)
        {
            this.mOnItemClickLitener = mOnItemClickLitener;
        }
    
        @Override
        public void onBindViewHolder(final MyViewHolder holder, final int position)
        {
            holder.tv.setText(mDatas.get(position));
    
            // 如果设置了回调,则设置点击事件
            if (mOnItemClickLitener != null)
            {
                holder.itemView.setOnClickListener(new OnClickListener()
                {
                    @Override
                    public void onClick(View v)
                    {
                        int pos = holder.getLayoutPosition();
                        mOnItemClickLitener.onItemClick(holder.itemView, pos);
                    }
                });
    
                holder.itemView.setOnLongClickListener(new OnLongClickListener()
                {
                    @Override
                    public boolean onLongClick(View v)
                    {
                        int pos = holder.getLayoutPosition();
                        mOnItemClickLitener.onItemLongClick(holder.itemView, pos);
                        return false;
                    }
                });
            }
        }
    //...
    }
    

    知道了如何设置Item的点击事件,我相信设置item中单独View的点击事件也一定难不住你了。

    不同类型的Item

    我们可以根据RecyclerView.Adapter提供的getItemViewType();方法根据Position位置返回我们不同的Type类型。
    在onCreateViewHolder中根据不同的type创建不同的布局和ViewHolder并返回。在onBindViewHolder中根据ViewHodler instanceof方法获取他数据哪个ViewHolder来进行赋值操作,例子如下:

    lass HomeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    
        private static final int TYPE_HEADER = 1;
        private static final int TYPE_ITEM = 2;
    
        @Override
        public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            if (viewType == TYPE_HEADER) {
                //...
                return new HeaderViewHolder(v);
            } else {
                //...
                return new MyViewHolder(v);
            }
        }
    
        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            if (holder instanceof HeaderViewHolder) {
                //...
            } else if (holder instanceof MyViewHolder) {
                //...
            }
        }
    
        @Override
        public int getItemCount() {
            return mDatas.size() + 1; //增加头部ItemView
        }
    
        @Override
        public int getItemViewType(int position) {
            if (position == 0) {
                return TYPE_HEADER;
            } else {
                return TYPE_ITEM;
            } 
        }
    
        class HeaderViewHolder extends RecyclerView.ViewHolder {
            //...
        }
    
        class MyViewHolder extends RecyclerView.ViewHolder {
            //...
        }
    }
    

    参考文章:https://www.jianshu.com/p/bb09d3ddc64f
    https://blog.csdn.net/lmj623565791/article/details/45059587
    https://www.jianshu.com/p/e742df6f59e2
    https://www.jianshu.com/p/6a093bcc6b83
    https://www.jianshu.com/p/3221b5c8fc38

    相关文章

      网友评论

          本文标题:RecyclerView的使用,一篇文章就够了。

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