RecyclerView自定義目錄快速索引
阿新 • • 發佈:2018-12-25
快速索引是眾多app中常用的功能,在及時通訊、使用者列表等功能中能夠快速定位,在此將我專案中使用到的索引抽取出來交流分享,效果如下:
一、繪製IndexBar
先自定義IndexBar繼承View;
public class IndexBar extends View { public IndexBar(Context context) { super(context); } public IndexBar(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public IndexBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } }
定義所需的屬性:
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="IndexrecyclerviewIndexBar"> <!--index文字大小--> <attr name="indexTextSize" format="dimension"></attr> <!--index按下的顏色--> <attr name="pressBackground" format="color|reference"></attr> <!--index按下時文字的顏色--> <attr name="pressTextColor" format="color|reference"></attr> <!--index文字的顏色--> <attr name="indexTextColor" format="color|reference"></attr> <!--index文字選中的顏色--> <attr name="selectTextColor" format="color|reference"></attr> </declare-styleable> </resources>
修改完善IndexBar;
public class IndexBar extends View { private Context mContext; /** * 預設索引 */ private static final String[] DEFAULT_INDEX = new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "#"}; /** * 每個index的高度 */ private int indexHeght; /** * view寬度 */ private int mWidth; /** * view高度 */ private int mHeight; /** * 畫筆 */ private Paint mPaint; /** * 按下時的背景顏色 */ private int mPressBackground; /** * 文字顏色 */ private int mTextColor; /** * 按下時文字的顏色 */ private int mPressTextColor; /** * 文字選中的顏色 */ private int mSelectTextColor; /** * 字型大小 */ private int textSize; private int DEFAULT_PRESS_COLOR = Color.GRAY; private int DEFAULT_BACKGROUND = Color.TRANSPARENT; List<String> indexDatas; public IndexBar(Context context) { super(context); init(context, null, -1); } public IndexBar(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(context, attrs, -1); } public IndexBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs, defStyleAttr); } private void init(Context context, AttributeSet attrs, int defStyleAttr) { this.mContext = context; //預設的TextSize int DEFAULT_SIZE = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()); TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.IndexrecyclerviewIndexBar); if (typedArray != null) { textSize = typedArray.getDimensionPixelSize(R.styleable.IndexrecyclerviewIndexBar_indexTextSize, DEFAULT_SIZE); mPressBackground = typedArray.getColor(R.styleable.IndexrecyclerviewIndexBar_pressBackground, DEFAULT_BACKGROUND); mTextColor = typedArray.getColor(R.styleable.IndexrecyclerviewIndexBar_indexTextColor, DEFAULT_PRESS_COLOR); mPressTextColor = typedArray.getColor(R.styleable.IndexrecyclerviewIndexBar_pressTextColor, mTextColor); mSelectTextColor = typedArray.getColor(R.styleable.IndexrecyclerviewIndexBar_selectTextColor, mTextColor); } initPaint(); initDatas(); } private void initDatas() { indexDatas = Arrays.asList(DEFAULT_INDEX); } private void initPaint() { mPaint = new Paint(); mPaint.setTextSize(textSize); mPaint.setAntiAlias(true); } boolean isPress; @Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { isPress = true; Drawable background = getBackground(); if (background != null) { color = ((ColorDrawable) background).getColor(); } setBackgroundColor(mPressBackground); computePressIndexLocation(event.getX(), event.getY()); } else if (event.getAction() == MotionEvent.ACTION_MOVE) { computePressIndexLocation(event.getX(), event.getY()); } else { isPress = false; //手指擡起時背景恢復透明 setBackgroundColor(color); //重置當前位置 currentIndex = -1; if (mOnIndexPressListener != null) { mOnIndexPressListener.onMotionEventEnd(); } } return true; } private int currentIndex = -1; /** * 計算按下的位置 */ private void computePressIndexLocation(float x, float y) { // 計算按下的區域位置 currentIndex = (int) ((y - getPaddingTop()) / indexHeght); if (currentIndex < 0) { currentIndex = 0; } else if (currentIndex >= indexDatas.size()) { currentIndex = indexDatas.size() - 1; } invalidateMySelft(); if (mOnIndexPressListener != null) { mOnIndexPressListener.onIndexChange(currentIndex, indexDatas.get(currentIndex)); } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mWidth = w; mHeight = h; computeIndexHeight(); } /** * 計算單個index高度 */ private void computeIndexHeight() { indexHeght = (mHeight - getPaddingTop() - getPaddingBottom()) / indexDatas.size(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); for (int i = 0; i < indexDatas.size(); i++) { String index = indexDatas.get(i); Paint.FontMetrics metrics = mPaint.getFontMetrics(); //計算baseline int baseLine = (int) ((indexHeght - metrics.bottom - metrics.top) / 2); if (currentIndex == i) { mPaint.setColor(mSelectTextColor); } else { mPaint.setColor(isPress ? mPressTextColor : mTextColor); } //繪製文字 canvas.drawText(index, mWidth / 2 - mPaint.measureText(index) / 2, getPaddingTop() + baseLine + indexHeght * i, mPaint); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //取出寬高的MeasureSpec Mode 和Size int wMode = MeasureSpec.getMode(widthMeasureSpec); int wSize = MeasureSpec.getSize(widthMeasureSpec); int hMode = MeasureSpec.getMode(heightMeasureSpec); int hSize = MeasureSpec.getSize(heightMeasureSpec); //最終測量出來的寬高 int measureWidth = 0, measureHeight = 0; //得到合適寬度: //存放每個繪製的index的Rect區域 Rect indexBounds = new Rect(); String index;//每個要繪製的index內容 for (int i = 0; i < indexDatas.size(); i++) { index = indexDatas.get(i); //測量計算文字所在矩形,可以得到寬高 mPaint.getTextBounds(index, 0, index.length(), indexBounds); //迴圈結束後,得到index的最大寬度 measureWidth = Math.max(indexBounds.width() + getPaddingLeft() + getPaddingRight(), measureWidth); //迴圈結束後,得到index的最大高度,然後*size measureHeight = Math.max(indexBounds.height(), measureHeight); } measureHeight *= indexDatas.size(); if (wMode == MeasureSpec.EXACTLY) { measureWidth = wSize; } else if (wMode == MeasureSpec.AT_MOST) { //wSize此時是父控制元件能給子View分配的最大空間 measureWidth = Math.min(measureWidth, wSize); } else if (wMode == MeasureSpec.UNSPECIFIED) { } //得到合適的高度: if (hMode == MeasureSpec.EXACTLY) { measureHeight = hSize; } else if (hMode == MeasureSpec.AT_MOST) { //wSize此時是父控制元件能給子View分配的最大空間 measureHeight = Math.min(measureHeight, hSize); } else if (hMode == MeasureSpec.UNSPECIFIED) { } setMeasuredDimension(measureWidth, measureHeight); } OnIndexPressListener mOnIndexPressListener; public void setOnIndexPressListener(OnIndexPressListener mOnIndexPressListener) { this.mOnIndexPressListener = mOnIndexPressListener; } public interface OnIndexPressListener { /** * @param index 當前選中的位置 * @param text 選中的文字 */ void onIndexChange(int index, String text); /** * 事件結束時回撥 */ void onMotionEventEnd(); } }
特別說明,由於手指按下的時候改變了IndexBar的背景,擡起時需要恢復背景顏色,因此需要將IndexBar的初始背景顏色做臨時儲存;試下效果:
現在indexbar只能使用固定的字母索引,在新增上使用源資料內容作為索引,程式碼如下:
/**
* 原始資料
*/
List<? extends BaseIndexBean> sourceDatas;
/**
* 設定原始資料
*
* @param sourceDatas
*/
public void setSourceDatas(List<? extends BaseIndexBean> sourceDatas) {
this.sourceDatas = sourceDatas;
initIndexDatas();
invalidateMySelft();
}
private void invalidateMySelft() {
if (isMainThread()) {
invalidate();
} else {
postInvalidate();
}
}
public boolean isMainThread() {
return Thread.currentThread() == Looper.getMainLooper().getThread();
}
/**
* 初始原始資料 並提取索引
*/
private void initIndexDatas() {
if (null == sourceDatas || sourceDatas.isEmpty()) {
return;
}
if (mDataHelper == null) {
mDataHelper = new IndexDataHelper();
}
mDataHelper.cover(sourceDatas);
//源資料無序
if (!isOrderly) {
mDataHelper.sortDatas(sourceDatas);
}
if (useDatasIndex) {
indexDatas = new ArrayList<>();
mDataHelper.getIndex(sourceDatas, indexDatas);
computeIndexHeight();
}
}
IndexDataHelper程式碼
public class IndexDataHelper implements IDataHelper {
@Override
public void cover(List<? extends BaseIndexBean> datas) {
if (datas == null || datas.isEmpty()) {
return;
}
for (BaseIndexBean data : datas) {
String pinyinUpper = getUpperPinYin(data.getOrderName());
data.setPinyin(pinyinUpper);
data.setFirstLetter(pinyinUpper.substring(0, 1));
}
}
@Override
public void sortDatas(List<? extends BaseIndexBean> datas) {
if (datas == null || datas.isEmpty()) {
return;
}
cover(datas);
Collections.sort(datas, new Comparator<BaseIndexBean>() {
@Override
public int compare(BaseIndexBean o1, BaseIndexBean o2) {
if ("#".equals(o1.getPinyin())) {
return 1;
} else if ("#".equals(o2.getPinyin())) {
return -1;
} else {
return o1.getPinyin().compareTo(o2.getPinyin());
}
}
});
}
@Override
public void sortDatasAndGetIndex(List<? extends BaseIndexBean> datas, List<String> indexDatas) {
if (datas == null || datas.isEmpty()) {
return;
}
sortDatas(datas);
getIndex(datas, indexDatas);
}
@Override
public void getIndex(List<? extends BaseIndexBean> datas, List<String> indexDatas) {
for (BaseIndexBean data : datas) {
//獲取拼音首字母
String pinyin = data.getIndexTag();
if (!indexDatas.contains(pinyin)) {
//如果是A-Z字母開頭
if (pinyin.matches("[A-Z]")) {
indexDatas.add(pinyin);
} else {//特殊字母這裡統一用#處理
indexDatas.add("#");
}
}
}
}
/**
* 獲取拼音 大寫
*
* @param text
* @return
*/
private String getUpperPinYin(String text) {
return PinyinUtils.ccs2Pinyin(text).toUpperCase();
}
}
public interface IDataHelper {
/**
* 資料轉換 根據getorderName生成pinyin
*
* @param datas
*/
void cover(List<? extends BaseIndexBean> datas);
/**
* 排序
*
* @param datas
*/
void sortDatas(List<? extends BaseIndexBean> datas);
/**
* 排序並獲取索引資料
*
* @param datas
*/
void sortDatasAndGetIndex(List<? extends BaseIndexBean> datas, List<String> indexDatas);
/**
* 獲取索引
*
* @param datas
* @param indexDatas
*/
void getIndex(List<? extends BaseIndexBean> datas, List<String> indexDatas);
}
BaseIndexBean程式碼:
public abstract class BaseIndexBean implements ISupperInterface {
private String firstLetter;
private String pinyin;
public String getPinyin() {
return pinyin;
}
public void setPinyin(String pinyin) {
this.pinyin = pinyin;
}
public String getFirstLetter() {
return firstLetter;
}
public void setFirstLetter(String firstLetter) {
this.firstLetter = firstLetter;
}
@Override
public String getIndexTag() {
return firstLetter;
}
/**
* 需要排序的內容
*
* @return
*/
public abstract String getOrderName();
}
ISupperInterface 程式碼:
public interface ISupperInterface {
/**
* title的顯示內容
*
* @return
*/
String getIndexTag();
}
這裡使用介面方式是為了方便在不同地方能夠通過改變IDataHelper,實現不同的排列方式;
在補上recyclerview懸停分割線LevitationDecoration:
public class LevitationDecoration extends RecyclerView.ItemDecoration {
/**
* 畫筆
*/
private Paint mPaint;
/**
* title背景
*/
private int mTitleColor;
/**
* title文字顏色
*/
private int mTextColor;
/**
* title文字尺寸
*/
private int mTextSize;
/**
* 左邊距
*/
private int mTextLeftPadding;
/**
* title高度
*/
private int mTitleHeight;
/**
* 上下文
*/
Context mContext;
/**
* recyclerview頭部view數量
*/
int mHeadCount;
/**
* 繪製內容
*/
List<? extends ISupperInterface> mDatas;
/**
* 滑動效果
*/
public static final int MODE_TRANSLATE = 1;
/**
* 重疊效果
*/
public static final int MODE_OVERLAP = 2;
@IntDef({MODE_TRANSLATE, MODE_OVERLAP})
@Retention(RetentionPolicy.SOURCE)
public @interface MODE {
}
int mode = MODE_TRANSLATE;
public LevitationDecoration(Context context) {
mContext = context;
mTitleColor = Color.GRAY;
mTextColor = Color.BLACK;
mTextSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 16f, mContext.getResources().getDisplayMetrics());
//預設設定title左邊距為字型大小,避免itemview的paddingleft為0時title緊靠螢幕
mTextLeftPadding = mTextSize;
mTitleHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 30, mContext.getResources().getDisplayMetrics());
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setTextSize(mTextSize);
}
public void setTextLeftPadding(int textLeftPadding) {
this.mTextLeftPadding = textLeftPadding;
}
public void setDatas(List<? extends ISupperInterface> datas) {
this.mDatas = datas;
}
public void setMode(@MODE int mode) {
this.mode = mode;
}
public int getHeadCount() {
return mHeadCount;
}
public void setHeadCount(int headCount) {
this.mHeadCount = headCount;
}
public void setTitleColor(@ColorInt int color) {
this.mTitleColor = color;
}
public void setTitleColorResource(@ColorRes int color) {
this.mTitleColor = mContext.getResources().getColor(color);
}
public void setTextColor(@ColorInt int color) {
this.mTextColor = color;
}
public void setTextColorResource(@ColorRes int color) {
this.mTextColor = mContext.getResources().getColor(color);
}
public void setTextSize(float textSize) {
this.mTextSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
textSize, mContext.getResources().getDisplayMetrics());
}
public void setTitleHeight(float height) {
this.mTitleHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
height, mContext.getResources().getDisplayMetrics());
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
final int left = parent.getPaddingLeft();
final int right = parent.getWidth() - parent.getPaddingRight();
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = parent.getChildAt(i);
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
//獲取child的position
int position = params.getViewLayoutPosition();
if (mDatas == null || mDatas.isEmpty() || position > mDatas.size() - 1 || position < 0) {
return;
}
//計算真實位置
position -= mHeadCount;
if (position > -1) {
if (position == 0) {
drawTitleArea(c, left, right, child, params, position);
} else {
if (null != mDatas.get(position).getIndexTag()
&& !mDatas.get(position).getIndexTag().equals(mDatas.get(position - 1).getIndexTag())) {
drawTitleArea(c, left, right, child, params, position);
}
}
}
}
}
/**
* 繪製title區域
*
* @param c
* @param left
* @param right
* @param child
* @param params
* @param position
*/
private void drawTitleArea(Canvas c, int left, int right, View child, RecyclerView.LayoutParams params, int position) {
mPaint.setColor(mTitleColor);
c.drawRect(left, child.getTop() - params.topMargin - mTitleHeight, right, child.getTop() - params.topMargin, mPaint);
mPaint.setColor(mTextColor);
String text = mDatas.get(position).getIndexTag();
Rect rect = new Rect();
mPaint.getTextBounds(text, 0, text.length(), rect);
c.drawText(text, child.getPaddingLeft() + mTextLeftPadding, child.getTop() - params.topMargin - (mTitleHeight / 2 - rect.height() / 2), mPaint);
}
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
int position = ((LinearLayoutManager) (parent.getLayoutManager())).findFirstVisibleItemPosition();
position -= mHeadCount;
if (mDatas == null || mDatas.isEmpty() || position > mDatas.size() - 1 || position < 0) {
return;
}
View child = parent.findViewHolderForLayoutPosition(position + mHeadCount).itemView;
//定義一個flag,Canvas是否位移過的標誌
String text = mDatas.get(position).getIndexTag();
boolean flag = false;
if ((position + 1) < mDatas.size()) {
String nextText = mDatas.get(position + 1).getIndexTag();
//當前第一個可見的Item的tag,不等於其後一個item的tag,說明懸浮的View要切換了
if (null != text && !text.equals(nextText)) {
//當第一個可見的item在螢幕中還剩的高度小於title區域的高度時,我們也該開始做懸浮Title的“交換動畫”
if (child.getHeight() + child.getTop() < mTitleHeight) {
c.save();//每次繪製前 儲存當前Canvas狀態,
flag = true;
if (mode == MODE_OVERLAP) {
//頭部摺疊起來的視效
//可與123行 c.drawRect 比較,只有bottom引數不一樣,由於 child.getHeight() + child.getTop() < mTitleHeight,所以繪製區域是在不斷的減小,有種摺疊起來的感覺
c.clipRect(parent.getPaddingLeft() + mTextSize, parent.getPaddingTop(), parent.getRight() - parent.getPaddingRight(), parent.getPaddingTop() + child.getHeight() + child.getTop());
} else {
//上滑時,將canvas上移 (y為負數
c.translate(0, child.getHeight() + child.getTop() - mTitleHeight);
}
}
}
}
mPaint.setColor(mTitleColor);
c.drawRect(parent.getPaddingLeft(), parent.getPaddingTop(),
parent.getRight() - parent.getPaddingRight(), parent.getPaddingTop() + mTitleHeight, mPaint);
mPaint.setColor(mTextColor);
Rect rect = new Rect();
mPaint.getTextBounds(text, 0, text.length(), rect);
c.drawText(text, child.getPaddingLeft() + mTextLeftPadding,
parent.getPaddingTop() + mTitleHeight - (mTitleHeight / 2 - rect.height() / 2),
mPaint);
if (flag) {
c.restore();//恢復畫布到之前儲存的狀態
}
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
int position = ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewLayoutPosition();
position -= mHeadCount;
if (mDatas == null || mDatas.isEmpty() || position > mDatas.size() - 1) {
return;
}
if (position > -1) {
if (position == 0) {
outRect.set(0, mTitleHeight, 0, 0);
} else {//其他的通過判斷
String text = mDatas.get(position).getIndexTag();
String lastText = mDatas.get(position - 1).getIndexTag();
if (null != text && !text.equals(lastText)) {
//不為空 且跟前一個tag不一樣了,說明是新的分類,也要title
outRect.set(0, mTitleHeight, 0, 0);
}
}
}
}
}
在看下完整效果:
當然還有Gridlayoutmanager的實現效果:
具體實現,請檢視原始碼,原始碼地址