一、效果图展示
无图不BB,先上图
二、功能与准备
2.1 功能
- 按照拼音顺序对好友进行排序,英文数字符号归为#
- 右侧字母导航条,既可拖动也可点击
- 粘性头布局
- 搜索(全拼+简拼)
2.2 准备
需要导入文字转拼音的库
com.belerweb:pinyin4j:2.5.1'
三、开工
3.1 右侧字母的索引
- 字母的绘画
private static final String[] DEFAULT_INDEX_ITEMS = {"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", "#"};
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
String index;
//y的位置是baseline线的位置 加上mTopMargin
//循环画上所有的字母
for (int i = 0; i < mIndexItems.size(); i++) {
index = mIndexItems.get(i);
Paint.FontMetrics fm = mPaint.getFontMetrics();
canvas.drawText(index,
(mWidth - mPaint.measureText(index)) / 2,
mItemHeight / 2 + (fm.bottom - fm.top) / 2 - fm.bottom + mItemHeight * i + mTopMargin,
i == mCurrentIndex ? mTouchedPaint : mPaint);
}
}
- 字母列表的触摸效果
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
float y = event.getY();
//得到字母个数
int indexSize = mIndexItems.size();
//计算按压的位置
int touchIndex = (int) (y / mItemHeight);
//小于0的话那就默认第一个,大于他的个数就是最后一个
if (touchIndex < 0) {
touchIndex = 0;
} else if (touchIndex >= indexSize) {
touchIndex = indexSize - 1;
}
if (mOnIndexChangedListener != null && touchIndex >= 0 && touchIndex < indexSize) {
if (touchIndex != mCurrentIndex) {
mCurrentIndex = touchIndex;
if (mCenterTextView!=null){
mCenterTextView.setText(mIndexItems.get(touchIndex));
mCenterTextView.setVisibility(VISIBLE);
}
mOnIndexChangedListener.onIndexChanged(mIndexItems.get(touchIndex), touchIndex);
invalidate();
}
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (mCenterTextView!=null){
mCenterTextView.setVisibility(VISIBLE);
}
mCurrentIndex=-1;
invalidate();
break;
}
return true;
}
//显示居中的字母View
public SideIndexBar setCenterTextView(TextView view){
this.mCenterTextView = view;
return this;
}
public SideIndexBar setOnIndexChangedListener(OnIndexTouchedChangedListener listener) {
this.mOnIndexChangedListener = listener;
return this;
}
//改变位置的接口
public interface OnIndexTouchedChangedListener {
void onIndexChanged(String index, int position);
}
3.2、通讯录分组
- 首先先判断是否是同组的第一个
//判断该是否是同组的第一个
private boolean isFirst(int position) {
if (mStrings == null) {
return false;
}
if (mStrings.isEmpty()) {
return false;
}
if (position <= 0) {
return true;
} else {
return !mStrings.get(position).getSection().equals(mStrings.get(position - 1).getSection());
}
}
- 再绘制子Item之间的距离
//getItemOffsets 可以实现类似于padding的效果
//
@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();
//mSectionHeight是粘性头部的高度,是同组的第一个就设置top
if (isFirst(position)) {
outRect.top = mSectionHeight;
} else {
outRect.top = 0;
}
}
- 绘制每组头部的背景和文字
//实现类似绘制背景的效果,内容在上面
//绘制背景和文字
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = parent.getChildAt(i);
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
int position = params.getViewLayoutPosition();
//是第一个就绘制背景以及文字
if (isFirst(position)) {
String name = mStrings.get(position).getSection();
c.drawRect(left, child.getTop() - params.topMargin - mSectionHeight, right, child.getTop() - params.topMargin, mBgPaint);
Paint.FontMetrics fm = mTextPaint.getFontMetrics();
c.drawText(name, child.getPaddingLeft(), (child.getTop() - (mSectionHeight / 2 - (fm.descent - fm.ascent) / 2 + fm.descent) - params.topMargin), mTextPaint);
}
}
}
- 绘制粘性头部,粘性头部就是滑动范围还在该组时,在最上方显示该组头部,其实就是头部覆盖在内容上。
//onDrawOver 绘制在内容的上面,覆盖内容
//这个是实现粘性头部的关键
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
//拿到屏幕上显示的第一个item的位置
int pos = ((LinearLayoutManager) (parent.getLayoutManager())).findFirstVisibleItemPosition();
if (pos < 0) return;
if (mStrings == null || mStrings.isEmpty()) return;
String section =mStrings.get(pos).getSection();
View child = parent.findViewHolderForLayoutPosition(pos).itemView;
boolean flag = false;
//添加一个平移替换效果
if ((pos + 1) < mStrings.size()) {
if (null != section && !section.equals(mStrings.get(pos + 1).getSection())) {
//如果子item的高度+加距离顶部的距离 小于 section的高度,则进行平移
if (child.getHeight() + child.getTop() < mSectionHeight) {
c.save();
flag = true;
c.translate(0, child.getHeight() + child.getTop() - mSectionHeight);
}
}
}
c.drawRect(parent.getPaddingLeft(),
parent.getPaddingTop(),
parent.getRight() - parent.getPaddingRight(),
parent.getPaddingTop() + mSectionHeight, mBgPaint);
Paint.FontMetrics fm = mTextPaint.getFontMetrics();
c.drawText(section,
child.getPaddingLeft(),
parent.getPaddingTop() + mSectionHeight -(mSectionHeight / 2 - (fm.descent - fm.ascent) / 2 + fm.descent),
mTextPaint);
if (flag)
c.restore();
}
3.3 数据整理排序
- 先看Bean类
public class Star implements Comparable {
private String name;
private String pinyin; //拼音
private String jianpin;//简拼
/***
* 获取悬浮栏文本,(#、定位、热门 需要特殊处理)
* @return
*/
public String getSection() {
String s= pinyin;
if (TextUtils.isEmpty(s)) {
return "#";
} else {
String c = s.substring(0, 1);
Pattern p = Pattern.compile("[a-zA-Z]");
Matcher m = p.matcher(c);
if (m.matches()) {
return c.toUpperCase();
} else {
return "#";
}
}
}
//排序 #都往后放
@Override
public int compareTo(@NonNull Star o) {
if (getSection().equals("#")&&!o.getSection().equals("#")){
return 1;
}else if (!getSection().equals("#")&&o.getSection().equals("#")){
return -1;
}else {
return getSection().compareToIgnoreCase(o.getSection());
}
}
//省略get···set方法
}
- 简拼和全拼的获取时通过
//获取全拼
public String getPinYi(String chines) {
sb.setLength(0);
char[] nameChar = chines.toCharArray();
HanyuPinyinOutputFormat defaultFormat = new HanyuPinyinOutputFormat();
defaultFormat.setCaseType(HanyuPinyinCaseType.LOWERCASE);
defaultFormat.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
for (int i = 0; i < nameChar.length; i++) {
if (nameChar[i] > 128) {
try {
sb.append(PinyinHelper.toHanyuPinyinStringArray(nameChar[i], defaultFormat)[0]);
} catch (Exception e) {
e.printStackTrace();
}
} else {
sb.append(nameChar[I]);
}
}
return sb.toString();
}
//获取简拼
public String getPinYinHeadChar(String chines) {
sb.setLength(0);
char[] chars = chines.toCharArray();
HanyuPinyinOutputFormat defaultFormat = new HanyuPinyinOutputFormat();
defaultFormat.setCaseType(HanyuPinyinCaseType.LOWERCASE);
defaultFormat.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
for (int i = 0; i < chars.length; i++) {
if (chars[i] > 128) {
try {
sb.append(PinyinHelper.toHanyuPinyinStringArray(chars[i], defaultFormat)[0].charAt(0));
} catch (Exception e) {
e.printStackTrace();
}
} else {
sb.append(chars[I]);
}
}
return sb.toString();
}
3.4 搜索结果处理
@Override
public void afterTextChanged(Editable s) {
String keyword = s.toString();
if (TextUtils.isEmpty(keyword)) {
mClearAllBtn.setVisibility(View.GONE);
mEmptyView.setVisibility(View.GONE);
mResults = mAllCities;
((SectionDividerDecoration) (mRecyclerView.getItemDecorationAt(0))).setData(mResults);
mAdapter.updateData(mResults);
} else {
mClearAllBtn.setVisibility(View.VISIBLE);
//search是匹配结果,下面显示
mResults = search(keyword, mAllCities);
((SectionDividerDecoration) (mRecyclerView.getItemDecorationAt(0))).setData(mResults);
if (mResults == null || mResults.isEmpty()) {
mEmptyView.setVisibility(View.VISIBLE);
} else {
mEmptyView.setVisibility(View.GONE);
mAdapter.updateData(mResults);
}
}
mRecyclerView.scrollToPosition(0);
}
public List search(String name, List list) {
List results = new ArrayList();
String patten = Pattern.quote(name);
Pattern pattern = Pattern.compile(patten, Pattern.CASE_INSENSITIVE);
for (int i = 0; i < list.size(); i++) {
//根据拼音
Matcher matcherPin = pattern.matcher((list.get(i)).getPinyin());
//根据简拼
Matcher jianPin = pattern.matcher((list.get(i)).getJianpin());
//根据名字
Matcher matcherName = pattern.matcher((list.get(i)).getName());
if (matcherPin.find() || matcherName.find() || jianPin.find()) {
results.add(list.get(i));
}
}
return results;
}
3.5 列表跟随索引移动
在Fragment中实现索引的接口
在实现里写上
/**
* 滚动RecyclerView到索引位置
*
* @param index
*/
public void scrollToSection(String index) {
if (mData == null || mData.isEmpty()) return;
if (TextUtils.isEmpty(index)) return;
int size = mData.size();
for (int i = 0; i < size; i++) {
if (TextUtils.equals(index.substring(0, 1), mData.get(i).getSection().substring(0, 1))) {
if (mLayoutManager != null) {
mLayoutManager.scrollToPositionWithOffset(i, 0);
return;
}
}
}
}