标签:
实际上字母索引表的效果,可以说在现在的众多APP中使用的非常流行,比如支付宝,微信中的联系人,还有购物,买票的APP中选择全国城市,切换城市的时候,这时候的城市也就是按照一个字母索引的顺序来显示,看起来是很方便的.其实这种字母索引表的效果最开始是出现在微信的联系人中.因为觉得这种效果功能在今后的项目中可以说是非常常见,可能会用的上,所以准备来波博客讲述一下实现的原理,一来方便以后自己复习,二来如果能够帮助一些android路上奋斗小伙伴也是蛮有意义的.那么我们就开始吧
第一,首先我们来解决自定义View实现浮动的字母索引项的列表.
package com.mikyou.contactdemo.myview; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Typeface; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; public class MikyouLetterListView extends View { private OnTouchingLetterChangedListener listener; //定义了显示在最右边的浮动的索引项的列表,当然这个是固定的,所以可以直接初始化,如果需要变动的话则可以通过自定义属性来指定 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"}; int choose = -1;//用于标记点击存放字母数组中的下标 Paint paint = new Paint(); boolean showBkg = false;//这个boolean变量主要是控制当我们点击索引列表中整个索引列表的背景有个变化,为false表示开始没点击背景为正常显示时候的背景 public MikyouLetterListView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } public MikyouLetterListView(Context context, AttributeSet attrs) { super(context, attrs); } public MikyouLetterListView(Context context) { super(context); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (showBkg) {//如果此时为true的话则表示需要改变整个canvas背景也即是索引项的背景 canvas.drawColor(Color.parseColor("#10000000")); } /** * 注意:在自定义view中的如果不需要设置wrap_content属性就不需要自己重写onMeasure方法 * 因为在onMeasure方法中系统默认会自动测量两种模式:一个是match_parent另一个则是自己指定明确尺寸大小 * 这两种方法对应着这一种MeasureSpec.AT_MOST测量模式,由于我们设置这个自定义浮动的字母索引表宽度是指定明确大小 * 高度是match_parent模式,所以这里就不要手动测量了直接通过getHeight和getWidth直接拿到系统自动测量好高度和宽度 * */ int height = getHeight(); int width = getWidth(); //让整个显示的每个字母均分整个屏幕高度尺寸,这个singleHeight就是每个字母占据的高度 int singleHeight = height / b.length; //遍历循环绘制每一个字母text for (int i = 0; i < b.length; i++) { //绘制字母text的颜色 paint.setColor(Color.parseColor("#515151")); //绘制字母text的字体大小 paint.setTextSize(25); //绘制字母text的字体样式 paint.setTypeface(Typeface.DEFAULT_BOLD); //设置抗锯齿样式 paint.setAntiAlias(true); if (i == choose) {//判断如果点击字母的下标等于i,那么就会设置绘制点击字母的样式用于高亮显示 paint.setColor(Color.parseColor("#3399ff")); paint.setFakeBoldText(true); } /** * 注意:canvas在绘制text的时候,他绘制的起点不是text的左上角而是它的左下角 * (xPos,yPos)表示每个字母左下角的位置的坐标 *xPos = width / 2 - paint.measureText(b[i]) / 2:意思很容易理解,就是用 * (总的view的宽度(可能还包括leftPadding和rightPadding的大小)-每个字母宽度)/2得到就是每个字母的左下角的X坐标, * 仔细想下每个text的起点的x坐标都是一样的.paint.measureText(b[i])得到每一个字母宽度大小 * 由于是左下角,所以它们的Y坐标:应该如下设置 yPos = singleHeight * i + singleHeight; * */ float xPos = width / 2 - paint.measureText(b[i]) / 2;//得到绘制字母text的起点的X坐标 float yPos = singleHeight * i + singleHeight;//得到绘制字母text的起点的Y坐标 canvas.drawText(b[i], xPos, yPos, paint);//开始绘制每个字母 paint.reset();//绘制完一个字母需要重置一下画笔对象 } } @Override public boolean dispatchTouchEvent(MotionEvent event) {//重写view的触摸事件分发方法 final int action = event.getAction(); final float y = event.getY();//由于只涉及到Y轴坐标,只获取y坐标 final int oldChoose = choose;//oldChoose用于记录上一次点击字母所在字母数组中的下标 final int c = (int) (y / getHeight() * b.length);//得到点击或触摸的位置从而确定对应点击或触摸的字母所在字母数组中的下标 switch (action) { case MotionEvent.ACTION_DOWN://监听按下事件 showBkg = true;//按下后整个view的背景变色,showBkg为true if (oldChoose != c && listener != null) {//如果此次点击的字母数组下标不等于上一次的且已经注册了监听事件的, if (c >= 0 && c <= b.length) {//并且点击得到数组下标在字母数组范围内的,我们就将此时的字母回调出去 listener.onTouchingLetterChanged(b[c]);//我们就将此时的对应在字母数组中的字母回调出去 choose = c;//并且更新当前选中的字母下标存储在choose变量中 invalidate();//最后通知canvas重新绘制 } } break; case MotionEvent.ACTION_MOVE://监听移动事件,因为按下的时候已经把背景showBkg设置true,这里就不需要重新设置,其他操作与按下的事件一致 if (oldChoose != c && listener != null) { if (c >= 0 && c <= b.length) { listener.onTouchingLetterChanged(b[c]); choose = c; invalidate(); } } break; case MotionEvent.ACTION_UP://监听手指抬起的动作 showBkg = false;//此时的背景将会恢复到初始状态,showBkg=false choose = -1;//此时记录下标的变量也需要重置 invalidate();//并且重绘整个view break; } return true; } @Override public boolean onTouchEvent(MotionEvent event) { return super.onTouchEvent(event); } /** * 注册自定义监听器 * */ public void setOnTouchingLetterChangedListener( OnTouchingLetterChangedListener listener) { this.listener = listener; } /** * 定义一个接口,用于回调出点击后的字母,显示在弹出的字母对话框中 * */ public interface OnTouchingLetterChangedListener { public void onTouchingLetterChanged(String s); } }第二,点击字母后弹出的字母框的布局和样式的实现(这个比较简单弹出框就是一个TextView控件):
overlay.xml(布局) <?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="warp_content" android:textSize="70sp" android:textColor="#FFFFFF" android:background="@drawable/overlay_bg" android:minWidth="80dip" android:maxWidth="80dip" android:padding="10dp" android:gravity="center" /> overlay_bg.xml(布局样式): <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" > <solid android:color="#56abe4" /> <stroke android:width="0.5dp" android:color="#56abe4" /> <corners android:radius="15dp" /> </shape>
package com.mikyou.myguardian.bean; import java.io.Serializable; /** * Created by mikyou on 16-7-19. */ public class ContactBean implements Serializable { private int iconId; private String title; private String phoneNum; private String firstHeadLetter; public ContactBean(int iconId, String title, String phoneNum, String firstHeadLetter) { this.iconId = iconId; this.title = title; this.phoneNum = phoneNum; this.firstHeadLetter=firstHeadLetter; } public ContactBean() { } public int getIconId() { return iconId; } public String getFirstHeadLetter() { return firstHeadLetter; } public void setFirstHeadLetter(String firstHeadLetter) { this.firstHeadLetter = firstHeadLetter; } public void setIconId(int iconId) { this.iconId = iconId; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getPhoneNum() { return phoneNum; } public void setPhoneNum(String phoneNum) { this.phoneNum = phoneNum; } @Override public String toString() { return "ContactBean{" + "iconId=" + iconId + ", title='" + title + '\'' + ", phoneNum='" + phoneNum + '\'' + ", descriptor='" + descriptor + '\'' + ", firstHeadLetter='" + firstHeadLetter + '\'' + ", headLetterNum='" + headLetterNum + '\'' + '}'; } }第四整个布局activity_main.xml:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <android.support.v7.widget.Toolbar android:id="@+id/id_toolbar" android:layout_width="match_parent" android:layout_height="wrap_content" app:title="手机联系人" app:navigationIcon="@mipmap/more" android:background="@color/colorPrimary" app:titleTextColor="#FFFFFF" > </android.support.v7.widget.Toolbar> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent"> <ListView android:id="@+id/id_listview" android:layout_width="match_parent" android:layout_height="wrap_content" android:divider="#22000000" android:dividerHeight="0.1dp" ></ListView> <com.mikyou.contactdemo.myview.MikyouLetterListView android:id="@+id/id_letterview" android:layout_width="30dp" android:layout_height="match_parent" android:layout_alignParentRight="true" /> </RelativeLayout> </LinearLayout>
package com.mikyou.myguardian.service; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.net.Uri; import com.mikyou.myguardian.bean.ContactBean; import java.util.ArrayList; import java.util.List; /** * Created by mikyou on 16-7-19. * 用于返回读取到联系人的集合 */ public class ContactInfoService { private Context context; public ContactInfoService(Context context) { this.context = context; } public List<ContactBean> getContactList(){ List<ContactBean> mContactBeanList=new ArrayList<>(); ContactBean mContactBean=null; ContentResolver mContentResolver=context.getContentResolver(); Uri uri=Uri.parse("content://com.android.contacts/raw_contacts"); Uri dataUri=Uri.parse("content://com.android.contacts/data"); Cursor cursor =mContentResolver.query(uri,null,null,null,null); while (cursor.moveToNext()){ mContactBean=new ContactBean(); String id=cursor.getString(cursor.getColumnIndex("_id")); String title=cursor.getString(cursor.getColumnIndex("display_name"));//获取联系人姓名 String firstHeadLetter=cursor.getString(cursor.getColumnIndex("phonebook_label"));//这个字段保存了每个联系人首字的拼音的首字母 mContactBean.setTitle(title); mContactBean.setFirstHeadLetter(firstHeadLetter); Cursor dataCursor=mContentResolver.query(dataUri,null,"raw_contact_id= ?",new String[]{id},null); while(dataCursor.moveToNext()){ String type=dataCursor.getString(dataCursor.getColumnIndex("mimetype")); if (type.equals("vnd.android.cursor.item/phone_v2")){//如果得到的mimeType类型为手机号码类型才去接收 String phoneNum=dataCursor.getString(dataCursor.getColumnIndex("data1"));//获取手机号码 mContactBean.setPhoneNum(phoneNum); } } dataCursor.close(); if (mContactBean.getTitle()!=null&&mContactBean.getPhoneNum()!=null){ mContactBeanList.add(mContactBean); } } cursor.close(); return mContactBeanList; } }第六,就是实现联系人的ListView大家可能会看到这个和我们平时的看到的ListView有些不一样,因为在此次联系人的ListView中还有"A","B","C","D"...这小的字母item这个主要是将相同联系人的第一个字的拼音的首字母放在一起。那怎么去实现这样的一个ListView呢?
具体看该ListView的Adapter: package com.mikyou.myguardian.adapter; import android.content.Context; import android.view.View; import android.widget.ImageView; import android.widget.TextView; import com.mikyou.adapter.CommonAdapter; import com.mikyou.myguardian.R; import com.mikyou.myguardian.bean.ContactBean; import com.mikyou.tools.ViewHolder; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Created by mikyou on 16-7-19. */ public class TestContactListAdapter extends CommonAdapter<ContactBean> { private final int VIEW_TYPE=3; private Map<String,Integer> alphaIndexer; private List<String> sections; private List<ContactBean> listBeans; private OnGetAlphaIndexerAndSectionsListener listener; private boolean flag;//标志用于只执行一次代码 public TestContactListAdapter(Context context, List<ContactBean> listBeans, int layoutId) { super(context, listBeans, layoutId); this.listBeans=listBeans; alphaIndexer=new HashMap<>(); sections=new ArrayList<>(); for (int i = 0; i <listBeans.size();i++) { //当前汉语拼音的首字母 String currentAlpha=listBeans.get(i).getFirstHeadLetter(); //上一个拼音的首字母,如果不存在则为"" String previewAlpha=(i-1)>=0?listBeans.get(i-1).getFirstHeadLetter():""; if (!previewAlpha.equals(currentAlpha)){ String firstAlpha=listBeans.get(i).getFirstHeadLetter(); alphaIndexer.put(firstAlpha,i); sections.add(firstAlpha); } } } @Override public int getItemViewType(int position) { int type=0; if (position==0){ type=2; }else if (position==1){ type=1; } return type; } @Override public int getCount() { //注意:为什么没有直接把回调方法的调用写在构造器中呢?因为构造器只会调用一次,当第一次调用listener的时候是为空的 //而要初始化listener对象,则需要先去创建对象再去通过对象调用set方法来初始化这个listener对象,再去new对象的时候又要去用到listener产生了矛盾 //所以放在getCount中调用,只会调用一次,符合需求 if (!flag){ if (listener!=null){ listener.getAlphaIndexerAndSectionsListner(alphaIndexer,sections); } flag=true; } return listBeans.size(); } @Override public int getViewTypeCount() { return VIEW_TYPE; } @Override public void convert(ViewHolder viewHolder, ContactBean contactBean) { int viewType=getItemViewType(viewHolder.getmPosition()); ImageView iv=viewHolder.getView(R.id.contact_icon_id); iv.setImageResource(R.mipmap.contact_user); viewHolder.setText(R.id.contact_title,contactBean.getTitle()).setText(R.id.contact_phone_num,contactBean.getPhoneNum()); if (viewHolder.getmPosition()>=1){ String currentAlpha=listBeans.get(viewHolder.getmPosition()).getFirstHeadLetter(); String previewAlpha=listBeans.get(viewHolder.getmPosition()-1).getFirstHeadLetter(); if (!previewAlpha.equals(currentAlpha)){//不相等表示有新的字母项产生且为该类字母堆中的第一个字母索引项 viewHolder.getView(R.id.first_alpha).setVisibility(View.VISIBLE);//把新的字母列表项设置VISIBlE TextView tv= viewHolder.getView(R.id.first_alpha); tv.setText(currentAlpha); }else {//表示没有新的字母堆出现,也就说明该item是属于同类字母堆中且不是第一个,那么就需要将这个索引项设置GONE viewHolder.getView(R.id.first_alpha).setVisibility(View.GONE); } } } public void setOnGetAlphaIndeserAndSectionListener(OnGetAlphaIndexerAndSectionsListener listener){ this.listener=listener; } public interface OnGetAlphaIndexerAndSectionsListener{ public void getAlphaIndexerAndSectionsListner(Map<String,Integer>alphaIndexer,List<String>sections); } }第七该ListView的Item布局:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" xmlns:android="http://schemas.android.com/apk/res/android"> <TextView android:id="@+id/first_alpha" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="14sp" android:padding="5dp" android:background="#cccccc" android:text="Z" android:gravity="center_vertical" android:paddingLeft="10dp" /> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="8dp" > <ImageView android:id="@+id/contact_icon_id" android:layout_width="45dp" android:layout_height="45dp" android:src="@mipmap/contact_user" android:layout_centerVertical="true" /> <TextView android:id="@+id/contact_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="张三" android:textSize="18sp" android:textColor="#9d55b8" android:layout_toRightOf="@id/contact_icon_id" android:layout_marginLeft="20dp" /> <TextView android:id="@+id/contact_phone_num" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="123456789" android:textColor="#ea8010" android:textSize="14sp" android:layout_below="@id/contact_title" android:layout_alignLeft="@id/contact_title" /> </RelativeLayout> </LinearLayout>
package com.mikyou.contactdemo; import android.content.Context; import android.graphics.PixelFormat; import android.os.Bundle; import android.os.Handler; import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.ListView; import android.widget.TextView; import com.mikyou.contactdemo.adapter.TestContactListAdapter; import com.mikyou.contactdemo.bean.ContactBean; import com.mikyou.contactdemo.myview.MikyouLetterListView; import com.mikyou.contactdemo.service.ContactInfoService; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; public class MainActivity extends AppCompatActivity implements TestContactListAdapter.OnGetAlphaIndexerAndSectionsListener{ private List<ContactBean> mContactBeanList;//所有联系人集合 private ListView mContactListView;//联系人ListView private MikyouLetterListView mLetterListView;//字母表 private TextView overLayout;//弹出对话框 private OverlayThread overlayThread; private Map<String, Integer> alphaIndexer;// 存放存在的汉语拼音首字母和与之对应的列表位置 private Handler handler; private TestContactListAdapter adapter; private List<String> sections;// 存放存在的汉语拼音首字母 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initData(); initView(); initOverlay(); } private void initView() { registerAllViewIds(); registerAllViewAdapters(); registerAllViewEvents(); } private void registerAllViewIds() { mContactListView= (ListView) findViewById(R.id.id_listview); mLetterListView= (MikyouLetterListView) findViewById(R.id.id_letterview); } private void registerAllViewAdapters() { adapter=new TestContactListAdapter(this,mContactBeanList,R.layout.contact_list_item); adapter.setOnGetAlphaIndeserAndSectionListener(this); mContactListView.setAdapter(adapter); } private void registerAllViewEvents() { mLetterListView.setOnTouchingLetterChangedListener(new LetterListViewListener()); } private void initData() { //alphaIndexer=new HashMap<>(); handler=new Handler(); overlayThread=new OverlayThread(); ContactInfoService mContactInfoService=new ContactInfoService(this); mContactBeanList=mContactInfoService.getContactList();//返回手机联系人对象集合 //按拼音首字母表排序 Collections.sort(mContactBeanList,comparator); } @Override public void getAlphaIndexerAndSectionsListner(Map<String, Integer> alphaIndexer, List<String> sections) { this.alphaIndexer=alphaIndexer; this.sections=sections; Log.d("list",alphaIndexer.toString()+"\n"+sections.toString()); } /** * @Mikyou * 字母列表点击滑动监听器事件 * */ private class LetterListViewListener implements MikyouLetterListView.OnTouchingLetterChangedListener { @Override public void onTouchingLetterChanged(final String s) { if (alphaIndexer.get(s) != null) {//判断当前选中的字母是否存在集合中 int position = alphaIndexer.get(s);//如果存在集合中则取出集合中该字母对应所在的位置,再利用对应的setSelection,就可以实现点击选中相应字母,然后联系人就会定位到相应的位置 mContactListView.setSelection(position); overLayout.setText(s); overLayout.setVisibility(View.VISIBLE); handler.removeCallbacks(overlayThread); // 延迟一秒后执行,让overlay为不可见 handler.postDelayed(overlayThread, 1500); } } } /** * @mikyou * 初始化汉语拼音首字母弹出提示框 * */ private void initOverlay() { LayoutInflater inflater = LayoutInflater.from(this); overLayout = (TextView) inflater.inflate(R.layout.overlay, null); overLayout.setVisibility(View.INVISIBLE); WindowManager.LayoutParams lp = new WindowManager.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.TYPE_APPLICATION, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, PixelFormat.TRANSLUCENT); WindowManager windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE); windowManager.addView(overLayout, lp); } /** * @Mikyou * 首字母按a-z排序 * */ Comparator<ContactBean> comparator=new Comparator<ContactBean>() { @Override public int compare(ContactBean t1, ContactBean t2) { String a=t1.getFirstHeadLetter(); String b=t2.getFirstHeadLetter(); int flag=a.compareTo(b); if (flag==0){ return a.compareTo(b); }else{ return flag; } } }; /** * @Mikyou * 设置overlay不可见 * */ private class OverlayThread implements Runnable{ @Override public void run() { overLayout.setVisibility(View.GONE); } } }到这里就谈得差不多了,这个很常用,准备给自己以后的项目中继续用。
标签:
原文地址:http://blog.csdn.net/u013064109/article/details/52013744