RecyclerView定制:通用ItemDecoration及全展開RecyclerView的實現(xiàn)

Android L面世之后九巡,Google就推薦在開發(fā)項目中使用RecyclerView來取代ListView倦畅,因為RecyclerView的靈活性跟性能都要比ListView更強(qiáng),但是麻汰,帶來的問題也不少速客,比如:列表分割線都要開發(fā)者自己控制,再者五鲫,RecyclerView的測量與布局的邏輯都委托給了自己LayoutManager來處理溺职,如果需要對RecyclerView進(jìn)行改造,相應(yīng)的也要對其LayoutManager進(jìn)行定制。本文主要就以以下場景給出RecyclerView使用參考:

RecyclerView的幾種常用場景

  • 如何實現(xiàn)帶分割線的列表式RecyclerView
  • 如何實現(xiàn)帶分割線網(wǎng)格式RecyclerView
  • 如何實現(xiàn)全展開的列表式RecyclerView(比如:嵌套到ScrollView中使用)
  • 如何實現(xiàn)全展開的網(wǎng)格式RecyclerView(比如:嵌套到ScrollView中使用)

先看一下實現(xiàn)樣式浪耘,為了方便控制乱灵,邊界的均不設(shè)置分割線,方便定制七冲,如果需要可以采用Padding或者M(jìn)argin來實現(xiàn)痛倚。Github連接 RecyclerItemDecoration

網(wǎng)格式列表樣式
全展開的網(wǎng)格式列表

全展開的線性列表

不同場景RecyclerView實現(xiàn)

默認(rèn)的縱向列表式RecyclerView

首先看一下最簡單的縱向線性RecyclerView,一般用以下代碼:

    LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
    linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
    mRecyclerView.setLayoutManager(linearLayoutManager);

以上就是最簡單的線性RecyclerView的實現(xiàn)癞埠,但默認(rèn)不帶分割線状原,如果想要使用比如20dp的黑色作為分割線聋呢,就需要自己定制苗踪,Google為RecyclerView提供了ItemDecoration,它的作用就是為Item添加一些附屬信息削锰,比如:分割線通铲,浮層等。

帶分割線的列表式RecyclerView--LinearItemDecoration

RecyclerView提供了addItemDecoration接口與ItemDecoration類用來定制分割線樣式器贩,那么颅夺,在RecyclerView源碼中,是怎么用使用ItemDecoration的呢蛹稍。與普通View的繪制流程一致吧黄,RecyclerView也要經(jīng)過measure->layout->draw,并且在measure唆姐、layout之后拗慨,就應(yīng)該按照ItemDecoration的限制,為RecyclerView的分割線挪出空間奉芦。RecyclerView的measure跟Layout其實都是委托給自己的LayoutManager的赵抢,在LinearLayoutManager測量或者布局時都會直接或者間接調(diào)用RecyclerView的measureChildWithMargins函數(shù),而measureChildWithMargins函數(shù)會進(jìn)一步找到addItemDecoration添加的ItemDecoration声功,通過其getItemOffsets函數(shù)獲取所需空間信息烦却,源碼如下:

  public void measureChildWithMargins(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);
      }
  }

可見measureChildWithMargins會首先通過getItemDecorInsetsForChild計算出每個child的ItemDecoration所限制的邊界信息,之后將邊界所需的空間作為已用空間為child構(gòu)造MeasureSpec先巴,最后用MeasureSpec對child進(jìn)行尺寸測量:child.measure(widthSpec, heightSpec);來看一下getItemDecorInsetsForChild函數(shù):

Rect getItemDecorInsetsForChild(View child) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    if (!lp.mInsetsDirty) {
        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;
}

一般而言,不會同時設(shè)置多類ItemDecoration伸蚯,太麻煩醋闭,對于普通的線性布局列表,其實就簡單設(shè)定一個自定義ItemDecoration即可朝卒,其中outRect參數(shù)主要是控制每個Item上下左右的分割線所占據(jù)的寬度跟高度证逻,這個尺寸跟繪制的時候的尺寸應(yīng)該對應(yīng)(如果需要繪制的話),看一下LinearItemDecoration的getItemOffsets實現(xiàn):

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
    if (mOrientation == VERTICAL_LIST) {
    
    <!--垂直方向 ,最后一個不設(shè)置padding-->
        if (parent.getChildAdapterPosition(view) < parent.getAdapter().getItemCount()1) {
            outRect.set(0, 0, 0, mSpanSpace);
        } else {
            outRect.set(0, 0, 0, 0);
        }
    } else {
     <!--水平方向 囚企,最后一個不設(shè)置padding-->
        if (parent.getChildAdapterPosition(view) < parent.getAdapter().getItemCount()1) {
            outRect.set(0, 0, mSpanSpace, 0);
        } else {
            outRect.set(0, 0, 0, 0);
        }
    }
}   

measure跟layout之后丈咐,再來看一下RecyclerView的onDraw函數(shù), RecyclerView在onDraw函數(shù)中會調(diào)用ItemDecoration的onDraw龙宏,繪制分割線或者其他輔助信息棵逊,ItemDecoration 支持上下左右四個方向定制占位分割線等信息,具體要繪制的樣式跟位置都完全由開發(fā)者確定银酗,所以自由度非常大辆影,其實如果不是太特殊的需求的話,onDraw函數(shù)完全可以不做任何處理黍特,僅僅用背景色就可以達(dá)到簡單的分割線的目的蛙讥,當(dāng)然,如果想要定制一些特殊的圖案之類的需話灭衷,就需要自己繪制次慢,來看一下LinearItemDecoration的onDraw(只看Vertical的)

@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
    if (mOrientation == VERTICAL_LIST) {
        drawVertical(c, parent);
    } else {
       ...
    }
}

其實,如果不是特殊的繪制需求翔曲,比如顯示七彩的迫像,或者圖片,完全不需要任何繪制瞳遍,如果一定要繪制闻妓,注意繪制的尺寸區(qū)域跟原來getItemOffsets所限制的區(qū)域一致,繪制的區(qū)域過大不僅不會顯示出來掠械,還會引起過度繪制的問題:

public void drawVertical(Canvas c, RecyclerView parent) {                              int totalCount = parent.getAdapter().getItemCount();            final int childCount = parent.getChildCount();              for (int i = 0; i < childCount; i++) {                  final View child = parent.getChildAt(i);                final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child                          .getLayoutParams();                 final int top = child.getBottom() + params.bottomMargin +                           Math.round(ViewCompat.getTranslationY(child));                  final int bottom = top + mVerticalSpan;                         final int left = child.getLeft() + params.leftMargin;                   final int right = child.getRight() + params.rightMargin;                        if (!isLastRaw(parent, i, mSpanCount, totalCount))                      if (childCounti > mSpanCount) {                         drawable.setBounds(left, top, right, bottom);                           drawable.draw(c);            }       
    }       
}

帶分割線的網(wǎng)格式RecyclerView--GridLayoutItemDecoration

網(wǎng)格式RecyclerView的處理流程跟上面的線性列表類似由缆,不過網(wǎng)格式的需要根據(jù)每個Item的位置為其設(shè)置好邊距,比如最左面的不需要左邊占位份蝴,最右面的不需要右面的占位犁功,最后一行不需要底部的占位,如下圖所示

網(wǎng)格式ItemDocration的限制

RecyclerView的每個childView都會通過getItemOffsets來設(shè)置自己ItemDecoration婚夫,對于網(wǎng)格式的RecyclerView浸卦,需要在四個方向上對其ItemDecoration進(jìn)行限制,來看一下其實現(xiàn)類GridLayoutItemDecoration的getItemOffsets:

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
    final int position = parent.getChildAdapterPosition(view);
    final int totalCount = parent.getAdapter().getItemCount();
    int left = (position % mSpanCount == 0) ? 0 : mHorizonSpan;
    int bottom = ((position + 1) % mSpanCount == 0) ? 0 : mVerticalSpan;
    if (isVertical(parent)) {
        if (!isLastRaw(parent, position, mSpanCount, totalCount)) {
            outRect.set(left, 0, 0, mVerticalSpan);
        } else {
            outRect.set(left, 0, 0, 0);
        }
    } else {
        if (!isLastColumn(parent, position, mSpanCount, totalCount)) {
            outRect.set(0, 0, mHorizonSpan, bottom);
        } else {
            outRect.set(0, 0, 0, bottom);
        }
    }
}

其實上面的代碼就是根據(jù)RecyclerView滑動方向(橫向或者縱向)以及child的位置(是不是最后一行或者最后一列)案糙,對附屬區(qū)域進(jìn)行限制限嫌,同樣,如果不是特殊的分割線樣式时捌,通過背景就基本可以實現(xiàn)需求怒医,不用特殊draw。

全展開的列表式RecyclerView--ExpandedLinearLayoutManager

RecyclerView全展開的邏輯跟分割線不同奢讨,全展開主要是跟measure邏輯相關(guān)稚叹,簡單看一下RecyclerView(v-22版本,相對簡單)的measure源碼:

@Override
protected void onMeasure(int widthSpec, int heightSpec) {
        ...
        
        <!--關(guān)鍵代碼,如果mLayout(LayoutManager)非空,就采用LayoutManager的mLayout.onMeasure-->
    if (mLayout == null) {
        defaultOnMeasure(widthSpec, heightSpec);
    } else {
        mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
    }

    mState.mInPreLayout = false; // clear
}

由以上代碼可以看出扒袖,在為RecyclerView設(shè)置了LayoutManager之后塞茅,RecyclerView的measure邏輯其實就是委托給了它的LayoutManager,這里以LinearLayoutManager為例季率,不過LinearLayoutManager源碼里面并沒有重寫onMeasure函數(shù)野瘦,也就是說,對于RecyclerView的線性樣式飒泻,對于尺寸的處理采用的是跟ViewGroup一樣的處理鞭光,完全由父控件限制,不過對于v-23里面有了一些修改泞遗,就是增加了對wrap_content的支持惰许。既然這樣,我們就可以把設(shè)置尺寸的時機(jī)放到LayoutManager的onMeasure中刹孔,對全展開的RecyclerView來說啡省,其實就是將所有child測量一遍娜睛,之后將每個child需要高度或者寬度累加髓霞,看一下ExpandedLinearLayoutManager的實現(xiàn):在測量child的時候,采用RecyclerView的measureChildWithMargins畦戒,該函數(shù)已經(jīng)將ItemDecoration的占位考慮進(jìn)去方库,之后通過getDecoratedMeasuredWidth獲取真正需要占用的尺寸。

@Override
public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state,
                      int widthSpec, int heightSpec) {
    final int widthMode = View.MeasureSpec.getMode(widthSpec);
    final int heightMode = View.MeasureSpec.getMode(heightSpec);
    final int widthSize = View.MeasureSpec.getSize(widthSpec);
    final int heightSize = View.MeasureSpec.getSize(heightSpec);
    int measureWidth = 0;
    int measureHeight = 0;
    int count;
    if (mMaxItemCount < 0 || getItemCount() < mMaxItemCount) {
        count = getItemCount();
    } else {
        count = mMaxItemCount;
    }
    for (int i = 0; i < count; i++) {
        int[] measuredDimension = getChildDimension(recycler, i);
        if (measuredDimension == null || measuredDimension.length != 2)
            return;
        if (getOrientation() == HORIZONTAL) {
            measureWidth = measureWidth + measuredDimension[0];
           <!--獲取最大高度-->
            measureHeight = Math.max(measureHeight, measuredDimension[1]);
        } else {
            measureHeight = measureHeight + measuredDimension[1];
            <!--獲取最大寬度-->
            measureWidth = Math.max(measureWidth, measuredDimension[0]);
        }
    }

    measureHeight = heightMode == View.MeasureSpec.EXACTLY ? heightSize : measureHeight;
    measureWidth = widthMode == View.MeasureSpec.EXACTLY ? widthSize : measureWidth;
    if (getOrientation() == VERTICAL && measureWidth > widthSize) {
        measureWidth = widthSize;
    } else if (getOrientation() == HORIZONTAL && measureHeight > heightSize) {
        measureHeight = heightSize;
    }
    setMeasuredDimension(measureWidth, measureHeight);
}


private int[] getChildDimension(RecyclerView.Recycler recycler, int position) {
    try {
        int[] measuredDimension = new int[2];
        View view = recycler.getViewForPosition(position);
        //測量childView障斋,以便獲得寬高(包括ItemDecoration的限制)
        super.measureChildWithMargins(view, 0, 0);
        //獲取childView纵潦,以便獲得寬高(包括ItemDecoration的限制),以及邊距
        RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) view.getLayoutParams();
        measuredDimension[0] = getDecoratedMeasuredWidth(view) + p.leftMargin + p.rightMargin;
        measuredDimension[1] = getDecoratedMeasuredHeight(view) + p.bottomMargin + p.topMargin;
        return measuredDimension;
    } catch (Exception e) {
        Log.d("LayoutManager", e.toString());
    }
    return null;
}

全展開的網(wǎng)格式RecyclerView--ExpandedGridLayoutManager

全展開的網(wǎng)格式RecyclerView的實現(xiàn)跟線性的十分相似垃环,唯一不同的就是在確定尺寸的時候邀层,不是將每個child的尺寸疊加,而是要將每一行或者每一列的尺寸疊加遂庄,這里假定行高或者列寬都是相同的寥院,其實在使用中這兩種場景也是最常見的,看如下代碼涛目,其實除了加了行與列判斷邏輯秸谢,其他基本跟上面的全展開線性的類似。

 @Override
public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec) {
    final int widthMode = View.MeasureSpec.getMode(widthSpec);
    final int heightMode = View.MeasureSpec.getMode(heightSpec);
    final int widthSize = View.MeasureSpec.getSize(widthSpec);
    final int heightSize = View.MeasureSpec.getSize(heightSpec);
    int measureWidth = 0;
    int measureHeight = 0;
    int count = getItemCount();
    int span = getSpanCount();
    for (int i = 0; i < count; i++) {
        measuredDimension = getChildDimension(recycler, i);
        if (getOrientation() == HORIZONTAL) {
            if (i % span == 0 ) {
                measureWidth = measureWidth + measuredDimension[0];
            }
            measureHeight = Math.max(measureHeight, measuredDimension[1]);
        } else {
            if (i % span == 0) {
                measureHeight = measureHeight + measuredDimension[1];
            }
            measureWidth = Math.max(measureWidth, measuredDimension[0]);
        }
    }
    measureHeight = heightMode == View.MeasureSpec.EXACTLY ? heightSize : measureHeight;
    measureWidth = widthMode == View.MeasureSpec.EXACTLY ? widthSize : measureWidth;
    setMeasuredDimension(measureWidth, measureHeight);
}

最后附上橫向滑動效果圖:

橫向滑動

以上就是比較通用的RecyclerView使用場景及所做的兼容 霹肝,最后附上Github鏈接RecyclerItemDecoration估蹄,歡迎star,fork沫换。

作者:看書的小蝸牛
原文鏈接: RecyclerView定制:通用ItemDecoration及全展開RecyclerView的實現(xiàn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末臭蚁,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌垮兑,老刑警劉巖炭晒,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異甥角,居然都是意外死亡网严,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門嗤无,熙熙樓的掌柜王于貴愁眉苦臉地迎上來震束,“玉大人,你說我怎么就攤上這事当犯」复澹” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵嚎卫,是天一觀的道長嘉栓。 經(jīng)常有香客問我,道長拓诸,這世上最難降的妖魔是什么侵佃? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮奠支,結(jié)果婚禮上馋辈,老公的妹妹穿的比我還像新娘。我一直安慰自己倍谜,他們只是感情好迈螟,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著尔崔,像睡著了一般答毫。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上季春,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天洗搂,我揣著相機(jī)與錄音,去河邊找鬼鹤盒。 笑死蚕脏,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的侦锯。 我是一名探鬼主播驼鞭,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼尺碰!你這毒婦竟也來了挣棕?” 一聲冷哼從身側(cè)響起译隘,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎洛心,沒想到半個月后固耘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡词身,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年厅目,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片法严。...
    茶點(diǎn)故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡损敷,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出深啤,到底是詐尸還是另有隱情拗馒,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布溯街,位于F島的核電站诱桂,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏呈昔。R本人自食惡果不足惜挥等,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望韩肝。 院中可真熱鬧触菜,春花似錦九榔、人聲如沸哀峻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽剩蟀。三九已至,卻和暖如春切威,著一層夾襖步出監(jiān)牢的瞬間育特,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工先朦, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留缰冤,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓喳魏,卻偏偏與公主長得像棉浸,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子刺彩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評論 2 344

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