Android ListView侧边字母索引栏SideBar控件的实现

今天记录的是Android SideBar控件的使用,SideBar就是在ListView右边出现的一个竖条,有A到Z字母索引,通过字母索引可以快速定位到ListView的位置,如下图所示:


下面我来说明这个功能的实现过程:

1、首先完成数据解析

上图中的ListView里显示的是城市名,城市数据来源于文件,放在res/raw目录下,如下图所示:


具体的解析过程我就不多写了,在上一篇博文中,我有记录城市数据的解析过程:http://blog.csdn.net/yubo_725/article/details/46530191

2、实现城市数据的排序

为了实现城市名的排序,我们需要将中文的城市名转化为拼音,然后根据拼音里的字母来排序,这里也使用了之前的一片博文里记录的汉字转拼音的方法:http://blog.csdn.net/yubo_725/article/details/44172427,排序主要使用了Java里的Collections.sort方法,比较的过程是,通过将两个中文的城市名转化为拼音,然后根据拼音来比较城市的先后次序,如澳门(aomen)跟北京(beijing)相比,a排在b前面,则澳门排在北京前面,排序的关键代码如下:

private Comparator<CityBean> comparator = new Comparator<CityBean>() {

	@Override
	public int compare(CityBean arg0, CityBean arg1) {
		//获取城市名对应的拼音,通过比较拼音来确定城市的先后次序
		String pinyin0 = CharacterParser.getInstance().getPinYinSpelling(arg0.getName());
		String pinyin1 = CharacterParser.getInstance().getPinYinSpelling(arg1.getName());
		return pinyin0.compareTo(pinyin1);
	}
};
3、ListView的显示

为了达到上图中的显示效果,即按A-Z排序后,每个字母首次出现时要标记一个字母,我们的ListView的item需要做如下定义:

list_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="match_parent"
    android:orientation="vertical" >
    
    <TextView 
        android:id="@+id/list_item_head"
        android:layout_width="fill_parent"
        android:layout_height="20dp"
        android:textColor="#ffffff"
        android:text="A"
        android:paddingLeft="15dp"
        android:gravity="center_vertical"
        android:textSize="15sp"
        android:visibility="gone"
        android:background="#ff9900"
        />
    
    <TextView 
        android:id="@+id/list_item_name"
        android:layout_width="fill_parent"
        android:layout_height="50dp"
        android:paddingLeft="15dp"
        android:textColor="#000000"
        android:textSize="15sp"
        android:gravity="center_vertical"
        />

</LinearLayout>
上面的list_item_head是为了在每个字母首次出现时显示出来,list_item_name才是显示城市名的,下面问题来了,怎么确定每个字母首次出现时在ListView的哪个位置呢?

4、为ListView编写adapter

为了达到上面的目的,这次的adapter除了继承万能适配器CommonAdapter之外(不知道万能适配器的可以看这篇博文:http://blog.csdn.net/lmj623565791/article/details/38902805/),还要实现一个接口:SectionIndexer,该接口声明了两个方法:getPositionForSection()和getSectionForPosition,刚刚使用这个接口的人可能不太明白这两个方法的含义,下面我用一张图解释一下,下图代表一个ListView通过字母排序后的结果,section索引代表排序后按块分类的索引:


将ListView中的数据排序后,按照字母a-z分组,这个组的索引就是上面的section索引
怎么确定某字母首次出现的位置呢?这就需要用到SectionIndexer接口中的方法了,这里的getPositionForSection方法和getSectionForPosition方法都需要我们自己去复写,其中getPositionForSection返回的是某个section index对应的ListView中的位置,如section索引为3,则getPositionForSection(3)返回的就是8,getPostionForSection(2)返回5。getSectionForPostion方法返回的是ListView中某个位置在哪个section index中,如getPostionForSection(6)返回2,getPostionForSection(9)返回3,下面是适配器的代码:

package com.example.searchlistview.adapter;

import java.util.ArrayList;
import java.util.List;

import android.content.Context;
import android.text.TextUtils;
import android.view.View;
import android.widget.SectionIndexer;
import android.widget.TextView;

import com.example.searchlistview.R;
import com.example.searchlistview.bean.CityBean;

/**
 * ListView的Adapter,关键的是SectionIndexer接口中的两个方法
 * @author Test
 *
 */
public class CityListAdapter extends CommonAdapter<CityBean> implements SectionIndexer {
	
	private List<CityBean> allData;
	private List<CityBean> queryData;

	public CityListAdapter(Context context, List<CityBean> mDatas,
			int itemLayoutId) {
		super(context, mDatas, itemLayoutId);
		allData = mDatas;
		queryData = new ArrayList<CityBean>();
	}
	
	public void queryData(String query){
		queryData.clear();
		for(CityBean bean : allData){
			String name = bean.getName();
			if(!TextUtils.isEmpty(name) && name.contains(query)){
				queryData.add(bean);
			}
		}
		mDatas = queryData;
		notifyDataSetChanged();
	}
	
	public void resetData(){
		mDatas = allData;
		notifyDataSetChanged();
	}

	@Override
	public void convert(ViewHolder holder, CityBean item, int position) {
		TextView nameTv = holder.getView(R.id.list_item_name);
		TextView headTv = holder.getView(R.id.list_item_head);
		//根据position == getPositionForSection(getSectionForPostion(position))判断某个字母是不是首次出现
		if(position == 0 && getPositionForSection(getSectionForPosition(position)) == -1){
			headTv.setVisibility(View.VISIBLE);
			headTv.setText("#");
		}else if(position == getPositionForSection(getSectionForPosition(position))){
			headTv.setVisibility(View.VISIBLE);
			//把section index转化为大写字母
			char c = (char) (getSectionForPosition(position) + 65);
			if(c >= 'A' && c <= 'Z'){
				headTv.setText(String.valueOf(c));
			}
		}else{
			headTv.setVisibility(View.GONE);
		}
		nameTv.setText(item.getName());
	}

	@Override
	public int getPositionForSection(int arg0) {              //关键方法,通过section index获取在ListView中的位置
		//根据参数arg0,加上65后得到对应的大写字母
		char c = (char)(arg0 + 65);
		//循环遍历ListView中的数据,遇到第一个首字母为上面的就是要找的位置
		for(int i = 0; i < getCount(); i++){
			if(mDatas.get(i).getFirstLetter() == c){
				return i;
			}
		}
		return -1;
	}

	@Override
	public int getSectionForPosition(int arg0) {              //关键方法,通过在ListView中的位置获取Section index
		//获取该位置的城市名首字母
		char c = (char) (mDatas.get(arg0).getFirstLetter());
		//如果该字母在A和Z之间,则返回A到Z的索引,从0到25
		if(c >= 'A' && c <= 'Z'){
			return c - 'A';
		}
		//如果首字母不是A到Z的字母,则返回26,该类型将会被分类到#下面
		return 26;
	}

	@Override
	public Object[] getSections() {
		return null;
	}


}

然后是SideBar控件的代码,如下:

package com.example.searchlistview.view;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

/**
 * ListView侧边按照A-Z排序的SideBar控件
 * @author Test
 *
 */
public class SideBar extends View {
	
	//SideBar上显示的字母和#号
	private static final String[] CHARACTERS = {"#", "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"};
	
	//SideBar的高度
	private int width;
	//SideBar的宽度
	private int height;
	//SideBar中每个字母的显示区域的高度
	private float cellHeight;
	//画字母的画笔
	private Paint characterPaint;
	//SideBar上字母绘制的矩形区域
	private Rect textRect;
	//手指触摸在SideBar上的横纵坐标
	private float touchY;
	private float touchX;
	
	private OnSelectListener listener;

	public SideBar(Context context, AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);
		init(context);
	}

	public SideBar(Context context, AttributeSet attrs) {
		super(context, attrs);
		init(context);
	}

	public SideBar(Context context) {
		super(context);
		init(context);
	}
	
	//初始化操作
	private void init(Context context){
		textRect = new Rect();
		characterPaint = new Paint();
		characterPaint.setColor(Color.parseColor("#6699ff"));
	}
	
	@Override
	protected void onLayout(boolean changed, int left, int top, int right,
			int bottom) {
		super.onLayout(changed, left, top, right, bottom);
		if(changed){ //在这里测量SideBar的高度和宽度
			width = getMeasuredWidth();
			height = getMeasuredHeight();
			//SideBar的高度除以需要显示的字母的个数,就是每个字母显示区域的高度
			cellHeight = height * 1.0f / CHARACTERS.length;
			//根据SideBar的宽度和每个字母显示的高度,确定绘制字母的文字大小,这样处理的好处是,对于不同分辨率的屏幕,文字大小是可变的
			int textSize = (int) ((width > cellHeight ? cellHeight : width) * (3.0f / 4));
			characterPaint.setTextSize(textSize);
		}
	}
	
	//画出SideBar上的字母
	private void drawCharacters(Canvas canvas){
		for(int i = 0; i < CHARACTERS.length; i++){
			String s = CHARACTERS[i];
			//获取画字母的矩形区域
			characterPaint.getTextBounds(s, 0, s.length(), textRect);
			//根据上一步获得的矩形区域,画出字母
			canvas.drawText(s, 
					(width - textRect.width()) / 2f, 
					cellHeight * i + (cellHeight + textRect.height()) / 2f, 
					characterPaint);
		}
	}
	
	@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);
		drawCharacters(canvas);
	}
	
	//根据手指触摸的坐标,获取当前选择的字母
	private String getHint(){
		int index = (int) (touchY / cellHeight);
		if(index >= 0 && index < CHARACTERS.length){
			return CHARACTERS[index];
		}
		return null;
	}
	
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		switch(event.getAction()){
		case MotionEvent.ACTION_DOWN:
		case MotionEvent.ACTION_MOVE:
			//获取手指触摸的坐标
			touchX = event.getX();
			touchY = event.getY();
			if(listener != null && touchX > 0){
				listener.onSelect(getHint());
			}
			if(listener != null && touchX < 0){
				listener.onMoveUp(getHint());
			}
			return true;
		case MotionEvent.ACTION_UP:
			touchY = event.getY();
			if(listener != null){
				listener.onMoveUp(getHint());
			}
			return true;
		}
		return super.onTouchEvent(event);
	}
	
	//监听器,监听手指在SideBar上按下和抬起的动作
	public interface OnSelectListener{
		void onSelect(String s);
		void onMoveUp(String s);
	}
	
	//设置监听器
	public void setOnSelectListener(OnSelectListener listener){
		this.listener = listener;
	}

}

不说太多了,源码里有详细说明


源代码下载点击这里



  • 4
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yubo_725

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值