IndexView
IndexView用于为ListView添加索引。
首先看一下两个demo的效果,分别是音乐列表和联系人列表:
![]()
![]()
如何实现
构造函数
1 2 3
  
 | 
public IndexView(Context context, AttributeSet attrs, int defStyle); public IndexView(Context context, AttributeSet attrs); public IndexView(Context context);
  
 | 
构造函数中做了如下几件事。
1 读取自定义属性
自定义属性包括如下这些。
1 2 3 4 5 6 7 8 9 10 11 12
  
 | 
<?xml version="1.0" encoding="utf-8"?> <resources>     <declare-styleable name="IndexView">         <attr name="heightOccupy" format="float" />         <attr name="indexTextColor" format="color" />         <attr name="selectIndexTextColor" format="color" />         <attr name="selectIndexBgColor" format="color" />         <attr name="indexTextSizeScale" format="float" />         <attr name="tipTextColor" format="color" />         <attr name="tipBg" format="color|reference" />     </declare-styleable> </resources>
  
 | 
 
描述如下:
| 属性名 | 描述 | 
| heightOccupy | 
0.0f-1.0f, 索引占据的高度比例,小于1的部分将分别在上下两端留白 | 
| indexTextColor | 
未选中的索引字体颜色 | 
| selectIndexTextColor | 
选中的索引字体颜色 | 
| selectIndexBgColor | 
选中的索引背景色 | 
| indexTextSizeScale | 
0.0f-1.0f, 控制索引字体相对大小, 默认0.65f | 
| tipTextColor | 
提示框字体颜色 | 
| tipBg | 
提示框背景 | 
2 初始化提示框
提示框就是用来提示刚刚点击或者滑到的索引的符号。并且提示框可以在显示一段时间后自动隐藏。
3 初始化用于绘制的画笔
onMeasure方法
该方法确定控件所要占据的宽和高。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
  
 | 
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
      int widthMode = MeasureSpec.getMode(widthMeasureSpec);     width = MeasureSpec.getSize(widthMeasureSpec);     height = MeasureSpec.getSize(heightMeasureSpec);     if(widthMode != MeasureSpec.EXACTLY){         width = Math.min(DEFAULT_WIDTH, width);     }     singleIndexHeight = height * heightOccupy / INDEXES.length();     indexTextSize = Math.min(singleIndexHeight, width) * indexTextSizeScale;     indexTextPaint.setTextSize(indexTextSize);     selectIndexTextPaint.setTextSize(indexTextSize);     setMeasuredDimension(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); }
  
 | 
 
onDraw方法
该方法实现对控件的绘制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
  
 | 
@Override protected void onDraw(Canvas canvas) {
      super.onDraw(canvas);     float singleIndexWidth = getMeasuredWidth();     for (int i = 0; i < INDEXES.length(); i++) {
          float x = (singleIndexWidth - indexTextPaint.measureText(INDEXES, i, i + 1)) / 2;         float baseline = (1 - heightOccupy) / 2.0f * height + i * singleIndexHeight + Util.getBaseline(0, singleIndexHeight, indexTextPaint);
          if(i == selectIndex){             float left = 0.1f * singleIndexWidth;             float top = (1 - heightOccupy) / 2.0f * height + i * singleIndexHeight + 0.05f * singleIndexHeight;             float right = 0.9f * singleIndexWidth;             float bottom = (1 - heightOccupy) / 2.0f * height + i * singleIndexHeight + 0.95f * singleIndexHeight;             canvas.drawRoundRect(new RectF(left, top, right, bottom), 5, 5, selectIndexBgPaint);             canvas.drawText(String.valueOf(INDEXES.charAt(i)), x, baseline, selectIndexTextPaint);         }else{             canvas.drawText(String.valueOf(INDEXES.charAt(i)), x, baseline, indexTextPaint);         }     } }
  
 | 
 
触摸事件
这里有一个注意点,当触摸到一个新的索引时,必然需要显示一个提示框,但不一定会切换到这个新的索引,因为这个新索引不一定存在数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
  
 | 
@Override public boolean onTouchEvent(MotionEvent event) {
      float y = event.getY();     switch (event.getAction()) {         case MotionEvent.ACTION_DOWN:             if (tipVisible) {                 this.removeCallbacks(hideTipRunnable);             }             break;         case MotionEvent.ACTION_UP:             this.postDelayed(hideTipRunnable, TIP_SHOW_TIME);             break;     }     int newSelectIndex = getSelectByY(y);     if (selectIndex != newSelectIndex && onIndexChangeListener != null) {         onIndexChangeListener.OnIndexChange(newSelectIndex, INDEXES.charAt(newSelectIndex));         tip.setText(String.valueOf(INDEXES.charAt(newSelectIndex)));         if (!tipVisible) {             tipVisible = true;             tip.setVisibility(VISIBLE);         }     }     return true; }
  
 | 
 
自定义监听器
IndexView提供了1个接口OnIndexChangeListener,由触摸事件检测到索引变化时触发。本接口不需要使用者自己实现该接口。
1 2 3
  
 | 
protected interface OnIndexChangeListener {     void OnIndexChange(int select, char index); }
 
 | 
 
如何绑定IndexView和ListView
IndexView和ListView显然是耦合的。
一方面,当触摸索引时,IndexView需要访问到ListView确认这个新的索引是否存在数据,以此决定是否切换索引,如果存在数据,还要通知ListView切换到相应的位置去;
另一方面,当滑动ListView时,随着ListView的位置变化,需要通知IndexView将索引切换到对应的位置去。
上面所述,通过抽象类Binder类来实现。在bind()方法中通过给IndexView设置一个OnIndexChangeListener监听器,给ListView设置一个OnScrollListener监听器就实现了二者的绑定。
使用本控件的人只需要实例化一个继承Binder的匿名内部类的对象即可,并且只需要实现public abstract String getListItemKey(int position);这个abstract函数即可。
getListItemKey()这个方法的意思是为每一个列表项指定一个用来索引的字符串。比如在demo1中用来索引的是歌曲名称,在demo2中用来索引的是用户名,如果在demo1中想改为用歌手名来索引,可以很方便的进行修改。
这里有一个注意点,当由于触摸IndexView导致ListView滑动时,也会触发ListView的OnScrollListener,这时需要避免掉IndexView再次发生索引切换,使用了一个标记flag来解决。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
  
 | 
public abstract class Binder {
      private ListView listView;     private IndexView indexView;     private boolean flag = false;
      public Binder(ListView listView, IndexView indexView){         this.listView = listView;         this.indexView = indexView;     }
      public void  bind(){         indexView.setOnIndexChangeListener(new IndexView.OnIndexChangeListener() {             @Override             public void OnIndexChange(int selectIndex, char index) {                 ListAdapter adapter = listView.getAdapter();                 int pos = -1;                 for (int i = 0; i < adapter.getCount(); i++) {                     char currentIndex = Util.getIndex(getListItemKey(i));                     if (currentIndex == index) {                         pos = i;                         break;                     }                 }                 if (pos != -1) {                     listView.setSelection(pos);                     flag = true;                     indexView.setSelectIndex(selectIndex);                 }             }         });
          listView.setOnScrollListener(new AbsListView.OnScrollListener() {
              @Override             public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {                 if(flag){                     flag = false;                     return;                 }                 indexView.setIndex(Util.getIndex(getListItemKey(firstVisibleItem)));             }
              @Override             public void onScrollStateChanged(AbsListView view, int scrollState) {             }         });     }     public abstract String getListItemKey(int position); }
 
 | 
 
拼音排序器
在获取了ListView的数据之后,需要先对数据进行排序。
使用抽象类PinyinComparator可以很方便的实现。该类已经实现了两个字符串的比较函数,使用时只要对实体类(java bean)进行排序即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
  
 | 
public abstract class PinyinComparator<T> implements Comparator<T> {     public abstract int compare(T s1, T s2);          public int compare(String s1, String s2) {         char i1 = Util.getIndex(s1);         char i2 = Util.getIndex(s2);         if(i1 == '#' && i2 == '#'){             return Util.getStringForSort(s1).compareTo(Util.getStringForSort(s2));         }else if(i1 == '#'){             return 1;         }else if(i2 == '#'){             return -1;         }else{             return Util.getStringForSort(s1).compareTo(Util.getStringForSort(s2));         }     } }
 
 | 
 
demo1中的使用的例子如下,仅需要使用匿名内部类,实现抽象方法即可。
1 2 3 4 5 6
  
 | 
Collections.sort(items, new PinyinComparator<Item>() {     @Override     public int compare(Item s1, Item s2) {         return compare(s1.getSong(), s2.getSong());     } });
 
 | 
 
汉字转拼音使用了开源项目jpinyin。
github
github项目主页: IndexView