Android——RecyclerView入门学习之ItemDecoration(一)

学习资料:

Piasy大神的每篇博客质量都很高,强烈推荐

网上有很多关于RecyclerView学习博客,之前看了几篇,但基本侧重点都是RecyclerView.Adapter。关于RecyclerView的侧滑删除,之前有过简单学习ItemTouchHleper实现RecyclerView侧滑删除,但对RecyclerView了解远远不够。除了Adapter外,RecyclerView还有很多其他强大的地方需要学习

天才木木同学收集整理的的Android开发之一些好用的RecyclerView轮子非常好


学习计划:


1. ItemDecoration 条目装饰<p>

是一个抽象类,顾名思义,就是用来装饰RecyclerView的子item的,通过名字就可以知道,功能并不仅仅是添加间距绘制分割线,是用来装饰item的。源码中的描述:

An ItemDecoration allows the application to add a special drawing and layout offset to specific item views from the adapter's data set. This can be useful for drawing dividers between items, highlights, visual grouping boundaries and more.

基本的功能是可以用来给RecyclerView的子item设置四边边距,以及上下左右绘制分割线。当然功能不止这些

ItemDecoration一个有6个抽象方法,有3个还废弃了,也就剩下3个需要学习

  • getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) 设置四边边距
  • onDraw(Canvas c, RecyclerView parent, State state) 绘制装饰
  • onDrawOver(Canvas c, RecyclerView parent, State state) 绘制蒙层

1.1 使用RecyclerView展示50条字符串数据 <p>

直接使用RecyclerView展示50条纯字符串数据,代码:

public class MainActivity extends AppCompatActivity {
    private RecyclerView rv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        init();
    }

    private void init() {
        rv = (RecyclerView) findViewById(R.id.rv_main_activity);
        //设置布局管理器
        LinearLayoutManager manager = new LinearLayoutManager(this);
        manager.setOrientation(LinearLayoutManager.VERTICAL);
        rv.setLayoutManager(manager);
        //设置ItemDecoration

        //适配器
        RecyclerViewAdapter adapter = new RecyclerViewAdapter(rv, R.layout.item_layout);
        rv.setAdapter(adapter);
        //添加数据
        addData(adapter);
    }

    /**
     * 添加数据
     */
    private void addData(RecyclerViewAdapter adapter) {
        List<String> listData = new ArrayList<>();
        for (int i = 0; i < 50; i++) {
            listData.add("英勇青铜5---->"+i);
        }
        adapter.setData(listData);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (null != rv) {
            rv.setAdapter(null);
        }
    }
}

代码中没有为RecyclerView设置ItemDecorationLayoutManagerLineatLayoutManager


子item布局文件:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
        android:id="@+id/tv_item_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/colorAccent"
        android:textAllCaps="false"
        android:textColor="@android:color/white"
        android:textSize="20sp" />
</LinearLayout>

布局也特别简单,给TextView设置了背景色,字体是白色

运行效果:

不设置ItemDecroation

item间就没有间距,也没有任何的分割线,TextView背景色导致整个RecyclerView看起来都设置了背景色

下面为每个item底部添加间距


1.2 getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) 设置四边偏移量

自定义一个RVItemDecoration继承ItemDecroation,重写getItemOffsets()

代码:

public class RVItemDecoration extends RecyclerView.ItemDecoration {

    private static final int HORIZONTAL = LinearLayoutManager.HORIZONTAL;//水平方向
    private static final int VERTICAL = LinearLayoutManager.VERTICAL;//垂直方向
    private int orientation;//方向
    private final int decoration;//边距大小 px

    public RVItemDecoration(@LinearLayoutCompat.OrientationMode int orientation int orientation, int decoration) {
        this.orientation = orientation;
        this.decoration = decoration;
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        final RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
        final int lastPosition = state.getItemCount() - 1;//整个RecyclerView最后一个item的position
        final int current = parent.getChildLayoutPosition(view);//获取当前要进行布局的item的position
        Log.e("0000", "0000---->" + current);
        Log.e("0000", "0000state.getItemCount()---->" + state.getItemCount());
        Log.e("0000", "0000getTargetScrollPosition---->" + state.getTargetScrollPosition());
        Log.e("0000", "0000state---->" + state.toString());
        if (current == -1) return;//holder出现异常时,可能为-1
        if (layoutManager instanceof LinearLayoutManager && !(layoutManager instanceof GridLayoutManager)) {//LinearLayoutManager
            if (orientation == LinearLayoutManager.VERTICAL) {//垂直
                outRect.set(0, 0, 0, decoration);
                if (current == lastPosition) {//判断是否为最后一个item
                    outRect.set(0, 0, 0, 0);
                } else {
                    outRect.set(0, 0, 0, decoration);
                }
            } else {//水平
                if (current == lastPosition) {//判断是否为最后一个item
                    outRect.set(0, 0, 0, 0);
                } else {
                   outRect.set(0, 0,decoration,  0);
                }
            }
        }
    }
}

Acivity中,初始化RecyclerView的时候使用:

//设置ItemDecoration
rv.addItemDecoration(new RVItemDecoration(LinearLayoutManager.VERTICAL,30));

运行后效果

添加底部间距

由于是入门学习,暂时也只是针对对LinearLayoutManager做了一点简单处理,最后1个item不再添加底部间距。实际开发的时候考虑的就要比这复杂的多。LinearLayoutManager大部分时候考虑itemposition就可以,但GridLayoutManagerStaggeredGridLayoutManager需要考虑行和列,情况就比较复杂。


方法中有4个参数

  • Rect outRect:可以简单理解为item四边边距奉封装在这个对象中,用来设置Itempadding
  • View view: childView,就是item,可以理解为item的根View,并不是item中的控件
  • RecyclerView parent:就是RecyclerView自身
  • RecyclerView.State state : RecyclerView的状态,但并不包含滑动状态

1.2.1 RecyclerView.State <p>

这个类是RecyclerView的一个静态内部类,源码中的解释:

Contains useful information about the current RecyclerView state like target scroll position or view focus. State object can also keep arbitrary data, identified by resource ids.

个人理解:
这个State封装着RecyclerView当前的状态,例如滑动目标的Position或者子控件的焦点。State对象也可以对任意的数据通过资源id进行保存或者识别

State中有3个用于标记当前所处步骤的常量值:

  • STEP_START :布局开始
  • STEP_LAYOUT :布局中
  • STEP_ANIMATIONS :处于动画中

RecyclerView的工作流程肯定也会是measure,layout,draw。3个值在RecyclerViewonMeasure()有使用,感觉是用来标识RecyclerView在测量过程中所处于的不同时机。目前并不清楚具体的影响,RecyclerView工作流程需要以后再进行深入学习

方法 作用
getItemCount() 得到整个RecyclerView中,目前的item的数量
isMeasuring() 是否正在测量
isPreLayout() 是否准备进行布局
get(int resourceId) 根据资源id获取item中的控件,建议使用R.id.*
put(int resourceId, Object data) 添加一个指定id映射的资源对象,建议使用R.id.*来避免冲突
remove(int resourceId) 根据使用R.id.*指定id来删除存入的控件对象
getTargetScrollPosition() 返回已经可见的滑动目标在Adapter的索引值,滑动目标由SmoothScroller来指定
hasTargetScrollPosition() 判断是否已经滑动到目标
willRunPredictiveAnimations() 判断是否进行预测模式的动画在布局过程中
willRunSimpleAnimations() 判断是否进行简单模式的动画在布局过程中

getItemCount()并不是完全等于getAdapter.getItemCount(),在源码的注释中,关于postion的计算,建议使用State.getItemCount()而非立即直接通过Adapter

State有些方法和属性涉及到其他的类,有些涉及RecyclerView的工作过程,目前我的学习程度也不是很了解,暂时并不打算继续深挖学习下去,总觉得理解有错误,知道的同学请指出


1.3 onDraw(Canvas c, RecyclerView parent, State state)绘制装饰 <p>

这个用于绘制divider,绘制在item的下一层,也就是说item会盖在divider所在层的上面

使用重写了onDrawer()方法和onDrawOver()ItemDecoration后,对RecyclerView在绘制item时有些影响,主要是由于绘制顺序:

mItemDecoration.onDraw()-->item.onDraw()--->mItemDecoration.onDrawOver()

onDraw()方法可以为divier设置绘制范围,并且绘制范围可以超出在 getItemOffsets 中设置的范围,但由于是在item下面一层进行绘制,会存在overdraw


简单使用,完整代码

public class RVItemDecoration extends RecyclerView.ItemDecoration {
    private final int orientation;//方向
    private final int decoration;//边距大小 px
    private final int lineSize ;//分割线厚度
    private final ColorDrawable mDivider;

    public RVItemDecoration(@LinearLayoutCompat.OrientationMode int orientation, int decoration, @ColorInt int color, int lineSize) {
        mDivider = new ColorDrawable(color);
        this.orientation = orientation;
        this.decoration = decoration;
        this.lineSize = lineSize;
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        final RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
        final int lastPosition = state.getItemCount() -1;//整个RecyclerView最后一个item的position
        final int current = parent.getChildLayoutPosition(view);//获取当前要进行布局的item的position
        if (current == -1) return;
        if (layoutManager instanceof LinearLayoutManager && !(layoutManager instanceof GridLayoutManager)) {//LinearLayoutManager
            if (orientation == LinearLayoutManager.VERTICAL) {//垂直
               if (current == lastPosition) {//判断是否为最后一个item
                    outRect.set(0, 0, 0, 0);
                } else {
                    outRect.set(0, 0, 0, decoration);
                }
            } else {//水平
                if (current == lastPosition) {//判断是否为最后一个item
                    outRect.set(0, 0, 0, 0);
                } else {
                    outRect.set(0, 0, decoration, 0);
                }
            }
        }
    }

  /**
     * 绘制装饰
     */
    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
        if (orientation == LinearLayoutManager.VERTICAL) {//垂直
            drawHorizontalLines(c, parent);
        } else {//水平
            drawVerticalLines(c, parent);
        }
    }

    /**
     * 绘制垂直布局 水平分割线
     */
    private void drawHorizontalLines(Canvas c, RecyclerView parent) {
         //  final int itemCount = parent.getChildCount()-1;//出现问题的地方  下面有解释
        final int itemCount = parent.getChildCount();
        Log.e("item","---->"+itemCount);
        final int left = parent.getPaddingLeft();
        final int right = parent.getWidth() - parent.getPaddingRight();
        for (int i = 0; i < itemCount; i++) {
            final View child = parent.getChildAt(i);
            if (child == null) return;
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
            final int top = child.getBottom() + params.bottomMargin;
            final int bottom = top +lineSize;
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    /**
     * 绘制水平布局 竖直的分割线
     */
    private void drawVerticalLines(Canvas c, RecyclerView parent) {
        final int itemCount = parent.getChildCount();
        final int top = parent.getPaddingTop();
        for (int i = 0; i < itemCount; i++) {
            final View child = parent.getChildAt(i);
            if (child == null) return;
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
            final int bottom = child.getHeight() - parent.getPaddingBottom();
            final int left = child.getRight() + params.rightMargin;
            final int right = left +lineSize;
            if (mDivider == null) return;
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }
}

运行后的效果:

绘制底部分割线

同样这里也只是考虑了最简单的LinerLayoutManager一种情况。使用这个方法时,注意绘制范围,尽量避免overdraw

当间距小于分割线的宽度时,分割线绘制的厚度会保持与间距一样


1.3 onDrawOver(Canvas c, RecyclerView parent, State state) 绘制蒙层<p>

这个方法是在itemonDraw()方法之后进行回调,也就绘制在了最上层

简单使用,绘制一个颜色红黄渐变的圆

 @Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
    super.onDrawOver(c, parent, state);
    //画笔
    final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    //圆心 x 坐标
    final float x = parent.getWidth() / 2;
    ////圆心 y 坐标
    final float y = 100;
    //半径
    final float radius = 100;
    //渐变着色器 坐标随意设置的
    final LinearGradient shader = new LinearGradient(x-50, 0, x+100, 200, Color.RED, Color.YELLOW, Shader.TileMode.REPEAT);
    paint.setShader(shader);
    //绘制圆
    c.drawCircle(x, y, radius, paint);
}
绘制一个圆

只要手指在RecyclerView上进行滑动,onDrawOver()方法就会被回调。但onDrawOver()每回调一次,会将上次的绘制清除,只有最后一次的绘制会被保留。也就是说绘制的蒙层在屏幕只会有一个


2. 遇到的问题<p>

在绘制底部分割线的时候,遇到一个问题:

遇到的问题

当快速滑动时,底部会闪动,造成体验不好,如果分割线比较窄,不是很明显,分割线宽的时候就很明显

已解决 ,原因分析在下面


2.1 补充,问题修复 <p>

问题原因:
问题出在drawHorizontalLines()方法中final int itemCount = parent.getChildCount()-1这行代码,之所以减一考虑的是为了使最后一个item下,不用再绘制分割线。

RecyclerView.getChildCount()方法的返回值并不是recyclerViewAdapter中所有的item的数量,而是当前屏幕中出现在RecyclerViewitem的数量,一个item只要露出一点点,就算出现,就会被包含在内。

-1就会导致RecycelrView统计已经出现的item时的数量少一个,就会导致滑动过程中,屏幕中最后一个item的底部分割线不进行绘制,造成闪屏


解决办法:

不减1,就OK,修改为:

final int itemCount = parent.getChildCount();

注意:
ViewGroupgetChildCount()方法的返回值itemCount便是 getChildAt(int index)这个方法index的区间上限 ,[0,itemCount)。例如:

position示例

当前屏幕显示的是25--到-->42parent.getChildCount()的返回结果itemCount便是18。凡是在屏幕上第一个出现的itemindex便是0,哪怕只是漏出一点点。在parent.getChildAt(int index)中,index的取值范围便是0<= index < 18

2016.10.17 13:48


3.0 补充 官方推出DividerItemDecoration <p>

2016.10.20
Android support libraries更新了25.0.0,新增了BottomNavigationView,并增加了一个官方版的DividerItemDecoration,可以学习下代码,有一些不错的细节优化

以上信息从drakeet 博客得知,果然关注大神,能够多了解信息


3. 最后 <p>

作为一个青铜5的选手,也是热爱LOL的,也有着一颗王者心,可RNG,EDG全输了,止步8强,郁闷

本人很菜,有错误请指出

一个完整的练习:TitleItemDecoration

慕课有一个不错的视屏不一样的RecyclerView优雅实现复杂列表布局

共勉 :)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容