Android——RecyclerView入门学习之ItemDecoration

    xiaoxiao2021-03-25  413

    学习资料:

    使用ItemDecoration为RecyclerView打造带悬停头部的分组列表深入理解 RecyclerView 系列之一:ItemDecoration

    Piasy大神的每篇博客质量都很高,强烈推荐。貌似博客网站安全证书有些问题,关注了他的微博,知道随意浏览也不会出现啥问题,我是直接无视浏览器警告进行浏览

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

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


    学习计划:

    ItemDecorationLayoutManagerRecyclerView.AdapterDiffUtilSimpleOnItemTouchListenerSmoothScrollerItemAnimator

    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<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设置ItemDecoration,LayoutManager为LineatLayoutManager


    子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大部分时候考虑item的position就可以,但GridLayoutManager和StaggeredGridLayoutManager需要考虑行和列,情况就比较复杂。


    方法中有4个参数

    Rect outRect:可以简单理解为item四边边距奉封装在这个对象中,用来设置Item的paddingView 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个值在RecyclerView的onMeasure()有使用,感觉是用来标识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); } } }

    运行后的效果:

    绘制底部分割线

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

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


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

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

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

    @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. 遇到的问题

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

    遇到的问题

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

    已解决 ,原因分析在下面


    2.1 补充,问题修复

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

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

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


    解决办法:

    不减1,就OK,修改为:

    final int itemCount = parent.getChildCount();

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

    position示例

    当前屏幕显示的是25--到-->42,parent.getChildCount()的返回结果itemCount便是18。凡是在屏幕上第一个出现的item的index便是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优雅实现复杂列表布局

    转载请注明原文地址: https://ju.6miu.com/read-8.html

    最新回复(0)