View-RecycleView的分割线源码(自己封装一个分割线)

分割线源码分析

/**
 * Measure a child view using standard measurement policy, taking the padding
 * of the parent RecyclerView and any added item decorations into account.
 *
 * <p>If the RecyclerView can be scrolled in either dimension the caller may
 * pass 0 as the widthUsed or heightUsed parameters as they will be irrelevant.</p>
 *
 * @param child Child view to measure
 * @param widthUsed Width in pixels currently consumed by other views, if relevant
 * @param heightUsed Height in pixels currently consumed by other views, if relevant
 */
public void measureChild(@NonNull View child, int widthUsed, int heightUsed) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    1.//下面三行代码就是把分割线的距离增加到每个view上
    final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
    widthUsed += insets.left + insets.right;
    heightUsed += insets.top + insets.bottom;

    final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
            getPaddingLeft() + getPaddingRight() + widthUsed, lp.width,
            canScrollHorizontally());
    final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
            getPaddingTop() + getPaddingBottom() + 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);
        2.//拿到所有的Item
        final int decorCount = mItemDecorations.size();
        for (int i = 0; i < decorCount; i++) {
            mTempRect.set(0, 0, 0, 0);
  // 调用 getItemOffsets()方法,给mTempRect赋值    
  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;
    }

绘制分割线:

final ArrayList<ItemDecoration> mItemDecorations = new ArrayList<>();

@Override
public void onDraw(Canvas c) {
  super.onDraw(c);
  final int count = mItemDecorations.size();
  for (int i = 0; i < count; i++) {
      1.//会回调分割线方法
      mItemDecorations.get(i).onDraw(c, this, mState);
  }
}

public abstract static class ItemDecoration {
    
      //通过重写此onDraw方法绘制分割线
      public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull State state) {
          onDraw(c, parent);
      }

       //此方法已过时
      @Deprecated
      public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent) {
      }
}

从上面的分割线源码分析可知:

1.在getItemOffset()方法中,设置分割线的大小最终是加在itemView的宽高上。这会造成一个问题,就是每个itemView的大小可能不一致

  1. 在确定了分割线大小后,在绘制itemView时,会调用分割线的onDraw()方法。我们可以重写分割线的onDraw()方法绘制分割线。

2.自定义一个分割线NiceItemDecoration

大致思路:定义三个变量(leftRight,topBottom,mColor),然后根据不同的layoutManager使用不同的引擎确定分割线的大小和绘制分割线

public class NiceItemDecoration extends RecyclerView.ItemDecoration {

  private int mColor;
  private int leftRight;
  private int topBottom;
  private NiceItemDecorationEntrust mEntrust;

  public NiceItemDecoration(int leftRight, int topBottom) {
      this.leftRight = leftRight;
      this.topBottom = topBottom;
  }

  public NiceItemDecoration(int leftRight, int topBottom, int mColor) {
      this(leftRight, topBottom);
      this.mColor = mColor;
  }

  //由不同的引擎确定分割线的大小
  @Override
  public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
      if (mEntrust == null) {
          mEntrust = getEntrust(parent);
      }
      mEntrust.getItemOffsets(outRect,view,parent,state);
  }

  //由不同的引擎绘制分割线
  @Override
  public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
      if (mEntrust == null) {
          mEntrust = getEntrust(parent);
      }
      mEntrust.onDraw(c,parent,state);
      super.onDraw(c, parent, state);
  }

 //根据layoutManager确定不同的引擎
  private NiceItemDecorationEntrust getEntrust(RecyclerView parent) {
      RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
      if (layoutManager instanceof GridLayoutManager) {
          mEntrust = new GridEntrust(leftRight, topBottom, mColor);
      } else if (layoutManager instanceof StaggeredGridLayoutManager) {
          mEntrust = new StaggeredGridEntrust(leftRight, topBottom, mColor);
      } else if (layoutManager instanceof LinearLayoutManager) {
          mEntrust = new LinearEntrust(leftRight, topBottom, mColor);
      }
      return mEntrust;
  }

}

注意:在判断时,要先判断GridLayoutManager再判断LinearLayoutManager,因为GridLayoutManager是继承LinearLayoutManager。getEntrust()方法根据不同的layoutManager分别生成LinearEntrust、GridEntrust、StaggeredGridEntrust引擎来确定分割线大小和绘制分割线。

一些基础知识:

1.关于LinearLayoutManager
1、 在getItemOffsets()方法中,可以获得childView在设配器中的位置,和childView的数量

@Override
  void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
      LinearLayoutManager layoutManager = (LinearLayoutManager) parent.getLayoutManager();
     //获得childView所在的位置
     int position = parent.getChildAdapterPosition(view);
     //获得itemView的数量
     int ItemSize = layoutManager.getItemCount();
  }

2、在onDraw()方法中 可以获得每个childView的分割线的上下的高度和左右的宽度

  //获得itemView的数量
 int itemSize = layoutManager.getChildCount();
 for (int i = 0; i < itemSize - 1; i++) {
 final View childView = parent.getChildAt(i);
  //获得childView分割线左右的宽度
 layoutManager.getLeftDecorationWidth(childView);
 layoutManager.getRightDecorationWidth(childView);
 //获得childView分割线上下的高度
 layoutManager.getTopDecorationHeight(childView);
layoutManager.getBottomDecorationHeight(childView);
}

2.关于GridLayoutManager
1.可以知道每个childView所在的行数,在行里的位置,每行的chilView比重

GridLayoutManager.LayoutParams.getSpanSize() //childView所占的比重
GridLayoutManager.LayoutParams.getSpanIndex() //childView在所在行的第几个
GridLayoutManager.getSpanCount() //每行多少个childView
GridLayoutManager.getSpanSizeLookup().getSpanSize(i); //childView所占的比重
GridLayoutManager.getSpanSizeLookup().getSpanIndex(childPosition,spanCount); //childView在所在行的第几个
GridLayoutManager.getSpanSizeLookup().getSpanGroupIndex(childPosition,spanCount) //childPosition的view处于第几排或第几列

2.可以设置每个view的比重

mLayoutManager = new GridLayoutManager(this, 3, GridLayoutManager.VERTICAL, false);
              final GridLayoutManager manager = (GridLayoutManager) mLayoutManager;
              manager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                  @Override
                  public int getSpanSize(int position) {
                      return position % (manager.getSpanCount() + 1) == 0 ? manager.getSpanCount() : 1;
                  }
              });

1.写一个抽象引擎,让它们共有属性和要重写的方法封装起来

public abstract class NiceItemDecorationEntrust {

  protected int leftRight;
  protected int topBottom;
  protected Drawable mDivider;

  public NiceItemDecorationEntrust(int leftRight, int topBottom, int mColor){
      this.leftRight = leftRight;
      this.topBottom = topBottom;
      if (mColor != 0){
          mDivider = new ColorDrawable(mColor);
      }
  }

  abstract void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state);

  abstract void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state);
}

2.LinearEntrust引擎

分析:在确定分割线大小时,分两种情况:垂直滑动:每个childView都有上左右,但最后一个childView时要下;水平滑动:每个childView都有上下右,但第一个又左。
在绘制分割线时只绘制ChildView的之间的距离,分两种情况:垂直滑动:每个childView画bottom,但最后一个不画(i<childCount - 1);水平滑动:每个childView画right,但最后一个不画(i<childCount - 1)。

public class LinearEntrust extends NiceItemDecorationEntrust {

  public LinearEntrust(int leftRight, int topBottom, int mColor) {
      super(leftRight, topBottom, mColor);
  }

  @Override
  void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
      LinearLayoutManager layoutManager = (LinearLayoutManager) parent.getLayoutManager();
      if (layoutManager.getOrientation() == LinearLayoutManager.VERTICAL) {
          //垂直滑动
          outRect.top = topBottom;
          outRect.left = leftRight;
          outRect.right = leftRight;
          //最后一个子view需要bottom
          if (parent.getChildAdapterPosition(view) == layoutManager.getItemCount() - 1) {
              outRect.bottom = topBottom;
          }
      } else {
          //横向滑动
          outRect.top = topBottom;
          outRect.right = leftRight;
          outRect.bottom = topBottom;
          if (parent.getChildAdapterPosition(view) == 0) {
              outRect.left = leftRight;
          }
      }

  }
  
  @Override
  void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
      LinearLayoutManager layoutManager = (LinearLayoutManager) parent.getLayoutManager();
      if (mDivider == null || layoutManager.getChildCount() == 0) {
          return;
      }
      //只画item之间的间隙
      int left, top, right, bottom;
      int childCount = parent.getChildCount();
      //垂直方向,第一排top不画,最后一排bottom不画,左右两边不画
      if (layoutManager.getOrientation() == LinearLayoutManager.VERTICAL) {
          for (int i = 0; i < childCount - 1; i++) {
              final View childView = parent.getChildAt(i);
              left = childView.getLeft();
              right = childView.getRight();
              top = childView.getBottom();
              bottom = top + topBottom;
              mDivider.setBounds(left, top, right, bottom);
              mDivider.draw(c);
          }

      } else {
          //水平方向  第一个left不画,最后一个right不画,上下不画
          for (int i = 0; i < childCount - 1; i++) {
              final View childView = parent.getChildAt(i);
              RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) childView.getLayoutParams();
              left = childView.getRight();
              right = left + layoutManager.getRightDecorationWidth(childView);
              top = childView.getTop();
              bottom = childView.getBottom();
              mDivider.setBounds(left, top, right, bottom);
              mDivider.draw(c);
          }
      }

  }
}

3.GridEntrust引擎

分析:确定分割线大小,分垂直和水平滑动。垂直滑动:每个childView都有下,但第一排有上。左右分两种情况:如果一个childView占满一行,则直接设置左右;如果不是,则按比例分配左右,因为分割线算在childView的区域,分割线分配不均会导致childView的大小不一样。
水平滑动:每个childView都有右,但第一列有左。上下分两种情况:如果一个childView占满一列,则直接设置上下,如果不是,则按比例分配上下,因为分割线算在childView的区域,分割线分配不均会导致childView的大小不一样。
绘制分割线,垂直滑动:画水平方向分割线:在不是第一个行,且childView在行的位置是0时,在chidView上方绘制一条分割线。画垂直方向:在不是最后列中,画childView右边的分割线。
水平滑动:画垂直方向分割线:在不是第一列中画ChildView的左边的垂直分割线。画水平方向分割线:在不是最后一行中,绘制每个childView的下方的分割线。

public class GridEntrust extends NiceItemDecorationEntrust {

  public GridEntrust(int leftRight, int topBottom, int mColor) {
      super(leftRight, topBottom, mColor);
  }


  @Override
  void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
      GridLayoutManager layoutManager = (GridLayoutManager) parent.getLayoutManager();
      final GridLayoutManager.LayoutParams layoutParams = (GridLayoutManager.LayoutParams) view.getLayoutParams();
      //在适配器中的位置 
      final int childPosition = parent.getChildAdapterPosition(view);
      //每行或列的比重
      final int spanCount = layoutManager.getSpanCount();
 
      //垂直方向,第一排还有top,每排都有bottom,每列都有left,最后一列有right
      if (layoutManager.getOrientation() == GridLayoutManager.VERTICAL) {
          outRect.bottom = topBottom;
          //是否在第一排
          if (layoutManager.getSpanSizeLookup().getSpanGroupIndex(childPosition, spanCount) == 0) {
              outRect.top = topBottom;
          }

          if (layoutParams.getSpanSize() == spanCount) {
              //一个item占满一行
              outRect.left = leftRight;
              outRect.right = leftRight;
          } else {

              outRect.left = (int)
                      (((float) (spanCount - layoutParams.getSpanIndex())) / spanCount * leftRight);

              outRect.right = (int)
                      (((float) leftRight * (spanCount + 1) / spanCount) - outRect.left);
          }

      } else {
          //水平 第一列有left,全部列有right, 全部有top,最后一个有bottom
          outRect.right = leftRight;
          if (layoutManager.getSpanSizeLookup().getSpanGroupIndex(childPosition, spanCount) == 0) {
              outRect.left = leftRight;
          }
          if (layoutParams.getSpanSize() == spanCount) {//占满
              outRect.top = topBottom;
              outRect.bottom = topBottom;
          } else {
              outRect.top = (int) (((float) (spanCount - layoutParams.getSpanIndex())) / spanCount * topBottom);
              outRect.bottom = (int) (((float) topBottom * (spanCount + 1) / spanCount) - outRect.top);
          }
      }
  }

  @Override
  void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
      GridLayoutManager layoutManager = (GridLayoutManager) parent.getLayoutManager();
      final GridLayoutManager.SpanSizeLookup spanSizeLookup = layoutManager.getSpanSizeLookup();
      if (mDivider == null || layoutManager.getChildCount() == 0) {
          return;
      }
      int spanCount = layoutManager.getSpanCount();
      int chileCount = layoutManager.getChildCount();
      int left, top, right, bottom;
      //垂直方向,每排画bottom,但最后一排不画bottom,只画right,但最后一列不画right
      if (layoutManager.getOrientation() == GridLayoutManager.VERTICAL) {
          for (int i = 0; i < chileCount; i++) {
              final View childView = parent.getChildAt(i);
              int childPosition = parent.getChildAdapterPosition(childView);

              RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) childView.getLayoutParams();
              int spanSize = spanSizeLookup.getSpanSize(i);
              int spanIndex = spanSizeLookup.getSpanIndex(childPosition, spanCount);

              //画水平方向,除了最后一排,每排都画bottom边的
              boolean isLast = spanSizeLookup.getSpanGroupIndex(childPosition, spanCount) == 0;
              if (!isLast && spanIndex == 0) {
                  left = childView.getLeft();
                  right = parent.getWidth() - leftRight;
                  top = childView.getTop() - topBottom;
                  bottom = top + layoutManager.getBottomDecorationHeight(childView);

                  mDivider.setBounds(left, top, right, bottom);
                  mDivider.draw(c);
              }

              //画垂直方向,每列都有right ,除了最右边

              boolean isRight = spanIndex + spanSize == spanCount;
              if (!isRight) {
                  left = childView.getRight();
                  right = left + leftRight;
                  top = childView.getTop();
                  bottom = parent.getBottom() - layoutManager.getBottomDecorationHeight(childView);
                  mDivider.setBounds(left, top, right, bottom);
                  mDivider.draw(c);
              }
          }
      } else {
          //水平方向
          for (int i = 0; i < chileCount; i++) {
              final View childView = parent.getChildAt(i);
              int childPosition = parent.getChildAdapterPosition(childView);
              int spanSize = spanSizeLookup.getSpanSize(childPosition);
              int spanIndex = spanSizeLookup.getSpanIndex(childPosition, spanCount);

              //垂直
              boolean isFirst = spanSizeLookup.getSpanGroupIndex(childPosition, spanCount) == 0;
              if (!isFirst && spanIndex == 0) {
                  left = childView.getLeft() - leftRight;
                  right = left + leftRight;
                  top = layoutManager.getTopDecorationHeight(childView);
                  bottom = parent.getHeight() - layoutManager.getTopDecorationHeight(childView);
                  mDivider.setBounds(left, top, right, bottom);
                  mDivider.draw(c);
              }

              //画水平
              boolean isRight = spanIndex + spanSize == spanCount;
              if (!isRight) {
                  left = childView.getLeft();
                  right = childView.getRight();
                  top = childView.getBottom();
                  bottom = top + leftRight;
                  mDivider.setBounds(left, top, right, bottom);
                  mDivider.draw(c);
              }

          }
      }

  }
}

项目代码

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