RecyclerView的自定義ItemDecoration與源碼分析

1.概述

recyclerView的要實(shí)現(xiàn)分割線要繼承RecyclerView.ItemDecoration抽象類(lèi)似扔。谷歌官方為我們提供實(shí)例DividerItemDecoration吨些。ItmDecoration有三個(gè)重要的方法,如果只是想要設(shè)置分割線搓谆,只要getItemOffsets就可以炒辉。

方法 作用
getItemOffsets 設(shè)置item上下左右的距離
onDraw 可以通過(guò)它繪制分割線的顏色圖片等,但它的區(qū)域超過(guò)item時(shí)會(huì)被item覆蓋
onDrawOver 同樣可以通過(guò)它繪制顏色和圖片等泉手,但它的區(qū)域會(huì)覆蓋item

看看源碼的注釋?zhuān)?/p>

 public abstract static class ItemDecoration {
        /**
         *將任何適當(dāng)?shù)难b飾畫(huà)到提供給RecyclerView的Canvas中黔寇。
         *用這種方法繪制的任何內(nèi)容都將在繪制項(xiàng)目視圖之前繪制,因此將出現(xiàn)在視圖下
         */
        public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull State state) {
            onDraw(c, parent);
        }

        /**
         *將任何適當(dāng)?shù)难b飾畫(huà)到提供給RecyclerView的Canvas中斩萌。
         * 用這種方法繪制的任何內(nèi)容都將在繪制項(xiàng)目視圖之后繪制缝裤,并因此出現(xiàn)在視圖上方。
         */
        public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent,
                @NonNull State state) {
            onDrawOver(c, parent);
        }

        /**
         *檢索給定項(xiàng)目的所有偏移量颊郎。
         * outRect的每個(gè)字段指定插入項(xiàng)目視圖的像素?cái)?shù)憋飞,類(lèi)似于填充或邊距。
         *默認(rèn)實(shí)現(xiàn)將outRect的邊界設(shè)置為0并返回姆吭。
         *
         * <p>
         *如果需要訪問(wèn)適配器以獲取其他數(shù)據(jù)榛做,
         *則可以調(diào)用{@link RecyclerView#getChildAdapterPosition(View)}
         *以獲取視圖的適配器位置。
         */
        public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
                @NonNull RecyclerView parent, @NonNull State state) {
            getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
                    parent);
        }
    }

2.自定義ItemDecoration

自定一個(gè)ItemDecoration來(lái)實(shí)現(xiàn)一個(gè)可以吸頂效果的列表内狸,看看效果圖:


image.png

要實(shí)現(xiàn)這個(gè)效果检眯,首先用onDraw繪制一個(gè)紅色背景的分割線,第二就是用onDrawOver繪制吸頂?shù)募t色頭部昆淡。

public class RecyclerViewDecoration extends RecyclerView.ItemDecoration {

  private int groupHeaderHeight;

  private Paint headPaint;
  private Paint textPaint;

  private Rect textRect;

  public StarDecoration(Context context) {
      groupHeaderHeight = dp2px(context, 100);
      
      //畫(huà)吸頂?shù)漠?huà)筆
      headPaint = new Paint();
      headPaint.setColor(Color.RED);
      // 畫(huà)文字的畫(huà)筆
      textPaint = new Paint();
      textPaint.setTextSize(50);
      textPaint.setColor(Color.WHITE);
      textRect = new Rect();
  }

  @Override
  public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
      super.onDraw(c, parent, state);
      if (parent.getAdapter() instanceof StarAdapter) {
          StarAdapter adapter = (StarAdapter) parent.getAdapter();
          int left = parent.getPaddingLeft();
          int right = parent.getWidth() - parent.getPaddingRight();
          int count = parent.getChildCount(); // 當(dāng)前屏幕的item個(gè)數(shù)
          for (int i = 0; i < count; i++) {
              // 獲取對(duì)應(yīng)i的View
              View view = parent.getChildAt(i);
              // 獲取View的布局位置
              int position = parent.getChildLayoutPosition(view);
              // 是否繪制紅色的頭部分割線
              boolean isGroupHeader = adapter.isGourpHeader(position);
              
              if (isGroupHeader && view.getTop() - groupHeaderHeight - parent.getPaddingTop() >= 0) {
                  //在view的頂部畫(huà)紅色頭部分割線
                  c.drawRect(left, view.getTop() - groupHeaderHeight, right, view.getTop(), headPaint);
                  //繪制文字
                  String groupName = adapter.getGroupName(position);
                  //得到文字需要的rect
                  textPaint.getTextBounds(groupName, 0, groupName.length(), textRect);
                  c.drawText(groupName, left + 20, view.getTop() -
                          groupHeaderHeight / 2 + textRect.height() / 2, textPaint);
              } else if (view.getTop() - groupHeaderHeight - parent.getPaddingTop() >= 0) {
                  // 繪制紅色分割線
                  c.drawRect(left, view.getTop() - 4, right, view.getTop(), headPaint);
              }
          }
      }
  }

  @Override
  public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
      super.onDrawOver(c, parent, state);
      if (parent.getAdapter() instanceof StarAdapter) {
          StarAdapter adapter = (StarAdapter) parent.getAdapter();
          // 返回可見(jiàn)區(qū)域內(nèi)的第一個(gè)item的position
          int position = ((LinearLayoutManager) parent.getLayoutManager()).findFirstVisibleItemPosition();
          // 獲取對(duì)應(yīng)position的View
          View itemView = parent.findViewHolderForAdapterPosition(position).itemView;
          
          int left = parent.getPaddingLeft();
          int right = parent.getWidth() - parent.getPaddingRight();
          int top = parent.getPaddingTop();
          // 當(dāng)?shù)诙€(gè)是組的頭部的時(shí)候
          boolean isGroupHeader = adapter.isGourpHeader(position + 1);
          //下一個(gè)紅色吸頂頭部位于第一個(gè)位置
          if (isGroupHeader) {
              int bottom = Math.min(groupHeaderHeight, itemView.getBottom() - parent.getPaddingTop());
              c.drawRect(left, top, right, top + bottom, headPaint);//上一個(gè)不斷縮短
              String groupName = adapter.getGroupName(position);
              c.clipRect(left, top, right, top + bottom);
              textPaint.getTextBounds(groupName, 0, groupName.length(), textRect);
              c.drawText(groupName, left + 20, top + bottom
                      - groupHeaderHeight / 2 + textRect.height() / 2, textPaint);
          } else {
              //繪制一個(gè)吸頂?shù)募t色頭部
              c.drawRect(left, top, right, top + groupHeaderHeight, headPaint);
              //繪制文字
              String groupName = adapter.getGroupName(position);
              textPaint.getTextBounds(groupName, 0, groupName.length(), textRect);
              c.drawText(groupName, left + 20, top + groupHeaderHeight / 2 + textRect.height() / 2, textPaint);
          }

      }
  }

  @Override
  public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
      super.getItemOffsets(outRect, view, parent, state);

      if (parent.getAdapter() instanceof StarAdapter) {
          StarAdapter adapter = (StarAdapter) parent.getAdapter();
          int position = parent.getChildLayoutPosition(view);
          boolean isGroupHeader = adapter.isGourpHeader(position);
          // 怎么判斷 itemView是頭部
          if (isGroupHeader) {
              // 如果是頭部锰瘸,預(yù)留更大的地方
              outRect.set(0, groupHeaderHeight, 0, 0);
          } else {
              // 1像素
              outRect.set(0, 4, 0, 0);
          }
      }
  }

  private int dp2px(Context context, float dpValue) {
      float scale = context.getResources().getDisplayMetrics().density;
      return (int) (dpValue * scale * 0.5f);
  }
}

通過(guò)代碼可以分為三步

  1. 通過(guò)getItemOffsets設(shè)置Item大和小的分割線
  2. onDraw中為每個(gè)大小分割線繪制成紅色,有文字的繪制文字
  3. onDrawOver繪制一個(gè)吸頂紅色頭部昂灵,然后在下一個(gè)紅色頭部的前一個(gè)item變成第一個(gè)可見(jiàn)時(shí)避凝,吸頂紅色頭部在[100,0]區(qū)間不斷的縮小舞萄。

3.ItemDecoration的原理

要搞懂ItemDecoration的原理,主要有兩方面:

1. ItemDecoration是怎樣實(shí)現(xiàn)ItemView之間的間距的
2. ItemDecoration是如何繪制的

3.1 ItemDecoration如何實(shí)現(xiàn)間距

要理解ItemDecoration的間距管削,還要從RecyclerView的測(cè)量和ItemView的布局開(kāi)始鹏氧。測(cè)量主要是通過(guò)LayoutManagermeasureChildWithMargins方法

      public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed) {
          final LayoutParams lp = (LayoutParams) child.getLayoutParams();
          //拿到全部分割線的左右上下全部總和
          final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
           //將需要的分割線總和大小加到寬高中
          widthUsed += insets.left + insets.right;
          heightUsed += insets.top + insets.bottom;
        
          //確定總寬高
          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);
          }
      }

這里有個(gè)getItemDecorInsetsForChild方法,總要是拿到全部分割線上下左右全部相加的距離佩谣。 第二步是把还,把分割線的寬高總大小和ItemView寬高總大小進(jìn)行相加,并跟可滑動(dòng)的方向確定RecyclerView的寬高茸俭。所以吊履,全部分割線所需要寬高已經(jīng)加到RecyclerView的寬高中

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

      final boolean positionWillChange =
              (lp.mViewHolder.getLayoutPosition() != lp.mViewHolder.getAbsoluteAdapterPosition());
      if (mState.isPreLayout()
              && (lp.isItemChanged() || lp.isViewInvalid() || positionWillChange)) {
          // 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;
  }

接下來(lái),看看ItemView的布局调鬓。代碼主要在LayoutManagerlayoutDecoratedWithMargins

public void layoutDecoratedWithMargins(@NonNull View child, int left, int top, int right,
              int bottom) {
          final LayoutParams lp = (LayoutParams) child.getLayoutParams();
         //拿到View的分割線大小
          final Rect insets = lp.mDecorInsets;
          //item布局時(shí)會(huì)在上下左右留出分割線的所需的大小
          child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin,
                  right - insets.right - lp.rightMargin,
                  bottom - insets.bottom - lp.bottomMargin);
      }

代碼很簡(jiǎn)單艇炎,itemview在布局的時(shí)候,就已經(jīng)留出分割線的大小了腾窝。所以我們的分割線的并不是onDraw onDrawOver繪制出來(lái)的缀踪,而是在ItemView布局時(shí)就根據(jù)getItemOffsets方法設(shè)置上下左右距離開(kāi)始留出間距。所以我們自定義分割線時(shí)虹脯,只寫(xiě)getItemOffsets方法也可以繪制分割線效果驴娃。

3.2 ItemDecoration的繪制

RecyclerViewdraw繪制流程,主要就是繪制ItemDecorationonDraw onDrawOver,和繪制itemView循集。

  @Override
  public void draw(Canvas c) {
      //通過(guò)調(diào)用onDraw繪制item和ItemDecoration的onDraw()
      super.draw(c);
     //繪制ItemDecoration的onDrawOver()
      final int count = mItemDecorations.size();
      for (int i = 0; i < count; i++) {
          mItemDecorations.get(i).onDrawOver(c, this, mState);
      }
      //padding滑動(dòng)的處理
      // TODO If padding is not 0 and clipChildrenToPadding is false, to draw glows properly, we
      // need find children closest to edges. Not sure if it is worth the effort.
    }
  @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);
      }
  }

代碼比較簡(jiǎn)單唇敞,不作分析。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末咒彤,一起剝皮案震驚了整個(gè)濱河市疆柔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌镶柱,老刑警劉巖旷档,帶你破解...
    沈念sama閱讀 219,490評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異歇拆,居然都是意外死亡鞋屈,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)查吊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)谐区,“玉大人,你說(shuō)我怎么就攤上這事逻卖∷瘟校” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,830評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵评也,是天一觀的道長(zhǎng)炼杖。 經(jīng)常有香客問(wèn)我灭返,道長(zhǎng),這世上最難降的妖魔是什么坤邪? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,957評(píng)論 1 295
  • 正文 為了忘掉前任熙含,我火速辦了婚禮,結(jié)果婚禮上艇纺,老公的妹妹穿的比我還像新娘怎静。我一直安慰自己,他們只是感情好黔衡,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評(píng)論 6 393
  • 文/花漫 我一把揭開(kāi)白布蚓聘。 她就那樣靜靜地躺著,像睡著了一般盟劫。 火紅的嫁衣襯著肌膚如雪夜牡。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,754評(píng)論 1 307
  • 那天侣签,我揣著相機(jī)與錄音塘装,去河邊找鬼。 笑死影所,一個(gè)胖子當(dāng)著我的面吹牛蹦肴,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播型檀,決...
    沈念sama閱讀 40,464評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼冗尤,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了胀溺?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤皆看,失蹤者是張志新(化名)和其女友劉穎仓坞,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體腰吟,經(jīng)...
    沈念sama閱讀 45,847評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡无埃,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了毛雇。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嫉称。...
    茶點(diǎn)故事閱讀 40,137評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖灵疮,靈堂內(nèi)的尸體忽然破棺而出织阅,到底是詐尸還是另有隱情,我是刑警寧澤震捣,帶...
    沈念sama閱讀 35,819評(píng)論 5 346
  • 正文 年R本政府宣布荔棉,位于F島的核電站闹炉,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏润樱。R本人自食惡果不足惜渣触,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望壹若。 院中可真熱鬧嗅钻,春花似錦、人聲如沸店展。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,023評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)壁查。三九已至觉至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間睡腿,已是汗流浹背语御。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,149評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留席怪,地道東北人应闯。 一個(gè)月前我還...
    沈念sama閱讀 48,409評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像挂捻,于是被迫代替她去往敵國(guó)和親碉纺。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容