RecyclerView之ItemDecoration

GitLqr發表於2017-05-03

本文已授權微信公眾號:鴻洋(hongyangAndroid)原創首發。

一、簡述

說到RecyclerView大家都很熟悉了,相比於ListView,它具有高度解耦、效能優化等優勢,而且現在大多數安卓開發者都已經將RecyclerView用來完全替代ListView和GridView,因為它功能十分強大,但往往功能強大的東西,反而不太好控制,例如今天要說的這個ItemDecoration,ItemDecoration是條目裝飾,下面來看看它的強大吧。

二、使用ItemDecoration繪製分割線

想想之前的ListView,要加條分割線,那是分分鐘解決的小事,只需要在佈局檔案中對ListView控制元件設定其divier屬性或者在動態中設定divider即可完成,但RecyclerView卻沒這麼簡單了,RecyclerView並沒有提供任何直接設定分割線的方法,除了在條目佈局中加入這種笨方法之外,也就只能通過ItemDecoration來實現了。

1、自定義ItemDecoration

要使用ItemDecoration,我們得必須先自定義,直接繼承ItemDecoration即可。

public class MyDecorationOne extends RecyclerView.ItemDecoration {

}
複製程式碼

2、重寫getItemOffsets()和onDraw()

在實現自定義的裝飾效果就必須重寫getItemOffsets()和onDraw()。

public class MyDecorationOne extends RecyclerView.ItemDecoration {

    /**
     * 畫線
     */
    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
    }

    /**
     * 設定條目周邊的偏移量
     */
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        
    }
}
複製程式碼

3、解析getItemOffsets()和onDraw()

在理清這兩個方法的作用之前,先理清下ItemDecoration的含義,直譯:條目裝飾,顧名思義,ItemDecoration是對Item起到了裝飾作用,更準確的說是對item的周邊起到了裝飾的作用,通過下面的圖應該能幫助你理解這話的含義。

RecyclerView之ItemDecoration

上圖中已經說到了,getItemOffsets()就是設定item周邊的偏移量(也就是裝飾區域的“寬度”)。而onDraw()才是真正實現裝飾的回撥方法,通過該方法可以在裝飾區域任意畫畫,這裡我們來畫條分割線。

4、“實現”getItemOffsets()和onDraw()

本例中實現的是線性列表的分割線(即使用LinearLayoutManager)。

1)當線性列表是水平方向時,分割線豎直的;當線性列表是豎直方向時,分割線是水平的。

2)當畫豎直分割線時,需要在item的右邊偏移出一條線的寬度;當畫水平分割線時,需要在item的下邊偏移出一條線的高度。

/**
 * 畫線
 */
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
    super.onDraw(c, parent, state);
    if (orientation == RecyclerView.HORIZONTAL) {
        drawVertical(c, parent, state);
    } else if (orientation == RecyclerView.VERTICAL) {
        drawHorizontal(c, parent, state);
    }
}

/**
 * 設定條目周邊的偏移量
 */
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
    super.getItemOffsets(outRect, view, parent, state);
    if (orientation == RecyclerView.HORIZONTAL) {
        //畫垂直線
        outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
    } else if (orientation == RecyclerView.VERTICAL) {
        //畫水平線
        outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
    }
}
複製程式碼

5、畫出一條華麗的分割線

因為getItemOffsets()是相對每個item而言的,即每個item都會偏移出相同的裝飾區域。而onDraw()則不同,它是相對Canvas來說的,通俗的說就是要自己找到要畫的線的位置,這是自定義ItemDecoration中唯一比較難的地方了。

/**
 * 在構造方法中載入系統自帶的分割線(就是ListView用的那個分割線)
 */
public MyDecorationOne(Context context, int orientation) {
    this.orientation = orientation;
    int[] attrs = new int[]{android.R.attr.listDivider};
    TypedArray a = context.obtainStyledAttributes(attrs);
    mDivider = a.getDrawable(0);
    a.recycle();
}    

/**
 * 畫豎直分割線
 */
private void drawVertical(Canvas c, RecyclerView parent, RecyclerView.State state) {
    int childCount = parent.getChildCount();
    for (int i = 0; i < childCount; i++) {
        View child = parent.getChildAt(i);
        RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
        int left = child.getRight() + params.rightMargin;
        int top = child.getTop() - params.topMargin;
        int right = left + mDivider.getIntrinsicWidth();
        int bottom = child.getBottom() + params.bottomMargin;
        mDivider.setBounds(left, top, right, bottom);
        mDivider.draw(c);
    }
}

/**
 * 畫水平分割線
 */
private void drawHorizontal(Canvas c, RecyclerView parent, RecyclerView.State state) {
    int childCount = parent.getChildCount();
    for (int i = 0; i < childCount; i++) {
        View child = parent.getChildAt(i);
        RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
        int left = child.getLeft() - params.leftMargin;
        int top = child.getBottom() + params.bottomMargin;
        int right = child.getRight() + params.rightMargin;
        int bottom = top + mDivider.getIntrinsicHeight();
        mDivider.setBounds(left, top, right, bottom);
        mDivider.draw(c);
    }
}
複製程式碼

下圖僅對水平分割線的左、上座標進行圖解,其他座標的計算以此類推。

RecyclerView之ItemDecoration

6、效果

看了下面的效果,你可能會吐槽說,不就是加條分割線嗎?要不要這麼大費周章?是的,一開始我也是這麼想,確實只是為了畫條分割線的話,這也太麻煩了,而且專案開發中很少對分割線有多高的定製要求,一般就是ListView那樣的,最多就是改改顏色這些。所以本人在之前有對RecyclerView進行過一次封裝,可以輕鬆實現分割線,有興趣的可以戳我看看!!。好了,下面繼續。

RecyclerView之ItemDecoration

三、使用ItemDecoration繪製表格

經過上面的學習,相信心中已經對ItemDecoration有個大概的底了,下面再來實現個其他的效果吧——繪製表格。

1、分析

我們知道ItemDecoration就是裝飾item周邊用的,畫條分割線只需要2步,1是在item的下方偏移出一定的寬度,2是在偏移出來的位置上畫線。畫表格線其實也一樣,除了畫item下方的線,還畫item右邊的線就好了(當然換成左邊也行)。

2、實現

為了完成表格的樣式,本例中使用的是網格列表(即使用GridLayoutManager)。

1)自定義分割線

為了效果更加明顯,這裡自定義分割線樣式。

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
       android:shape="rectangle">

    <solid android:color="#f00"/>
    <size
        android:width="2dp"
        android:height="2dp"/>

</shape>
複製程式碼

2)自定義ItemDecoration

實現上跟畫分割線沒多大差別,瞄一下就明白了。

public class MyDecorationTwo extends RecyclerView.ItemDecoration {

    private final Drawable mDivider;

    public MyDecorationTwo(Context context) {
        mDivider = context.getResources().getDrawable(R.drawable.divider);
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
        drawVertical(c, parent);
        drawHorizontal(c, parent);
    }

    private void drawVertical(Canvas c, RecyclerView parent) {
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = parent.getChildAt(i);
            RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
            int left = child.getRight() + params.rightMargin;
            int top = child.getTop() - params.topMargin;
            int right = left + mDivider.getIntrinsicWidth();
            int bottom = child.getBottom() + params.bottomMargin;
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    private void drawHorizontal(Canvas c, RecyclerView parent) {
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = parent.getChildAt(i);
            RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
            int left = child.getLeft() - params.leftMargin;
            int top = child.getBottom() + params.bottomMargin;
            int right = child.getRight() + params.rightMargin;
            int bottom = top + mDivider.getMinimumHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        outRect.set(0, 0, mDivider.getIntrinsicWidth(), mDivider.getIntrinsicHeight());
    }
}
複製程式碼

3、效果(有瑕疵)

可以看出下面的效果是有問題的,表格的最後一列和最後一行不應該出現邊邊。

RecyclerView之ItemDecoration

4、修復

既然知道表格的最後一列和最後一行不應該出現邊邊,那就讓最後一列和最後一行的邊邊消失就好了。有以下幾個思路。

  1. 在onDraw()方法中,判斷當前列是否為最後一列和判斷當前行是否為最後一行來決定是否繪製邊邊。
  2. 在getItemOffsets()方法中對行列進行判斷,來決定是否設定條目偏移量(當偏移量為0時,自然就看不出邊邊了)。

這裡我選用第二種方式。這裡要說明一下,getItemOffsets()有兩個,一個是getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state),另一個是getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent),第二個已經過時,但是該方法中有回傳當前item的position,所以我選用了過時的getItemOffsets()。

@Override
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
    super.getItemOffsets(outRect, itemPosition, parent);
    int right = mDivider.getIntrinsicWidth();
    int bottom = mDivider.getIntrinsicHeight();

    if (isLastSpan(itemPosition, parent)) {
        right = 0;
    }

    if (isLastRow(itemPosition, parent)) {
        bottom = 0;
    }
    outRect.set(0, 0, right, bottom);
}

public boolean isLastRow(int itemPosition, RecyclerView parent) {
    RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
    if (layoutManager instanceof GridLayoutManager) {
        int spanCount = ((GridLayoutManager) layoutManager).getSpanCount();
        int itemCount = parent.getAdapter().getItemCount();
        if ((itemCount - itemPosition - 1) < spanCount)
            return true;
    }
    return false;
}

public boolean isLastSpan(int itemPosition, RecyclerView parent) {
    RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
    if (layoutManager instanceof GridLayoutManager) {
        int spanCount = ((GridLayoutManager) layoutManager).getSpanCount();
        if ((itemPosition + 1) % spanCount == 0)
            return true;
    }
    return false;
}
複製程式碼

程式碼理解上並不難,這裡不做多餘的解釋。

5、效果(幾乎沒有瑕疵)

RecyclerView之ItemDecoration

四、使用ItemDecoration實現側邊字母提示

上面的兩個例子僅僅只是畫線,下面的這個例子就來畫字吧。先看下效果。

RecyclerView之ItemDecoration

1、分析

說到底也就是在item左邊偏移出來的空間區域中心畫個字母而已。下面是大體思路:

  1. 在item的左邊偏移出一定的空間(本例偏移量是40dp)。
  2. 在onDraw()時,使用Pinyin工具類獲取item中名字拼音的第一個字母。
  3. 判斷如果當前是第一個item,就畫出字母。
  4. 若不是第一個item則判斷當前item的名字字母與上一個字母是否一致,不一致則畫出當前字母。

2、實現

1)自定義文字拼音工具類

*該工具類需要用到pinyin4j-2.5.0.jar

public class PinyinUtils {

    public static String getPinyin(String str) {

        HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
        format.setCaseType(HanyuPinyinCaseType.UPPERCASE);
        format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);

        StringBuilder sb = new StringBuilder();

        char[] charArray = str.toCharArray();
        for (int i = 0; i < charArray.length; i++) {
            char c = charArray[i];
            // 如果是空格, 跳過
            if (Character.isWhitespace(c)) {
                continue;
            }
            if (c >= -127 && c < 128 || !(c >= 0x4E00 && c <= 0x9FA5)) {
                // 肯定不是漢字
                sb.append(c);
            } else {
                String s = "";
                try {
                    // 通過char得到拼音集合. 單 -> dan, shan 
                    s = PinyinHelper.toHanyuPinyinStringArray(c, format)[0];
                    sb.append(s);
                } catch (BadHanyuPinyinOutputFormatCombination e) {
                    e.printStackTrace();
                    sb.append(s);
                }
            }
        }

        return sb.toString();
    }
}
複製程式碼

2)自定義ItemDecoration

public class MyDecorationThree extends RecyclerView.ItemDecoration {

    Context mContext;
    List<String> mData;
    Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);


    public MyDecorationThree(Context context, List<String> data) {
        mContext = context;
        mData = data;
        paint.setTextSize(sp2px(16));
        paint.setColor(Color.RED);
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
        drawLetterToItemLeft(c, parent);
    }

    private void drawLetterToItemLeft(Canvas c, RecyclerView parent) {
        RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
        if (!(layoutManager instanceof LinearLayoutManager))
            return;
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            int position = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition() + i;
            View child = parent.getChildAt(i);
            RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
            float left = 0;
            float top = child.getTop() - params.topMargin;
            float right = child.getLeft() - params.leftMargin;
            float bottom = child.getBottom() + params.bottomMargin;
            float width = right - left;
            float height = bottom - (bottom - top) / 2;
            //當前名字拼音的第一個字母
            String letter = PinyinUtils.getPinyin(mData.get(position)).charAt(0) + "";
            if (position == 0) {
                drawLetter(letter, width, height, c, parent);
            } else {
                String preLetter = PinyinUtils.getPinyin(mData.get(position - 1)).charAt(0) + "";
                if (!letter.equalsIgnoreCase(preLetter)) {
                    drawLetter(letter, width, height, c, parent);
                }
            }
        }
    }

    private void drawLetter(String letter, float width, float height, Canvas c, RecyclerView parent) {
        float fontLength = getFontLength(paint, letter);
        float fontHeight = getFontHeight(paint);
        float tx = (width - fontLength) / 2;
        float ty = height - fontHeight / 2 + getFontLeading(paint);
        c.drawText(letter, tx, ty, paint);
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        outRect.set(dip2px(40), 0, 0, 0);
    }

    private int dip2px(int dip) {
        float density = mContext.getResources().getDisplayMetrics().density;
        int px = (int) (dip * density + 0.5f);
        return px;
    }

    public int sp2px(int sp) {
        return (int) (TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, mContext.getResources().getDisplayMetrics()) + 0.5f);
    }


    /**
     * 返回指定筆和指定字串的長度
     */
    private float getFontLength(Paint paint, String str) {
        return paint.measureText(str);
    }

    /**
     * 返回指定筆的文字高度
     */
    private float getFontHeight(Paint paint) {
        Paint.FontMetrics fm = paint.getFontMetrics();
        return fm.descent - fm.ascent;
    }


    /**
     * 返回指定筆離文字頂部的基準距離
     */
    private float getFontLeading(Paint paint) {
        Paint.FontMetrics fm = paint.getFontMetrics();
        return fm.leading - fm.ascent;
    }
}
複製程式碼

最後附上Demo連結

github.com/GitLqr/Mate…

歡迎關注微信公眾號:全棧行動

相關文章