android自定义View之仿通讯录侧边栏滑动,实现A-Z字母检索

我们的手机通讯录一般都有这样的效果,如下图:

OK,这种效果大家都见得多了,基本上所有的Android手机通讯录都有这样的效果。那我们今天就来看看这个效果该怎么实现。

一.概述

1.页面功能分析

整体上来说,左边是一个ListView,右边是一个自定义View,但是左边的ListView和我们平常使用的ListView还有一点点不同,就是在ListView中我对所有的联系人进行了分组,那么这种效果的实现最常见的就是两种思路:

1.使用ExpandableListView来实现这种分组效果

2.使用普通ListView,在构造Adapter时实现SectionIndexer接口,然后在Adapter中做相应的处理

这两种方式都不难,都属于普通控件的使用,那么这里我们使用第二种方式来实现,第一种方式的实现方法大家可以自行研究,如果你还不熟悉ExpandableListView的使用,可以参考我的另外两篇博客:

1.使用ExpandableListView实现一个时光轴

2.android开发之ExpandableListView的使用,实现类似QQ好友列表

OK,这是我们左边ListView的实现思路,右边这个东东就是我们今天的主角,这里我通过自定义一个View来实现,View中的A、B......#这些字符我都通过canvas的drawText方法绘制上去。然后重写onTouchEvent方法来实现事件监听。

2.要实现的效果

要实现的效果如上图所示,但是大家看图片有些地方可能还不太清楚,所以这里我再强调一下:

1.左边的ListView对数据进行分组显示

2.当左边ListView滑动的时候,右边滑动控件中的文字颜色能够跟随左边ListView的滑动自动变化

3.当手指在右边的滑动控件上滑动时,手指滑动到的地方的文字颜色应当发生变化,同时在整个页面的正中央有一个TextView显示手指目前按下的文字

4.当手指按下右边的滑动控件时,右边的滑动控件背景变为灰色,手指松开后,右边的滑动控件又变为透明色

二.左边ListView分组效果的实现

无论多大的工程,我们都要将之分解为一个个细小的功能块分步来实现,那么这里我们就先来看看左边的ListView的分组的实现,这个效果实现之后,我们再来看看右边的滑动控件该怎么实现。

首先我需要在布局文件中添加一个ListView,这个很简单,和普通的ListView一模一样,我就不贴代码了,另外,针对ListView中的数据集,我需要自建一个实体类,该实体类如下:

[java] view plain copy print?

  1. /**
  2. * Created by wangsong on 2016/4/24.
  3. */
  4. public class User {
  5. private int img;
  6. private String username;
  7. private String pinyin;
  8. private String firstLetter;
  9. public User() {
  10. }
  11. public String getFirstLetter() {
  12. return firstLetter;
  13. }
  14. public void setFirstLetter(String firstLetter) {
  15. this.firstLetter = firstLetter;
  16. }
  17. public int getImg() {
  18. return img;
  19. }
  20. public void setImg(int img) {
  21. this.img = img;
  22. }
  23. public String getPinyin() {
  24. return pinyin;
  25. }
  26. public void setPinyin(String pinyin) {
  27. this.pinyin = pinyin;
  28. }
  29. public String getUsername() {
  30. return username;
  31. }
  32. public void setUsername(String username) {
  33. this.username = username;
  34. }
  35. public User(String firstLetter, int img, String pinyin, String username) {
  36. this.firstLetter = firstLetter;
  37. this.img = img;
  38. this.pinyin = pinyin;
  39. this.username = username;
  40. }
  41. }

username用来存储用户名,img表示用户图像的资源id(这里我没有准备相应的图片,大家有兴趣可以自行添加),pinyin表示用户姓名的拼音,firstLetter表示用户姓名拼音的首字母,OK

,就这么简单的几个属性。至于数据源,我在strings.xml文件中添加了许多数据,这里就不贴出来了,大家可以直接在文末下载源码看。知道了数据源,知道了实体类,我们来看看在MainActivity中怎么样来初始化数据:

[java] view plain copy print?

  1. private void initData() {
  2. list = new ArrayList<>();
  3. String[] allUserNames = getResources().getStringArray(R.array.arrUsernames);
  4. for (String allUserName : allUserNames) {
  5. User user = new User();
  6. user.setUsername(allUserName);
  7. String convert = ChineseToPinyinHelper.getInstance().getPinyin(allUserName).toUpperCase();
  8. user.setPinyin(convert);
  9. String substring = convert.substring(0, 1);
  10. if (substring.matches("[A-Z]")) {
  11. user.setFirstLetter(substring);
  12. }else{
  13. user.setFirstLetter("#");
  14. }
  15. list.add(user);
  16. }
  17. Collections.sort(list, new Comparator<User>() {
  18. @Override
  19. public int compare(User lhs, User rhs) {
  20. if (lhs.getFirstLetter().contains("#")) {
  21. return 1;
  22. } else if (rhs.getFirstLetter().contains("#")) {
  23. return -1;
  24. }else{
  25. return lhs.getFirstLetter().compareTo(rhs.getFirstLetter());
  26. }
  27. }
  28. });
  29. }

首先创建一个List集合用来存放所有的数据,然后从strings.xml文件中读取出来所有的数据,遍历数据然后存储到List集合中,在遍历的过程中,我通过ChineseToPinyinHelper这个工具类来将中文转为拼音,然后截取拼音的第一个字母,如果该字母是A~Z,那么直接设置给user对象的firstLetter属性,否则user对象的firstLetter属性为一个#,这是由于我的数据源中有一些不是以汉字开头的姓名,而是以其他字符开头的姓名,那么我将这些统一归为#这个分组。

OK,数据源构造好之后,我还需要对List集合进行一个简单的排序,那么这个排序是Java中的操作,我这里就不再赘述。

构造完数据源之后,接着就该是构造ListView的Adapter了,我们来看看这个怎么做,先来看看源码:

[java] view plain copy print?

  1. /**
  2. * Created by wangsong on 2016/4/24.
  3. */
  4. public class MyAdapter extends BaseAdapter implements SectionIndexer {
  5. private List<User> list;
  6. private Context context;
  7. private LayoutInflater inflater;
  8. public MyAdapter(Context context, List<User> list) {
  9. this.context = context;
  10. this.list = list;
  11. inflater = LayoutInflater.from(context);
  12. }
  13. @Override
  14. public int getCount() {
  15. return list.size();
  16. }
  17. @Override
  18. public Object getItem(int position) {
  19. return list.get(position);
  20. }
  21. @Override
  22. public long getItemId(int position) {
  23. return position;
  24. }
  25. @Override
  26. public View getView(int position, View convertView, ViewGroup parent) {
  27. ViewHolder holder;
  28. if (convertView == null) {
  29. convertView = inflater.inflate(R.layout.listview_item, null);
  30. holder = new ViewHolder();
  31. holder.showLetter = (TextView) convertView.findViewById(R.id.show_letter);
  32. holder.username = (TextView) convertView.findViewById(R.id.username);
  33. convertView.setTag(holder);
  34. } else {
  35. holder = (ViewHolder) convertView.getTag();
  36. }
  37. User user = list.get(position);
  38. holder.username.setText(user.getUsername());
  39. //获得当前position是属于哪个分组
  40. int sectionForPosition = getSectionForPosition(position);
  41. //获得该分组第一项的position
  42. int positionForSection = getPositionForSection(sectionForPosition);
  43. //查看当前position是不是当前item所在分组的第一个item
  44. //如果是,则显示showLetter,否则隐藏
  45. if (position == positionForSection) {
  46. holder.showLetter.setVisibility(View.VISIBLE);
  47. holder.showLetter.setText(user.getFirstLetter());
  48. } else {
  49. holder.showLetter.setVisibility(View.GONE);
  50. }
  51. return convertView;
  52. }
  53. @Override
  54. public Object[] getSections() {
  55. return new Object[0];
  56. }
  57. //传入一个分组值[A....Z],获得该分组的第一项的position
  58. @Override
  59. public int getPositionForSection(int sectionIndex) {
  60. for (int i = 0; i < list.size(); i++) {
  61. if (list.get(i).getFirstLetter().charAt(0) == sectionIndex) {
  62. return i;
  63. }
  64. }
  65. return -1;
  66. }
  67. //传入一个position,获得该position所在的分组
  68. @Override
  69. public int getSectionForPosition(int position) {
  70. return list.get(position).getFirstLetter().charAt(0);
  71. }
  72. class ViewHolder {
  73. TextView username, showLetter;
  74. }
  75. }

这个Adapter大部分还是和我们之前的Adapter一样的,只不过这里实现了SectionIndexer接口,实现了这个接口,我们就要实现该接口中的三个方法,分别是getSections(),getPositionForSection(),getSectionForPosition()这三个方法,我们这里用到的主要是后面这两个方法,那我来详细说一下:

1.getPositionForSection(int sectionIndex)

这个方法接收一个int类型的参数,该参数实际上就是指我们的分组,我们在这里传入分组的值【A.....Z】,然后我们在方法中通过自己的计算,返回该分组中第一个item的position。

2.getSectionForPosition(int position)

这个方法接收一个int类型的参数,该参数实际上就是我们的ListView即将要显示的item的position,我们通过传入这个position,可以获得该position的item所属的分组,然后再将这个分组的值返回。

说了这么多,大家可能有疑问了,我为什么要实现这个接口呢?大家来看看我的item的布局文件:

[java] view plain copy print?

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent"
  5. android:orientation="vertical">
  6. <TextView
  7. android:id="@+id/show_letter"
  8. android:layout_width="match_parent"
  9. android:layout_height="wrap_content"
  10. android:layout_marginTop="3dp"/>
  11. <RelativeLayout
  12. android:layout_width="match_parent"
  13. android:layout_height="wrap_content">
  14. <ImageView
  15. android:id="@+id/userface"
  16. android:layout_width="72dp"
  17. android:layout_height="72dp"
  18. android:padding="12dp"
  19. android:src="@mipmap/ic_launcher"/>
  20. <TextView
  21. android:id="@+id/username"
  22. android:layout_width="wrap_content"
  23. android:layout_height="match_parent"
  24. android:layout_centerVertical="true"
  25. android:layout_marginLeft="36dp"
  26. android:layout_toRightOf="@id/userface"
  27. android:gravity="center"
  28. android:text="username"/>
  29. </RelativeLayout>
  30. </LinearLayout>

在我的item的布局文件中,我所有的item实际上都是一样的,都有一个显示分组数据的TextView,因此我需要在Adapter的getView方法中根据所显示的item的不同来确定是否将显示分组的TextView隐藏掉。所以我们再回过头来看看我的ListView中的getView方法,getView前面的写法没啥好说的,和普通ListView都一样,我们主要来看看这几行:

[java] view plain copy print?

  1. //获得当前position是属于哪个分组
  2. int sectionForPosition = getSectionForPosition(position);
  3. //获得该分组第一项的position
  4. int positionForSection = getPositionForSection(sectionForPosition);
  5. //查看当前position是不是当前item所在分组的第一个item
  6. //如果是,则显示showLetter,否则隐藏
  7. if (position == positionForSection) {
  8. holder.showLetter.setVisibility(View.VISIBLE);
  9. holder.showLetter.setText(user.getFirstLetter());
  10. } else {
  11. holder.showLetter.setVisibility(View.GONE);
  12. }

我首先判断当前显示的item是属于哪个分组的,然后获得这个分组中第一个item的位置,最后判断我当前显示的item的position到底是不是它所在分组的第一个item,如果是的话,那么就将showLetter这个TextView显示出来,同时显示出相应的分组信息,否则将这个showLetter隐藏。就是这么简单。做完这些之后,我们在Activity中再来简单的添加两行代码:

[java] view plain copy print?

  1. ListView listView = (ListView) findViewById(R.id.lv);
  2. MyAdapter adapter = new MyAdapter(this, list);
  3. listView.setAdapter(adapter);

这个时候左边的分组ListView就可以显示出来了。就是这么简单。

三.右边滑动控件的实现

右边这个东东很明显是一个自定义View,那我们就一起来看看这个自定义View吧。

首先这个自定义控件继承自View,继承自View,需要实现它里边的构造方法,关于这三个构造方法的解释大家可以查看我的另一篇博客android自定义View之钟表诞生记,这里对于构造方法我不再赘述。在这个自定义View中,我需要首先声明5个变量,如下:

[java] view plain copy print?

  1. //当前手指滑动到的位置
  2. private int choosedPosition = -1;
  3. //画文字的画笔
  4. private Paint paint;
  5. //右边的所有文字
  6. private String[] letters = new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L",
  7. "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "#"};
  8. //页面正中央的TextView,用来显示手指当前滑动到的位置的文本
  9. private TextView textViewDialog;
  10. //接口变量,该接口主要用来实现当手指在右边的滑动控件上滑动时ListView能够跟着滚动
  11. private UpdateListView updateListView;

五个变量的作用我在注释中已经说的很详细了。OK,变量声明完成之后,我还要初始化一些变量,变量的初始化当然放在构造方法中来进行了:

[java] view plain copy print?

  1. public LetterIndexView(Context context, AttributeSet attrs, int defStyleAttr) {
  2. super(context, attrs, defStyleAttr);
  3. paint = new Paint();
  4. paint.setAntiAlias(true);
  5. paint.setTextSize(24);
  6. }

OK,这里要初始化的实际上只有paint一个变量。

准备工作做完之后,接下来就是onDraw了,代码如下:

[java] view plain copy print?

  1. @Override
  2. protected void onDraw(Canvas canvas) {
  3. int perTextHeight = getHeight() / letters.length;
  4. for (int i = 0; i < letters.length; i++) {
  5. if (i == choosedPosition) {
  6. paint.setColor(Color.RED);
  7. } else {
  8. paint.setColor(Color.BLACK);
  9. }
  10. canvas.drawText(letters[i], (getWidth() - paint.measureText(letters[i])) / 2, (i + 1) * perTextHeight, paint);
  11. }
  12. }

在绘制的时候,我需要首先获得每一个文字所占空间的大小,每一个文本的可用高度应该是总高度除以文字的总数,然后,通过一个for循环将26个字母全都画出来。在画的时候,如果这个文本所处的位置刚好就是我手指按下的位置,那么该文本的颜色为红色,否则为黑色,最后的drawText不需要我再说了吧。

绘制完成之后,就是重写onTouchEvent了,如下:

[java] view plain copy print?

  1. @Override
  2. public boolean onTouchEvent(MotionEvent event) {
  3. int perTextHeight = getHeight() / letters.length;
  4. float y = event.getY();
  5. int currentPosition = (int) (y / perTextHeight);
  6. String letter = letters[currentPosition];
  7. switch (event.getAction()) {
  8. case MotionEvent.ACTION_UP:
  9. setBackgroundColor(Color.TRANSPARENT);
  10. if (textViewDialog != null) {
  11. textViewDialog.setVisibility(View.GONE);
  12. }
  13. break;
  14. default:
  15. setBackgroundColor(Color.parseColor("#cccccc"));
  16. if (currentPosition > -1 && currentPosition < letters.length) {
  17. if (textViewDialog != null) {
  18. textViewDialog.setVisibility(View.VISIBLE);
  19. textViewDialog.setText(letter);
  20. }
  21. if (updateListView != null) {
  22. updateListView.updateListView(letter);
  23. }
  24. choosedPosition = currentPosition;
  25. }
  26. break;
  27. }
  28. invalidate();
  29. return true;
  30. }

对于右边的滑动控件的事件操作我整体上可以分为两部分,手指抬起分为一类,其他所有的操作归为一类。那么当控件感知到我手指的操作事件之后,它首先需要知道我手指当前所点击的item是什么,那么这个值要怎么获取呢?我可以先获得到手指所在位置的Y坐标,然后除以每一个文字的高度,就知道当前手指点击位置的position,然后从letters数组中读取出相应的值即可。知道了当前点击了哪个字母之后,剩下的工作就很简单了,修改控件的背景颜色,然后将相应的字母显示在TextView上即可,然后把当前的position传给choosedPosition,最后调用invalidate()方法重绘控件。重绘控件时由于choosedPosition的值已经发生了变化,所以相应的文本颜色也会改变。另外,我希望手指在右边控件滑动时,ListView也能跟着滚动,这个毫无疑问使用接口回调,具体大家看代码,简单的东西不赘述。最后,我希望ListView滚动时,右边控件中文本的颜色应该实时更新,那么这个也很简单,在自定义View中公开一个方法即可,如下:

[java] view plain copy print?

  1. public void updateLetterIndexView(int currentChar) {
  2. for (int i = 0; i < letters.length; i++) {
  3. if (currentChar == letters[i].charAt(0)) {
  4. choosedPosition = i;
  5. invalidate();
  6. break;
  7. }
  8. }
  9. }

最后再来看一眼Activity中现在的代码:

[java] view plain copy print?

  1. TextView textView = (TextView) findViewById(R.id.show_letter_in_center);
  2. final LetterIndexView letterIndexView = (LetterIndexView) findViewById(R.id.letter_index_view);
  3. letterIndexView.setTextViewDialog(textView);
  4. letterIndexView.setUpdateListView(new LetterIndexView.UpdateListView() {
  5. @Override
  6. public void updateListView(String currentChar) {
  7. int positionForSection = adapter.getPositionForSection(currentChar.charAt(0));
  8. listView.setSelection(positionForSection);
  9. }
  10. });
  11. listView.setOnScrollListener(new AbsListView.OnScrollListener() {
  12. @Override
  13. public void onScrollStateChanged(AbsListView view, int scrollState) {
  14. }
  15. @Override
  16. public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
  17. int sectionForPosition = adapter.getSectionForPosition(firstVisibleItem);
  18. letterIndexView.updateLetterIndexView(sectionForPosition);
  19. }
  20. });

就是这么简单。

源码下载http://download.csdn.net/detail/u012702547/9501208

以上。

时间: 2024-08-25 15:54:18

android自定义View之仿通讯录侧边栏滑动,实现A-Z字母检索的相关文章

Android自定义view之仿支付宝芝麻信用仪表盘

自定义view练习 仿支付宝芝麻信用的仪表盘 对比图: 首先是自定义一些属性,可自己再添加,挺基础的,上代码 <?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="RoundIndicatorView"> <!--最大数值--> <attr name="maxNum" form

Android自定义View之仿QQ侧滑菜单实现

最近,由于正在做的一个应用中要用到侧滑菜单,所以通过查资料看视频,学习了一下自定义View,实现一个类似于QQ的侧滑菜单,顺便还将其封装为自定义组件,可以实现类似QQ的侧滑菜单和抽屉式侧滑菜单两种菜单. 下面先放上效果图: 我们这里的侧滑菜单主要是利用HorizontalScrollView来实现的,基本的思路是,一个布局中左边是菜单布局,右边是内容布局,默认情况下,菜单布局隐藏,内容布局显示,当我们向右侧滑,就会将菜单拉出来,而将内容布局的一部分隐藏,如下图所示: 下面我们就一步步开始实现一个

Android 自定义View,仿微信视频播放按钮

闲着,尝试实现了新版微信视频播放按钮,使用的是自定义View,先来个简单的效果图...真的很简单哈. 由于暂时用不到,加上时间原因,加上实在是没意思,加上……,本控件就没有实现自定义属性,有兴趣的朋友可以自己去添加一下,方法都给你们准备好了.- = 其实这个控件主要步骤 1.画外环的圆 2.画进度的圆或者画三角形播放按钮 其余剩下的都是围绕以上两步准备或者收尾的. 接下来贴主要我们的自定义控件代码,注释很全,我就不过多解释了,请各位看官自己分析,有疑问可以在评论区一起讨论. package co

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

效果图: 1.attrs.xml中 <declare-styleable name="QQStepView"> <attr name="outerColor" format="color"/> <attr name="innerColor" format="color"/> <attr name="borderWidth" format=&quo

android自定义View之NotePad出鞘记

现在我们的手机上基本都会有一个记事本,用起来倒也还算方便,记事本这种东东,如果我想要自己实现,该怎么做呢?今天我们就通过自定义View的方式来自定义一个记事本.OK,废话不多说,先来看看效果图. 整个页面还是很简单的. 1.自定义View的分类 OK,那么在正文开始之前,我想先来说说自定义View的分类,自定义View我们一共分为三类 1.自绘控件 自绘控件就是我们自定义View继承自已有控件,然后扩展其功能,之前两篇自定义View的博客(android自定义View之钟表诞生记,android

【Android自定义View实战】之仿百度加载动画,一种优雅的Loading方式

转载请注明出处:http://blog.csdn.net/linglongxin24/article/details/53470872 本文出自[DylanAndroid的博客] Android自定义View实战之仿百度加载动画一种优雅的Loading方式 第一个仿百度加载动画用ObjectAnimator属性动画操作ImageView的属性方法实现 第二个仿百度加载动画第二种实现方式用ValueAnimator原生的ondraw方法实现 第三个扔球动画-水平旋转动画 第四个扔球动画-垂直旋转动

android自定义View之(六)------高仿华为荣耀3C的圆形刻度比例图(ShowPercentView)

为什么写这篇文章: 显示当前的容量所占的比例,表现当前计划的进度,一般都会采用百分比的方式,而图形显示,以其一目了然的直观性和赏心悦目的美观形成为了我们的当然的首选. 在图形表示百分比的方法中,我们有用画圆的圆形进度条的方法<<android自定义View之(二)------简单进度条显示样例篇>>,也有用画弧形的进度条的方法<<android自定义View之(三)------视频音量调控样例>>,今天看到华为荣耀3C的一个界面: 个人觉得这个表示比例的圆形

Android 自定义View合集

自定义控件学习 https://github.com/GcsSloop/AndroidNote/tree/master/CustomView 小良自定义控件合集 https://github.com/Mr-XiaoLiang 自定义控件三部曲 http://blog.csdn.net/harvic880925?viewmode=contents Android 从0开始自定义控件之View基础知识与概念 http://blog.csdn.net/airsaid/article/details/5

自定义View之仿淘宝详情页

自定义View之仿淘宝详情页 转载请标明出处: http://blog.csdn.net/lisdye2/article/details/52353071 本文出自:[Alex_MaHao的博客] 项目中的源码已经共享到github,有需要者请移步[Alex_MaHao的github] 基本介绍 现在的一些购物类App例如淘宝,京东等,在物品详情页,都采用了类似分层的模式,即上拉加载详情的方式,节省了空间,使用户的体验更加的舒适.只要对于某个东西的介绍很多时,都可以采取这样的方式,第一个页面显示