实习时遇到这么一个需求的更改,把一个带字母快速定位的滑块的listview,将其中的滑块显示全部字母a-z改成listview中数据有哪些,就只显示有数据的字母。在网上搜索了一下这个自定义view的实现,发现和项目中实现的代码基本一致,好吧,原来也是从网上百度到直接复制下来的,汗~
结合项目中的代码和网上的资料,我自己学习分析了一下这个自定义sidebar的实现。先贴上源码,源码参考自:
http://blog.csdn.net/woaieillen/article/details/12712137
public class SideBar extends View {
// 触摸事件
private OnTouchingLetterChangedListener onTouchingLetterChangedListener;
// 26个字母
public static String[] b = { "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 int choose = -1;// 选中
private Paint paint = new Paint();
private TextView mTextDialog;
public void setTextView(TextView mTextDialog) {
this.mTextDialog = mTextDialog;
}
public SideBar(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public SideBar(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SideBar(Context context) {
super(context);
}
/** * 重写这个方法 */
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 获取焦点改变背景颜色.
int height = getHeight();// 获取对应高度
int width = getWidth(); // 获取对应宽度
int singleHeight = height / b.length;// 获取每一个字母的高度
for (int i = 0; i < b.length; i++) {
paint.setColor(Color.rgb(33, 65, 98));
// paint.setColor(Color.WHITE);
paint.setTypeface(Typeface.DEFAULT_BOLD);
paint.setAntiAlias(true);
paint.setTextSize(20);
// 选中的状态
if (i == choose) {
paint.setColor(Color.parseColor("#3399ff"));
paint.setFakeBoldText(true);
}
// x坐标等于中间-字符串宽度的一半.
float xPos = width / 2 - paint.measureText(b[i]) / 2;
float yPos = singleHeight * i + singleHeight;
canvas.drawText(b[i], xPos, yPos, paint);
paint.reset();// 重置画笔
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
final int action = event.getAction();
final float y = event.getY();// 点击y坐标
final int oldChoose = choose;
final OnTouchingLetterChangedListener listener = onTouchingLetterChangedListener;
final int c = (int) (y / getHeight() * b.length);// 点击y坐标所占总高度的比例*b数组的长度就等于点击b中的个数.
switch (action) {
case MotionEvent.ACTION_UP:
setBackgroundDrawable(new ColorDrawable(0x00000000));
choose = -1;//
invalidate();
if (mTextDialog != null) {
mTextDialog.setVisibility(View.INVISIBLE);
}
break;
default:
setBackgroundResource(R.drawable.sidebar_background);
if (oldChoose != c) {
if (c >= 0 && c < b.length) {
if (listener != null) {
listener.onTouchingLetterChanged(b[c]);
}
if (mTextDialog != null) {
mTextDialog.setText(b[c]);
mTextDialog.setVisibility(View.VISIBLE);
}
choose = c;
invalidate();
}
}
break;
}
return true;
}
/** * 向外公开的方法 * * @param onTouchingLetterChangedListener */
public void setOnTouchingLetterChangedListener(
OnTouchingLetterChangedListener onTouchingLetterChangedListener) {
this.onTouchingLetterChangedListener = onTouchingLetterChangedListener;
}
/** * 接口 * * @author coder * */
public interface OnTouchingLetterChangedListener {
public void onTouchingLetterChanged(String s);
}
}
很容易看出这个自定义重写了onDraw方法,在方法里绘制了定义好的包含a-z的字母数组。我要完成我的需求,只需要获取到listview里包含的所有item的字母数据,然后做一下去除相同字母的过滤,把得到的字母传给sidebar然后再重新绘制一下就可以了。所以我在其中添加了一个方法setLetters(String[] letters)传入一个String数组,然后将传入的数组赋给绘制的数组,调用invalidate()方法重新绘制就可以了。
在完成任务之后,我继续研究了下这个自定义的view的具体绘制过程和事件的处理。正好复习下之前看的《Android群英传》里讲的事件传递机制和view绘制过程原理。
在onDraw()方法里先获取到控件的宽度和高度,然后根据需要绘制的字母个数获得每个字母的高度,初始化画笔paint设置画笔相关参数,计算绘制的x,y坐标的位置,循环字母数组绘制每个字母,如果是处于choose状态,修改画笔颜色并加粗。
关于事件的处理,在事件拦截方法dispatchTouchEvent中,获取到事件,并根据触摸的位置坐标计算出是哪一个字母位置,在根据事件类型ACTION_UP,触摸离开时,修改view的背景资源为透明,调用invalidate重新绘制,将中间的TextDialog设置为不可见。其他时候修改背景资源,再根据触摸选中的位置设置TextDialog的文字内容和可见,完成监听回调,重新绘制view。
看都看到这里了就把这个Demo看完吧,到MainActivity.java中,看看这个带特殊的listview是怎么实现的。
在initView()方法里,首先实例化了一个汉字转化拼音的类对象和一个按照拼音排序的比较器。
//实例化汉字转拼音类
characterParser = CharacterParser.getInstance();
//比较器
pinyinComparator = new PinyinComparator();
然后设置sidebar的触摸监听事件,根据触摸的字母位置快速定位到listview中该字母首次出现的位置。
//设置右侧触摸监听
sideBar.setOnTouchingLetterChangedListener(new OnTouchingLetterChangedListener() {
@Override
public void onTouchingLetterChanged(String s) {
//该字母首次出现的位置
int position = adapter.getPositionForSection(s.charAt(0));
if(position != -1){
sortListView.setSelection(position);
}
}
});
接下来listview的监听很简单,就是弹出一个吐司,filledData()方法是从资源文件里读取填充listview的数据,其中用到了characterParser,把数据封装成SortModel对象,存储到一个SourceDateList集合中,并用pinyinComparator进行排序后,初始化了一个SortAdapter,为这个listview设置适配器和数据。最后就是对上方搜索框的设置,初始化mClearEditText后,设置addTextChangedListener监听,根据输入内容调用filterData()方法,过滤数据并更新ListView。
//根据输入框输入值的改变来过滤搜索
mClearEditText.addTextChangedListener(new TextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
//当输入框里面的值为空,更新为原来的列表,否则为过滤数据列表
filterData(s.toString());
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
}
@Override
public void afterTextChanged(Editable s) {
}
});
private void filterData(String filterStr){
List<SortModel> filterDateList = new ArrayList<SortModel>();
if(TextUtils.isEmpty(filterStr)){
filterDateList = SourceDateList;
}else{
filterDateList.clear();
for(SortModel sortModel : SourceDateList){
String name = sortModel.getName();
if(name.indexOf(filterStr.toString()) != -1 || characterParser.getSelling(name).startsWith(filterStr.toString())){
filterDateList.add(sortModel);
}
}
}
// 根据a-z进行排序
Collections.sort(filterDateList, pinyinComparator);
adapter.updateListView(filterDateList);
}
至此看来,想知道listview的实现,就得去看SortAdapter里是怎么设置数据item的了。在getView()方法里对每个item进行设置,调用getSectionForPosition()和getPositionForSection()这两个方法来获取某个字母第一次出现位置来控制tvLetter就是那个显示字母的TextView显示或隐藏。关于getSectionForPosition()和getPositionForSection()这两个方法,是因为SortAdaoter继承了SectionIndexer接口必须实现的,getSectionForPosition()通过该项的位置,获得所在分类组的索引号,getPositionForSection() 根据分类列的索引号获得该序列的首个位置,这两个方法这里http://blog.csdn.net/jack_l1/article/details/14165291写的很清楚了,一看就懂。
/** * 根据ListView的当前位置获取分类的首字母的Char ascii值 */
public int getSectionForPosition(int position) {
return list.get(position).getSortLetters().charAt(0);
}
/** * 根据分类的首字母的Char ascii值获取其第一次出现该首字母的位置 */
public int getPositionForSection(int section) {
for (int i = 0; i < getCount(); i++) {
String sortStr = list.get(i).getSortLetters();
char firstChar = sortStr.toUpperCase().charAt(0);
if (firstChar == section) {
return i;
}
}
return -1;
}
知道这两个方法后,再看getView里的代码就很清晰了,每次先获取当前位置的selection值,根据selection值获取这个值第一次出现的位置,如果和当前位置一样就显示tvLetter可见并设置tvLetter上的字母,如果不是第一次就把tvLetter隐藏。
public View getView(final int position, View view, ViewGroup arg2) {
ViewHolder viewHolder = null;
final SortModel mContent = list.get(position);
if (view == null) {
viewHolder = new ViewHolder();
view = LayoutInflater.from(mContext).inflate(R.layout.item, null);
viewHolder.tvTitle = (TextView) view.findViewById(R.id.title);
viewHolder.tvLetter = (TextView) view.findViewById(R.id.catalog);
view.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) view.getTag();
}
//根据position获取分类的首字母的Char ascii值
int section = getSectionForPosition(position);
//如果当前位置等于该分类首字母的Char的位置 ,则认为是第一次出现
if(position == getPositionForSection(section)){
viewHolder.tvLetter.setVisibility(View.VISIBLE);
viewHolder.tvLetter.setText(mContent.getSortLetters());
}else{
viewHolder.tvLetter.setVisibility(View.GONE);
}
viewHolder.tvTitle.setText(this.list.get(position).getName());
return view;
}
final static class ViewHolder {
TextView tvLetter;
TextView tvTitle;
}