RecyclerView系列之(2):为RecyclerView

作者: 牛牛技术 | 来源:发表于2016-05-23 12:24 被阅读98935次

    就在昨天中午,我在简书上发布了我个人的第一篇技术文档:RecyclerView系列之: RecyclerView系列之(1)为RecyclerView添加Header和Footer,也很有幸,能够得到那么多人的支持,这让我迫不及待的赶紧写第二篇文章。今天我将谈谈:为RecyclerView添加分隔线。


    一. 理解ListView和RecyclerView中的ChildView

    在讲为Item加入分割线本质的前,先来介绍,认识一下ChildView,也就是平时我们用到的ListView,RecyclerView中的getChildAt(int position)这个返回的ChildView是哪一部分?到底是哪一部分呢?一开始的时候,我理解错了,但是经过下面两张图这么一比较,你就明白了:

    Item布局layout_margin == 0 Item布局Layout_margin == 16dp

    下面看代码的区别:
    第一张图的代码, 也就是每一个list_item的布局文件(下同)如下:

    <?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="50dp">    
          <TextView        
            android:id="@+id/list_item"        
            android:layout_width="match_parent" 
            android:layout_height="match_parent"        
            android:gravity="center"        
            android:textSize="20sp"        
            android:textColor="#262526"        
            android:background="#08da1d"/>
    </LinearLayout>
    

    第二张图的代码:

    <?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="50dp"
    android:layout_margin="16dp">    
          <TextView        
            android:id="@+id/list_item"        
            android:layout_width="match_parent" 
            android:layout_height="match_parent"        
            android:gravity="center"        
            android:textSize="20sp"        
            android:textColor="#262526"        
            android:background="#08da1d"/>
    </LinearLayout>
    

    仔细看一下,它们的不同之处, 就是第二个图的代码中多了:

    android:layout_margin = "16dp"
    

    就多这一句而已。

    所以到这里我们应该知道了ChildView是哪一部分了,就是图二中绿色这一部分,边距这一部分并不属于ChildView, 而是属于ChildView的布局。
    这样我们了解ChildView之后,下面再来理解加入分隔线的原理就简单多了。


    二. 理解加入分隔线的原理

    在ListView中,Google为我们提供了SetDivider(Drawable divider)这样的方法来设置分隔线,那么在RecyclerView中,Google又为我们提供了什么样的方法去添加分隔线呢?通过查看官方文档,它,提供了:addItemDecoration(RecyclerView.ItemDecoration decor)这个方法了设置分隔线,那问题又来了,RecyclerView.ItemDecoration是什么东西呢?继续查:然后发现如下:它原来是一个类,里面封装了三个方法:
    (1)void getItemOffsets ()
    (2)void onDraw ()
    (3)void onDrawOver ()


    通过上面的三个方法,可以看出,这是要自己直接画上去,准确的说这几个方法是:添加Divider,主要是找到添加Divider的位置, 而Divider是在drawable文件中写好了的。 利用onDraw和onDrawOver都差不多,我们在创建自己的Decoration类继承RecyclerView.ItemDecoration的时候,我们只要重写getItemOffsets(),还有onDraw()和onDrawOver两者其中之一就可以了.


    那getItemOffsets()方法有什么用呢?从字面意思就是Item要偏移, 由于我们在Item和Item之间加入了分隔线,线其实本质就是一个长方形,也是用户自定义的,既然线也有长宽高,就画横线来说,上面的Item加入了分隔线,那下面的Item就要往下平移,平移的量就是分隔线的高度。不理解每关系,后面看代码就容易理解了。


    现在我们知道了如何添加了,就是通过画,那到底是画在哪里呢?画的位置又怎么确定呢?下面看图:

    分隔线的位置图

    我现在拿画横线来说,从上面这个图中,我们很容易就可以看到,我们画分隔线的位置,是在每一个Item的布局之间,注意:是布局之间。

    好了,我们确定了画在哪里,那我们怎么确定画线的具体的坐标位置呢?也就是我们要确定:分隔线的left, top, right, Bottom. 在Adapter中,我们很容易通过parent(这个parent它其实就是我们能看到的部分)获取每一个childView:
    (1)left:parent.getPaddingLeft()
    (2)right: parent. getWidth()-parent.getPaddingRight();
    (3)top : 就是红线的上面:我们通过ChildView.getBottom()来得到这个Item的底部的高度,也就是蓝线位置,蓝线和红线之间间距:就是这个Item布局文件的:layout_marginBottom, 然后top的位置就是两者之和。
    (4)bttom: 就是top加上分隔线的高度:top+线高


    通过上面的解析,你也许知道了加入分隔线的原理,不理解也没有关系,说也不是说得很清楚,下面直接上代码,通过代码来理解。

    三. Talk is cheap, show you the code.

    (1)首先,先来看主布局文件:activity_main.xml:
    <?xml version="1.0" encoding="utf-8"?>
    <android.support.design.widget.CoordinatorLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools" 
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true"
        tools:context="com.study.wnw.recyclerviewdivider.MainActivity">
    
        <android.support.v7.widget.RecyclerView
            android:id="@+id/recyclerview"
            android:layout_width="match_parent"
           android:layout_height="match_parent">    
        </android.support.v7.widget.RecyclerView>
    </android.support.design.widget.CoordinatorLayout>
    

    我在这里面仅仅加入了一个RecyclerView


    (2)RecyclerView里面每个Item的布局文件:item_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="50dp"
                  android:layout_margin="16sp">
        <TextView
            android:id="@+id/list_item"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:textSize="20sp"
            android:textColor="#f7f4f7"
            android:background="#08da1d"/>
    </LinearLayout>
    

    这也没有什么可讲的,就是在里面添加一个TextView用来显示文本


    (3)我们RecyclerView的适配器MyAdapater.java:
    package com.study.wnw.recyclerviewdivider;
    import android.content.Context;
    import android.support.v7.widget.RecyclerView;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.TextView;
    import java.util.List;
    import java.util.concurrent.CopyOnWriteArrayList;
    /** * Created by wnw on 16-5-22. */
    public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    
        //定义一个集合,接收从Activity中传递过来的数据和上下文
        private List<String> mList;
        private Context mContext;
    
        MyAdapter(Context context, List<String> list){
            this.mContext = context;
            this.mList = list;
        }
    
        //得到child的数量
        @Override
        public int getItemCount() {
            return mList.size();
        }
        
        //创建ChildView
        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View layout = LayoutInflater.from(mContext).inflate(R.layout.item_view, parent, false);
            return new MyHolder(layout);
        }
    
        //将数据绑定到每一个childView中
        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            if (holder instanceof MyHolder){
                final String itemText = mList.get(position);
                ((MyHolder)holder).tv.setText(itemText);
            }
        }
    
        // 通过holder的方式来初始化每一个ChildView的内容
        class MyHolder extends RecyclerView.ViewHolder{
            TextView tv;
            public MyHolder(View itemView) {
                super(itemView);
                tv = (TextView)itemView.findViewById(R.id.list_item);
            }
        }
    }
    
    

    好了,这里也没有什么好讲的,也不是我们这篇文章的重点,下面重点来了。


    (4)我们自定义的MyDecoration.java:(继承RecyclerView.ItemDecoration)
    package com.study.wnw.recyclerviewdivider;
    import android.content.Context;
    import android.content.res.TypedArray;
    import android.graphics.Canvas;
    import android.graphics.Rect;
    import android.graphics.drawable.Drawable;
    import android.support.v7.widget.LinearLayoutManager;
    import android.support.v7.widget.RecyclerView;
    import android.util.Log;
    import android.view.View;
    
    /** * Created by wnw on 16-5-22. */
    
    public class MyDecoration extends RecyclerView.ItemDecoration{
    
        private Context mContext;
        private Drawable mDivider;
        private int mOrientation;
        public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
        public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
    
        //我们通过获取系统属性中的listDivider来添加,在系统中的AppTheme中设置
        public static final int[] ATRRS  = new int[]{
                android.R.attr.listDivider
        };
    
        public MyDecoration(Context context, int orientation) {
            this.mContext = context;
            final TypedArray ta = context.obtainStyledAttributes(ATRRS);
            this.mDivider = ta.getDrawable(0);
            ta.recycle();
            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, RecyclerView.State state) {
            if (mOrientation == HORIZONTAL_LIST){
                drawVerticalLine(c, parent, state);
            }else {
                drawHorizontalLine(c, parent, state);
            }
        }
    
        //画横线, 这里的parent其实是显示在屏幕显示的这部分
        public void drawHorizontalLine(Canvas c, RecyclerView parent, RecyclerView.State state){
            int left = parent.getPaddingLeft();
            int right = parent.getWidth() - parent.getPaddingRight();
            final int childCount = parent.getChildCount();
            for (int i = 0; i < childCount; i++){
                final View child = parent.getChildAt(i);
    
                //获得child的布局信息
                final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)child.getLayoutParams();
                final int top = child.getBottom() + params.bottomMargin;
                final int bottom = top + mDivider.getIntrinsicHeight();
                mDivider.setBounds(left, top, right, bottom);
                mDivider.draw(c);
                //Log.d("wnw", left + " " + top + " "+right+"   "+bottom+" "+i);
            }
        }
    
        //画竖线
        public void drawVerticalLine(Canvas c, RecyclerView parent, RecyclerView.State state){
            int top = parent.getPaddingTop();
            int bottom = parent.getHeight() - parent.getPaddingBottom();
            final int childCount = parent.getChildCount();
            for (int i = 0; i < childCount; i++){
                final View child = parent.getChildAt(i); 
    
               //获得child的布局信息
                final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)child.getLayoutParams();
                final int left = child.getRight() + params.rightMargin;
                final int right = left + mDivider.getIntrinsicWidth();
                mDivider.setBounds(left, top, right, bottom);
                mDivider.draw(c);
            }
        }
    
        //由于Divider也有长宽高,每一个Item需要向下或者向右偏移
        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
            if(mOrientation == HORIZONTAL_LIST){
                //画横线,就是往下偏移一个分割线的高度
                outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
            }else {
                //画竖线,就是往右偏移一个分割线的宽度
                outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
            }
        }
    }
    
    

    从上面的代码中,我们还通过系统属性来适应屏幕的横屏和竖屏,然后确定画横的,还是竖的Divider,其实在里面我们做了三件事,第一件是:获取到系统中的listDivider, 我们就是通过它在主题中去设置的,下面第(6)小点看一下代码就知道了。第二件事:就是找到我们需要添加Divider的位置,从onDraw方法中去找到,并将Divider添加进去。第三个是:得到Item的偏移量。


    (5)看看我们的MainActivity.java
    package com.study.wnw.recyclerviewdivider;
    import android.os.Bundle;
    import android.support.v7.app.AppCompatActivity;
    import android.support.v7.widget.LinearLayoutManager;
    import android.support.v7.widget.RecyclerView;
    import java.util.ArrayList;
    import java.util.List;
    public class MainActivity extends AppCompatActivity {
        //定义RecyclerView
        private RecyclerView mRecyclerView = null;
    
        //定义一个List集合,用于存放RecyclerView中的每一个数据
        private List<String> mData = null;
    
        //定义一个Adapter
        private MyAdapter mAdapter; 
    
       //定义一个LinearLayoutManager
        private LinearLayoutManager mLayoutManager;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            //RecyclerView三步曲+LayoutManager
            initView();
            initData();
            mAdapter = new MyAdapter(this,mData);
            mRecyclerView.setLayoutManager(mLayoutManager);
            mRecyclerView.setAdapter(mAdapter); 
    
            //这句就是添加我们自定义的分隔线
            mRecyclerView.addItemDecoration(new MyDecoration(this, MyDecoration.VERTICAL_LIST));
        }
    
        //初始化View
        private void initView(){
            mLayoutManager = new LinearLayoutManager(this);
            mRecyclerView = (RecyclerView)findViewById(R.id.recyclerview);
        }
    
        //初始化加载到RecyclerView中的数据, 我这里只是给每一个Item添加了String类型的数据
        private void initData(){
            mData = new ArrayList<String>();
            for (int i = 0; i < 20; i++){
                mData.add("Item" + i);
            }
        }
    }
    
    (6)分隔线Divider的drawable文件:divider..xml
    <?xml version="1.0" encoding="utf-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle">
        <solid android:color="#7b7a7a"/>
        <size android:height="1dp"/>
    </shape>
    

    我们在这里面,画了一个:rectangle, 给它填充颜色,还有高度,这样就搞定了,高度小,显示出来也是一条线:其实线的本质就是长方形。这里可以根据个人需要,画不同类型的divider


    (7)在styles.xml的AppTheme中,设置listDivider为我们的divider.xml文件:

    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <item name="android:listDivider">@drawable/divider</item>
    </style>
    

    这样,我们将系统的listDivider设置成我们自定义的divider. 还记得我们在MyDecoration中获取系统的listDivider这个属性吗,这样通过这个属性,我们就可以将我们的divider.xml文件和MyDecoration.java进行关联了。


    到这里所有的工作就完成了,下面展示一下运行结果:

    竖屏效果图 横屏效果图

    经过几个小时的写作,终于搞定了,虽然仅仅是一个添加分隔线的功能,但是还是想尽可能的通过自己的语言去理解,去认知它的原理,这样做起来就简单多了。一开始的时候,我夜不知道怎么去用,也参考了别人写的文章,特别是鸿洋大神的:Android RecyclerView 使用完全解析 体验艺术般的控件, 写得特别的棒,从中也学到了一些知识。


    好了,这篇文章暂时写到这里了,简单的介绍了一些RecyclerView分隔线的原理和添加方法,希望大家能够多多交流,过几天我会继续写下一篇文章,RecyclerView系列之(3):为RecyclerView添加下拉刷新和上拉加载的功能。最后还是要感谢大家,感谢这个平台,能够让我们一起交流,一切学习。

    相关文章

      网友评论

      • Ktink:如果不是特殊情况的话 根本不用去 写那么多代码 直接去控制它的 Item 就可以了! 直接在onBindViewHolder里面控制即可
      • 7c7ea27ffeb1:打印 log 可以发现,getItemOffset 中的 mDivider.getIntrinsicHeight() 的值是 -1,
      • largecatfish:很精髓
      • Keasal:写的很用心:smile:
      • 月下白牛:每次下拉请求数据 分割线总是变宽一倍 什么原因
      • Android调调:写的很棒,我想问一下为什么itme加了背景色 分割线就不管用了呢
      • bobgun:判断写错了啊! 博主,还不改过来
      • Jooyer: //由于Divider也有长宽高,每一个Item需要向下或者向右偏移
        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        if(mOrientation == HORIZONTAL_LIST){
        //画横线,就是往下偏移一个分割线的高度
        outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
        }else {
        //画竖线,就是往右偏移一个分割线的宽度
        outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
        }
        }
        }

        判断条件错了,当水平时,应该增加左右距离,那么因该是 outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
        7c7ea27ffeb1:打印 log 可以发现,mDivider.getIntrinsicHeight() 的值是 -1,
      • 李云龙_:哥,你那判断(mOrientation == HORIZONTAL_LIST)那,绝壁写反了,,
      • HanlyJiang:感谢分享
        建议可以提下 Android 提供的几种已实现的 ItemDecoration 一般可以满足大部分需求 那种使用更快速
      • 青蛙要fly:《所以到这里我们应该知道了ChildView是哪一部分了,就是图二中绿色这一部分,边距这一部分并不属于ChildView, 而是属于ChildView的布局。》

        错别字吧。最后是而是属于RecycleView的布局

      • RabbitL:画竖线和横线的代码写反了,计算偏移量那块也写反了,题主有时间改一下吧,要不会误导一些人的~
      • 不如来吃茶:为什么我的mDivider.getIntrinsicHeight()总是返回-1
      • 迷人的萨克斯:通俗易懂,接地气。
      • 若邪:本身listview是默认带分割线的,但是RecyclerView却默认取消了分割线,但是为了加一个简单的分割线还得这么麻烦的去写,还是想不明白为何RecyclerView没有个直接加默认分割线的方法
      • Yuven:nice :clap:
      • 热爱生活的思绪缓慢者:写的很详细,不过有个错误。在getItemOffsets()中,你的判断写反了,你看看
        太阳晒得我丶好干瘪:是写反了,害我找了半天
        Find_A_Way:我也觉得设置偏移量的时候写反了
      • 粘豆包儿:我感觉你现在仍然理解错了 :joy:
      • wo叫天然呆:如果列表第一项上面要加分割线的话,要如何实现?
      • 黄小黄啊:一般的需求的话直接在item.xml🀄️设置margin就可以了,
      • a26c897ad0db:Divider mDivider = context.getResources().getDrawable(R.drawable.xxx);也可以设置分割线样式,不一定要去获取系统的listDivider来修改! :relaxed:
      • 3ab5d611f959:增加一个构造函数,你这种写不够灵活。
        public MyDecoration(Context context,int orientation,int resId)
        {
        this.mContext = context;
        this.mDivider = this.mContext.getResources().getDrawable(resId);
        setOrientation(orientation);
        }

        这样就可以在activity直接设置,当然也可以用图片当做divider.

        mRecyclerView.addItemDecoration(new MyDecoration(this, MyDecoration.VERTICAL_LIST,R.drawable.divider));

        Done!

      • 码海浮沉:你好,我参考hongyang大神的博客,grid部分的划线却没有左右,只有下划线,不知道怎么回事
      • 仰望星空的张拭心:研究的精神很棒。但只要在 item 的布局文件里加个水平的线,一劳永逸
        4a7f1e2ef079:@仰望星空的张拭心 那如果滑动的时候 也会带着那条水平线滑动吧。
      • 冰冰的冻结:颜色 和 高度如何自定义呢
      • 码读先生: top 和 left 是直接计算的

        对应的right和bottom,则如下:
        right = left + width;
        bottom = top + height;

        所以对应的如下:
        final int top = child.getBottom() + params.bottomMargin;
        final int bottom = top + mDivider.getIntrinsicHeight();
        mDivider.setBounds(left, top, right, bottom);

        还要注意getChildAt()所指的部分。
      • 牛牛技术:恩恩,我项目加了点击事件,就是在RecyclerView的Adapter中,加入接口OnItemClickListener和setOnItemClickListener()这个方法,然后在onBindViewHolder中给加添加事件:
        holder.itemView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
        mItemClickListener.onItemClick(position, item);
        }
        });

        然后在Activity或者Fragment中为RecyclerView加入这个事件。这样就可以了,至于selector这个,我没有做选择变色,不是很了解为什么没有变化 :joy:
      • MarcoWong:嗯,不错。我说我之前一直分割线一直不显示,然后加了Margin之后就出来了。 :joy:

        对了,item的点击事件 你研究到了没?我加了selector做选择变色,好像没有变化。
        ec0c29b91ec5:@MarcoWong 我用到RecycleView,它的Item是左边图片,右边文字。因为recycle没有Item的点击事件,我就把占比较大图片设置一个tag。
        holder.iv_icon.setTag(position);
        然后下面就可以
        iv_icon.setOnClickListener(new View.OnClickListener()
        对了,这写成一个内部类个时候适配器
      • ray_lee_:写的不错。
      • 牛牛技术:代码开源地址:https://github.com/wangnaiwen/RecyclerViewDivider
        cjcj125125:@右眼皮的爱 Gridview样式怎么设置分隔线呢??????
      • 人间入画:讲的非常详细

      本文标题:RecyclerView系列之(2):为RecyclerView

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