zoukankan      html  css  js  c++  java
  • 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中的数据集,我需要自建一个实体类,该实体类如下:

    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.   
    10.     public User() {  
    11.     }  
    12.   
    13.     public String getFirstLetter() {  
    14.         return firstLetter;  
    15.     }  
    16.   
    17.     public void setFirstLetter(String firstLetter) {  
    18.         this.firstLetter = firstLetter;  
    19.     }  
    20.   
    21.     public int getImg() {  
    22.         return img;  
    23.     }  
    24.   
    25.     public void setImg(int img) {  
    26.         this.img = img;  
    27.     }  
    28.   
    29.     public String getPinyin() {  
    30.         return pinyin;  
    31.     }  
    32.   
    33.     public void setPinyin(String pinyin) {  
    34.         this.pinyin = pinyin;  
    35.     }  
    36.   
    37.     public String getUsername() {  
    38.         return username;  
    39.     }  
    40.   
    41.     public void setUsername(String username) {  
    42.         this.username = username;  
    43.     }  
    44.   
    45.     public User(String firstLetter, int img, String pinyin, String username) {  
    46.         this.firstLetter = firstLetter;  
    47.         this.img = img;  
    48.         this.pinyin = pinyin;  
    49.         this.username = username;  
    50.     }  
    51. }  


    username用来存储用户名,img表示用户图像的资源id(这里我没有准备相应的图片,大家有兴趣可以自行添加),pinyin表示用户姓名的拼音,firstLetter表示用户姓名拼音的首字母,OK ,就这么简单的几个属性。至于数据源,我在strings.xml文件中添加了许多数据,这里就不贴出来了,大家可以直接在文末下载源码看。知道了数据源,知道了实体类,我们来看看在MainActivity中怎么样来初始化数据:

    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了,我们来看看这个怎么做,先来看看源码:

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


    这个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的布局文件:

    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.   
    7.     <TextView  
    8.         android:id="@+id/show_letter"  
    9.         android:layout_width="match_parent"  
    10.         android:layout_height="wrap_content"  
    11.         android:layout_marginTop="3dp"/>  
    12.   
    13.     <RelativeLayout  
    14.         android:layout_width="match_parent"  
    15.         android:layout_height="wrap_content">  
    16.   
    17.         <ImageView  
    18.             android:id="@+id/userface"  
    19.             android:layout_width="72dp"  
    20.             android:layout_height="72dp"  
    21.             android:padding="12dp"  
    22.             android:src="@mipmap/ic_launcher"/>  
    23.   
    24.         <TextView  
    25.             android:id="@+id/username"  
    26.             android:layout_width="wrap_content"  
    27.             android:layout_height="match_parent"  
    28.             android:layout_centerVertical="true"  
    29.             android:layout_marginLeft="36dp"  
    30.             android:layout_toRightOf="@id/userface"  
    31.             android:gravity="center"  
    32.             android:text="username"/>  
    33.     </RelativeLayout>  
    34. </LinearLayout>  


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

    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中再来简单的添加两行代码:

    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个变量,如下:

    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,变量声明完成之后,我还要初始化一些变量,变量的初始化当然放在构造方法中来进行了:

    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了,代码如下:

    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了,如下:

    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中公开一个方法即可,如下:

    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中现在的代码:

    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.             }  
    16.   
    17.             @Override  
    18.             public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {  
    19.                 int sectionForPosition = adapter.getSectionForPosition(firstVisibleItem);  
    20.                 letterIndexView.updateLetterIndexView(sectionForPosition);  
    21.             }  
    22.         });  


    就是这么简单。

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

     

    以上。

  • 相关阅读:
    C语言|博客作业08
    C语言|博客作业04
    C语言|博客作业02
    C语言|博客作业06
    C语言|博客作业03
    第一周作业
    C语言|博客作业05
    C语言|博客作业07
    C语言|博客作业09
    为什么get比post更快
  • 原文地址:https://www.cnblogs.com/Free-Thinker/p/6544080.html
Copyright © 2011-2022 走看看