Recyclerview 学习系类之 ItemDecoration(一)

本贴最后更新于 2172 天前,其中的信息可能已经渤澥桑田

Google 官方解释

  • 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.

  • All ItemDecorations are drawn in the order they were added, before the item views (in onDraw() and after the items (in onDrawOver(Canvas, RecyclerView, RecyclerView.State).

个人理解:

大致意思是:

  • ItemDecoration 允许应用程序从适配器的数据集中为制定的 view 添加制定的图形和布局偏移量。该特性一般被用于在两个 item 之间绘制分割线,高亮度以及视觉分组等等。

  • 所有的 ItemDecorations 都按照它们被添加的顺序在 item 被绘制之前(在 onDraw 方法中)和在 Items 被绘制之后(在 onDrawOver(Canvas,RecyclerView,RecyclerView.State))进行绘制。

    可以看到,ItemDecoration 是相当强大和灵活的。

method 学习:

getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent)
This method was deprecated in API level 22.0.0. Use getItemOffsets(Rect, View, RecyclerView, State)

  • 该方法在 API 22.0.0 之后已被废弃,我们可以看代替的方法

getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)
Retrieve any offsets for the given item.

  • 我们可以通过该方法中的 outRect 来设置 item 的 padding 值。比如你要在 item 底部添加一条分割线,此时为了不影响 item 原来的布局参数,我们一般会返回一个地步 padding 为某个 pd 的 outRect,在 recyclerview 绘制 item 的时候会讲该布局数据加入,我们原来的 item 就会多出一个底部 padding,是不是解耦的很完美呢?

onDraw(Canvas c, RecyclerView parent)
_This method was deprecated in API level 22.0.0. Override onDraw(Canvas, RecyclerView, RecyclerView.State) _

  • 该方法也已经过期了,看下面的

onDraw(Canvas c, RecyclerView parent, RecyclerView.State state)
Draw any appropriate decorations into the Canvas supplied to the RecyclerView.

  • 该方法会在绘制 item 之前调用,也就是说他的层级是在 item 之下的,通过该方法,我们可以爱绘制 item 之前绘制我们需要的内容。

onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state)
Draw any appropriate decorations into the Canvas supplied to the RecyclerView.

  • 该方法已过期,看下面的

onDrawOver(Canvas c, RecyclerView parent)
_This method was deprecated in API level 22.0.0. Override onDrawOver(Canvas, RecyclerView, RecyclerView.State) _

  • 该方法于 onDrawOver 类似,在绘制 item 之后会调用该方法。

此时,也许你会疑问,他真的是这样执行的么?为了一探究竟,我们来看下源码吧。

 @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);
    }...}

  • 从 recyclerview 的源码中我们可以看到,在 draw 方法中后会遍历 recyclerview 里面的 itemDecoration 然后调用 itemdecoration 的 onDrawOver 方法;而 recyclerview 调用了 super.draw(c)之后会先,父类会先调用 recyclerview 的 onDraw 方法;

     @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);
      } }
    
    
  • 在 recyclerview 的 onDraw 里又会调用 itemDecoration 的 onDraw 方法,当 recyclerview 的 onDraw 方法执行完之后,recyclerview 的 draw 方法中 super.draw(c);后面的代码才会继续执行,而 recyclerview 是在绘制了自己之后才会去绘制 item。

  • 结论:itemDecoration 的 onDraw 方法在 item 绘制之前调用,itemDecoration 的 onDrawOver 方法在绘制 item 之后调用。

接下来我们在看下 getItemOffsets 这个方法。他真的把我们的 outRect 加到 item 的布局参数里面了么?预知真相,看源码。

   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);
        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;
}

  • 首先我们可以看到,getItemOffsets 这个方法在 recyclerview 的 getItemDecorInsetsForChild 中被调用,该方法会把所有的 itemDecortion 中的 rect 累加后返回;我们再看下 getItemDecorInsetsForChild 在哪被调用的。

      public void measureChild(View child, int widthUsed, int heightUsed) {
          final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    
          final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
    
    
  • 在 measureChild 方法中被调用,也就是 recyclerview 在测量 childView 的时候

      public void measureChildWithMargins(View child, int widthUsed, int heightUsed) {
          final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    
          final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
    
    
  • 使用 margins 测量 childView 时会用到

  • 结论,在 getItemOffsets 方法中 outRect 会影响到 recyclerview 中 childView 的布局。

使用 ItemDecoration 实现分割线的都调用过 addItemDecoration 方法。发现,只要调用一次 addItemDecoration 将自定义的分割线 ItemDecoration 添加进去就可以实现分割线效果了,如果我们添加多次会如何呢?

public void addItemDecoration(ItemDecoration decor, int index) {
    if (mLayout != null) {
        mLayout.assertNotInLayoutOrScroll("Cannot add item decoration during a scroll  or"
                + " layout");
    }
    if (mItemDecorations.isEmpty()) {
        setWillNotDraw(false);
    }
    if (index < 0) {
        mItemDecorations.add(decor);
    } else {
        mItemDecorations.add(index, decor);
    }
    markItemDecorInsetsDirty();
    requestLayout();
}

  • 从 RecyclerView.addItemDecoration 方法源码可以看到,内部使用了一个 ArrayList 类型的 mItemDecorations 存储我们添加的所有 ItemDecoration。markItemDecorInsetsDirty 方法有什么用呢?我们看下源码

    void markItemDecorInsetsDirty() {
      final int childCount = mChildHelper.getUnfilteredChildCount();
      for (int i = 0; i < childCount; i++) {
          final View child = mChildHelper.getUnfilteredChildAt(i);
          ((LayoutParams) child.getLayoutParams()).mInsetsDirty = true;
      }
      mRecycler.markItemDecorInsetsDirty();
    }
    
    
  • 里面有一个 mInsetsDirty 被重置为 true,最终调用 mRecycler.markItemDecorInsetsDirty();我们继续看 mRecycler.markItemDecorInsetsDirty();方法源码:

    void markItemDecorInsetsDirty() {

          final int cachedCount = mCachedViews.size();
          for (int i = 0; i < cachedCount; i++) {
              final ViewHolder holder = mCachedViews.get(i);
              LayoutParams layoutParams = (LayoutParams) holder.itemView.getLayoutParams();
              if (layoutParams != null) {
                  layoutParams.mInsetsDirty = true;
              }
          }
      }
    
    
  • 里面也是将 layoutParams 的 mInsetsDirty 重置为 true,这个 mInsetsDirty 有什么用呢 ?我们继续看源码:

    Rect getItemDecorInsetsForChild(View child) {
      final LayoutParams lp = (LayoutParams) child.getLayoutParams();
      if (!lp.mInsetsDirty) {
          return lp.mDeorInsets;
      }
    
      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);
          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;
    }
    
    
  • 看到这段代码感觉应该是它了,可以看到,

    • 判断 childView 的 layoutParams 的 mInsetsDirty 是不是 false 是 false 直接返回 mDecorInsets。
    • 判断 itemDecoration 是否已改变或者已不可用,mState.isPreLayout 是 recyclerview 用来处理动画的。
    • 如果前面的都不是,就会从新调用 itemDecoration 的 getItemOffsets 方法,重新计算 layout 偏离值之后返回。
  • 出于性能的考虑,如果之前为 ChildView 生成过 DecorInsets,那么会缓存在 ChildView 的 LayoutParam 中(mDecorInsets), 同时为了保证 mDecorInsets 的时效性,还同步维护了一个 mInsetsDirty 标记在 LayoutParam 中

  • 在获取 ChidlView 的 DecorInsets 时,如果其 mInsetsDirty 为 false,那么代表缓存没有过期,直接返回缓存的 mDecorInsets。

  • 如果 mInsetsDirty 为 true,表示缓存已过期,需要根据 ItemDecoration 集合重新生成

    • 添加或者删除 ItemDecoration 的时候,会将所有 ChildView 包括 Recycler 中的 mInsetsDirty 设置为 true 来使 DecorInsets 缓存失效

总结:其实 getItemDecorInsetsForChild 方法我们之前在本章前面有分析到。他就是在测量 childView 的时候会调用,所以如果我们的 itemDecortion 中途需要更新,我们需要调用 markItemDecorInsetsDirty 方法,然后调用 requestLayout 请求重新绘制,这样在重新绘制 childView 的时候,就会重新计算 ItemDecortion 中返回的 layout 偏离值。达到我们想要的效果。

  • Android

    Android 是一种以 Linux 为基础的开放源码操作系统,主要使用于便携设备。2005 年由 Google 收购注资,并拉拢多家制造商组成开放手机联盟开发改良,逐渐扩展到到平板电脑及其他领域上。

    334 引用 • 323 回帖
  • Recyclerview
    2 引用

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...