自定義View 之 RecyclerView.ItemDecoration

天星技術團隊發表於2019-03-02

作者:點先生 時間:2019.1.26

是這麼一回事

年底了,趕專案,於是忙了一個月業務,忙了一個月沒有營養的東西。為啥說沒營養,因為就是很簡簡單單的展示,沒有啥東西可寫。我差點要搬出11月份的騰訊面試經歷了,就在這時我給自己挖了個坑。 我本人的自定義View的能力是很差的,之前也沒有寫過,一直都用android自帶或者github上寫好的東西。所以這個坑挖的還是值。

坑的來源

之前我們有一個報警訊息展示介面,是這樣的;

自定義View 之 RecyclerView.ItemDecoration

有個功能是這樣的,紅點顯示未讀,點選一下就能消滅紅點。
問題就來了:後臺表示不能提供是否已讀的狀態,我表示我這邊本地儲存報警訊息狀態並不合理。然後我就騷了一波,說介面不用改,我自己這邊處理。其實我想的就是仿微信朋友圈裡面的文字分割線“以下是已讀內容”,這樣就不用處理每一條訊息了,哈哈哈哈哈哈哈。

兩種方案與思路

一開始我想到了兩種方案:
A :類似於新增head,footer,寫個新的viewholder進去。
優點:網文較多;佈局複雜的情況下比較好管理修改;
缺點:修改的東西比較多。
B:自定義RecyclerView.ItemDecoration
優點:修改東西較少;自定義的優點;
缺點:自定義的缺點;\

思路:無論是A方案還是B方案,我都需要知道這個分割線的position,在這裡我是將上一次請求到的資料中最新一條的createTime存入SP中,我將通過這個值去對比每一次請求下來的資料集的createTime,當他相等時,這個item的position,就應該是分割線的position。(這裡選擇對比條件是一定要選擇一個唯一,不重複的)。

在A方案中,adapter得到list後,可以找到分割線的position,然後在此position返回TextDivider的Viewholder。麻煩在於position之後的資料,TextDivider之後的每一個資料的position都必須+1。每一次都得重新去算。每次滑動都會算,這裡處理起來可能不是很方便,而且會增加許多屬性幫助確定真正的position。棄之

所以我選擇了B方案。也是對自己個機會去學習自定義view。

“懶惰是第一生產力” —— 沃·茲基朔德

RecyclerView.ItemDecoration

public class TextDivider extends RecyclerView.ItemDecoration {
    public void onDraw(Canvas c, RecyclerView parent, State state){}
    public void onDrawOver(Canvas c, RecyclerView parent, State state){}
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state){}
}
複製程式碼

建立一個類去繼承RecyclerView.ItemDecoration,有三個方法需要重寫;
執行順序是getItemOffsets(),onDraw(),onDrawOver();

看名字,ItemDecoration是一個裝飾者,並且是給每一個item加一個裝飾。我們常用場景是寫個分割線,各種分割線,希望大家能通過我這篇文章,對ItemDecoration有更多新的騷操作。

我們先來說關於這三個方法的用法。

getItemOffsets

第一個引數Rect,看名字不是不太容易知道有啥用。其實它就是我們當前item的矩形。我們可以通過這個引數獲取到他的top、bottom、left、right。也可以給這幾個屬性賦值。當我們不給這幾個引數賦值時,預設為0;

自定義View 之 RecyclerView.ItemDecoration

當我們設定了rect的引數之後,就有了上圖左邊的效果,如果不賦值,預設就是右邊這個樣子。

onDraw與onDrawOver

這就是當靈魂畫家的部分了,用canvas可以畫你想畫的東西。
parent幫助你獲取當前item的屬性。
state獲取當前recycleView的狀態。
這兩個方法的區別在於先後順序。

onDraw畫的東西會被item佈局擋住;
item佈局裡的東西會被onDrawOver擋住;
明白了吧?

自定義View 之 RecyclerView.ItemDecoration
左邊的圓就是onDraw畫的,右邊的圓就是onDrawOver畫的
tips!!! 上一個的item可能會被下一個item的onDraw東西給擋住,所以在畫的時候一定要控制好你的範圍。

程式碼!安排!

    private int bottomDevider;//分割線寬度
    private int topDevider;//文字分割寬度
    private String textString;//分割線的文字

    Rect textBounds = new Rect();

    private Paint dividerPaint;
    private Paint textPaint;

    private Long lastReadMsgDate;//上次獲取資料集的最新資料的createtime
複製程式碼

除了textBounds ,其他都很容易理解是幹嘛的。

    public TextDivider(Context context) {
        dividerPaint = new Paint();
        textPaint = new Paint();
        //設定分割線顏色
        dividerPaint.setColor(context.getResources().getColor(R.color.whitesmoke));
        textPaint.setColor(context.getResources().getColor(R.color.vpi__bright_foreground_disabled_holo_dark));
        textString = "--------------以-下-是-已-閱-讀-內-容--------------";
        textPaint.setTextSize(32);
        textPaint.setTextAlign(Paint.Align.CENTER);
        //設定分割線寬度
        bottomDevider = context.getResources().getDimensionPixelSize(R.dimen.space_2);
        topDevider = 100;
        lastReadMsgDate = Long.parseLong(SPM.getStr(BaseApp.getContext(), LC.CONSTANT, LC.LAST_REMIND_MSG_DATA, "0"));
    }
複製程式碼

textPaint.setTextAlign(Paint.Align.CENTER); 這句程式碼是讓所寫的文字,居於原點水平居中。

    private CreateTimeListener mListener;

    public void setCreateTimeListener(CreateTimeListener listener) {
        mListener = listener;
    }
    public interface CreateTimeListener {
        long getCreateTime(int position);
    }
複製程式碼

這是介面用來從外部獲取當前item的createTime。

    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        outRect.bottom = bottomDevider;
        if(lastReadMsgDate == mListener.getCreateTime(parent.getChildAdapterPosition(view))){
            outRect.top = topDevider;
        }
    }
複製程式碼

給每個item下方增加一段距離,用於畫普通的分割線。
在需要畫文字分割線的上方增加一段距離,用於畫文字分割線

    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        final int childCount = parent.getChildCount();
        final int left = parent.getLeft() + parent.getPaddingLeft();
        final int right = parent.getRight() - parent.getPaddingRight();
        for (int i = 0; i < childCount; i++) {
            View view = parent.getChildAt(i);
            int position = parent.getChildAdapterPosition(view);
            if(lastReadMsgDate == mListener.getCreateTime(position)){
                float top = view.getBottom();
                float bottom = view.getBottom() + bottomDevider;
                c.drawRect(left, top, right, bottom, dividerPaint);
                top = view.getTop() - bottomDevider;
                bottom = view.getTop();
                c.drawRect(left, top, right, bottom, dividerPaint);

                //文字居中線
                float x = (view.getRight() - view.getLeft())/2;
                //文字所佔用的邊框top,bottom位置
                top = view.getTop() - topDevider;
                bottom = view.getTop() - bottomDevider;
                //獲取文字的Bounds
                textPaint.getTextBounds(textString, 0, textString.length(), textBounds);
                //計算文字的基線
                float y = ((bottom + top)/2) + (textBounds.height()/2);

                c.drawText(textString, x, y, textPaint);
            }else {
                float top = view.getBottom();
                float bottom = view.getBottom() + bottomDevider;
                c.drawRect(left, top, right, bottom, dividerPaint);
            }
        }
    }
複製程式碼

在畫文字分割線的時候我覺得比較煩的就是算距離。
通常我們用canvas畫東西的時候的原點,在左上角。

自定義View 之 RecyclerView.ItemDecoration

而文字分割線的原點在第一個字的左下角偏左一點點的距離。

自定義View 之 RecyclerView.ItemDecoration

文字垂直居中

關於點先生有多帥就不多講了。這裡說一說文字居中的問題。
本帥瞭解也不是很深, 就只找到了一種方法讓它居中。
水平居中很簡單,上面已經說到過了。

自定義View 之 RecyclerView.ItemDecoration

item的原點在左上角藍色圓的位置,文字要想垂直居中,原點應該在紫色圓的位置。 找到紫圓的Y軸座標就可以了。
((bottom + top)/ 2) + (文字所佔的高度 / 2)

文字所佔高度,就是最後的難點了。 各種get方法都找不到文字高度,最後在畫文字時候傳的一個引數Rect給找到方法了。

  textPaint.getTextBounds(textString, 0, textString.length(), textBounds);
複製程式碼

跟上文說的一樣,就是矩形,這裡傳進去的textBounds就是Rect,穿進去之後可以獲取到當前文字的一些屬性,問題迎刃而解。

自定義View 之 RecyclerView.ItemDecoration

在recycleView使用處呼叫也很簡單。

        textDivider = new TextDivider(getContext());
        textDivider.setCreateTimeListener(new TextDivider.CreateTimeListener() {
            @Override
            public long getCreateTime(int position) {
                if (cacherRmindMsgList.size()==0) return 0L;
                else return cacherRmindMsgList.get(position).getCreateTime();
            }
        });
        recyclerView.addItemDecoration(textDivider);

複製程式碼

嘻嘻!

自定義View 之 RecyclerView.ItemDecoration

後續

做完之後有個疑問。為啥獲取文字屬性的沒有一個叫get***()的方法!
還要我親自傳一個引數進去接受這些東西。給個回撥介面也好啊!

打臉也挺快,自己親手寫過的介面隔離原則都差點忘了。
Rect裡面這麼多屬性,它又不知道我要什麼東西,全都回撥給我,也太傻逼了。

相關文章