说到字母索引栏,在联系人??榫;峒?。iOS中这种控件是自带的,安卓中是没有的,所以只能自定义解决了。
需求分析
- 拖动索引栏时。选中状态
- 字母栏背景加深
- 文字颜色变白色
- 悬浮字母提示显示
- 松手时,非选中状态
- 字母栏背景透明
- 文字颜色变黑色
- 悬浮字母提示隐藏
需要自定义的属性
- 选中时字体颜色
- 未选中时字体颜色
- 选中时,索引栏背景颜色
- 非选中时,索引栏背景颜色
提供的回调
选中和未选中时,回调位置和字母文字。
索引条定义步骤(注释上已经写得很清楚,就不再一步步赘述了)
- 自定义需要的属性
<declare-styleable name="SlideBar">
<!-- 选中时,文字颜色 -->
<attr name="slb_select_txt_color" format="color|reference" />
<!-- 非选中时,文字颜色 -->
<attr name="slb_un_select_txt_color" format="color|reference" />
<!-- 选中时,滑动条背景颜色 -->
<attr name="slb_select_bg_color" format="color|reference" />
<!-- 非选中时,滑动条背景颜色 -->
<attr name="slb_un_select_bg_color" format="color|reference" />
</declare-styleable>
- 自定义View,继承View,获取设置的自定义属性,配置画笔等
public class SlideBar extends View {
/**
* 默认选中时,文字颜色
*/
private final int mDefaultSelectTextColor = Color.parseColor("#FFFFFF");
/**
* 默认未选中时,文字颜色
*/
private final int mDefaultUnSelectTextColor = Color.parseColor("#202020");
/**
* 默认选中时,滑动条背景颜色
*/
private final int mDefaultSelectBgColor = Color.parseColor("#66202020");
/**
* 默认未选中时,滑动条背景颜色
*/
private final int mDefaultUnSelectBgColor = Color.parseColor("#00000000");
/**
* 字母表
*/
private String[] mLetter = 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"};
/**
* 是否拖动索引条中
*/
private boolean mTouched = false;
/**
* 画笔
*/
private Paint mPaint;
private int mSelectTextColor;
private int mUnSelectTextColor;
private int mSelectBgColor;
private int mUnSelectBgColor;
public SlideBar(Context context) {
super(context);
init(context, null);
}
public SlideBar(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public SlideBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, @Nullable AttributeSet attrs) {
initAttrs(context, attrs);
initPaint();
}
private void initAttrs(Context context, @Nullable AttributeSet attrs) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.SlideBar);
//选中时文字的颜色
mSelectTextColor = array.getColor(R.styleable.SlideBar_slb_select_txt_color, mDefaultSelectTextColor);
//未选中时文字的颜色
mUnSelectTextColor = array.getColor(R.styleable.SlideBar_slb_un_select_txt_color, mDefaultUnSelectTextColor);
//选中时,滑动条背景颜色
mSelectBgColor = array.getColor(R.styleable.SlideBar_slb_select_bg_color, mDefaultSelectBgColor);
//未选中时,滑动条背景颜色
mUnSelectBgColor = array.getColor(R.styleable.SlideBar_slb_un_select_bg_color, mDefaultUnSelectBgColor);
array.recycle();
}
private void initPaint() {
mPaint = new Paint();
mPaint.setTextSize(sp2px(getContext(), 11f));
if (mTouched) {
mPaint.setColor(mSelectTextColor);
} else {
mPaint.setColor(mUnSelectTextColor);
}
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
mTextRect = new Rect();
}
}
- 定义回调接口
public interface OnSelectItemListener {
/**
* 选择时回调
*
* @param position 选中的位置
* @param selectLetter 选中的字母
*/
void onItemSelect(int position, String selectLetter);
/**
* 松手取消选中时回调
*/
void onItemUnSelect();
}
public void setOnSelectItemListener(OnSelectItemListener listener) {
mListener = listener;
}
- 配置测量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(measureWidth(widthMeasureSpec), heightMeasureSpec);
}
/**
* 测量本身的大小,这里只是测量宽度
*
* @param widthMeaSpec 传入父View的测量标准
* @return 测量的宽度
*/
private int measureWidth(int widthMeaSpec) {
/*定义view的宽度*/
int width;
/*获取当前 View的测量模式*/
int mode = MeasureSpec.getMode(widthMeaSpec);
/*
* 获取当前View的测量值,这里得到的只是初步的值,
* 我们还需根据测量模式来确定我们期望的大小
* */
int size = MeasureSpec.getSize(widthMeaSpec);
/*
* 如果,模式为精确模式
* 当前View的宽度,就是我们的size
* */
if (mode == MeasureSpec.EXACTLY) {
width = size;
} else {
/*否则的话我们就需要结合padding的值来确定*/
int desire = size + getPaddingLeft() + getPaddingRight();
if (mode == MeasureSpec.AT_MOST) {
width = Math.min(desire, size);
} else {
width = desire;
}
}
return width;
}
- 获取到宽高时,计算每个字母的高度
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w;
//每行的高度,计算平均分每个字母占的高度
mCellHeight = h / mLetter.length;
}
- 绘制文字和背景,触摸和松手时改变变量来达到绘制不同的颜色
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//触摸时改变背景颜色
if (mTouched) {
canvas.drawColor(mSelectBgColor);
mPaint.setColor(mSelectTextColor);
} else {
canvas.drawColor(mUnSelectBgColor);
mPaint.setColor(mUnSelectTextColor);
}
for (int i = 0; i < mLetter.length; i++) {
String text = mLetter[i];
//测量文字宽高
mPaint.getTextBounds(text, 0, text.length(), mTextRect);
int textWidth = mTextRect.width();
int textHeight = mTextRect.height();
//文字一半的宽度
float textHalfWidth = textWidth / 2.0f;
//字母文字的起点X坐标,控件的宽度的一半再减去文字的一半
float x = (mWidth / 2.0f) - textHalfWidth;
//起点文字的Y坐标
float y = (mCellHeight / 2.0f + textHeight / 2.0f + mCellHeight * i);
//画文字
canvas.drawText(mLetter[i], x, y, mPaint);
}
}
- 处理触摸和松手判断,并进行回调
@Override
public boolean onTouchEvent(MotionEvent event) {
float y = event.getY();
//计算当前触摸的字母的位置
int index = (int) (y / mCellHeight);
//触摸
if (event.getAction() == MotionEvent.ACTION_DOWN || event.getAction() == MotionEvent.ACTION_MOVE) {
mTouched = true;
if (index >= 0 && index < mLetter.length) {
if (mListener != null) {
mListener.onItemSelect(index, mLetter[index]);
}
} else {
return super.onTouchEvent(event);
}
} else {
//松手
mTouched = false;
if (mListener != null) {
mListener.onItemUnSelect();
}
}
//触摸改变时,不断通知重绘来绘制索引条
invalidate();
return true;
}
界面布局
- RecyclerView列表
- 索引栏
- 字母提示(我们直接用个TextView,设置背景,显示隐藏即可)
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<android.support.v7.widget.RecyclerView
android:id="@+id/refresh_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<me.zh.indexslidebar.SlideBar
android:id="@+id/slide_bar"
android:layout_width="24dp"
android:layout_height="match_parent"
android:layout_gravity="end"
android:visibility="visible"
app:slb_select_bg_color="@android:color/darker_gray"
app:slb_select_txt_color="@android:color/white"
app:slb_un_select_bg_color="@android:color/transparent"
app:slb_un_select_txt_color="@color/colorPrimary" />
<TextView
android:id="@+id/check_letter"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_gravity="center"
android:background="@drawable/bg_contact_letter_flow"
android:gravity="center"
android:textColor="#FFFFFF"
android:textSize="40sp"
android:visibility="gone"
tools:text="A"
tools:visibility="visible" />
</FrameLayout>
具体使用
- 提供一组联系人名字
/**
* 联系人数组
*/
public static String[] mNames = new String[]{"宋江", "卢俊义", "吴用",
"公孙胜", "关胜", "林冲", "秦明", "呼延灼", "花荣", "柴进", "李应", "朱仝", "鲁智深",
"武松", "董平", "张清", "杨志", "徐宁", "索超", "戴宗", "刘唐", "李逵", "史进", "穆弘",
"雷横", "李俊", "阮小二", "张横", "阮小五", "张顺", "阮小七", "杨雄", "石秀", "解珍",
"解宝", "燕青", "朱武", "黄信", "孙立", "宣赞", "郝思文", "韩滔", "彭玘", "单廷珪",
"魏定国", "萧让", "裴宣", "欧鹏", "邓飞", "燕顺", "杨林", "凌振", "蒋敬", "吕方",
"郭 盛", "安道全", "皇甫端", "王英", "扈三娘", "鲍旭", "樊瑞", "孔明", "孔亮", "项充",
"李衮", "金大坚", "马麟", "童威", "童猛", "孟康", "侯健", "陈达", "杨春", "郑天寿",
"陶宗旺", "宋清", "乐和", "龚旺", "丁得孙", "穆春", "曹正", "宋万", "杜迁", "薛永", "施恩",
"周通", "李忠", "杜兴", "汤隆", "邹渊", "邹润", "朱富", "朱贵", "蔡福", "蔡庆", "李立",
"李云", "焦挺", "石勇", "孙新", "顾大嫂", "张青", "孙二娘", "王定六", "郁保四", "白胜",
"时迁", "段景柱", "&张三", "11级李四", "12级小明"};
}
- 字母和字母条的位置映射
/**
* 用于保存联系人首字母在列表的位置
*/
private HashMap<String, Integer> mLetterPositionMap = new HashMap<>();
- 配置好3个View(Rv使用自己熟悉的框架即可,这里使用的是MultiType)
public class MainActivity extends AppCompatActivity {
private RecyclerView vRefreshList;
/**
* 字母侧滑栏
*/
private SlideBar mSlideBar;
/**
* 提示View
*/
private TextView vCheckLetterView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findView();
bindView();
setData();
}
private void findView() {
mSlideBar = findViewById(R.id.slide_bar);
vRefreshList = findViewById(R.id.refresh_list);
vCheckLetterView = findViewById(R.id.check_letter);
}
private void bindView() {
//配置列表
vRefreshList.setLayoutManager(new LinearLayoutManager(this));
mListItems = new Items();
mListAdapter = new MultiTypeAdapter(mListItems);
//联系人字母条目
mListAdapter.register(ContactLetterModel.class, new ContactLetterViewBinder());
//联系人条目
mListAdapter.register(ContactModel.class, new ContactViewBinder());
vRefreshList.setAdapter(mListAdapter);
}
- 配置索引条回调,在选中时,将列表滚动到记录的位置,有些字母在我们的字母表里面可能没有对应的,会找不到,所以需要判空
//配置索引条
mSlideBar.setOnSelectItemListener(new SlideBar.OnSelectItemListener() {
@Override
public void onItemSelect(int position, String selectLetter) {
if (vCheckLetterView.getVisibility() != View.VISIBLE) {
vCheckLetterView.setVisibility(View.VISIBLE);
}
vCheckLetterView.setText(selectLetter);
Integer letterStickyPosition = mLetterPositionMap.get(selectLetter);
//这里可能拿不到,因为并不是所有的字母联系人名字上都有
if (letterStickyPosition != null) {
vRefreshList.scrollToPosition(letterStickyPosition);
}
}
@Override
public void onItemUnSelect() {
vCheckLetterView.setVisibility(View.GONE);
}
});
- 根据姓名表,构造联系人列表和记录字母条位置条目,这里需要将姓名的第一个子的首字母,用到了一个拼音库。
implementation 'com.github.promeg:tinypinyin:2.0.3'
先来将姓名表按姓名的第一个字的英文字母来排序,这样是为了后面遍历判断字母,对同一组字母的条目的第一个条目插入一个字母条目。
List<String> nameList = Arrays.asList(mNames);
Collections.sort(nameList, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
char letter = Character.toUpperCase(Pinyin.toPinyin(o1.charAt(0)).charAt(0));
char nextLetter = Character.toUpperCase(Pinyin.toPinyin(o2.charAt(0)).charAt(0));
if (letter == nextLetter) {
return 0;
} else {
return letter - nextLetter;
}
}
});
- 遍历姓名列表,构造条目模型。怎么让多个同英文字母的姓名分组前插入一个字母条目呢?很简单,每次遍历时判断是不是和上一个字母一致,不一致才添加一个。记录字母条目的位置,其实就是添加字母条目时,获取自己在列表数据集的位置,因为每次都添加到尾部,所以直接取列表数据集的最后一位的位置即可。
//字母
char letter = 0;
for (int i = 0; i < nameList.size(); i++) {
if (i == 0) {
//获取文字第一个字母
String letterPinyin = Pinyin.toPinyin(nameList.get(i).charAt(0));
letter = Character.toUpperCase(letterPinyin.charAt(0));
mListItems.add(new ContactLetterModel(String.valueOf(letter)));
} else {
//如果下一个条目的首字母和上一个的不一样,则插入一条新的字母条目
String letterPinyin = Pinyin.toPinyin(nameList.get(i).charAt(0));
char nextLetter = Character.toUpperCase(letterPinyin.charAt(0));
if (nextLetter != letter) {
letter = nextLetter;
mListItems.add(new ContactLetterModel(String.valueOf(letter)));
//记录字母条目的位置,后续拉动字母选择条时跳转位置
mLetterPositionMap.put(String.valueOf(letter).toUpperCase(), mListItems.size() - 1);
}
}
//添加联系人条目
mListItems.add(new ContactModel(nameList.get(i)));
}
mListAdapter.notifyDataSetChanged();
完整代码
public class SlideBar extends View {
/**
* 默认选中时,文字颜色
*/
private final int mDefaultSelectTextColor = Color.parseColor("#FFFFFF");
/**
* 默认未选中时,文字颜色
*/
private final int mDefaultUnSelectTextColor = Color.parseColor("#202020");
/**
* 默认选中时,滑动条背景颜色
*/
private final int mDefaultSelectBgColor = Color.parseColor("#66202020");
/**
* 默认未选中时,滑动条背景颜色
*/
private final int mDefaultUnSelectBgColor = Color.parseColor("#00000000");
/**
* 索引条宽度
*/
private int mWidth;
/**
* 字母表
*/
private String[] mLetter = 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"};
/**
* 文字区域,保存为成员变量是为了复用
*/
private Rect mTextRect;
/**
* 每个字母的高度
*/
private int mCellHeight;
/**
* 是否拖动索引条中
*/
private boolean mTouched = false;
/**
* 选择回调
*/
private OnSelectItemListener mListener;
/**
* 画笔
*/
private Paint mPaint;
private int mSelectTextColor;
private int mUnSelectTextColor;
private int mSelectBgColor;
private int mUnSelectBgColor;
public SlideBar(Context context) {
super(context);
init(context, null);
}
public SlideBar(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public SlideBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, @Nullable AttributeSet attrs) {
initAttrs(context, attrs);
initPaint();
}
private void initAttrs(Context context, @Nullable AttributeSet attrs) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.SlideBar);
//选中时文字的颜色
mSelectTextColor = array.getColor(R.styleable.SlideBar_slb_select_txt_color, mDefaultSelectTextColor);
//未选中时文字的颜色
mUnSelectTextColor = array.getColor(R.styleable.SlideBar_slb_un_select_txt_color, mDefaultUnSelectTextColor);
//选中时,滑动条背景颜色
mSelectBgColor = array.getColor(R.styleable.SlideBar_slb_select_bg_color, mDefaultSelectBgColor);
//未选中时,滑动条背景颜色
mUnSelectBgColor = array.getColor(R.styleable.SlideBar_slb_un_select_bg_color, mDefaultUnSelectBgColor);
array.recycle();
}
private void initPaint() {
mPaint = new Paint();
mPaint.setTextSize(sp2px(getContext(), 11f));
if (mTouched) {
mPaint.setColor(mSelectTextColor);
} else {
mPaint.setColor(mUnSelectTextColor);
}
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
mTextRect = new Rect();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w;
//每行的高度,计算平均分每个字母占的高度
mCellHeight = h / mLetter.length;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(measureWidth(widthMeasureSpec), heightMeasureSpec);
}
/**
* 测量本身的大小,这里只是测量宽度
*
* @param widthMeaSpec 传入父View的测量标准
* @return 测量的宽度
*/
private int measureWidth(int widthMeaSpec) {
/*定义view的宽度*/
int width;
/*获取当前 View的测量模式*/
int mode = MeasureSpec.getMode(widthMeaSpec);
/*
* 获取当前View的测量值,这里得到的只是初步的值,
* 我们还需根据测量模式来确定我们期望的大小
* */
int size = MeasureSpec.getSize(widthMeaSpec);
/*
* 如果,模式为精确模式
* 当前View的宽度,就是我们的size
* */
if (mode == MeasureSpec.EXACTLY) {
width = size;
} else {
/*否则的话我们就需要结合padding的值来确定*/
int desire = size + getPaddingLeft() + getPaddingRight();
if (mode == MeasureSpec.AT_MOST) {
width = Math.min(desire, size);
} else {
width = desire;
}
}
return width;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//触摸时改变背景颜色
if (mTouched) {
canvas.drawColor(mSelectBgColor);
mPaint.setColor(mSelectTextColor);
} else {
canvas.drawColor(mUnSelectBgColor);
mPaint.setColor(mUnSelectTextColor);
}
for (int i = 0; i < mLetter.length; i++) {
String text = mLetter[i];
//测量文字宽高
mPaint.getTextBounds(text, 0, text.length(), mTextRect);
int textWidth = mTextRect.width();
int textHeight = mTextRect.height();
//文字一半的宽度
float textHalfWidth = textWidth / 2.0f;
//字母文字的起点X坐标,控件的宽度的一半再减去文字的一半
float x = (mWidth / 2.0f) - textHalfWidth;
//起点文字的Y坐标
float y = (mCellHeight / 2.0f + textHeight / 2.0f + mCellHeight * i);
//画文字
canvas.drawText(mLetter[i], x, y, mPaint);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float y = event.getY();
//计算当前触摸的字母的位置
int index = (int) (y / mCellHeight);
//触摸
if (event.getAction() == MotionEvent.ACTION_DOWN || event.getAction() == MotionEvent.ACTION_MOVE) {
mTouched = true;
if (index >= 0 && index < mLetter.length) {
if (mListener != null) {
mListener.onItemSelect(index, mLetter[index]);
}
} else {
return super.onTouchEvent(event);
}
} else {
//松手
mTouched = false;
if (mListener != null) {
mListener.onItemUnSelect();
}
}
//触摸改变时,不断通知重绘来绘制索引条
invalidate();
return true;
}
public interface OnSelectItemListener {
/**
* 选择时回调
*
* @param position 选中的位置
* @param selectLetter 选中的字母
*/
void onItemSelect(int position, String selectLetter);
/**
* 松手取消选中时回调
*/
void onItemUnSelect();
}
public void setOnSelectItemListener(OnSelectItemListener listener) {
mListener = listener;
}
public static int dip2px(Context context, float dipValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dipValue * scale + 0.5f);
}
public static int px2dp(Context context, float pxValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (pxValue / scale + 0.5f);
}
private int sp2px(Context context, float spVal) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
spVal, context.getResources().getDisplayMetrics());
}
}