Android 学习笔记 自定义控件之仿微信联系人快速导航控件
2017-03-02 本文已影响234人
maoqitian
- 自定义控件第三篇笔记。
平时使用微信查找联系人的时候,我们可以根据想要找的联系人的名字首字母快速找到该联系人,这个快速检索的控件有不少地方使用,比如手机原生的联系人应用中也有这个功能。今天我们就来手动实现一下这个自定义控件。
按照惯例,首先来一发效果图:

-
要实现这个控件,首先我们可以实现右边的导航栏MyQuickIndexBar;有些人看到这个导航栏可能会想到继承ViewGroup,然后再将字母作为子View去摆放,其实大可不必,这个导航栏的目的就是你点中那个字母,控件就可以根据你点中的字母自动找到对应联系人,也就是调用者需要知道导航控件点中的是哪个字母,这样其实就可以集成View来实现这个自定义控件,我们只需要将每个字母在View上等分绘制出来就可以了;具体思想:
- 将26字母定义在String数组中,循环遍历将每个字母绘制在View中,绘制的坐标X应该等于控件宽度一半,而y坐标等于控件格子高度一半+文本字母高度的一半+格子所处位置*每个格子的高度(相当于把View等分成26格子,让后在每个格子中间绘制字母)
-
2.记下来是处理触摸事件,手指按下(ACTION_DOWN)和手指滑动(ACTION_MOVE)获取此时的Y坐标,除与每个格子的高度的余数,就是手指所处字母的索引,记录下这个索引,并将这个索引对应的字母通过
接口回调的方式传告诉调用者,并在手指抬起的时候将之前记录的索引清除,防止下次点击重复位置没有反应,并且只要按下则调用invalidate();方法引起界面重绘使按下的字母颜色变化;下面上代码:
/**
* Created by 毛麒添 on 2017/2/26 0026.
* 触摸View的时候,能快速定位获取触摸的是哪个字母
*/
public class MyQuickIndexBar extends View {
private String[] indexArr = { "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 Paint paint;
private int width;//控件宽度
private float latticeHeight;//将控件等分每个部分的高度
private int lastIndex=-1;//上一次点击字母记录的索引
public MyQuickIndexBar(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public MyQuickIndexBar(Context context) {
super(context);
init();
}
public MyQuickIndexBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
//初始化画笔参数
paint=new Paint(Paint.ANTI_ALIAS_FLAG);//设置抗锯齿
paint.setTextSize(40);
paint.setTextAlign(Paint.Align.CENTER);
paint.setColor(Color.GRAY);
}
//初始化宽高
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
width = getMeasuredWidth();
latticeHeight=getMeasuredHeight()*1f/indexArr.length;
}
//绘制
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < indexArr.length; i++) {
float x=width/2;
//每一个格子字母的摆放位置 控件格子高度一半+文本字母高度的一半+格子所处位置*每个格子的高度
float y=latticeHeight/2+getTextHeight(indexArr[i])/2+i*latticeHeight;
//如果索引相等,则改变字体的颜色
paint.setColor(lastIndex==i?Color.BLACK:Color.GRAY);
canvas.drawText(indexArr[i],x,y,paint);
}
}
//触摸监听,需要有自己的处理,返回true
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
//获取按下的位置
float y = event.getY();
//获取按下位置与每个格子高度的余数,这个余数就是每个字母的索引
int index= (int) (y/latticeHeight);
if(lastIndex!=index){
//Log.w("毛麒添",indexArr[index]);
if(index>=0 && index<indexArr.length){//索引安全性检查
if(listener!=null){
listener.onTouchIndex(indexArr[index]);
}
}
}
lastIndex=index;
break;
case MotionEvent.ACTION_UP:
lastIndex=-1;
break;
}
//引发重绘
invalidate();
return true;
}
/**
* 获取文本的高度
* @param text 需要获取高度的文本
* @return 文本的高度
*/
private int getTextHeight(String text) {
Rect rect=new Rect();
paint.getTextBounds(text,0,text.length(),rect);
return rect.height();
}
//回调监听,把点击的字母暴露给使用者
private onTouchIndexListener listener;
public void setOnTouchIndexListener(onTouchIndexListener listener){
this.listener=listener;
}
public interface onTouchIndexListener{
void onTouchIndex(String text);
}
}
- 这时候我们可以给ListView填充一些假数据,并设置每个ListView的item由两个TextView组成,一个是联系人名字,一个是首字母标题,在下面的步骤中当我们就可以做根据是否是相同首字母的联系人来让首字母标题是否隐藏的处理;ListView数据适配器代码:
**
* Created by 毛麒添 on 2017/2/27 0027.
* 联系人数据适配器
*/
public class MyAdapter extends BaseAdapter {
private ArrayList<Contacts> contactsArrayList=new ArrayList<Contacts>();
private Context context;
public MyAdapter(Context context, ArrayList<Contacts> contactsArrayList){
this.context=context;
this.contactsArrayList=contactsArrayList;
}
@Override
public int getCount() {
return contactsArrayList.size();
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if(convertView==null){
convertView=View.inflate(context, R.layout.layout_listview_item,null);
}
ViewHolder viewHolder=ViewHolder.getHolder(convertView);
Contacts contacts = contactsArrayList.get(position);
viewHolder.tv_name.setText(contacts.getName());
//当前汉字的首字母
String currentWord=contacts.getPinYin().charAt(0)+"";
if(position>0){
//获取上一个汉字的首字母
String lastcurrentWord = contactsArrayList.get(position - 1).getPinYin().charAt(0) + "";
if(lastcurrentWord.equals(currentWord)){
//如果首字母相同,将其头部隐藏
viewHolder.tv_index.setVisibility(View.GONE);
}else {//不一样则显示(ListView的复用,所以需要设置显示)
viewHolder.tv_index.setVisibility(View.VISIBLE);
viewHolder.tv_index.setText(currentWord);
}
}else {
viewHolder.tv_index.setVisibility(View.VISIBLE);
viewHolder.tv_index.setText(currentWord);
}
return convertView;
}
static class ViewHolder{
TextView tv_index;
TextView tv_name;
//重新封装ViewHolder
public ViewHolder(View convertView){
tv_index= (TextView) convertView.findViewById(R.id.tv_index);
tv_name= (TextView) convertView.findViewById(R.id.tv_name);
}
public static ViewHolder getHolder(View convertView){
ViewHolder viewHolder = (ViewHolder) convertView.getTag();
if(viewHolder==null){
viewHolder=new ViewHolder(convertView);
convertView.setTag(viewHolder);
}
return viewHolder;
}
}
}
- 接下来就是处理汉字获取拼音,这里需要用到类库pinyin4j.jar,我们可以把这个处理写成一个工具类PinYinUtil,具体思路为:
- 首先判断传入的字符型联系人名字是否为空,不为空则将其转换成字节数组(由于pinyin4j.jar只能将单个汉字的转换成对应的拼音,所有转换成字节数组),
- 一个汉字占2个字节,一字节8位,范围-128~~127, 大于127则可能是汉字,如果转换成功,则将去返回的拼音字母的第一个作为工具类返回值,而如果是其他字符而不是汉字也需要将其拼接起来返回,下面上代码:
/**
* Created by 毛麒添 on 2017/3/1 0001.
* 汉字转换成英文字母工具类
*/
public class PinYinUtil {
/**
* 汉字转换成英文字母工具类
* @param chinese 需要转换的汉字
* @return 汉字的大写拼音字母
*/
public static String getPinYin(String chinese){
String pinYin="";
//设置转换的格式
HanyuPinyinOutputFormat format=new HanyuPinyinOutputFormat();
format.setCaseType(HanyuPinyinCaseType.UPPERCASE);//大写字母
format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);//没有声调
//如果传入的汉字为空,返回null
if(TextUtils.isEmpty(chinese))return null;
//转化拼音的类库只能转化单个汉字,所以需要将传入的汉字转换成字节数组
char[] charArray = chinese.toCharArray();
for (int i = 0; i <charArray.length ; i++) {
//过滤空格
if(Character.isWhitespace(charArray[i]))continue;
//一个汉字占两个字节,一字节8位,范围-128~~127
if(charArray[i]>127){//可能是汉字
try {
String[] pinYinArray = PinyinHelper.toHanyuPinyinStringArray(charArray[i], format);
if(pinYinArray!=null){//转换成功
//返回数组是因为有可能该汉字是多音字,但是不管是否多音字都只取第一个
pinYin+=pinYinArray[0];
}else {
//转化失败,不做操作,忽略
}
} catch (BadHanyuPinyinOutputFormatCombination badHanyuPinyinOutputFormatCombination) {
badHanyuPinyinOutputFormatCombination.printStackTrace();
//转化失败则将其忽略
}
}else {//不是汉字,不需要转换将其和返回值拼接在一起
pinYin+=charArray[i];
}
}
return pinYin;
}
}
- 最后,给存放联系人的集合排序,汉字转换成拼音处理好则直接获取他的首字母与手指点击的字母相比较后将ListView所对应的Item设置到顶部就可以了,并将手指锁点击的字母用TextView显示在正中央,背景加入圆角,并给TextView的显示加入一些缩放动画效果,显得更加高端;布局跟结点为RelativeLayout,比较简单,就不贴了(详情可以查看源码),下面上MainActivity代码:
public class MainActivity extends AppCompatActivity {
private MyQuickIndexBar my_quick_index_bar;
private ListView listView;
private TextView tv_text;
private ArrayList<Contacts> contactsArrayList=new ArrayList<Contacts>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
//初始化ListView数据
fillList();
//对通讯录数据进行排序
Collections.sort(contactsArrayList);
//设置数据
listView.setAdapter(new MyAdapter(this,contactsArrayList));
my_quick_index_bar.setOnTouchIndexListener(new MyQuickIndexBar.onTouchIndexListener() {
@Override
public void onTouchIndex(String text) {
//根据点击的字母与遍历集合获取的首字母相比较
for (int i = 0; i < contactsArrayList.size(); i++) {
String firstWord = contactsArrayList.get(i).getPinYin().charAt(0) + "";
if(text.equals(firstWord)){//首字母相同
listView.setSelection(i);
break;//只要找到就直接显示就可以
}
}
//显示当前触摸的字母
showCurrentWord(text);
}
});
//一开始隐藏TextView控件
ViewHelper.setScaleX(tv_text,0);
ViewHelper.setScaleY(tv_text,0);
}
private Handler handler=new Handler();
private boolean isScale=false;//动画是否缩放
//显示当前触摸的字母
private void showCurrentWord(String text) {
tv_text.setText(text);
if(!isScale){
isScale=true;
ViewPropertyAnimator.animate(tv_text).scaleX(1f).setInterpolator(new OvershootInterpolator()).setDuration(350).start();
ViewPropertyAnimator.animate(tv_text).scaleY(1f).setInterpolator(new OvershootInterpolator()).setDuration(350).start();
}
//执行延迟前清除所有消息
handler.removeCallbacksAndMessages(null);
//执行延迟缩放动画,让TextView缩小至消失
handler.postDelayed(new Runnable() {
@Override
public void run() {
ViewPropertyAnimator.animate(tv_text).scaleX(0f).setInterpolator(new OvershootInterpolator()).setDuration(350).start();
ViewPropertyAnimator.animate(tv_text).scaleY(0f).setInterpolator(new OvershootInterpolator()).setDuration(350).start();
isScale=false;
}
},1000);
}
private void initView(){
my_quick_index_bar= (MyQuickIndexBar) findViewById(R.id.my_quick_index_bar);
listView= (ListView) findViewById(R.id.listview);
tv_text= (TextView) findViewById(R.id.tv_text);
}
private void fillList() {
// 虚拟数据
contactsArrayList.add(new Contacts("路飞"));
contactsArrayList.add(new Contacts("路飞1"));
contactsArrayList.add(new Contacts("索隆"));
.....
}
}
好了,到此快速导航控件已经完成。温故而知新,这应该就是学习笔记存在的意义吧。如果有错的地方,请大家指出,让我们一起共同进步!