本文已受權微信公衆號:鴻洋(hongyangAndroid)原創首發。 javascript
說到RecyclerView你們都很熟悉了,相比於ListView,它具備高度解耦、性能優化等優點,並且如今大多數安卓開發者都已經將RecyclerView用來徹底替代ListView和GridView,由於它功能十分強大,但每每功能強大的東西,反而不太好控制,例現在天要說的這個ItemDecoration,ItemDecoration是條目裝飾,下面來看看它的強大吧。java
想一想以前的ListView,要加條分割線,那是分分鐘解決的小事,只須要在佈局文件中對ListView控件設置其divier屬性或者在動態中設置divider便可完成,但RecyclerView卻沒這麼簡單了,RecyclerView並無提供任何直接設置分割線的方法,除了在條目佈局中加入這種笨方法以外,也就只能經過ItemDecoration來實現了。android
要使用ItemDecoration,咱們得必須先自定義,直接繼承ItemDecoration便可。git
public class MyDecorationOne extends RecyclerView.ItemDecoration {
}複製代碼
在實現自定義的裝飾效果就必須重寫getItemOffsets()和onDraw()。github
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);
}
}複製代碼
在理清這兩個方法的做用以前,先理清下ItemDecoration的含義,直譯:條目裝飾,顧名思義,ItemDecoration是對Item起到了裝飾做用,更準確的說是對item的周邊起到了裝飾的做用,經過下面的圖應該能幫助你理解這話的含義。性能優化
上圖中已經說到了,getItemOffsets()就是設置item周邊的偏移量(也就是裝飾區域的「寬度」)。而onDraw()纔是真正實現裝飾的回調方法,經過該方法能夠在裝飾區域任意畫畫,這裏咱們來畫條分割線。微信
本例中實現的是線性列表的分割線(即便用LinearLayoutManager)。app
1)當線性列表是水平方向時,分割線豎直的;當線性列表是豎直方向時,分割線是水平的。ide
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());
}
}複製代碼
由於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);
}
}複製代碼
下圖僅對水平分割線的左、上座標進行圖解,其餘座標的計算以此類推。
看了下面的效果,你可能會吐槽說,不就是加條分割線嗎?要不要這麼大費周章?是的,一開始我也是這麼想,確實只是爲了畫條分割線的話,這也太麻煩了,並且項目開發中不多對分割線有多高的定製要求,通常就是ListView那樣的,最多就是改改顏色這些。因此本人在以前有對RecyclerView進行過一次封裝,能夠輕鬆實現分割線,有興趣的能夠戳我看看!!。好了,下面繼續。
通過上面的學習,相信心中已經對ItemDecoration有個大概的底了,下面再來實現個其餘的效果吧——繪製表格。
咱們知道ItemDecoration就是裝飾item周邊用的,畫條分割線只須要2步,1是在item的下方偏移出必定的寬度,2是在偏移出來的位置上畫線。畫表格線其實也同樣,除了畫item下方的線,還畫item右邊的線就行了(固然換成左邊也行)。
爲了完成表格的樣式,本例中使用的是網格列表(即便用GridLayoutManager)。
爲了效果更加明顯,這裏自定義分割線樣式。
<?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>複製代碼
實現上跟畫分割線沒多大差異,瞄一下就明白了。
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());
}
}複製代碼
能夠看出下面的效果是有問題的,表格的最後一列和最後一行不該該出現邊邊。
既然知道表格的最後一列和最後一行不該該出現邊邊,那就讓最後一列和最後一行的邊邊消失就行了。有如下幾個思路。
這裏我選用第二種方式。這裏要說明一下,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;
}複製代碼
代碼理解上並不難,這裏不作多餘的解釋。
上面的兩個例子僅僅只是畫線,下面的這個例子就來畫字吧。先看下效果。
說到底也就是在item左邊偏移出來的空間區域中心畫個字母而已。下面是大致思路:
*該工具類須要用到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();
}
}複製代碼
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;
}
}複製代碼