今天记录的是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;
}
}
不说太多了,源码里有详细说明