RecyclerView的ItemDecoration分析与实际使用

起因

在项目开发中遇到了一些实际的需求,为了满足这些需求不得不去了解新的知识点或者加深对已知知识点的认识,现在就总结一下在实际开发中对RecyclerView的ItemDecoration的使用

ItemDecoration的原理

1.类的方法简介

这个类是RecyclerView的一个静态内部类,正如它的名字,它可以用来对RV的item做一些item之外装饰,它只有定义了三个方法,如下:

  • getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent)
    这个方法用来设置RV的每个item的上下左右的间距,设置的值保存在outRect这个类中
  • public void onDraw(Canvas c, RecyclerView parent)
    绘制方法,Canvas就是RV的Canvas,在调用RV的子View绘制之前被调用,所以在绘制子View之前绘制
  • public void onDrawOver( Canvas c, RecyclerView parent)
    和onDraw方法一样,只不过是在绘制完子View之后才被调用,所以可能会绘制在子View视图之上

ItemDecoration只有这三个方法,和普通的View绘制一样,它是依托于RecyclerView绘制子View的绘制周期来实现方法描述的这些功能的,接着就来看看这三个方法被调用的时机

2.调用getItemOffsets()

以LinearLayoutManager为例,首先是测量,LinearLayoutManager布局子View时会调用layoutChunk方法,其中的measureChildWithMargins测量子View时会调用getItemDecorInsetsForChild方法,如下:

//==RV中==
void layoutChunk(){
      measureChildWithMargins(view, 0, 0);
      result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
      。。。。
  }

public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed) {
      final LayoutParams lp = (LayoutParams) child.getLayoutParams();
      //累加所有的ItemDecoration的设置的offset
      final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
      widthUsed += insets.left + insets.right;
      heightUsed += insets.top + insets.bottom;
      //计算widthSpec和heightSpec,getPaddingLeft() + getPaddingRight()
      //  + lp.leftMargin + lp.rightMargin + widthUsed用来确定在子View在AT——MOST
      //模式下的最大宽高
      final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
              getPaddingLeft() + getPaddingRight()
                      + lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
              canScrollHorizontally());
      final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
              getPaddingTop() + getPaddingBottom()
                      + lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
              canScrollVertically());
      if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
          child.measure(widthSpec, heightSpec);
      }
  }

  Rect getItemDecorInsetsForChild(View child) {
     final LayoutParams lp = (LayoutParams) child.getLayoutParams();
     if (!lp.mInsetsDirty) {
         return lp.mDecorInsets;
     }

     if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
         // changed/invalid items should not be updated until they are rebound.
         return lp.mDecorInsets;
     }
     final Rect insets = lp.mDecorInsets;
     insets.set(0, 0, 0, 0);
     final int decorCount = mItemDecorations.size();
     for (int i = 0; i < decorCount; i++) {
         mTempRect.set(0, 0, 0, 0);
         //获得保存在Rect中的上下左右的offset
         mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
         insets.left += mTempRect.left;
         insets.top += mTempRect.top;
         insets.right += mTempRect.right;
         insets.bottom += mTempRect.bottom;
     }
     lp.mInsetsDirty = false;
     return insets;
 }

getItemDecorInsetsForChild方法将mItemDecorations中设置的间距累加并这个值保存在了RecyclerView.LayoutParams的mDecorInsets中

3.RecyclerView布局子View

前面在测量时将ItemDecoration设置的间距保存在了RecyclerView.LayoutParams的mDecorInsets中,在布局的时候就会用这个值来计算布局的偏移。layoutChunk方法中测量完了紧接着就是布局,如下


//假如竖直方向布局
void layoutChunk(){
      。。。
      measureChildWithMargins(view, 0, 0);
      //getDecoratedMeasurement返回就是竖直整个子View包括ItemDecoration的所占用的高度
      result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
      int left, top, right, bottom;
      if (mOrientation == VERTICAL) {
              //左边界,只是RV的padding
              left = getPaddingLeft();
              //右边界包括子view的宽度、左右margin以及RV.LayoutParams的mDecorInsets
              right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
              //上边界就是整体的偏移
              top = layoutState.mOffset;
              //下边界包括子View的高度、上下margin以及RV.LayoutParams的mDecorInsets
              bottom = layoutState.mOffset + result.mConsumed;

      } else {
          。。。
      }
      // We calculate everything with View's bounding box (which includes decor and margins)
      // To calculate correct layout position, we subtract margins.
      layoutDecoratedWithMargins(view, left, top, right, bottom);
}

public void layoutDecoratedWithMargins(@NonNull View child, int left, int top, int right,
        int bottom)
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    final Rect insets = lp.mDecorInsets;
    //真正布局确定子View的上下左右边界时去除了margin和mDecorInsets
    child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin,
            right - insets.right - lp.rightMargin,
            bottom - insets.bottom - lp.bottomMargin);
}

假设LinearLayoutManager竖直布局,在计算这个View需要多大的空间时是把View的margin和ItemDecoration设置的偏移全都算进去了的,但在真正布局子View的时候却去除了子View的margin和mDecorInsets,这样就预留了多的空间出来了。

4.onDraw() 和 onDrawOver()的调用

测量、布局都已经完成,现在就剩绘制了,哪是怎样控制这个先后顺序的呢?
一般情况下ViewGroup的draw方法都是不会被调用的,但在给RV添加ItemDecoration的时候,会调用setWillNotDraw(false)来开启ViewGroup的绘制,如下


    public void addItemDecoration(ItemDecoration decor, int index) {

       if (mItemDecorations.isEmpty()) {
         //开启ViewGroup的绘制方法
           setWillNotDraw(false);
       }
       。。。
     }

   @Override
   public void draw(Canvas c) {
      //第一步
      super.draw(c);
      //第三步
      final int count = mItemDecorations.size();
      for (int i = 0; i < count; i++) {
          mItemDecorations.get(i).onDrawOver(c, this, mState);
      }


    @Override
     public void onDraw(Canvas c) {
         //第二步
         super.onDraw(c);
         final int count = mItemDecorations.size();
         for (int i = 0; i < count; i++) {
             mItemDecorations.get(i).onDraw(c, this, mState);
         }
     }

RV的draw方法首先是调用了super.draw(),ViewGroup的绘制会先调用自己的onDraw方法之后就把绘制事件分发给了子View,最后才回到draw方法继续执行,所以ItemDecoration是利用了ViewGroup调用绘制方法的先后顺序来达到目的的。

接下来就是实际的开发中需求

实现重叠的子View

项目中要求RV的图片与图片之间有重叠的部份,在点击某张图片的时候将图片完全显示出来,效果如下

4_rv_item.gif

ItemDecoration可以设置间距,但不影响子View的大小,间距为正数叫间距,间距为负数就是重叠,所以可以把Rect的top设置为负数就可以实现子View的重叠了。但还有一个问题,绘制都是从第一个子View开始挨个绘制的,要让点击的子View全部显示出来就需要改变绘制子View的顺序,将点击的View最后绘制,ViewGroup有一个getChildDrawingOrder,这个方法可以设置子View的绘制顺序

/**
     * Returns the index of the child to draw for this iteration. Override this
     * if you want to change the drawing order of children. By default, it
     * returns i.
     * <p>
     * NOTE: In order for this method to be called, you must enable child ordering
     * first by calling {@link #setChildrenDrawingOrderEnabled(boolean)}.
     *
     * @param i The current iteration.
     * @return The index of the child to draw this iteration.
     *
     * @see #setChildrenDrawingOrderEnabled(boolean)
     * @see #isChildrenDrawingOrderEnabled()
     */
    protected int getChildDrawingOrder(int childCount, int i) {
        return i;
    }

在RV中不用去复写getChildDrawingOrder方法,它提供了RecyclerView.ChildDrawingOrderCallback,可以通过设置这个Callback来改变子View的绘制顺序,最后大概的代码如下

    rv.setChildDrawingOrderCallback(new RecyclerView.ChildDrawingOrderCallback() {
              @Override
              public int onGetChildDrawingOrder(int childCount, int i) {
                  View v = rv.getFocusedChild();

                  int focusIndex = rv.indexOfChild(v);
                  if (focusIndex == RecyclerView.NO_POSITION) {
                      return i;
                  }
                  // supposely 0 1 2 3 4 5 6 7 8 9, 4 is the center item
                  // drawing order is 0 1 2 3 9 8 7 6 5 4
                  if (i < focusIndex) {
                      return i;
                  } else if (i < childCount - 1) {
                      return focusIndex + childCount - 1 - i;
                  } else {
                      return focusIndex;
                  }
              }
          });

        rv.addItemDecoration(new RecyclerView.ItemDecoration() {
                   @Override
                   public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
                       outRect.top = -200;

                   }
               });

实现子View的间距不一样

比如需求要求第一排的子View需要预留300px的空白空间用于显示后面的海报,其他子View不变,但它们的类型一样,一种解决方法就是设置ItemDecoration,把第一排的子view的top offset设置的大一些

rv.addItemDecoration(new RecyclerView.ItemDecoration() {
      @Override
      public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        //第一排的子View设置top值
          int pos = parent.getChildLayoutPosition(view);
          if (pos < GRID_COLUMN_COUNT) {
              outRect.top = 50;

          }

      }

关于ItemDecoration的总结就完了,最后看代码时还发现用来拖拽子View的ItemTouchHelper居然是继承自ItemDecoration,大概就是在onDraw方法中去改变子View的x,y坐标来实现的。

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

推荐阅读更多精彩内容