Android-联系人A~Z列表

本文详细介绍如何在Android应用中实现带有A~Z快速定位索引的联系人列表。包括自定义A~Z索引视图、添加点击监听、结合ListView展示数据等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Android-联系人A~Z列表实现旅途

将右侧A~Z显示出来

  1. 自定义一个A~Z垂直显示的View(自定义控件命名为:LetterView.java)

    /**
    * 靠右的字母控件
    */
    public class LetterView extends View
    {
    /**纵向显示的所有字符*/
    public static final String letters = "*ABCDEFGHIJKLMNOPQRSTUVWXYZ#";
    private int width;//控件宽度
    private int height;//控件高度
    private int abcHeight;//每个字母的高度
    private Paint paint;
    public LetterView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    private void init()
    {
        paint = new Paint();
        //抗锯齿
        paint.setAntiAlias(true);
        //加粗
        paint.setFakeBoldText(true);
        //字体大小
        paint.setTextSize(Float.parseFloat(getResources().getString(R.string.text_size)));
    }
    
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (width == 0 || height == 0)//判断是否是第一次绘画,给宽高赋值
        {
            width = getWidth();
            height = getHeight();
            abcHeight = height / letters.length();
        }
        for (int i = 0, length = letters.length(); i < length; i++)
        {
            //计算字母绘制的xy坐标,paint.measureText(letters.charAt(i)+"")  得到字母的宽
            float x = (width - paint.measureText(letters.charAt(i) + "")) / 2;
            float y = abcHeight * i + abcHeight - paint.measureText(letters.charAt(i) + "")/2;
            canvas.drawText(letters.charAt(i) + "", x, y, paint);
        }
    }
    }
  2. 在xml布局文件中添加(布局命名为:letters_layout.xml)

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">
    <com.view.LetterView
        android:layout_width="30dp"
        android:layout_height="match_parent"
        android:layout_alignParentRight="true"/>
    </RelativeLayout>
  3. 效果图
    A~Z显示

为A~Z设置点击监听

1、 自定义控件LetterView.java(#开头的注释表示新添加的内容)


/**
 * 靠右的字母
 */
public class LetterView extends View {
    /**
     * #>触碰时候的背景颜色
     */
    public static final int COLOR_BG = 0x17000000;
    /**
     * #>没有触碰时的背景颜色
     */
    public static final int COLOR_NO_BG = 0x07000000;
    /**
     * #>触碰状态下所有字母的颜色
     */
    public static final int TEXT_COLOR_NORMAL = 0xff545454;
    /**
     * #>选中的字母颜色
     */
    public static final int TEXT_COLOR_SELECTED = 0xffff5e00;
    /**
     * 没有触碰状态下的字母颜色
     */
    public static final int TEXT_COLOR_UNTOUCH = 0xffa3a3a3;
    public static final String letters = "☆ABCDEFGHIJKLMNOPQRSTUVWXYZ#";
    private int width;
    private int height;
    //每个字母的高度
    private int abcHeight;
    private Paint paint;
    private int selectedIndex = 1;//#>被选中字母的下标
    private boolean isTouch = false;//#>是否处于触碰的状态
    public LetterView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        paint = new Paint();
        paint.setAntiAlias(true);
        //加粗
        paint.setFakeBoldText(true);
        paint.setTextSize(Float.parseFloat(getResources().getString(R.string.text_size)));
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //#>根据触碰状态改变控件的背景颜色
        if (isTouch) {
            setBackgroundColor(COLOR_BG);
        } else {
            setBackgroundColor(COLOR_NO_BG);
        }
        if (width == 0 || height == 0) {
            width = getWidth();
            height = getHeight();
            abcHeight = height / letters.length();
        }
        for (int i = 0, length = letters.length(); i < length; i++) {
            if (selectedIndex == i)//#>设置被选中的字母颜色
            {
                paint.setColor(TEXT_COLOR_SELECTED);
            } else {
                if (isTouch)//#>设置触碰状态下的所有的字母颜色
                {
                    paint.setColor(TEXT_COLOR_NORMAL);
                } else//#>设置非点击状态下的所有的字母颜色
                {
                    paint.setColor(TEXT_COLOR_UNTOUCH);
                }
            }
            //计算字母绘制的xy坐标,paint.measureText(letters.charAt(i)+"")  得到字母的宽
            float x = (width - paint.measureText(letters.charAt(i) + "")) / 2;
            float y = abcHeight * i + abcHeight - paint.measureText(letters.charAt(i) + "") / 2;
            canvas.drawText(letters.charAt(i) + "", x, y, paint);
        }
    }

    private float y;//#>点击的y坐标
    private int lastSelectedIndex = -1;//#>记录上一次的位置
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        y = event.getY();//#>获取当前触摸时的y坐标
        selectedIndex = (int) (y / abcHeight);//#>计算出当前触碰到的字母下标
        if (selectedIndex <= 0) selectedIndex = 1;//#>如果下标处于0将,下标改为1,不让下标为0的☆产生监听
        if (selectedIndex >= letters.length() - 1) selectedIndex = letters.length() - 2;//#>如果下标处于最后将,下标改为letters.length() - 2,不让最下面的#产生监听
        if (selectedIndex != lastSelectedIndex) {//#>如果触摸的地方不是上一次的y轴位置,重绘,调用回调中间显示字母
            invalidate();
            if (letterChangeListener != null)
            {
                letterChangeListener.onLetterChange(selectedIndex);
            }
        }
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                isTouch = true;
                break;
            case MotionEvent.ACTION_UP:
                letterChangeListener.onClickUp();//
                isTouch = false;
                break;
        }
        invalidate();
        return true;
    }

    /**
     * #>回调接口,处理字母的点击事件
     */
    //#>回调接口(当前是哪一栏,则字母就显示哪一个)
    public interface OnLetterChangeListener {
        void onLetterChange(int selectedIndex);//#>当位置发生改变时调用
        void onClickUp();//#>当触摸后,放开时调用
    }

    private OnLetterChangeListener letterChangeListener;

    public void setOnLetterChangeListener(OnLetterChangeListener letterChangeListener) {
        this.letterChangeListener = letterChangeListener;
    }

    //设置当前那个字母被选中
    public void setSelected(int section) {

        this.selectedIndex = section;
        invalidate();
    }
}

2、 在xml布局文件letters_layout.xml中

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">
    <com.view.LetterView
        android:id="@+id/letterView"
        android:layout_width="30dp"
        android:layout_height="match_parent"
        android:layout_alignParentRight="true"/>
        <!--中间显示被点到的字母-->
    <TextView
        android:id="@+id/show_now_abc"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="A"
        android:layout_centerInParent="true"
        android:visibility="gone"
        android:textSize="60sp"/>
</RelativeLayout>

3、在Activity中(MainActivity.java)

public class MainActivityextends Activity{
    private LetterView letterView;
    private TextView tvToast;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.letters_layout);
        letterView = (LetterView) findViewById(R.id.letterView);
        tvToast = (TextView) findViewById(R.id.show_now_abc);
        letterView.setOnLetterChangeListener(new LetterView.OnLetterChangeListener() {
            @Override
            public void onLetterChange(int selectedIndex) {
                tvToast.setText(LetterView.letters.charAt(selectedIndex) + "");//设置中间显示的字母
                tvToast.setVisibility(View.VISIBLE);//设置为可见
            }
            @Override
            public void onClickUp() {
                tvToast.setVisibility(View.GONE);//当放开时,设置为不可见
            }
        });
    }
  • 当前效果图
    当前效果图
  • 最终要实现效果,在下面解析
    最终实现效果

加上一个ListView列表布局

1、主布局letters_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ListView
        android:id="@+id/listView_express"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:scrollbars="none"/>
    <!-- 引用另一个布局文件 -->

    <include
        layout="@layout/express_overlay"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" />

    <com.view.LetterView
        android:id="@+id/letterView"
        android:layout_width="30dp"
        android:layout_height="match_parent"
        android:layout_alignParentRight="true" />

    <TextView
        android:id="@+id/show_now_abc"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="A"
        android:textColor="#ff5e00"
        android:textSize="60sp"
        android:visibility="gone" />
</RelativeLayout>

2、顶部布局express_overlay.xml
顶部布局

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/viewOverlay_express"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <TextView
        android:id="@+id/tvOverlay_express"
        android:layout_width="fill_parent"
        android:layout_height="45dp"
        android:background="@android:color/holo_orange_light"
        android:gravity="center_vertical"
        android:paddingLeft="15dp"
        android:text="A"
        android:textColor="@android:color/holo_green_dark"
        android:textSize="20sp"
        android:textStyle="bold" />

</FrameLayout>

3、item子布局item_express.xml
item子布局

<?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/tvLetter_item_express"
        android:layout_width="fill_parent"
        android:layout_height="45dp"
        android:background="#17000000"
        android:clickable="true"
        android:gravity="center_vertical"
        android:paddingLeft="15dp"
        android:text="A"
        android:textSize="20sp"
        android:textStyle="bold" />

    <RelativeLayout
        android:layout_width="fill_parent"
        android:layout_height="60dp">

        <TextView
            android:id="@+id/tvCompanyName_item_express"
            android:layout_width="fill_parent"
            android:layout_height="60dp"
            android:gravity="center_vertical"
            android:paddingLeft="20dp"
            android:singleLine="true"
            android:text="申通快递"
            android:textColor="#000000"
            android:textSize="20sp" />
    </RelativeLayout>

</LinearLayout>

为ListView设置适配器

1、这里使用了一个数据库,表结构如下图

数据库表结构

2、这里使用了AlphabetIndexer字母索引辅助类。去了解AlphabetIndexer
3、下面是适配器代码
public class OrderAdapter extends BaseAdapter
{
    private Cursor cursor;//接收根据字母排序好了的cursor
    private LayoutInflater inflater;//布局填充器,加载ListView子布局
    private AlphabetIndexer indexer;//AlphabetIndexer字母索引辅助类
    public OrderAdapter(Context context, Cursor cursor, AlphabetIndexer indexer)
    {
        this.cursor = cursor;
        inflater = LayoutInflater.from(context);
        this.indexer = indexer;
    }
    @Override
    public int getCount() {
        return cursor.getCount();
    }

    @Override
    public Cursor getItem(int position) {
        cursor.moveToPosition(position);
        return cursor;
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder = null;
        if (convertView == null)
        {
            convertView = inflater.inflate(R.layout.item_express, parent, false);
            holder = new ViewHolder();
            holder.tvLetter = (TextView) convertView.findViewById(R.id.tvLetter_item_express);
            holder.tvCompanyName = (TextView) convertView.findViewById(R.id.tvCompanyName_item_express);
            convertView.setTag(holder);
        }
        else
        {
            holder = (ViewHolder) convertView.getTag();
        }
        cursor.moveToPosition(position);
        holder.tvCompanyName.setText(cursor.getString(cursor.getColumnIndex(ExpressDbHelper.TABLE_COMPANY_COMPANY_NAME)));
        //获取这个位置代表的字符在字符表中的位置
        int section = indexer.getSectionForPosition(position);
        //判断当前位置是否是第一个出现这个字符,indexer.getPositionForSection(section)获取第一次出现的位置
        if (position == indexer.getPositionForSection(section))
        {
            holder.tvLetter.setVisibility(View.VISIBLE);
            holder.tvLetter.setText(cursor.getString(cursor.getColumnIndex(ExpressDbHelper.TABLE_COMPANY_COMPANY_INITIAL)));
        }
        else
        {
            holder.tvLetter.setVisibility(View.GONE);
        }
        return convertView;
    }
    private class ViewHolder
    {
        TextView tvLetter;
        TextView tvCompanyName;
    }
}
4、数据库帮助类(ExpressDbHelper.java)
public class ExpressDbHelper extends SQLiteOpenHelper{
    public static final String DB_NAME = "express.db";
    public static final int VERSION = 1;
    /**快递公司名字**/
    public static final String TABLE_COMPANY_COMPANY_NAME = "company_name";
    /**快递公司对应code**/
    public static final String TABLE_COMPANY_COMPANY_CODE = "company_code";
    /**公司名字对应的首字母**/
    public static final String TABLE_COMPANY_COMPANY_INITIAL = "initial";
    public ExpressDbHelper(Context context) {
        super(context, DB_NAME, null, VERSION);
    }
    @Override
    public void onCreate(SQLiteDatabase db) {

    }
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }
}
5、在MainActivity.java中
public class MainActivity extends Activity {
    private ExpressDbHelper helper;
    private LetterView letterView;
    private TextView tvToast;
    private ListView lv;
    /*顶部的view*/
    private View viewTop;
    /*顶部显示的字母*/
    private TextView tvTop;
    private RelativeLayout.LayoutParams params;
    /*字母索引辅助类*/
    private AlphabetIndexer indexer;
    /*字母索引*/
    private String alphabet = "☆ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        copyDatabase();//将assets目录下的数据库拷贝到程序的包中
        initView();
        afterInit();//主要处理部分
    }

    protected void initView() {
        letterView = (LetterView) findViewById(R.id.letterView);
        tvToast = (TextView) findViewById(R.id.show_now_abc);
        lv = (ListView) findViewById(R.id.listView_express);
        viewTop =  findViewById(R.id.viewOverlay_express);
        tvTop = (TextView) findViewById(R.id.tvOverlay_express);
        params = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
    }

    protected void afterInit() {
        helper = new ExpressDbHelper(this);//获得一个数据库帮助类
        /*从数据库获取所有数据并根据公司对应的首字母排序*/
        Cursor cursor = helper.getReadableDatabase().rawQuery("select * from company order by initial", null);
                /*
         * 参数1:包含数据的Cursor对象
         * 参数2:进行索引排序的列号
         * 参数3:字母表(空格将会作为第一个字符。字母要大写,并且按ascii/unicode排序。)
         */
        indexer =  new AlphabetIndexer(cursor, cursor.getColumnIndex("initial"), alphabet);
        lv.setAdapter(new OrderAdapter(this, cursor, indexer));

        letterView.setOnLetterChangeListener(new LetterView.OnLetterChangeListener() {
            @Override
            public void onLetterChange(int selectedIndex) {
                lv.setSelection(indexer.getPositionForSection(selectedIndex));//跳到以这个字母为索引的第一个名字位置
                tvToast.setText(LetterView.letters.charAt(selectedIndex) + "");//设置中间显示的字母
                tvToast.setVisibility(View.VISIBLE);//设置为可见
            }

            @Override
            public void onClickUp() {
                tvToast.setVisibility(View.GONE);//当放开时,设置为不可见
            }
        });
        //给ListView设置滑动的监听,动态的改变顶部标题的显示
        lv.setOnScrollListener(new AbsListView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(AbsListView view, int scrollState) {

            }
            /*
             * firstVisibleItem表示在现时屏幕第一个ListItem(部分显示的ListItem也算)
             * totalItemCount表示ListView的ListItem总数
             * view 表示整个ListView
             */
            @Override
            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
                //按指定数据项的位置,返回匹配的索引项。
                int section = indexer.getSectionForPosition(firstVisibleItem);
                //得到下一个索引项
                int nextSection = section+1;
                //按指定索引查找,返回匹配的第一行数据项位置或比较接近的数据项的位置
                int nextPosition = indexer.getPositionForSection(nextSection);
                //#=>判断ListView第二排的item的位置是否是下一个索引项的开始
                if (firstVisibleItem + 1 != nextPosition)
                {//标题显示不改变的状态下
                    params.topMargin = 0;
                    viewTop.setLayoutParams(params);
                    tvTop.setText(alphabet.charAt(section) + "");
                }
                else
                {//标题显示改变的状态下
                    View v = view.getChildAt(0);
                    if (v == null)
                    {
                        return;
                    }
                    int dex = v.getBottom() - tvTop.getHeight();
                    if (dex <= 0)
                    {
                        params.topMargin = dex;
                    }
                    else
                    {
                        params.topMargin = 0;
                    }
                    tvTop.setText(alphabet.charAt(section) + "");
                    viewTop.setLayoutParams(params);
                }
                letterView.setSelected(section);
            }
        });
    }

    /**
     * 拷贝express.db到数据库
     */
    private void copyDatabase(){
        try {
            InputStream in = getAssets().open("express.db");
            //得到数据库文件的路径
            File file = getDatabasePath("express.db");
            if(!file.exists()){
                if(!file.getParentFile().exists()){
                file.getParentFile().mkdir();
            }
                file.createNewFile();
            }
            else
            {
                return;
            }
            FileOutputStream fos = new FileOutputStream(file);
            byte[] buffer = new byte[1024];
            int lenght;
            while((lenght = in.read(buffer))!=-1){
                fos.write(buffer, 0, lenght);
            }
            fos.close();
            in.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

项目代码地址

http://download.youkuaiyun.com/detail/u014314614/9326443

我的感受

  • 把这玩意理解完一下子就觉得简单多了,开始老是看代码去理解并不能真的理清思路,下次要看代码最好先知道整体的结构,然后从一个突破点一步步做下去,避免东想西想浪费时间。记住一切复杂都是从简单的小结构出发
  • 这里的主要使用的新知识点就是AlphabetIndexer字母索引辅助类,让右侧字母索引和ListView的item显示联系起来
  • 通过这个例子,以后若要用,我只要提供了一个数据库,数据库里面包含名字和名字的开头字母,我就能改下数据库就能方便的使用这个案例了

想说的话

  • 博客坚持写,今后学习了新的东西就在这里记录一下,以便今后回顾,也希望小小笔记能帮助你们
  • 若内容有什么地方不对、不清楚,还望吐槽,希望大家能一起成长
  • 来一个: 阅读源码是一个学好编程的重要途径,不仅学习到编程思路、实现技巧和风格,更是能形成自己独特思路和风格。其中一定有很多无法理解的代码、英文看不懂等问阻扰着,坚持下去,突破重重困难。了解全局理清细节
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值