自定义控件:快速索引

来源:互联网 发布:hive sql 时间相减 编辑:程序博客网 时间:2024/04/30 06:35

快速索引在应用中很常见,在联系人,微信,省市列表,应用管理,文件管理等应用场景都可以看到快速索引的身影,本篇博客将讲解快速索引的自定义,从中你可以学到获取汉字首字母的方法,绘制字母时,纵坐标的计算方法

这里写图片描述

1. 静态绘制

1.1 初始化数据

创建自定义控件QuickIndexBar 继承View

public class QuickIndexBar extends View {        private Paint paint;        // 字母数组        private static final 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" };        public QuickIndexBar(Context context) {            this(context, null);        }        public QuickIndexBar(Context context, AttributeSet attrs) {            this(context, attrs, 0);        }        public QuickIndexBar(Context context, AttributeSet attrs, int defStyle) {            super(context, attrs, defStyle);            paint = new Paint();            // 设置抗锯齿,设置后画出来的边缘更加平滑            paint.setAntiAlias(true);            //设置字体为粗体            paint.setTypeface(Typeface.DEFAULT_BOLD);            // 设置字体颜色            paint.setColor(Color.WHITE);        }        @Override        protected void onDraw(Canvas canvas) {            super.onDraw(canvas);            //画文字的方法            canvas.drawText("A", 10f, 10f, paint);        }    }
  • 第3-6 行初始化字母数组
  • 第7-12 行串连构造方法
  • 第15-21 行初始化画笔
  • 第27 行画文字的方法

将QuickIndexBar 布局到activity_main.xml 中

<RelativeLayout    xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent">    <com.example.quickindexer.widget.QuickIndexBar        android:id="@+id/quick_bar"        android:layout_width="30dp"        android:layout_height="match_parent"        android:layout_alignParentRight="true"        android:background="#ff0000"/></RelativeLayout>

1.2 计算字母坐标

这里写图片描述

画文字的x,y 坐标是文字左下角的位置

  1. 第一个字母的x 坐标则等于单元格宽度的一半减去文字宽度的一半,由于所有字母离左边的距离
    一样,所以x 不变int x = cellWidth/2 - textWidth/2
  2. 第一个字母的y 坐标则等于单元格高度的一半加上文字,第二个字母需要加上一个单元格的宽度
    由此类推int y = cellHeight/2 +textHeight/2 +i*cellHeight
  3. 单元格cellWidth 为QuickIndexBar 宽度,cellHeight 为QuickIndexBar 高度/字母数组的长度
@Override    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);        //遍历字母数组,计算坐标,进行绘制        for (int i = 0; i < LETTERS.length; i++) {            String letter = LETTERS[i];            //计算x 坐标            float x = cellWidth*0.5f - paint.measureText(letter)*0.5f;            //计算y 坐标            Rect bounds = new Rect();            //获取文本的矩形区域            paint.getTextBounds(letter, 0, letter.length(), bounds);            float y = cellHeight*0.5f + bounds.height()+ i*cellHeight;            //绘制文本            canvas.drawText(letter, x, y, paint);        }    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);        //控件高度        int height = getMeasuredHeight();        //控件宽度,也为单元格宽度        cellWidth = getMeasuredWidth();        //单元格宽度,控件高度除以字母数组长度,此处需要用float 类型        //10/3 = 3.333 如果用int 接收则为3,此时高度比实际分配的高度小,所以用float 接收        cellHeight = height*1.0f/LETTERS.length;    }
  • 第19-26 行获取单元的宽高,注意单元格高度需要用float 类型
  • 第8-12 行通过paint 测量文字的宽高,并计算出每个字母的坐标

2. Android drawText获取text宽度的三种方式

原文链接:http://blog.csdn.net/chuekup/article/details/7518239

String str = "Hello";  canvas.drawText( str , x , y , paint);  //1. 粗略计算文字宽度  Log.d(TAG, "measureText=" + paint.measureText(str));  //2. 计算文字所在矩形,可以得到宽高  Rect rect = new Rect();  paint.getTextBounds(str, 0, str.length(), rect);  int w = rect.width();  int h = rect.height();  Log.d(TAG, "w=" +w+"  h="+h);  //3. 精确计算文字宽度  int textWidth = getTextWidth(paint, str);  Log.d(TAG, "textWidth=" + textWidth);      public static int getTextWidth(Paint paint, String str) {          int iRet = 0;          if (str != null && str.length() > 0) {              int len = str.length();              float[] widths = new float[len];              paint.getTextWidths(str, widths);              for (int j = 0; j < len; j++) {                  iRet += (int) Math.ceil(widths[j]);              }          }          return iRet;      }  //4. mPaint.getTextSize();

3. 响应触摸事件

重写onTouchEvent()方法,解析触摸事件

//初始值需要设置为-1,不能为0,因为按下第一个字母的索引是0    private int lastIndex = -1;    @Override    public boolean onTouchEvent(MotionEvent event) {        float y ;        int currentIndex;        switch (event.getAction()) {            case MotionEvent.ACTION_DOWN:                y = event.getY();                //根据y 计算当前按下的字母索引                //例如:y = 21,cellHeight = 10 =>(int) (y/cellHeight)= 2                currentIndex = (int) (y/cellHeight);                if(lastIndex != currentIndex){                    //判断计算出来的索引值,避免数组越界                    if(0 <= currentIndex && currentIndex < LETTERS.length){                        String letter = LETTERS[currentIndex];                        Utils.showToast(getContext(), letter);                        //记录上次按下的索引                        lastIndex = currentIndex;                    }                }                break;            case MotionEvent.ACTION_MOVE:                y = event.getY();                //根据y 计算当前按下的字母索引                //例如:y = 21,cellHeight = 10 =>(int) (y/cellHeight)= 2                currentIndex = (int) (y/cellHeight);                //判断计算出来的索引值,避免数组越界                if(0 <= currentIndex && currentIndex < LETTERS.length){                    String letter = LETTERS[currentIndex];                    Utils.showToast(getContext(), letter);                    //记录上次按下的索引                    lastIndex = currentIndex;                }                break;            case MotionEvent.ACTION_UP:                //手指抬起时需要将记录的值设为-1,否则再次按下该字母不会弹出toast                lastIndex = -1;                break;            default:                break;        }        //事件已被处理,返回true        return true;    }
  • 第12 行通过触摸的y 值计算按下字母的索引值
  • 第19 行记录上次按下字母的索引值,通过判断上次按下字母的索引与本次按下字母的索引是否相同,如果不同才弹出toast,避免在同一个字母上来回移动也一直弹出toast
  • 第38 行手指抬起需要将lastIndex 还原为初始值
  • 第45 行一定要返回true,代表事件已被消费
  • 第17 行是单例Toast
public class Utils {        private static Toast toast;        public static void showToast(Context context, String msg) {            if (toast == null) {                toast = Toast.makeText(context, "", Toast.LENGTH_SHORT);            }            toast.setText(msg);            toast.show();        }    }

4. 监听回调

定义监听回调接口

 private OnLetterUpdateListener onLetterUpdateListener;    public OnLetterUpdateListener getOnLetterUpdateListener() {        return onLetterUpdateListener;    }    public void setOnLetterUpdateListener(            OnLetterUpdateListener onLetterUpdateListener) {        this.onLetterUpdateListener = onLetterUpdateListener;    }    public interface OnLetterUpdateListener{        public void onLetterUpdate(String letter);    }

在弹出Toast 的地方替换成调用接口方法

public boolean onTouchEvent(MotionEvent event) {        float y ;        int currentIndex;        switch (event.getAction()) {            case MotionEvent.ACTION_DOWN:                y = event.getY();                //根据y 计算当前按下的字母索引                //例如:y = 21,cellHeight = 10 =>(int) (y/cellHeight)= 2                currentIndex = (int) (y/cellHeight);                if(lastIndex != currentIndex){                    //判断计算出来的索引值,避免数组越界                    if(0 <= currentIndex && currentIndex < LETTERS.length){                        String letter = LETTERS[currentIndex];                        if(onLetterUpdateListener != null){                            onLetterUpdateListener.onLetterUpdate(letter);                        }                        //记录上次按下的索引                        lastIndex = currentIndex;                    }                }                break;            case MotionEvent.ACTION_MOVE:                y = event.getY();                //根据y 计算当前按下的字母索引                //例如:y = 21,cellHeight = 10 =>(int) (y/cellHeight)= 2                currentIndex = (int) (y/cellHeight);                //判断计算出来的索引值,避免数组越界                if(0 <= currentIndex && currentIndex < LETTERS.length){                    String letter = LETTERS[currentIndex];                    if(onLetterUpdateListener != null){                        onLetterUpdateListener.onLetterUpdate(letter);                    }                    //记录上次按下的索引                    lastIndex = currentIndex;                }                break;            case MotionEvent.ACTION_UP:                //手指抬起时需要将记录的值设为-1,否则再次按下该字母不会弹出toast                lastIndex = -1;                break;            default:                break;        }        //事件已被处理,返回true        return true;    }
  • 第14-16 调用回调接口方法,通知外面当前触摸的字母
  • 第30-32 调用回调接口方法,通知外面当前触摸的字母

主Activity 中给QuickIndexBar 设置回调监听

protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        QuickIndexBar quickBar = (QuickIndexBar) findViewById(R.id.quick_bar);        quickBar.setOnLetterUpdateListener(new OnLetterUpdateListener() {            @Override            public void onLetterUpdate(String letter) {                Utils.showToast(getApplicationContext(), letter);            }        });    }

5. 根据拼音排序

5.1.1 创建PinyinUtil.java

GitHub上有可以将汉字转拼音的开源项目,TinyPinyin,pinyin4j

  • TinyPinyin:https://github.com/promeG/TinyPinyin
  • pinyin4j:https://github.com/belerweb/pinyin4j

汉字转拼音需要导入pinyin4j-2.5.0.jar

public class PinyinUtil {        /**         * 根据指定的汉字字符串, 返回其对应的拼音         * @param string         * @return         */        public static String getPinyin(String string) {            // 黑-> HEI 马-> MA            // 黑马*&^*            // 黑123dfasdf 马            HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();            // 不需要音标            format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);            // 设置转换出大写字母            format.setCaseType(HanyuPinyinCaseType.UPPERCASE);            char[] charArray = string.toCharArray();            StringBuilder sb = new StringBuilder();            for (int i = 0; i < charArray.length; i++) {                char c = charArray[i];                // 如果是空格, 跳过当前循环                if(Character.isWhitespace(c)){                    continue;                }                if(c >= -128 && c < 127){                    // 不可能是汉字, 直接拼接                    sb.append(c);                }else {                    try {                        // 获取某个字符对应的拼音. 可以获取到多音字. 单->DAN, SHAN                        String s = PinyinHelper.toHanyuPinyinStringArray(c, format)[0];                        sb.append(s);                    } catch (BadHanyuPinyinOutputFormatCombination e) {                        e.printStackTrace();                    }                }            }            return sb.toString();        }    }

5.1.2 填充ListView

用于填充ListView 的姓名数组

public class Cheeses {        public static final String[] NAMES = new String[] { "宋江", "卢俊义", "吴用",                "公孙胜", "关胜", "林冲", "秦明", "呼延灼", "花荣", "柴进", "李应", "朱仝", "鲁智                深",                "武松", "董平", "张清", "杨志", "徐宁", "索超", "戴宗", "刘唐", "李逵", "史进", "                穆弘",                "雷横", "李俊", "阮小二", "张横", "阮小五", " 张顺", "阮小七", "杨雄", "石秀", "                解珍",                " 解宝", "燕青", "朱武", "黄信", "孙立", "宣赞", "郝思文", "韩滔", "彭玘", "单廷珪                ",                "魏定国", "萧让", "裴宣", "欧鹏", "邓飞", " 燕顺", "杨林", "凌振", "蒋敬", "吕方                ",                "郭盛", "安道全", "皇甫端", "王英", "扈三娘", "鲍旭", "樊瑞", "孔明", "孔亮", "                项充",                "李衮", "金大坚", "马麟", "童威", "童猛", "孟康", "侯健", "陈达", "杨春", "郑天寿                ",                "陶宗旺", "宋清", "乐和", "龚旺", "丁得孙", "穆春", "曹正", "宋万", "杜迁", "薛永                ", "施恩",                "周通", "李忠", "杜兴", "汤隆", "邹渊", "邹润", "朱富", "朱贵", "蔡福", "蔡庆", "                李立",                "李云", "焦挺", "石勇", "孙新", "顾大嫂", "张青", "孙二娘", " 王定六", "郁保四", "                白胜",                "时迁", "段景柱" };    }

将姓名转化为HaoHan 对象

public class HaoHan implements Comparable<HaoHan>{        private String name;        private String pinyin;        public HaoHan(String name) {            super();            this.name = name;            this.pinyin = PinyinUtil.getPinyin(name);        }        public String getName() {            return name;        }        public void setName(String name) {            this.name = name;        }        public String getPinyin() {            return pinyin;        }        public void setPinyin(String pinyin) {            this.pinyin = pinyin;        }        @Override        public int compareTo(HaoHan another) {            return this.pinyin.compareTo(another.pinyin);        }    }

第24-27 行实现Comparable 接口,用于排序操作
activity_main.xml 中添加ListView

<RelativeLayout    xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent">    <ListView        android:id="@+id/lv"        android:layout_width="match_parent"        android:layout_height="match_parent">    </ListView>    <com.example.quickindexer.widget.QuickIndexBar        android:id="@+id/quick_bar"        android:layout_width="30dp"        android:layout_height="match_parent"        android:layout_alignParentRight="true"        android:background="#ff0000"/></RelativeLayout>

为ListView 创建数据适配器
ListView 条目的布局item_person.xml,每一个条目上都加上显示首字母的TextView

<?xml version="1.0" encoding="utf-8"?><LinearLayout    xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:orientation="vertical">    <TextView        android:id="@+id/tv_index"        android:layout_width="match_parent"        android:layout_height="40dp"        android:background="#666666"        android:gravity="center_vertical"        android:paddingLeft="15dp"        android:text="A"        android:textColor="#FFFFFF"        android:textSize="18sp"/>    <TextView        android:id="@+id/tv_name"        android:layout_width="match_parent"        android:layout_height="50dp"        android:gravity="center_vertical"        android:paddingLeft="15dp"        android:text="宋江"        android:textSize="22sp"/></LinearLayout>

数据适配器代码

public class HaoHanAdapter extends BaseAdapter {        private ArrayList<HaoHan> persons = new ArrayList<HaoHan>();        private final Context context;        public HaoHanAdapter(ArrayList<HaoHan> persons, Context context) {            super();            this.persons = persons;            this.context = context;        }        @Override        public int getItemViewType(int position) {            // TODO Auto-generated method stub            return super.getItemViewType(position);        }        @Override        public int getViewTypeCount() {            // TODO Auto-generated method stub            return super.getViewTypeCount();        }        @Override        public int getCount() {            return persons.size();        }        @Override        public View getView(int position, View convertView, ViewGroup parent) {            View view;            if(convertView == null){                view = View.inflate(context, R.layout.item_person, null);            }else {                view = convertView;            }            TextView tv_index = (TextView) view.findViewById(R.id.tv_index);            TextView tv_name = (TextView) view.findViewById(R.id.tv_name);            HaoHan haoHan = persons.get(position);            // 当前首字母            String currentStr = haoHan.getPinyin().charAt(0) + "";            tv_index.setText(currentStr);            tv_name.setText(haoHan.getName());            return view;        }        @Override        public Object getItem(int position) {            return null;        }        @Override        public long getItemId(int position) {            return 0;        }    }

6. 根据首字母分组

6.1 修改数据适配器的getView()方法

 @Override    public View getView(int position, View convertView, ViewGroup parent) {        View view;        if(convertView == null){            view = View.inflate(context, R.layout.item_person, null);        }else {            view = convertView;        }        TextView tv_index = (TextView) view.findViewById(R.id.tv_index);        TextView tv_name = (TextView) view.findViewById(R.id.tv_name);        HaoHan haoHan = persons.get(position);        // 当前首字母        String currentStr = haoHan.getPinyin().charAt(0) + "";        String indexStr = null;        // 如果是第一个, 直接显示        if(position == 0){            indexStr = currentStr;        }else {            // 判断当前首字母和上一个条目的首字母是否一致, 不一致时候显示.            String lastStr = persons.get(position - 1).getPinyin().charAt(0) + "";            if(!TextUtils.equals(lastStr, currentStr)){                // 不一致时候赋值indexStr                indexStr = currentStr;            }        }        tv_index.setVisibility(indexStr != null ? View.VISIBLE : View.GONE);        tv_index.setText(currentStr);        tv_name.setText(haoHan.getName());        return view;    }

第16-32 行如果是第一行直接显示首字母条目,如果不是第一行判断当前首字母和上一个条目的首字母是否一致, 不一致时候显示,一致则隐藏

7. ListView 和自定义控件结合

修改回调接口代码,for 循环persons 集合找到与传回来的letter 值相同的索引值,ListView 直接滚动到对应位置即可

QuickIndexBar quickBar = (QuickIndexBar) findViewById(R.id.quick_bar);    quickBar.setOnLetterUpdateListener(new OnLetterUpdateListener() {        @Override        public void onLetterUpdate(String letter) {            Utils.showToast(MainActivity.this, letter);            for (int i = 0; i < persons.size(); i++) {                String l = persons.get(i).getPinyin().charAt(0) + "";                if(TextUtils.equals(letter, l)){                    // 找到第一个首字母是letter 条目.                    lv.setSelection(i);                    break;                }            }        }    });

8. 细节优化置

置为当前选中的字母

public boolean onTouchEvent(MotionEvent event) {        float y ;        int currentIndex;        switch (event.getAction()) {            case MotionEvent.ACTION_DOWN:                y = event.getY();                //根据y 计算当前按下的字母索引                //例如:y = 21,cellHeight = 10 =>(int) (y/cellHeight)= 2                currentIndex = (int) (y/cellHeight);                if(lastIndex != currentIndex){                    //判断计算出来的索引值,避免数组越界                    if(0 <= currentIndex && currentIndex < LETTERS.length){                        String letter = LETTERS[currentIndex];                        if(onLetterUpdateListener != null){                            onLetterUpdateListener.onLetterUpdate(letter);                        }                        //记录上次按下的索引                        lastIndex = currentIndex;                    }                }                break;            case MotionEvent.ACTION_MOVE:                y = event.getY();                //根据y 计算当前按下的字母索引                //例如:y = 21,cellHeight = 10 =>(int) (y/cellHeight)= 2                currentIndex = (int) (y/cellHeight);                //判断计算出来的索引值,避免数组越界                if(0 <= currentIndex && currentIndex < LETTERS.length){                    String letter = LETTERS[currentIndex];                    if(onLetterUpdateListener != null){                        onLetterUpdateListener.onLetterUpdate(letter);                    }                    //记录上次按下的索引                    lastIndex = currentIndex;                }                break;            case MotionEvent.ACTION_UP:                //手指抬起时需要将记录的值设为-1,否则再次按下该字母不会弹出toast                lastIndex = -1;                break;            default:                break;        }        //重绘一次界面,会再次调用onDraw()方法,通过判断lastIndex 的值,把按下的字母置为灰色        invalidate();        //事件已被处理,返回true        return true;    }

第46 行为新增代码,此时lastIndex 为当前按下的字母索引,调用一次invalidate()方法,会再次调用onDraw()方法,在onDraw()方法中通过判断lastIndex 的值,把当前按下的字母置为灰色

protected void onDraw(Canvas canvas) {        super.onDraw(canvas);        //遍历字母数组,计算坐标,进行绘制        for (int i = 0; i < LETTERS.length; i++) {            String letter = LETTERS[i];            //计算x 坐标            float x = cellWidth*0.5f - paint.measureText(letter)*0.5f;            //计算y 坐标            Rect bounds = new Rect();            //获取文本的矩形区域            paint.getTextBounds(letter, 0, letter.length(), bounds);            float y = cellHeight*0.5f + bounds.height()+ i*cellHeight;            //把当前选中的字母置为灰色            if(lastIndex == i){                paint.setColor(Color.GRAY);            }else{                paint.setColor(Color.WHITE);            }            //绘制文本            canvas.drawText(letter, x, y, paint);        }    }

第13-18 行如果当前字母的索引是选中的,则将画笔颜色改为灰色,没有选中的将画笔改为白色

把回调方法中Toast 的显示方式改为TextView 显示

<RelativeLayout    xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent">    <ListView        android:id="@+id/lv"        android:layout_width="match_parent"        android:layout_height="match_parent">    </ListView>    <TextView        android:id="@+id/tv_center"        android:layout_width="160dp"        android:layout_height="100dp"        android:layout_centerInParent="true"        android:background="@drawable/shape_tv_center"        android:gravity="center"        android:text="A"        android:textColor="#ffffff"        android:textSize="32sp"        android:visibility="gone"/>    <com.example.quickindexer.widget.QuickIndexBar        android:id="@+id/quick_bar"        android:layout_width="30dp"        android:layout_height="match_parent"        android:layout_alignParentRight="true"        android:background="#ff0000"/></RelativeLayout>
  • 第11-21 行在屏幕中间添加一个提示框,默认情况为不显示
  • 第16 行提示框的背景文件,需要在res 下新建一个drawable 文件夹,将shape_tv_center.xml 放在此文件夹下
<?xml version="1.0" encoding="utf-8"?><shape xmlns:android="http://schemas.android.com/apk/res/android"       android:shape="rectangle" >    <!-- android:shape="rectangle" 该形状为矩形-->    <solid android:color="#66000000" /><!--填充颜色-->    <corners android:radius="20dp" /><!--圆角半径--></shape>

修改回调接口中字母提示方式

//屏幕中间的提示框    tvCenter = (TextView) findViewById(R.id.tv_center);    quickBar.setOnLetterUpdateListener(new OnLetterUpdateListener() {        @Override        public void onLetterUpdate(String letter) {            //Utils.showToast(MainActivity.this, letter);            //将Toast 改成文本提示框            showLetter(letter);            for (int i = 0; i < persons.size(); i++) {                String l = persons.get(i).getPinyin().charAt(0) + "";                if(TextUtils.equals(letter, l)){                    // 找到第一个首字母是letter 条目.                    lv.setSelection(i);                    break;                }            }        }    });    private Handler mHandler = new Handler();    /**     * 在屏幕中间显示一个字母提示     * @param letter     */    private void showLetter(String letter) {        tvCenter.setText(letter);        tvCenter.setVisibility(View.VISIBLE);        //移除所有的消息及任务        mHandler.removeCallbacksAndMessages(null);        //用消息机制延迟隐藏提示框        mHandler.postDelayed(new Runnable() {            @Override            public void run() {                //2 秒后隐藏提示框                tvCenter.setVisibility(View.GONE);            }        }, 2000);    }
  • 第6-8 行将Toast 提示改为文本框提示
  • 第28-36 行用消息机制延迟隐藏文本提示框,如果快速滑动showLetter()方法会频繁调用,会有多个延迟任务在消息队列中,其实当前延迟任务之前的任务都是没有必要执行的,所以可以先移除队列中所有的任务再将本次任务添加到队列中

QuickIndexBar

/** * 快速索引 *  * 用于根据字母快速定位联系人 * @author AllenIverson * */public class QuickIndexBar extends View {    private static final 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"};    private static final String TAG = "TAG";    private Paint mPaint;    private int cellWidth;    private float cellHeight;    /**     * 暴露一个字母的监听     */    public interface OnLetterUpdateListener{        void onLetterUpdate(String letter);    }    private OnLetterUpdateListener listener;    public OnLetterUpdateListener getListener() {        return listener;    }    /**     * 设置字母更新监听     * @param listener     */    public void setListener(OnLetterUpdateListener listener) {        this.listener = listener;    }    public QuickIndexBar(Context context) {        this(context, null);    }    public QuickIndexBar(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    public QuickIndexBar(Context context, AttributeSet attrs, int defStyle) {        super(context, attrs, defStyle);        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);        mPaint.setColor(Color.WHITE);        mPaint.setTypeface(Typeface.DEFAULT_BOLD);    }    @Override    protected void onDraw(Canvas canvas) {        for (int i = 0; i < LETTERS.length; i++) {            String text = LETTERS[i];            // 计算坐标            int x = (int) (cellWidth / 2.0f - mPaint.measureText(text) / 2.0f);            // 获取文本的高度            Rect bounds = new Rect();// 矩形            mPaint.getTextBounds(text, 0, text.length(), bounds);            int textHeight = bounds.height();            int y = (int) (cellHeight / 2.0f + textHeight / 2.0f + i * cellHeight);            // 根据按下的字母, 设置画笔颜色            mPaint.setColor(touchIndex == i ? Color.GRAY : Color.WHITE);            // 绘制文本A-Z            canvas.drawText(text, x, y, mPaint);        }    }    int touchIndex = -1;    @Override    public boolean onTouchEvent(MotionEvent event) {        int index = -1;        switch (MotionEventCompat.getActionMasked(event)) {            case MotionEvent.ACTION_DOWN:                // 获取当前触摸到的字母索引                index = (int) (event.getY() / cellHeight);                if(index >= 0 && index < LETTERS.length){                    // 判断是否跟上一次触摸到的一样                    if(index != touchIndex) {                        if(listener != null){                            listener.onLetterUpdate(LETTERS[index]);                        }                        Log.d(TAG, "onTouchEvent: " + LETTERS[index]);                        touchIndex = index;                    }                }                break;            case MotionEvent.ACTION_MOVE:                index = (int) (event.getY() / cellHeight);                if(index >= 0 && index < LETTERS.length){                    // 判断是否跟上一次触摸到的一样                    if(index != touchIndex){                        if(listener != null){                            listener.onLetterUpdate(LETTERS[index]);                        }                        Log.d(TAG, "onTouchEvent: " + LETTERS[index]);                        touchIndex = index;                    }                }                break;            case MotionEvent.ACTION_UP:                touchIndex = -1;                break;            default:                break;        }        invalidate();        return true;    }    @Override    protected void onSizeChanged(int w, int h, int oldw, int oldh) {        super.onSizeChanged(w, h, oldw, oldh);        // 获取单元格的宽和高        cellWidth = getMeasuredWidth();        int mHeight = getMeasuredHeight();        cellHeight = mHeight * 1.0f / LETTERS.length;    }}

MainActivity

public class MainActivity extends Activity {    private ListView mMainList;    private ArrayList<Person> persons;    private TextView tv_center;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        QuickIndexBar bar = (QuickIndexBar) findViewById(R.id.bar);        // 设置监听        bar.setListener(new OnLetterUpdateListener() {            @Override            public void onLetterUpdate(String letter) {//              Utils.showToast(getApplicationContext(), letter);                showLetter(letter);                // 根据字母定位ListView, 找到集合中第一个以letter为拼音首字母的对象,得到索引                for (int i = 0; i < persons.size(); i++) {                    Person person = persons.get(i);                    String l = person.getPinyin().charAt(0) + "";                    if(TextUtils.equals(letter, l)){                        // 匹配成功                        mMainList.setSelection(i);                        break;                    }                }            }        });        mMainList = (ListView) findViewById(R.id.lv_main);        persons = new ArrayList<Person>();        // 填充数据 , 排序        fillAndSortData(persons);        mMainList.setAdapter(new HaoHanAdapter(MainActivity.this , persons));        tv_center = (TextView) findViewById(R.id.tv_center);    }    private Handler mHandler = new Handler();    /**     * 显示字母     * @param letter     */    protected void showLetter(String letter) {        tv_center.setVisibility(View.VISIBLE);        tv_center.setText(letter);        mHandler.removeCallbacksAndMessages(null);        mHandler.postDelayed(new Runnable() {            @Override            public void run() {                tv_center.setVisibility(View.GONE);                         }        }, 2000);    }    private void fillAndSortData(ArrayList<Person> persons) {        // 填充数据        for (int i = 0; i < Cheeses.NAMES.length; i++) {            String name = Cheeses.NAMES[i];            persons.add(new Person(name));        }        // 进行排序        Collections.sort(persons);    }}

HaoHanAdapter

public class HaoHanAdapter extends BaseAdapter {    private Context mContext;    private ArrayList<Person> persons;    public HaoHanAdapter(Context mContext, ArrayList<Person> persons) {        this.mContext = mContext;        this.persons = persons;    }    @Override    public int getCount() {        // TODO Auto-generated method stub        return persons.size();    }    @Override    public Object getItem(int position) {        return persons.get(position);    }    @Override    public long getItemId(int position) {        return position;    }    @Override    public View getView(int position, View convertView, ViewGroup parent) {        View view = convertView;        if(convertView == null){            view = view.inflate(mContext, R.layout.item_list, null);        }        ViewHolder mViewHolder = ViewHolder.getHolder(view);        Person p = persons.get(position);        String str = null;        String currentLetter = p.getPinyin().charAt(0) + "";        // 根据上一个首字母,决定当前是否显示字母        if(position == 0){            str = currentLetter;        }else {            // 上一个人的拼音的首字母            String preLetter = persons.get(position - 1).getPinyin().charAt(0) + "";            if(!TextUtils.equals(preLetter, currentLetter)){                str = currentLetter;            }        }        // 根据str是否为空,决定是否显示索引栏        mViewHolder.mIndex.setVisibility(str == null ? View.GONE : View.VISIBLE);        mViewHolder.mIndex.setText(currentLetter);        mViewHolder.mName.setText(p.getName());        return view;    }    static class ViewHolder {        TextView mIndex;        TextView mName;        public static ViewHolder getHolder(View view) {            Object tag = view.getTag();            if(tag != null){                return (ViewHolder)tag;            }else {                ViewHolder viewHolder = new ViewHolder();                viewHolder.mIndex = (TextView) view.findViewById(R.id.tv_index);                viewHolder.mName = (TextView) view.findViewById(R.id.tv_name);                view.setTag(viewHolder);                return viewHolder;            }        }    }}

FancyListIndexer

public class FancyIndexer extends View {    public interface OnTouchLetterChangedListener {        public void onTouchLetterChanged(String s);    }    private static final String TAG = "FancyIndexer";    /////////////////////////////////////////////////////////////////////////    //Properties    // 向右偏移多少画字符, default 30    float mWidthOffset = 30.0f;    // 最小字体大小    int mMinFontSize = 24;    // 最大字体大小    int mMaxFontSize = 48;    // 提示字体大小    int mTipFontSize = 52;    // 提示字符的额外偏移    float mAdditionalTipOffset = 20.0f;    // 贝塞尔曲线控制的高度    float mMaxBezierHeight = 150.0f;    // 贝塞尔曲线单侧宽度    float mMaxBezierWidth = 240.0f;    // 贝塞尔曲线单侧模拟线量    int  mMaxBezierLines = 32;    // 列表字符颜色    int  mFontColor = 0xffffffff;    // 提示字符颜色//  int  mTipFontColor = 0xff3399ff;    int  mTipFontColor = 0xffd33e48;    /////////////////////////////////////////////////////////////////////////    private OnTouchLetterChangedListener mListener;    private final String[] ConstChar = {"#","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"};    int mChooseIndex = -1;    Paint mPaint = new Paint();    PointF mTouch = new PointF();    PointF[] mBezier1;    PointF[] mBezier2;    float mLastOffset[] = new float[ConstChar.length]; // 记录每一个字母的x方向偏移量, 数字<=0    PointF mLastFucusPostion = new PointF();    Scroller mScroller;    boolean mAnimating = false;    float mAnimationOffset;    boolean mHideAnimation = false;    int mAlpha = 255;     Handler mHideWaitingHandler = new Handler() {        @Override        public void handleMessage(Message msg) {            if( msg.what == 1 )            {//              mScroller.startScroll(0, 0, 255, 0, 1000);                mHideAnimation = true;                mAnimating = false;                FancyIndexer.this.invalidate();                return;            }            super.handleMessage(msg);        }    };    public FancyIndexer(Context context, AttributeSet attrs, int defStyle) {        super(context, attrs, defStyle);        initData(context, attrs);    }    public FancyIndexer(Context context, AttributeSet attrs) {        super(context, attrs);        initData(context, attrs);    }    public FancyIndexer(Context context) {        super(context);        initData(null, null);    }    private void initData(Context context, AttributeSet attrs) {        if( context != null && attrs != null ) {            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FancyIndexer, 0, 0);            mWidthOffset    = a.getDimension(R.styleable.FancyIndexer_widthOffset, mWidthOffset);            mMinFontSize    = a.getInteger(R.styleable.FancyIndexer_minFontSize, mMinFontSize);            mMaxFontSize    = a.getInteger(R.styleable.FancyIndexer_maxFontSize, mMaxFontSize);            mTipFontSize    = a.getInteger(R.styleable.FancyIndexer_tipFontSize, mTipFontSize);            mMaxBezierHeight = a.getDimension(R.styleable.FancyIndexer_maxBezierHeight, mMaxBezierHeight);            mMaxBezierWidth = a.getDimension(R.styleable.FancyIndexer_maxBezierWidth, mMaxBezierWidth);                     mMaxBezierLines =  a.getInteger(R.styleable.FancyIndexer_maxBezierLines, mMaxBezierLines);            mAdditionalTipOffset = a.getDimension(R.styleable.FancyIndexer_additionalTipOffset, mAdditionalTipOffset);            mFontColor = a.getColor(R.styleable.FancyIndexer_fontColor, mFontColor);            mTipFontColor = a.getColor(R.styleable.FancyIndexer_tipFontColor, mTipFontColor);            a.recycle();        }        mScroller = new Scroller( getContext() );        mTouch.x = 0;        mTouch.y = -10*mMaxBezierWidth;        mBezier1 = new PointF[mMaxBezierLines];        mBezier2 = new PointF[mMaxBezierLines];        calculateBezierPoints();    }    @Override    protected void onDraw(Canvas canvas) {        // 控件宽高        int height = getHeight();        int width = getWidth();        // 单个字母高度        float singleHeight = height / (float)ConstChar.length;        int workHeight = 0;        if( mAlpha == 0 )            return;        mPaint.reset();        int saveCount = 0;        if( mHideAnimation )        {            saveCount = canvas.save();            canvas.saveLayerAlpha( 0, 0, width, height, mAlpha, Canvas.ALL_SAVE_FLAG );        }        for(int i=0;i<ConstChar.length;i++) {           mPaint.setColor(mFontColor);           mPaint.setAntiAlias(true);           float xPos = width - mWidthOffset;           float yPos = workHeight + singleHeight/2;           //float adjustX = adjustXPos( yPos, i == mChooseIndex );           // 根据当前字母y的位置计算得到字体大小           int fontSize = adjustFontSize(i, yPos );            mPaint.setTextSize(fontSize);           // 添加一个字母的高度           workHeight += singleHeight;           // 绘制字母           drawTextInCenter(canvas, ConstChar[i], xPos + ajustXPosAnimation(i,  yPos )  , yPos );           // 绘制的字母和当前触摸到的一致, 绘制红色被选中字母           if(i == mChooseIndex) {               mPaint.setColor( mTipFontColor );               mPaint.setFakeBoldText(true);               mPaint.setTextSize( mTipFontSize );               yPos = mTouch.y;               float pos = 0;               if( mAnimating || mHideAnimation ) {                   pos = mLastFucusPostion.x;                   yPos = mLastFucusPostion.y;               } else {                   pos = xPos + ajustXPosAnimation(i, yPos ) - mAdditionalTipOffset;                   mLastFucusPostion.x = pos;                   mLastFucusPostion.y = yPos;               }               drawTextInCenter(canvas, ConstChar[i], pos, yPos );//             mPaint.setStrokeWidth(5);//             canvas.drawLine(0, yPos, width, yPos, mPaint);           }           mPaint.reset();        }        if( mHideAnimation )         {            canvas.restoreToCount(saveCount);        }    }    /**     * @param canvas 画板     * @param string 被绘制的字母     * @param xCenter 字母的中心x方向位置     * @param yCenter 字母的中心y方向位置     */    private void drawTextInCenter(Canvas canvas, String string, float xCenter, float yCenter) {        FontMetrics fm = mPaint.getFontMetrics();        //float fontWidth = paint.measureText(string);        float fontHeight = mPaint.getFontSpacing();        float drawY = yCenter + fontHeight/2 - fm.descent;        if( drawY < -fm.ascent -fm.descent )            drawY = -fm.ascent -fm.descent;        if( drawY > getHeight() )            drawY = getHeight() ;        mPaint.setTextAlign(Align.CENTER);        canvas.drawText(string, xCenter, drawY, mPaint);    }    private int adjustFontSize(int i, float yPos ) {        // 根据水平方向偏移量计算出一个放大的字号        float adjustX = Math.abs(ajustXPosAnimation(i,  yPos ));        int adjustSize =(int)( (mMaxFontSize - mMinFontSize ) * adjustX / (float)mMaxBezierHeight) + mMinFontSize;        return adjustSize;    }    /**     * x 方向的向左偏移量     * @param i 当前字母的索引     * @param yPos y方向的初始位置     * @return     */    private float ajustXPosAnimation (int i, float yPos ) {        float offset ;        if( this.mAnimating || this.mHideAnimation ) {            // 正在动画中或在做隐藏动画            offset = mLastOffset[i];            if( offset !=0.0f ) {                offset += this.mAnimationOffset;                if( offset > 0)                    offset = 0;            }        } else {            // 根据当前字母y方向位置, 计算水平方向偏移量            offset = adjustXPos( yPos );            // 当前触摸的x方向位置            float xPos = mTouch.x  ;            float width = getWidth() - mWidthOffset;            width = width - 60;            // 字母绘制时向左偏移量 进行修正, offset需要是<=0的值            if( offset != 0.0f  && xPos > width )                offset +=  ( xPos - width );            if( offset > 0)                offset = 0;            mLastOffset[i] = offset;        }        return offset;    }    private float adjustXPos(float yPos ) {        float dis = yPos - mTouch.y; // 字母y方向位置和触摸时y值坐标的差值, 距离越小, 得到的水平方向偏差越大         if( dis > -mMaxBezierWidth  && dis < mMaxBezierWidth ) {            // 在2个贝赛尔曲线宽度范围以内 (一个贝赛尔曲线宽度是指一个山峰的一边)            // 第一段 曲线            if( dis > mMaxBezierWidth/4 ) {                for( int i = mMaxBezierLines-1; i>0 ; i-- ) {                    // 从下到上, 逐个计算                    if( dis == -mBezier1[i].y ) // 落在点上                        return mBezier1[i].x;                    // 如果距离dis落在两个贝塞尔曲线模拟点之间, 通过三角函数计算得到当前dis对应的x方向偏移量                    if( dis > -mBezier1[i].y && dis < -mBezier1[i-1].y ) {                        return (dis + mBezier1[i].y) * ( mBezier1[i-1].x - mBezier1[i].x ) / ( -mBezier1[i-1].y + mBezier1[i].y ) + mBezier1[i].x;                    }                }                return mBezier1[0].x;            }            // 第三段 曲线, 和第一段曲线对称            if( dis < -mMaxBezierWidth/4 ) {                for( int i = 0; i< mMaxBezierLines-1; i++ ) {                    // 从上到下                    if( dis == mBezier1[i].y ) // 落在点上                        return mBezier1[i].x;                    // 如果距离dis落在两个贝塞尔曲线模拟点之间, 通过三角函数计算得到当前dis对应的x方向偏移量                    if( dis > mBezier1[i].y && dis < mBezier1[i+1].y ) {                        return (dis - mBezier1[i].y )* (mBezier1[i+1].x - mBezier1[i].x ) / ( mBezier1[i+1].y - mBezier1[i].y ) + mBezier1[i].x;                    }                }                return mBezier1[mMaxBezierLines-1].x;            }            // 第二段 峰顶曲线            for( int i = 0; i< mMaxBezierLines-1; i++ ) {                if( dis == mBezier2[i].y )                    return mBezier2[i].x;                // 如果距离dis落在两个贝塞尔曲线模拟点之间, 通过三角函数计算得到当前dis对应的x方向偏移量                if( dis > mBezier2[i].y && dis < mBezier2[i+1].y ) {                    return ( dis - mBezier2[i].y) * ( mBezier2[i+1].x - mBezier2[i].x ) / (mBezier2[i+1].y - mBezier2[i].y ) + mBezier2[i].x;                }            }               return mBezier2[mMaxBezierLines-1].x;               }        return 0.0f;    }    @Override    public boolean dispatchTouchEvent(MotionEvent event) {        final int action = event.getAction();        final float y = event.getY();        final int oldmChooseIndex = mChooseIndex;        final OnTouchLetterChangedListener listener = mListener;        final int c = (int) (y/getHeight()*ConstChar.length);        switch (action) {            case MotionEvent.ACTION_DOWN:                if( this.getWidth() > mWidthOffset ) {                    if ( event.getX() < this.getWidth() - mWidthOffset )                        return false;                }                mHideWaitingHandler.removeMessages(1);                mScroller.abortAnimation();                mAnimating = false;                mHideAnimation = false;                mAlpha = 255;                mTouch.x = event.getX();                mTouch.y = event.getY();                if(oldmChooseIndex != c && listener != null){                    if(c > 0 && c< ConstChar.length){                        listener.onTouchLetterChanged(ConstChar[c]);                        mChooseIndex = c;                    }                }                invalidate();                               break;            case MotionEvent.ACTION_MOVE:                mTouch.x = event.getX();                mTouch.y = event.getY();                invalidate();                if(oldmChooseIndex != c && listener != null){                    if(c >= 0 && c< ConstChar.length){                        listener.onTouchLetterChanged(ConstChar[c]);                        mChooseIndex = c;                    }                }                break;            case MotionEvent.ACTION_UP:            case MotionEvent.ACTION_CANCEL:                mTouch.x = event.getX();                mTouch.y = event.getY();                //this.mChooseIndex = -1;                mScroller.startScroll(0, 0, (int)mMaxBezierHeight, 0, 2000);                mAnimating = true;                postInvalidate();                break;        }        return true;    }    @Override    public void computeScroll() {        super.computeScroll();        if (mScroller.computeScrollOffset()) {            if( mAnimating ) {                float x = mScroller.getCurrX();                mAnimationOffset = x;                           } else if( mHideAnimation ) {                mAlpha = 255 - (int) mScroller.getCurrX();            }            invalidate();        } else if( mScroller.isFinished() ) {            if( mAnimating ) {                mHideWaitingHandler.sendEmptyMessage(1);            } else if( mHideAnimation ) {                mHideAnimation = false;                this.mChooseIndex = -1;                mTouch.x = -10000;                mTouch.y = -10000;            }        }    }    public void setOnTouchLetterChangedListener( OnTouchLetterChangedListener listener) {        this.mListener = listener;    }    /**     * 计算出所有贝塞尔曲线上的点      * 个数为 mMaxBezierLines * 2 = 64     */    private void calculateBezierPoints() {        PointF mStart = new PointF();   // 开始点        PointF mEnd = new PointF();     // 结束点        PointF mControl = new PointF(); // 控制点        // 计算第一段红色部分 贝赛尔曲线的点        // 开始点        mStart.x = 0.0f;        mStart.y = -mMaxBezierWidth;        // 控制点        mControl.x = 0.0f;        mControl.y = -mMaxBezierWidth/2;        // 结束点        mEnd.x = - mMaxBezierHeight / 2;        mEnd.y = - mMaxBezierWidth / 4;        mBezier1[0] = new PointF();        mBezier1[mMaxBezierLines-1] = new PointF();        mBezier1[0].set(mStart);        mBezier1[mMaxBezierLines-1].set(mEnd);        for( int i = 1; i< mMaxBezierLines -1; i++ ) {            mBezier1[i] = new PointF();            mBezier1[i].x = calculateBezier( mStart.x, mEnd.x, mControl.x, i / (float) mMaxBezierLines );            mBezier1[i].y = calculateBezier( mStart.y, mEnd.y, mControl.y, i / (float) mMaxBezierLines );        }        // 计算第二段蓝色部分 贝赛尔曲线的点        mStart.y = -mMaxBezierWidth / 4;        mStart.x = -mMaxBezierHeight / 2;        mControl.y = 0.0f;        mControl.x = -mMaxBezierHeight;        mEnd.y = mMaxBezierWidth / 4;        mEnd.x = -mMaxBezierHeight / 2;        mBezier2[0] = new PointF();        mBezier2[mMaxBezierLines-1] = new PointF();        mBezier2[0].set(mStart);        mBezier2[mMaxBezierLines-1].set(mEnd);        for( int i = 1; i< mMaxBezierLines -1 ; i++ ) {            mBezier2[i]= new PointF();            mBezier2[i].x = calculateBezier( mStart.x, mEnd.x, mControl.x,  i / (float) mMaxBezierLines );            mBezier2[i].y = calculateBezier( mStart.y, mEnd.y, mControl.y,  i / (float) mMaxBezierLines );        }    }    /**     * 贝塞尔曲线核心算法     * @param start     * @param end     * @param control     * @param val     * @return     * 公式及动图, 维基百科: https://en.wikipedia.org/wiki/B%C3%A9zier_curve     * 中文可参考此网站: http://blog.csdn.net/likendsl/article/details/7852658     *      */    private float calculateBezier(float start, float end, float control, float val) {        float t = val;        float s = 1-t;        float ret = start * s * s + 2 * control * s * t + end * t * t;        return ret;    }   }
0 0
原创粉丝点击