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

学习资料:

  • 张旭童同学的使用ItemDecoration为RecyclerView打造带悬停头部的分组列表
  • Piasy大神的深入理解 RecyclerView 系列之一:ItemDecoration

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

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

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


学习计划:

  • ItemDecoration
  • LayoutManager
  • RecyclerView.Adapter
  • DiffUtil
  • SimpleOnItemTouchListener
  • SmoothScroller
  • ItemAnimator

1. ItemDecoration 条目装饰

是一个抽象类,顾名思义,就是用来装饰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条字符串数据

直接使用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 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布局文件:



    

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

运行效果:

Android——RecyclerView入门学习之ItemDecoration(一)_第1张图片
不设置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));

运行后效果

Android——RecyclerView入门学习之ItemDecoration(一)_第2张图片
添加底部间距

由于是入门学习,暂时也只是针对对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

这个类是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)绘制装饰

这个用于绘制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);
        }
    }
}

运行后的效果:

Android——RecyclerView入门学习之ItemDecoration(一)_第3张图片
绘制底部分割线

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

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


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

这个方法是在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);
}
Android——RecyclerView入门学习之ItemDecoration(一)_第4张图片
绘制一个圆

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


2. 遇到的问题

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

Android——RecyclerView入门学习之ItemDecoration(一)_第5张图片
遇到的问题

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

已解决 ,原因分析在下面


2.1 补充,问题修复

问题原因:
问题出在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)。例如:

Android——RecyclerView入门学习之ItemDecoration(一)_第6张图片
position示例

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

2016.10.17 13:48


3.0 补充 官方推出DividerItemDecoration

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

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


3. 最后

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

本人很菜,有错误请指出

一个完整的练习:TitleItemDecoration

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

共勉 :)

你可能感兴趣的:(Android——RecyclerView入门学习之ItemDecoration(一))