快速索引是众多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的实现效果:
具体实现,请查看源码,源码地址