博主声明:

转载请在开头附加本文链接及作者信息,并标记为转载。本文由博主 威威喵 原创,请多支持与指教。

本文首发于此   博主:威威喵  |  博客主页:https://blog.csdn.net/smile_running

我们来实现这样的一个小Demo,联系人的快速检索功能,左侧带有字母快速定位,滑动列表时字母相继切换的效果。那么我们来看看实现的Demo,显而易见,这是使用列表控件,而我使用的是ListView控件。至于ListView的一些用法,您可以看这博主的这篇文章ListView使用技巧、优化和用法拓展,掌握ListView,这篇文章我们就不说明它的使用姿势了

  • 源码下载:
  • CSDN资源链接:联系人列表(右侧带首字母检索、分类和快速定位)
  • GitHub下载地址

效果图

想来想去,我就搞了个王者荣耀的英雄名称来作为我们的联系人。因为模拟器上也没有联系人,也懒得搞,手机的话这个真实的联系人也就不放出来,这也是个人隐私嘛。搞这么多英雄的名字还挺累的,大家将就着看,其实原理都一样。所以,这不是我们的关键,重要的是如何实现这种效果呢?

  • 思路及实现

一、左侧字母检索器

左边是一个自定义View,通过获得屏幕的高度,将26个字母逐一从上往下绘制。通过点击事件绘制不同的颜色,再通过监听右边列表的滑动检索字母选择器中的相对应字母下标,然后绘制改变字母的颜色。

二、列表显示字母分类

       列表添加数据,主要说一下头部的字母分类实现。通过获取汉字的原拼音,再检索出首字母。在适配器中判断当前字母是否与前一个字母相同。相同则归类,不相同则分类。

三、中间的字母提示

        这个应该是最简单的了,在我们的自定义View里通过接口回调获取点击或滑动切换时所显示的字母,然后在设置到一个TextView上,通过定时器将它延迟隐藏。

大致思路就是这样了,其实自己使劲的想一想,还是可以实现出这样的效果的,也不是特别难。思路讲完了,那么我们就上代码吧,由于代码比较多,所以这里只贴比较核心的地方来看看,若有需要请到我的Github上查看完整的项目。

一、左侧字母检索器 

/*** @Created by xww.* @Creation time 2018/8/18.*/public class ContactIndexView extends View {private int mRealWidth;private int mRealHeight;private int mWidth;private int mHeight;private int mEachHeight;private int mTouchIndex = 0;private Paint mPaint;private Rect mRect;private onShowLetterListener onShowLetterListener = null;private int colorBackground;private int colorNormal;private int colorChecked;public interface onShowLetterListener {void showLatter(String letter);}public void setOnShowLetter(onShowLetterListener showLetterListener) {this.onShowLetterListener = showLetterListener;}private String[] letters = 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"};@TargetApi(Build.VERSION_CODES.M)private void init() {EventBus.getDefault().register(this);colorBackground = getContext().getResources().getColor(R.color.colorDivider, null);colorNormal = getContext().getResources().getColor(R.color.colorWhite, null);colorChecked = getContext().getResources().getColor(R.color.colorPrimaryDark, null);mRect = new Rect();mPaint = new Paint();mPaint.setStrokeWidth(3f);mPaint.setAntiAlias(true);mPaint.setTextSize(38f);}public ContactIndexView(Context context, @Nullable AttributeSet attrs) {super(context, attrs);init();}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);final int widthMode = MeasureSpec.getMode(widthMeasureSpec);final int widthSize = MeasureSpec.getSize(widthMeasureSpec);final int heightMode = MeasureSpec.getMode(heightMeasureSpec);final int heightSize = MeasureSpec.getSize(heightMeasureSpec);switch (widthMode) {case MeasureSpec.UNSPECIFIED:case MeasureSpec.AT_MOST:break;case MeasureSpec.EXACTLY:mRealWidth = widthSize;break;}switch (heightMode) {case MeasureSpec.UNSPECIFIED:case MeasureSpec.AT_MOST:break;case MeasureSpec.EXACTLY:mRealHeight = heightSize;break;}setMeasuredDimension(mRealWidth, mRealHeight);}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);canvas.drawColor(colorBackground);mWidth = canvas.getWidth();mHeight = canvas.getHeight() - getStatusbarHeight();mEachHeight = mHeight / letters.length;for (int i = 0; i < letters.length; i++) {final String _latter = letters[i];mPaint.getTextBounds(_latter, 0, 1, mRect);final int letterWidth = mRect.width();final int letterHeight = mRect.height();if (mTouchIndex == i) {mPaint.setColor(colorChecked);} else {mPaint.setColor(colorNormal);}canvas.drawText(_latter, mWidth / 2 - letterWidth / 2, (i + 1) * mEachHeight - letterHeight / 2, mPaint);}}@SuppressLint("ClickableViewAccessibility")@Overridepublic boolean onTouchEvent(MotionEvent event) {final int y = (int) event.getY();switch (event.getAction()) {case MotionEvent.ACTION_DOWN:refreshLetterIndex(y);break;case MotionEvent.ACTION_MOVE:refreshLetterIndex(y);break;}return true;}@Subscribepublic void onListScrollEvent(ScrollEvent event) {if (event.isLast()) {return;}for (int i = 0; i < letters.length; i++) {if (event.getLetter().equals(letters[i])) {mTouchIndex = i;invalidate();return;}}}private void refreshLetterIndex(int y) {//y坐标 / 每个字母高度 = 当前字母下标int index = y / mEachHeight;if (index != mTouchIndex) {mTouchIndex = index;//回调选中的字母if (onShowLetterListener != null) {onShowLetterListener.showLatter(letters[mTouchIndex]);}invalidate();}}private int getStatusbarHeight() {int resId = getContext().getResources().getIdentifier("status_bar_height", "dimen", "android");return resId > 0 ? getContext().getResources().getDimensionPixelSize(resId) : 0;}
}

代码挺长的,最重要的是计算字母所在的x,y坐标,坐标计算准了,才可以绘制到控件的中心去,否则你会发现字母将会偏右或偏左等,显示效果比较难看。

            final String _latter = letters[i];mPaint.getTextBounds(_latter, 0, 1, mRect);final int letterWidth = mRect.width();final int letterHeight = mRect.height();

这段代码片段,它主要是给文本绘制了一个边框,然后我们从边框的宽、高间接得到文本的宽和高。

二、列表显示字母分类

/*** @Created by xww.* @Creation time 2018/8/18.*/public class MainActivity extends AppCompatActivity {private ListView mListView;private ContactIndexView mIndexView;private TextView mShowTextView;private CountDownTimer mCountDownTimer;private ContactAdapter mAdapter;private ArrayList<Contact> mContacts;private String mLetter;private static final String[] NAME = new String[]{"露娜", "李白", "韩信", "太乙真人", "李元芳", "阿珂", "夏侯惇", "关羽", "张飞", "刘备", "貂蝉", "吕布", "王昭君", "武则天","百里守约", "百里玄策", "司马懿", "孙策", "干将莫邪", "裴擒虎", "张良", "诸葛亮", "达摩", "蒙奇", "曹操", "钟馗", "钟无艳","程咬金", "米莱狄", "狄仁杰", "后羿", "大乔", "小乔", "刘邦", "杨玉环", "马可波罗", "狂铁", "苏烈", "赵云", "公孙离", "鬼谷子","成吉思汗", "哪吒", "杨戬", "嬴政", "女娲", "周瑜", "弈星", "扁鹊", "甄姬墨子", "高渐离", "亚瑟", "姜子牙", "宫本武藏","牛魔", "庄周", "蔡文姬", "黄忠", "鲁班七号", "铠", "妲己", "白起", "安其拉", "不知火舞", "芈月", "项羽", "刘禅", "橘右京","兰陵王", "典韦", "元歌", "明世隐", "雅典娜", "娜可露露", "东皇太一", "花木兰", "孙尚香", "孙膑", "虞姬", "孙悟空", "老夫子"};@SuppressWarnings("unchecked")private <T extends View> T $(int id) {View view = findViewById(id);return (T) view;}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);initView();}private void initView() {mListView = $(R.id.list_contact);mIndexView = $(R.id.view_contact);mShowTextView = $(R.id.tv_show_letter);initData();initListener();}private void initData() {mContacts = new ArrayList<>();for (String name : NAME) {mContacts.add(new Contact(name));}Collections.sort(mContacts, (o1, o2) -> o1.getPinyin().compareTo(o2.getPinyin()));mAdapter = new ContactAdapter(mContacts);mListView.setAdapter(mAdapter);}@SuppressWarnings("UnusedAssignment")private void initListener() {mIndexView.setOnShowLetter(letter -> {mShowTextView.setText(letter);mLetter = letter;if (mCountDownTimer == null) {mCountDownTimer = new CountDownTimer(1500, 500) {@Overridepublic void onTick(long millisUntilFinished) {mShowTextView.setVisibility(View.VISIBLE);}@Overridepublic void onFinish() {mShowTextView.setVisibility(View.INVISIBLE);if (mCountDownTimer != null) {mCountDownTimer.cancel();mCountDownTimer = null;}}}.start();} else {mCountDownTimer.start();}for (int i = 0; i < mContacts.size(); i++) {final String letterName = mContacts.get(i).getPinyin().substring(0, 1);if (letterName.equals(mLetter)) {mListView.setSelection(i);return;}}});mListView.setOnScrollListener(new AbsListView.OnScrollListener() {@Overridepublic void onScrollStateChanged(AbsListView view, int scrollState) {}@Overridepublic void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {String letter = mContacts.get(firstVisibleItem).getPinyin().substring(0, 1);boolean isLast = false;if (firstVisibleItem + visibleItemCount == totalItemCount) {isLast = true;}EventBus.getDefault().post(new ScrollEvent(letter, isLast));}});}
}

我们的Activity没什么好说的,就是添加数据和一些监听事件。主要注意列表的滑动监听,我们要判断是否已经滑动到了最底部,如果在列表最底部,我们不再处理滑动监听事件。其实,我之前用的是自己随机组的一些名字,发现有点影响美观,所以就换了。

三、列表适配器的逻辑代码

/*** @Created by xww.* @Creation time 2018/8/18.*/public class ContactAdapter extends BaseAdapter {private ArrayList<Contact> mContacts;ContactAdapter(ArrayList<Contact> contacts) {this.mContacts = contacts;}@Overridepublic int getCount() {return mContacts == null ? 0 : mContacts.size();}@Overridepublic Object getItem(int position) {return mContacts.get(position);}@Overridepublic long getItemId(int position) {return position;}@Overridepublic View getView(int position, View convertView, ViewGroup parent) {ViewHolder holder;if (convertView == null) {convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.adapter_contact, parent, false);holder = new ViewHolder(convertView);convertView.setTag(holder);} else {holder = (ViewHolder) convertView.getTag();}final String name = mContacts.get(position).getName();final String pinyin = mContacts.get(position).getPinyin().substring(0, 1);if (position == 0) {holder.tvPinYin.setVisibility(View.VISIBLE);} else {final String prePinyin = mContacts.get(position - 1).getPinyin().substring(0, 1);if (pinyin.equals(prePinyin)) {holder.tvPinYin.setVisibility(View.GONE);} else {holder.tvPinYin.setVisibility(View.VISIBLE);}}holder.tvName.setText(name);holder.tvPinYin.setText(pinyin);return convertView;}private static class ViewHolder {private TextView tvName;private TextView tvPinYin;ViewHolder(View itemView) {tvName = itemView.findViewById(R.id.tv_name);tvPinYin = itemView.findViewById(R.id.tv_pinyin);}}
}

我们看关键代码:

        if (position == 0) {holder.tvPinYin.setVisibility(View.VISIBLE);} else {final String prePinyin = mContacts.get(position - 1).getPinyin().substring(0, 1);if (pinyin.equals(prePinyin)) {holder.tvPinYin.setVisibility(View.GONE);} else {holder.tvPinYin.setVisibility(View.VISIBLE);}}

这段是实现列表头部的字母分类的,我们知道第一个字母永远不会重复,而从第二个字母开始,我们判断是否与前一个字母一样来决定字母的显示与否。

四、汉字转拼音的一个工具类

/*** @Created by xww.* @Creation time 2018/8/18.*/final class PinYinUtils {public static String getPinYin(String hanzi) {StringBuilder pinyin = new StringBuilder();HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();format.setCaseType(HanyuPinyinCaseType.UPPERCASE);format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);char[] arr = hanzi.toCharArray();for (char anArr : arr) {if (Character.isWhitespace(anArr)) continue;if (anArr > 127) {try {String[] pinyinArr = PinyinHelper.toHanyuPinyinStringArray(anArr, format);if (pinyinArr != null) {pinyin.append(pinyinArr[0]);} else {pinyin.append(anArr);}} catch (BadHanyuPinyinOutputFormatCombination e) {e.printStackTrace();pinyin.append(anArr);}} else {pinyin.append(anArr);}}return pinyin.toString();}
}

这个工具栏比较固定 ,那么所有的关键是的代码基本就这些了。这是我个人写的一个联系人的Demo,个人认为界面还不错,哈哈。

自定义 View 之联系人字母索引及定位效果相关推荐

  1. 28自定义View 模仿联系人字母侧栏

    自定义View LetterView.java package com.qf.sxy.customview02;import android.content.Context; import andro ...

  2. 浅谈android中手机联系人字母索引表的实现

    实际上字母索引表的效果,可以说在现在的众多APP中使用的非常流行,比如支付宝,微信中的联系人,还有购物,买票的APP中选择全国城市,切换城市的时候,这时候的城市也就是按照一个字母索引的顺序来显示,看起 ...

  3. Android自定义View——实现联系人列表字母索引

    相信大家对这个列表字母索引已经不陌生了,在很多app中也随处可见,像没团的城市地址选择,微信联系人列表,手机通讯录-等等.既然是个这么nb这么实用的功能我们怎么能不Get到来呢,下面就让我们一起造一个 ...

  4. Android自定义View实现仿QQ实现运动步数效果

    效果图: 1.attrs.xml中 <declare-styleable name="QQStepView"><attr name="outerColo ...

  5. Android自定义View之仿QQ运动步数进度效果

    文章目录 前言 先看效果图 ![在这里插入图片描述](https://img-blog.csdnimg.cn/6e4ddec17933496ea4830fa08d8ffbe5.png?x-oss-pr ...

  6. 自定义View之仿QQ运动步数进度效果

    前言 今天接着上一篇来写关于自定义View方面的东西,我是近期在学习整理这方面的知识点,所以把相关的笔记都放到这个Android自定义View的专栏里了,方便自己下次忘记的时候能回来翻翻,今天的内容是 ...

  7. android 自定义view实现仿QQ运动步数进度效果

    最近公司在策划一个新的项目,原型还没出来,再说这公司人都要走没了,估计又要找工作了,所以必须要学习,争取每个写个关于自定义view方面的,这样几个月积累下来,也能学习到东西,今天就带来简单的效果,就是 ...

  8. <android>音乐频谱显示效果 音乐播放动画 自定义view Visualizer 对接MediaPlayer 声音频率 动画效果

    最近写了一个音乐频谱显示效果的自定义view,通过Visualizer 函数对接了MediaPlayer的声源byte数据的回调,全部封装到了view的里面,外部只需要设置一个MediaPlayer即 ...

  9. Android自定义View分享——仿网易云音乐留声机效果

    写在前面 这是笔者自学习自定义View以来,分享的第五篇效果,之前分享过一篇动态时钟效果的自定义View,如果有兴趣的可以看看: Android自定义View分享--一个时钟 之前的博客笔者一般都会说 ...

最新文章

  1. android SDK manager 无法获取更新版本列表
  2. ubuntu 系统中如何截图
  3. boost::phoenix模块使用 istreambuf_iterator 测试 lambda 函数对象
  4. Ajax请求session超时处理流程(DWZ)
  5. java 张量运算,博客 | Tensorflow_01_运算符与张量值
  6. (转) 基于MapReduce的ItemBase推荐算法的共现矩阵实现(一)
  7. 1小时教你学会正则表达式
  8. scrapy setting
  9. 【读书笔记】周志华《机器学习》第三版课后习题讨第一章-绪论
  10. 特别看好高校团队的联想创投,在中科大拉开高校AI精英挑战赛大幕
  11. Java学习关于时间操作的应用类--Date类、Calendar类及其子类
  12. java素数(质数)计算
  13. DOA算法1:MUSIC算法(二)
  14. 如何Diskgenius将U盘分区
  15. 借助YunOS ,开发技术、运营能力大幅提升
  16. 学习笔记(一)---Docker概述
  17. cadence如何导入gds_Tanner LEdit系列 | 导入GDSII文件
  18. 计算机科学计算器CE符号,计算器的ce和c是什么意思???
  19. python break函数用法_Python break用法详解
  20. 牛客网 Wannafly挑战赛20 A-染色

热门文章

  1. 官网显示500内部服务器有错误代码,【500错误】http 500 - 内部服务器错误(错误代码500)解决方法...
  2. 笔记本电脑出现“正在锁定”,然后就自动关机的解决方案
  3. MySQL多表查询之纵向合并
  4. 百度云盘资源搜索神器仅有400kb
  5. android recyclerView Binary XML file line #7: Error inflating class android.support.v7.widget.Recycl
  6. Spring Boot 参考文档
  7. Jan. 1, 2020 at 7:47 a.m. GMT+8遇到这种时间,转换成中国标准时间
  8. 中国有多少个省,多少个直辖市,多少个特别行政区,多少个自治区
  9. GPS Tools For Android
  10. 【网站架构】一招搞定90%的分布式事务,实打实介绍数据库事务、分布式事务的工作原理应用场景