作者:點先生 時間:2019.1.26
是這麼一回事
年底了,趕專案,於是忙了一個月業務,忙了一個月沒有營養的東西。為啥說沒營養,因為就是很簡簡單單的展示,沒有啥東西可寫。我差點要搬出11月份的騰訊面試經歷了,就在這時我給自己挖了個坑。
我本人的自定義View的能力是很差的,之前也沒有寫過,一直都用android自帶或者github上寫好的東西。所以這個坑挖的還是值。
坑的來源
之前我們有一個報警訊息展示介面,是這樣的;
有個功能是這樣的,紅點顯示未讀,點選一下就能消滅紅點。
問題就來了:後臺表示不能提供是否已讀的狀態,我表示我這邊本地儲存報警訊息狀態並不合理。然後我就騷了一波,說介面不用改,我自己這邊處理。其實我想的就是仿微信朋友圈裡面的文字分割線“以下是已讀內容”,這樣就不用處理每一條訊息了,哈哈哈哈哈哈哈。
兩種方案與思路
一開始我想到了兩種方案:
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;
當我們設定了rect的引數之後,就有了上圖左邊的效果,如果不賦值,預設就是右邊這個樣子。
onDraw與onDrawOver
這就是當靈魂畫家的部分了,用canvas可以畫你想畫的東西。
parent幫助你獲取當前item的屬性。
state獲取當前recycleView的狀態。
這兩個方法的區別在於先後順序。
onDraw畫的東西會被item佈局擋住;
item佈局裡的東西會被onDrawOver擋住;
明白了吧?
左邊的圓就是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畫東西的時候的原點,在左上角。
而文字分割線的原點在第一個字的左下角偏左一點點的距離。
文字垂直居中
關於點先生有多帥就不多講了。這裡說一說文字居中的問題。
本帥瞭解也不是很深, 就只找到了一種方法讓它居中。
水平居中很簡單,上面已經說到過了。
item的原點在左上角藍色圓的位置,文字要想垂直居中,原點應該在紫色圓的位置。
找到紫圓的Y軸座標就可以了。
((bottom + top)/ 2) + (文字所佔的高度 / 2)
文字所佔高度,就是最後的難點了。
各種get方法都找不到文字高度,最後在畫文字時候傳的一個引數Rect給找到方法了。
textPaint.getTextBounds(textString, 0, textString.length(), textBounds);
複製程式碼
跟上文說的一樣,就是矩形,這裡傳進去的textBounds就是Rect,穿進去之後可以獲取到當前文字的一些屬性,問題迎刃而解。
在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);
複製程式碼
嘻嘻!
後續
做完之後有個疑問。為啥獲取文字屬性的沒有一個叫get***()的方法!
還要我親自傳一個引數進去接受這些東西。給個回撥介面也好啊!
打臉也挺快,自己親手寫過的介面隔離原則都差點忘了。
Rect裡面這麼多屬性,它又不知道我要什麼東西,全都回撥給我,也太傻逼了。