Android 仿微信通讯录功能+字母索引+好友列表

一、效果图展示

a.gif

二、功能特点

1.好友排序:按照拼音顺序对好友进行排序,兼容英文数字符号等

2.字母索引:右侧字母导航条,既可拖动也可点击,联动ListView滑动

三、实现

接下来就让我们一步步显示这个效果吧。

1.右侧字母索引的导航条

这个我们可以在网上找到很多类似的,你大可找一个自己喜欢的甚至自己写一个出来,这里我在网上找了一个带波浪效果的,看起来比较炫酷一点吧。

这是原地址:https://github.com/AlexLiuSheng/AnimSideBar

然后我把它导入到了我们项目中并修改了部分代码,以下是我项目中的

SideBar.java

package com.afei.indexlistview; import android.content.Context;import android.content.res.TypedArray;import android.graphics.Canvas;import android.graphics.Paint;import android.util.AttributeSet;import android.view.MotionEvent;import android.widget.TextView; public class SideBar extends TextView {    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", "#"};    private Paint textPaint;    private Paint bigTextPaint;    private Paint scaleTextPaint;     private Canvas canvas;    private int itemH;    private int w;    private int h;    /**     * 普通情况下字体大小     */    float singleTextH;    /**     * 缩放离原始的宽度     */    private float scaleWidth;    /**     * 滑动的Y     */    private float eventY = 0;    /**     * 缩放的倍数     */    private int scaleSize = 1;    /**     * 缩放个数item,即开口大小     */    private int scaleItemCount = 6;    private ISideBarSelectCallBack callBack;     public SideBar(Context context) {        this(context, null);    }     public SideBar(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }     public SideBar(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        init(attrs);    }     private void init(AttributeSet attrs) {        if (attrs != null) {            TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.SideBar);            scaleSize = ta.getInteger(R.styleable.SideBar_scaleSize, 1);            scaleItemCount = ta.getInteger(R.styleable.SideBar_scaleItemCount, 6);            scaleWidth = ta.getDimensionPixelSize(R.styleable.SideBar_scaleWidth, dp(100));            ta.recycle();        }        textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);        textPaint.setColor(getCurrentTextColor());        textPaint.setTextSize(getTextSize());        textPaint.setTextAlign(Paint.Align.CENTER);        bigTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);        bigTextPaint.setColor(getCurrentTextColor());        bigTextPaint.setTextSize(getTextSize() * (scaleSize + 3));        bigTextPaint.setTextAlign(Paint.Align.CENTER);        scaleTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);        scaleTextPaint.setColor(getCurrentTextColor());        scaleTextPaint.setTextSize(getTextSize() * (scaleSize + 1));        scaleTextPaint.setTextAlign(Paint.Align.CENTER);    }     public void setDataResource(String[] data) {        letters = data;        invalidate();    }     public void setOnStrSelectCallBack(ISideBarSelectCallBack callBack) {        this.callBack = callBack;    }     /**     * 设置字体缩放比例     *     * @param scale     */    public void setScaleSize(int scale) {        scaleSize = scale;        invalidate();    }     /**     * 设置缩放字体的个数,即开口大小     *     * @param scaleItemCount     */    public void setScaleItemCount(int scaleItemCount) {        this.scaleItemCount = scaleItemCount;        invalidate();    }     private int dp(int px) {        final float scale = getContext().getResources().getDisplayMetrics().density;        return (int) (px * scale + 0.5f);    }     @Override    public boolean onTouchEvent(MotionEvent event) {        switch (event.getAction()) {            case MotionEvent.ACTION_DOWN:            case MotionEvent.ACTION_MOVE:                if (event.getX() > (w - getPaddingRight() - singleTextH - 10)) {                    eventY = event.getY();                    invalidate();                    return true;                } else {                    eventY = 0;                    invalidate();                    break;                }            case MotionEvent.ACTION_CANCEL:                eventY = 0;                invalidate();                return true;            case MotionEvent.ACTION_UP:                if (event.getX() > (w - getPaddingRight() - singleTextH - 10)) {                    eventY = 0;                    invalidate();                    return true;                } else                    break;        }        return super.onTouchEvent(event);    }      @Override    protected void onDraw(Canvas canvas) {        this.canvas = canvas;        DrawView(eventY);    }     private void DrawView(float y) {        int currentSelectIndex = -1;        if (y != 0) {            for (int i = 0; i < letters.length; i++) {                float currentItemY = itemH * i;                float nextItemY = itemH * (i + 1);                if (y >= currentItemY && y < nextItemY) {                    currentSelectIndex = i;                    if (callBack != null) {                        callBack.onSelectStr(currentSelectIndex, letters[i]);                    }                    //画大的字母                    Paint.FontMetrics fontMetrics = bigTextPaint.getFontMetrics();                    float bigTextSize = fontMetrics.descent - fontMetrics.ascent;                    canvas.drawText(letters[i], w - getPaddingRight() - scaleWidth - bigTextSize, singleTextH + itemH * i, bigTextPaint);                }            }        }        drawLetters(y, currentSelectIndex);    }     private void drawLetters(float y, int index) {        //第一次进来没有缩放情况,默认画原图        if (index == -1) {            w = getMeasuredWidth();            h = getMeasuredHeight();            itemH = h / letters.length;            Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();            singleTextH = fontMetrics.descent - fontMetrics.ascent;            for (int i = 0; i < letters.length; i++) {                canvas.drawText(letters[i], w - getPaddingRight(), singleTextH + itemH * i, textPaint);            }            //触摸的时候画缩放图        } else {            //遍历所有字母            for (int i = 0; i < letters.length; i++) {                //要画的字母的起始Y坐标                float currentItemToDrawY = singleTextH + itemH * i;                float centerItemToDrawY;                if (index < i)                    centerItemToDrawY = singleTextH + itemH * (index + scaleItemCount);                else                    centerItemToDrawY = singleTextH + itemH * (index - scaleItemCount);                float delta = 1 - Math.abs((y - currentItemToDrawY) / (centerItemToDrawY - currentItemToDrawY));                float maxRightX = w - getPaddingRight();                //如果大于0,表明在y坐标上方                scaleTextPaint.setTextSize(getTextSize() + getTextSize() * delta);                float drawX = maxRightX - scaleWidth * delta;                //超出边界直接花在边界上                if (drawX > maxRightX)                    canvas.drawText(letters[i], maxRightX, singleTextH + itemH * i, textPaint);                else                    canvas.drawText(letters[i], drawX, singleTextH + itemH * i, scaleTextPaint);            }        }    }     public interface ISideBarSelectCallBack {        void onSelectStr(int index, String selectStr);    } }

然后还有3个自定义的属性

<?xml version="1.0" encoding="utf-8"?><resources>    <declare-styleable name="SideBar">        <attr name="scaleSize" format="integer"/>        <attr name="scaleItemCount" format="integer"/>        <attr name="scaleWidth" format="dimension"/>    </declare-styleable></resources>

2.汉字转拼音工具类

我们知道,java中是没有提供接口和方法让我们直接将汉字转成拼音的。

这里,可以参见我的另一篇博客:Java/Android中汉字转拼音的两种方法,优劣比较

然后在此我选择了使用第三方jar包的方式,因为它体积不大而且更加准确。以下是我的

Cn2Spell.java

package com.afei.indexlistview; import net.sourceforge.pinyin4j.PinyinHelper;import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType;import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat;import net.sourceforge.pinyin4j.format.HanyuPinyinToneType; /** * 汉字转换位汉语拼音,英文字符不变 */public class Cn2Spell {     public static StringBuffer sb = new StringBuffer();     /**     * 获取汉字字符串的首字母,英文字符不变     * 例如:阿飞→af     */    public static String getPinYinHeadChar(String chines) {        sb.setLength(0);        char[] chars = chines.toCharArray();        HanyuPinyinOutputFormat defaultFormat = new HanyuPinyinOutputFormat();        defaultFormat.setCaseType(HanyuPinyinCaseType.LOWERCASE);        defaultFormat.setToneType(HanyuPinyinToneType.WITHOUT_TONE);        for (int i = 0; i < chars.length; i++) {            if (chars[i] > 128) {                try {                    sb.append(PinyinHelper.toHanyuPinyinStringArray(chars[i], defaultFormat)[0].charAt(0));                } catch (Exception e) {                    e.printStackTrace();                }            } else {                sb.append(chars[i]);            }        }        return sb.toString();    }     /**     * 获取汉字字符串的第一个字母     */    public static String getPinYinFirstLetter(String str) {        sb.setLength(0);        char c = str.charAt(0);        String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(c);        if (pinyinArray != null) {            sb.append(pinyinArray[0].charAt(0));        } else {            sb.append(c);        }        return sb.toString();    }     /**     * 获取汉字字符串的汉语拼音,英文字符不变     */    public static String getPinYin(String chines) {        sb.setLength(0);        char[] nameChar = chines.toCharArray();        HanyuPinyinOutputFormat defaultFormat = new HanyuPinyinOutputFormat();        defaultFormat.setCaseType(HanyuPinyinCaseType.LOWERCASE);        defaultFormat.setToneType(HanyuPinyinToneType.WITHOUT_TONE);        for (int i = 0; i < nameChar.length; i++) {            if (nameChar[i] > 128) {                try {                    sb.append(PinyinHelper.toHanyuPinyinStringArray(nameChar[i], defaultFormat)[0]);                } catch (Exception e) {                    e.printStackTrace();                }            } else {                sb.append(nameChar[i]);            }        }        return sb.toString();    } }

3.让你的好友可以根据拼音来排序

我们选择实现comparable接口,并重写comparaTo方法。以下是我的User.java

package com.afei.indexlistview; /** * Created by Administrator on 2016/5/25. */public class User implements Comparable<User> {     private String name; // 姓名    private String pinyin; // 姓名对应的拼音    private String firstLetter; // 拼音的首字母     public User() {    }     public User(String name) {        this.name = name;        pinyin = Cn2Spell.getPinYin(name); // 根据姓名获取拼音        firstLetter = pinyin.substring(0, 1).toUpperCase(); // 获取拼音首字母并转成大写        if (!firstLetter.matches("[A-Z]")) { // 如果不在A-Z中则默认为“#”            firstLetter = "#";        }    }     public String getName() {        return name;    }     public String getPinyin() {        return pinyin;    }     public String getFirstLetter() {        return firstLetter;    }      @Override    public int compareTo(User another) {        if (firstLetter.equals("#") && !another.getFirstLetter().equals("#")) {            return 1;        } else if (!firstLetter.equals("#") && another.getFirstLetter().equals("#")){            return -1;        } else {            return pinyin.compareToIgnoreCase(another.getPinyin());        }    }}

原理很简单,就是先根据首字母判断,首字母为“#”都放在最后,都为“#”或者都是字母时才根据拼音来比较排序

4.万事俱备只欠东风,接下来就是组装这些东西了

activity_main.xml布局文件

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="match_parent"    tools:context="com.afei.indexlistview.MainActivity">     <ListView        android:id="@+id/listView"        android:layout_width="match_parent"        android:layout_height="match_parent" />     <com.afei.indexlistview.SideBar        android:id="@+id/side_bar"        android:layout_width="match_parent"        android:layout_height="match_parent"        android:layout_alignParentRight="true"        android:paddingRight="10dp"        android:textColor="@color/colorAccent"        android:textSize="15sp" /> </RelativeLayout>

MainActivity.java

package com.afei.indexlistview; import android.support.v7.app.AppCompatActivity;import android.os.Bundle;import android.widget.ListView; import java.util.ArrayList;import java.util.Collections; public class MainActivity extends AppCompatActivity {     private ListView listView;    private SideBar sideBar;    private ArrayList<User> list;     @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        initView();        initData();    }     private void initView() {        listView = (ListView) findViewById(R.id.listView);        sideBar = (SideBar) findViewById(R.id.side_bar);        sideBar.setOnStrSelectCallBack(new SideBar.ISideBarSelectCallBack() {            @Override            public void onSelectStr(int index, String selectStr) {                for (int i = 0; i < list.size(); i++) {                    if (selectStr.equalsIgnoreCase(list.get(i).getFirstLetter())) {                        listView.setSelection(i); // 选择到首字母出现的位置                        return;                    }                }            }        });    }     private void initData() {        list = new ArrayList<>();        list.add(new User("亳州")); // 亳[bó]属于不常见的二级汉字        list.add(new User("大娃"));        list.add(new User("二娃"));        list.add(new User("三娃"));        list.add(new User("四娃"));        list.add(new User("五娃"));        list.add(new User("六娃"));        list.add(new User("七娃"));        list.add(new User("喜羊羊"));        list.add(new User("美羊羊"));        list.add(new User("懒羊羊"));        list.add(new User("沸羊羊"));        list.add(new User("暖羊羊"));        list.add(new User("慢羊羊"));        list.add(new User("灰太狼"));        list.add(new User("红太狼"));        list.add(new User("孙悟空"));        list.add(new User("黑猫警长"));        list.add(new User("舒克"));        list.add(new User("贝塔"));        list.add(new User("海尔"));        list.add(new User("阿凡提"));        list.add(new User("邋遢大王"));        list.add(new User("哪吒"));        list.add(new User("没头脑"));        list.add(new User("不高兴"));        list.add(new User("蓝皮鼠"));        list.add(new User("大脸猫"));        list.add(new User("大头儿子"));        list.add(new User("小头爸爸"));        list.add(new User("蓝猫"));        list.add(new User("淘气"));        list.add(new User("叶峰"));        list.add(new User("楚天歌"));        list.add(new User("江流儿"));        list.add(new User("Tom"));        list.add(new User("Jerry"));        list.add(new User("12345"));        list.add(new User("54321"));        list.add(new User("_(:з」∠)_"));        list.add(new User("……%¥#¥%#"));        Collections.sort(list); // 对list进行排序,需要让User实现Comparable接口重写compareTo方法        SortAdapter adapter = new SortAdapter(this, list);        listView.setAdapter(adapter);    }}

这里负责初始化UI和数据,并且实现滑动或选择字母索引时的回调接口。既然用到了ListView,我们就还需要一个适配器。

SortAdapter.java

package com.afei.indexlistview; import android.content.Context;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;import android.widget.BaseAdapter;import android.widget.TextView; import java.util.List; public class SortAdapter extends BaseAdapter{     private List<User> list = null;    private Context mContext;     public SortAdapter(Context mContext, List<User> list) {        this.mContext = mContext;        this.list = list;    }     public int getCount() {        return this.list.size();    }     public Object getItem(int position) {        return list.get(position);    }     public long getItemId(int position) {        return position;    }     public View getView(final int position, View view, ViewGroup arg2) {        ViewHolder viewHolder;        final User user = list.get(position);        if (view == null) {            viewHolder = new ViewHolder();            view = LayoutInflater.from(mContext).inflate(R.layout.item, null);            viewHolder.name = (TextView) view.findViewById(R.id.name);            viewHolder.catalog = (TextView) view.findViewById(R.id.catalog);            view.setTag(viewHolder);        } else {            viewHolder = (ViewHolder) view.getTag();        }         //根据position获取首字母作为目录catalog        String catalog = list.get(position).getFirstLetter();         //如果当前位置等于该分类首字母的Char的位置 ,则认为是第一次出现        if(position == getPositionForSection(catalog)){            viewHolder.catalog.setVisibility(View.VISIBLE);            viewHolder.catalog.setText(user.getFirstLetter().toUpperCase());        }else{            viewHolder.catalog.setVisibility(View.GONE);        }         viewHolder.name.setText(this.list.get(position).getName());         return view;     }     final static class ViewHolder {        TextView catalog;        TextView name;    }     /**     * 获取catalog首次出现位置     */    public int getPositionForSection(String catalog) {        for (int i = 0; i < getCount(); i++) {            String sortStr = list.get(i).getFirstLetter();            if (catalog.equalsIgnoreCase(sortStr)) {                return i;            }        }        return -1;    } }

适配器还用到了一个布局,即

item.xml

<?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="wrap_content"    android:gravity="center_vertical"    android:orientation="vertical" >     <TextView        android:id="@+id/catalog"        android:layout_width="match_parent"        android:layout_height="match_parent"        android:background="#E0E0E0"        android:textColor="#454545"        android:textSize="20sp"        android:padding="10dp"/>     <TextView        android:id="@+id/name"        android:layout_width="match_parent"        android:layout_height="match_parent"        android:gravity="center_vertical"        android:textColor="#336598"        android:textSize="16sp"        android:padding="10dp"/> </LinearLayout>

布局有两部分,一个是目录,即A,B,C,D这样的索引,仅当该目录下的第一项出现时才显示;一个则是姓名

四、项目地址

Git地址:http://git.oschina.net/afei_/IndexListView

@希望能帮到大家!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容