说到字母索引栏,在联系人模块经常会见到。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;}
- 配置测量
@Overrideprotected 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;}
- 获取到宽高时,计算每个字母的高度
@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mWidth = w; //每行的高度,计算平均分每个字母占的高度 mCellHeight = h / mLetter.length;}
- 绘制文字和背景,触摸和松手时改变变量来达到绘制不同的颜色
@Overrideprotected 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); }}
- 处理触摸和松手判断,并进行回调
@Overridepublic 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()); }}