相信大部分在拥有第一部手机的时候,干的第一件事情都是将一些你以前记录在笔记本或者同学录里面的手机号记录到手机的通讯录里面,没错今天我们就要讲到手机通讯录原型的实现步骤,当然也借鉴了网上的一些资源,只不过还是记录一下具体的实现步骤和逻辑思维实现步骤,方便以后用到的时候又到网上到处胡乱搜索还要去看别人的毫无注释的代码(PS本人非常不喜欢看一些别人写的没有注释的代码)。废话不多说,先上图,来点视觉效果。
左侧为进入应用的默认显示图,右侧为输入内容之后搜索的结果显示图。
首先看看右侧的导航栏,我们仔细观察一下,这个控件不是我们Android原生态的控件能够实现的,因此,我选择自定义控件来实现,相信大家关于自定义控件的三部曲已经非常熟悉了吧。在此我选择继承View来实现右侧的字母导航栏。
/**
* 通讯录右侧字母导航的bar
*/
public class RightLettersSlideBar extends View {
private static final int color = Color.parseColor("#00B4C9"); //点击之后显示的颜色
private static final int color_normal = Color.parseColor("#999999"); //默认的颜色
private Paint paint;
int choose = -1; //标志哪一个字母被点击的index
//初始化右侧导航的26个字母和#
public static String[] charaters = {"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 OnTouchingLetterChangedListener onTouchingLetterChangedListener;
public RightLettersSlideBar(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public RightLettersSlideBar(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
//初始化函数 可以初始化画笔等等
public void init(){
paint=new Paint();
paint.setTypeface(Typeface.DEFAULT_BOLD);
paint.setAntiAlias(true);
paint.setTextSize(24);
paint.setStyle(Style.FILL);
paint.setTextAlign(Paint.Align.CENTER);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthResult=0,heightResult=0;
int widthMode=MeasureSpec.getMode(widthMeasureSpec);
int heightMode=MeasureSpec.getMode(heightMeasureSpec);
int width=MeasureSpec.getSize(widthMeasureSpec);
int height=MeasureSpec.getSize(heightMeasureSpec);
if(widthMode==MeasureSpec.EXACTLY){
widthResult=width;
}else{
widthResult=Math.max(widthResult, width);
}
if(heightMode==MeasureSpec.EXACTLY){
heightResult=height;
}else{
heightResult=Math.max(heightResult, height);
}
setMeasuredDimension(widthResult, heightResult);
}
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
float xPos = getMeasuredWidth() / 2;
float singleHeight = getMeasuredHeight() / charaters.length;
for(int i=0;i<charaters.length;i++){
paint.setColor(color_normal);
if (i == choose) {
paint.setColor(color);
paint.setFakeBoldText(true);
}
float yPos = singleHeight * i + singleHeight / 2;
canvas.drawText(charaters[i], xPos, yPos, paint);
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int action = event.getAction();
int y=(int) event.getY(); //得到触摸点的Y坐标
int index=y/(getMeasuredHeight()/charaters.length);
switch(action){
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
if (index >= 0 && index < charaters.length) {
choose = index;
onTouchingLetterChangedListener.onTouchingLetterChanged(charaters[index]);
invalidate(); //重绘 主要为了显示点击之后画笔所画字母的不同颜色
}
break;
case MotionEvent.ACTION_UP:
choose=-1;
invalidate();
break;
}
return true;
}
public void setOnTouchingLetterChangedListener(
OnTouchingLetterChangedListener onTouchingLetterChangedListener) {
this.onTouchingLetterChangedListener = onTouchingLetterChangedListener;
}
//触摸某个字符之后的回调接口
public interface OnTouchingLetterChangedListener {
public void onTouchingLetterChanged(String s);
}
}
代码的第5-11行,初始化了一些自定义右侧导航的画笔,字符集,初始颜色,点击之后的颜色,还有记录触摸事件发生的所对应字符集的一个索引choose。当然,构造函数必不可少,init方法中初始化了画笔,也可以将字符集的初始化放在init方法中onMeasure方法对我们控件进行了简单的测量,因为不涉及到具体的位置或者位置变化所以就没有重写onLayout方法,onDraw方法中绘制了字符集,设置了字符的具体的x,y坐标还有画笔的颜色,当choose==i 相当于该字符被触摸到,显示不同的颜色 。字母的X坐标固定,Y坐标在循环里面随之改动。
由于,我们在设计右边导航栏的时候,在导航栏触摸和滑动都会根据触摸或者滑动的坐标,来响应OnTouchingLetterChangedListener,ACTION_DOWN和ACTION_MOVE事件会回调OnTouchingLetterChangedListener,并且调用invalidate()方法让控件重绘,由于在ACTION_UP的时候将choose重置为-1,并且重绘了界面,所以 画笔颜色的改变不会保留在控件之上,只会在点击或者滑动的时候瞬间显示。
代码92-100行定义回调接口,并且设置监听。
看了自定义的导航控件之后,就将注意力注意到我们用来显示数据的listview,这里只是使用了系统原生态的listview,提到listview大家应该都会联想到adapter,没错,来看看adapter的编写
package com.ling.contacts.adapter;
import java.util.List;
import com.ling.contacts.R;
import com.ling.contacts.bean.Contacts;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.SectionIndexer;
import android.widget.TextView;
public class ContactsAdapter extends BaseAdapter implements SectionIndexer {
private Context context;
private List<Contacts> data;
private LayoutInflater inflater;
public ContactsAdapter(Context context, List<Contacts> data) {
this.context = context;
this.data = data;
inflater = LayoutInflater.from(this.context);
}
@Override
public int getCount() {
// TODO Auto-generated method stub
return data == null ? 0 : data.size();
}
@Override
public Object getItem(int position) {
// TODO Auto-generated method stub
return data.get(position);
}
/**
* 当ListView数据发生变化时,调用此方法来更新ListView
* @param list
*/
public void updateListView(List<Contacts> list){
this.data = list;
notifyDataSetChanged();
}
@Override
public long getItemId(int position) {
// TODO Auto-generated method stub
return position;
}
public View getView(final int position, View view, ViewGroup arg2) {
ViewHolder viewHolder = null;
final Contacts contact = data.get(position);
if (view == null) {
viewHolder = new ViewHolder();
view = inflater.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(contact.getSortLetters());
} else {
viewHolder.tvLetter.setVisibility(View.GONE);
}
viewHolder.tvTitle.setText(contact.getName());
return view;
}
final static class ViewHolder {
TextView tvLetter;
TextView tvTitle;
}
/**
* 根据ListView的当前位置获取分类的首字母的char ascii值
*/
public int getSectionForPosition(int position) {
return data.get(position).getSortLetters().charAt(0);
}
/**
* 根据分类的首字母的Char ascii值获取其第一次出现该首字母的位置
* 例如AA AB同样属于A的section,但是通过此方法发现AA的position与Item中的position相同
* 所以AA显示最上边的A的Section AB就不会显示
*/
public int getPositionForSection(int section) {
for (int i = 0; i < getCount(); i++) {
String sortStr = data.get(i).getSortLetters();
char firstChar = sortStr.toUpperCase().charAt(0);
if (firstChar == section) {
return i;
}
}
return -1;
}
@Override
public Object[] getSections() {
return null;
}
}
其实在adapter中最核心的方法就是getView方法了,里面决定了view该怎么显示。我们显示lisview中的每一个item的xml文件如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="vertical" >
<TextView
android:id="@+id/catalog"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#E0E0E0"
android:textColor="#454545"
android:layout_weight="1.0"
android:paddingLeft="5dip"
android:paddingTop="5dip"
android:paddingBottom="5dip"
android:text="A"/>
<TextView
android:id="@+id/title"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_gravity="center_vertical"
android:gravity="center_vertical"
android:layout_weight="1.0"
android:textColor="#336598"
android:layout_marginLeft="5dip"
android:paddingTop="10dip"
android:paddingBottom="10dip"
android:text="hhhh"/>
</LinearLayout>
第一个id为catalog的TextView用来显示最顶部的GroupName的 第二个id为title的TextView显示具体的内容,控制id为catalog的TextView的显示与隐藏来显示groupName的显示与隐藏,只是每一个部分的最顶部的title的id为catalog的TextView显示,其他的隐藏。
再回到ListView的Adapter中,ContactsAdapter实现了SectionIndexer接口,我们重写了一下两个比较重要的方法
/**
* 根据ListView的当前位置获取分类的首字母的char ascii值
*/
public int getSectionForPosition(int position) {
return data.get(position).getSortLetters().charAt(0);
}
/**
* 根据分类的首字母的Char ascii值获取其第一次出现该首字母的位置
* 例如AA AB同样属于A的section,但是通过此方法发现AA的position与Item中的position相同
* 所以AA显示最上边的A的Section AB就不会显示
*/
public int getPositionForSection(int section) {
for (int i = 0; i < getCount(); i++) {
String sortStr = data.get(i).getSortLetters();
char firstChar = sortStr.toUpperCase().charAt(0);
if (firstChar == section) {
return i;
}
}
return -1;
}
上面的注释写的非常清楚了,这两个方法主要控制id为cataloge的TextView的显示与隐藏。
最后就是我们的主要的Activity的实现了:
package com.ling.contacts;
import java.util.ArrayList;
import java.util.List;
import com.ling.contacts.adapter.ContactsAdapter;
import com.ling.contacts.bean.Contacts;
import com.ling.contacts.view.RightLettersSlideBar;
import com.ling.contacts.view.RightLettersSlideBar.OnTouchingLetterChangedListener;
import android.app.Activity;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.widget.EditText;
import android.widget.ListView;
public class MainActivity extends Activity {
private String mDatas[];
private ListView listview;
private RightLettersSlideBar slideBar;
private ContactsAdapter adapter;
private List<Contacts> list;
private EditText search_edit;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mDatas=getResources().getStringArray(R.array.countries);
listview=(ListView) findViewById(R.id.listview);
slideBar=(RightLettersSlideBar) findViewById(R.id.slidebar);
search_edit=(EditText) findViewById(R.id.search_edit);
list=filledData(mDatas);
adapter=new ContactsAdapter(this, list);
listview.setAdapter(adapter);
slideBar.setOnTouchingLetterChangedListener(new OnTouchingLetterChangedListener() {
@Override
public void onTouchingLetterChanged(String s) {
// TODO Auto-generated method stub
int position=adapter.getPositionForSection(s.charAt(0));
if(position!=-1){
listview.setSelection(position);
}
}
});
//监听EditText的text变化
search_edit.addTextChangedListener(new TextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
getResult(s.toString());
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
// TODO Auto-generated method stub
}
@Override
public void afterTextChanged(Editable s) {
// TODO Auto-generated method stub
}
});
}
private List<Contacts> filledData(String [] data){
List<Contacts> mSortList = new ArrayList<Contacts>();
for(int i=0; i<data.length; i++){
Contacts c = new Contacts();
c.setName(data[i]);
String sortString = data[i].substring(0, 1).toUpperCase();
// 正则表达式,判断首字母是否是英文字母
if(sortString.matches("[A-Z]")){
c.setSortLetters(sortString.toUpperCase());
}else{
c.setSortLetters("#");
}
mSortList.add(c);
}
return mSortList;
}
/**
* 根据输入框中的值来过滤数据并更新ListView
* 如果是在数据中进行操作可以讲filterStr作为数据库表模糊查询的标记
* 类似like %filterStr% 还可以进行排序操作
* @param filterStr
*/
private void getResult(String filterStr) {
List<Contacts> filterDateList = new ArrayList<Contacts>();
if (TextUtils.isEmpty(filterStr)) {
filterDateList = list;
} else {
filterDateList.clear();
for (Contacts contact : list) {
String name = contact.getName();
if (name.toUpperCase().indexOf(
filterStr.toString().toUpperCase()) != -1
|| contact.getName().toUpperCase()
.startsWith(filterStr.toString().toUpperCase())) {
filterDateList.add(contact);
}
}
}
adapter.updateListView(filterDateList);
}
}
其实写到这里通讯录的简单原型也写的差不多了,比较简单,但是,在IOS和最新的Android操作系统中我们的通讯录都有类似QQ好友列表的效果,当滑动到某一个分组的时候,该分组的标题一直显示在list的最顶部,直至该部分的所有的item实现完成,所以下一篇我将具体写一下这种效果的实现。