最近项目需要,于是自己实现了一个带导航栏的通讯录,上代码!
一、数据准备
(1)bean:
public class Friend { private String remark; private String account; private String nickName; private String phoneNumber; private String area; private String headerUrl; private String pinyin; public String getPinyin() { return pinyin; } public void setPinyin(String pinyin) { this.pinyin = pinyin; } public void setAccount(String account) { this.account = account; } public String getAccount() { return account; } public void setArea(String area) { this.area = area; } public String getArea() { return area; } public void setNickName(String nickName) { this.nickName = nickName; } public String getNickName() { return nickName; } public String getPhoneNumber() { return phoneNumber; } public void setPhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; } public String getRemark() { return remark; } public void setRemark(String remark) { this.remark = remark; } public void setHeaderUrl(String headerUrl) { this.headerUrl = headerUrl; } public String getHeaderUrl() { return headerUrl; } public String getFirstPinyin(){ return pinyin!=null?pinyin.substring(0,1):""; } }
(2)测试数据:
从网上找了一堆姓名存进一个txt文件中,并放在raw文件下,然后在程序运行时读入到List中:
private List<Friend> getFriendList(){ List<Friend> friends=new ArrayList<>(); InputStream inputStream=getResources().openRawResource(R.raw.names); BufferedReader reader=new BufferedReader(new InputStreamReader(inputStream)); String input=null; try { while ((input=reader.readLine())!=null){ Friend friend=new Friend(); friend.setAccount(input); String pinyin=Pinyin4jUtil.convertToFirstSpell(input); if (Pinyin4jUtil.isPinYin(pinyin)){ friend.setPinyin(pinyin); }else { friend.setPinyin("#"); } friends.add(friend); } if (friends.size()>1){ Collections.sort(friends,new PinYinComparator()); } } catch (IOException e) { e.printStackTrace(); } return friends; }
注意:
a.关于拼音的处理,这里使用的是pinyin4j,对录入的数据进行拼音转化和判断,若不是拼音则存入“#”,为的是后面将一些将用户名存储为数字和特殊符号的数据能够排到最后面。
附上拼音转换和判断代码:
public class Pinyin4jUtil { public static String convertToFirstSpell(String chinese){ StringBuffer pinyinName=new StringBuffer(); char[] nameChar=chinese.toCharArray(); HanyuPinyinOutputFormat defaultFormat=new HanyuPinyinOutputFormat(); defaultFormat.setCaseType(HanyuPinyinCaseType.UPPERCASE); defaultFormat.setToneType(HanyuPinyinToneType.WITHOUT_TONE); for (char c:nameChar){ if (c>128){ try { String[] strs= PinyinHelper.toHanyuPinyinStringArray(c,defaultFormat); if (strs!=null){ for (int i=0;i<strs.length;i++){ pinyinName.append(strs[i].charAt(0)); if (i!=strs.length-1){ pinyinName.append(","); } } } } catch (BadHanyuPinyinOutputFormatCombination badHanyuPinyinOutputFormatCombination) { badHanyuPinyinOutputFormatCombination.printStackTrace(); } }else { pinyinName.append(c); } pinyinName.append(" "); } return parseTheChineseByObject(discountTheChinese(pinyinName.toString())); } public static String convertToSpell(String chinese){ StringBuffer pinyinName=new StringBuffer(); char[] nameChar=chinese.toCharArray(); HanyuPinyinOutputFormat defaultFormat=new HanyuPinyinOutputFormat(); defaultFormat.setCaseType(HanyuPinyinCaseType.LOWERCASE); defaultFormat.setToneType(HanyuPinyinToneType.WITHOUT_TONE); for (char c:nameChar){ if (c>128){ try { String[] strs= PinyinHelper.toHanyuPinyinStringArray(c,defaultFormat); if (strs!=null){ for (int i=0;i<strs.length;i++){ pinyinName.append(strs[i]); if (i!=strs.length-1){ pinyinName.append(","); } } } } catch (BadHanyuPinyinOutputFormatCombination badHanyuPinyinOutputFormatCombination) { badHanyuPinyinOutputFormatCombination.printStackTrace(); } }else { pinyinName.append(c); } pinyinName.append(" "); } return parseTheChineseByObject(discountTheChinese(pinyinName.toString())); } public static List<String> convertToSpellList(String chinese){ StringBuffer pinyinName=new StringBuffer(); char[] nameChar=chinese.toCharArray(); HanyuPinyinOutputFormat defaultFormat=new HanyuPinyinOutputFormat(); defaultFormat.setCaseType(HanyuPinyinCaseType.LOWERCASE); defaultFormat.setToneType(HanyuPinyinToneType.WITHOUT_TONE); for (char c:nameChar){ if (c>128){ try { String[] strs= PinyinHelper.toHanyuPinyinStringArray(c,defaultFormat); if (strs!=null){ for (int i=0;i<strs.length;i++){ pinyinName.append(strs[i]); if (i!=strs.length-1){ pinyinName.append(","); } } } } catch (BadHanyuPinyinOutputFormatCombination badHanyuPinyinOutputFormatCombination) { badHanyuPinyinOutputFormatCombination.printStackTrace(); } }else { pinyinName.append(c); } pinyinName.append(" "); } return parseTheChineseByObjectToList(discountTheChinese(pinyinName.toString())); } private static List<Map<String ,Integer>> discountTheChinese(String theStr){ List<Map<String,Integer>> mapList=new ArrayList<Map<String, Integer>>(); Map<String,Integer> onlyOne=null; String[] firsts=theStr.split(" "); for (String str:firsts){ onlyOne=new Hashtable<String, Integer>(); String[] china=str.split(","); for (String s:china){ Integer count=onlyOne.get(s); if (count==null){ onlyOne.put(s,new Integer(1)); }else { onlyOne.remove(s); count++; onlyOne.put(s,count); } } mapList.add(onlyOne); } return mapList; } private static String parseTheChineseByObject(List<Map<String,Integer>> list){ Map<String,Integer> first=null; for (int i=0;i<list.size();i++){ Map<String,Integer> temp=new Hashtable<String, Integer>(); if (first!=null){ for (String s:first.keySet()){ for (String s1:list.get(i).keySet()){ String str=s+s1; temp.put(str,1); } } if (temp!=null&&temp.size()>0){ first.clear(); } }else { for (String s:list.get(i).keySet()){ String str=s; temp.put(str,1); } } if (temp!=null&&temp.size()>0){ first=temp; } } String returnStr=""; List<String> returnList=new ArrayList<>(); if (first!=null){ for (String str:first.keySet()){ returnStr+=(str+" "); returnList.add(str); } } if (returnStr.length()>0){ returnStr=returnStr.substring(0,returnStr.length()-1); } return returnList.get(0); } private static List<String> parseTheChineseByObjectToList(List<Map<String,Integer>> list){ Map<String,Integer> first=null; for (int i=0;i<list.size();i++){ Map<String,Integer> temp=new Hashtable<String, Integer>(); if (first!=null){ for (String s:first.keySet()){ for (String s1:list.get(i).keySet()){ String str=s+s1; temp.put(str,1); } } if (temp!=null&&temp.size()>0){ first.clear(); } }else { for (String s:list.get(i).keySet()){ String str=s; temp.put(str,1); } } if (temp!=null&&temp.size()>0){ first=temp; } } List<String> returnList=new ArrayList<>(); if (first!=null){ for (String str:first.keySet()){ returnList.add(str); } } return returnList; } public static boolean isPinYin(String string){ char[] chars=string.toCharArray(); for (char c:chars){ if ((c>=65&&c<=90)||(c>=97&&c<=122)){ }else { return false; } } return true; } }
b.在上面录入数据到List中之后需要对数据按照字典序进行排序,使用的是comparator接口:
public class PinYinComparator implements Comparator<Friend> { @Override public int compare(Friend o1, Friend o2) { if (o1.getPinyin().equals("#")){ return 1; }else if (o2.getPinyin().equals("#")){ return -1; } return o1.getPinyin().compareToIgnoreCase(o2.getPinyin()); } }
二、RecyclerView和Adapter的使用
这里不对RecycleView做过多介绍,主要是在Adapter的使用上有所不同,需要在onBindViewHolder中做一些判断。
对于不同拼音首字母开头的分组,并且产生如下效果的解决方法是:
a.列表项布局文件的处理:
在每一个列表布局中都放一个首字母头的布局,只是设为gone
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content"> <LinearLayout android:id="@+id/stick_container" android:visibility="gone" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="40dp" android:background="@color/txtGray" android:gravity="center_vertical" > <TextView android:id="@+id/header" android:text="A" android:textSize="20sp" android:layout_marginLeft="20dp" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout> <LinearLayout android:gravity="center" android:layout_width="match_parent" android:layout_height="80dp"> <TextView android:id="@+id/name" android:textSize="25sp" android:text="name" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout> </LinearLayout>
b.onBindViewHolder的判断处理
Friend friend=getItem(position); ((TextView)holder.getView(R.id.name)).setText(friend.getAccount()); if (position==0){ holder.getView(R.id.stick_container).setVisibility(View.VISIBLE); ((TextView)holder.getView(R.id.header)).setText(friend.getFirstPinyin()); }else { if (!TextUtils.equals(friend.getFirstPinyin(),getItem(position-1).getFirstPinyin())){ holder.getView(R.id.stick_container).setVisibility(View.VISIBLE); ((TextView)holder.getView(R.id.header)).setText(friend.getFirstPinyin()); }else { holder.getView(R.id.stick_container).setVisibility(View.GONE); } } holder.itemView.setContentDescription(friend.getFirstPinyin());
三、首字母导航栏实现
效果如下
右边的首字母导航栏采用自定义View实现,代码如下:
public class PinYinSlideView extends View implements View.OnTouchListener{ private Paint textPaint; private Paint backgroundPaint; private Paint circlePaint,circleTextPaint; private int height; private float textHeight; private float paddingHeight; private float radius; private float backgroundSize; private boolean hasTouch; private float lastY,lastX; private float screenX,screenY; private OnShowTextListener onShowTextListener; public PinYinSlideView(Context context) { this(context,null); } public PinYinSlideView(Context context, AttributeSet attrs) { super(context, attrs); initView(); } public void setOnShowTextListener(OnShowTextListener onShowTextListener) { this.onShowTextListener = onShowTextListener; } private void initView(){ textPaint=new Paint(); textPaint.setAntiAlias(true); textPaint.setStyle(Paint.Style.FILL); textPaint.setColor(getResources().getColor(R.color.txtGray)); float textSize= TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,10,getResources().getDisplayMetrics()); textPaint.setTextSize(textSize); textPaint.setTextAlign(Paint.Align.CENTER); Paint.FontMetrics metrics=textPaint.getFontMetrics(); textHeight=metrics.bottom-metrics.top; backgroundPaint=new Paint(); backgroundPaint.setAntiAlias(true); backgroundPaint.setStyle(Paint.Style.FILL); backgroundPaint.setColor(getResources().getColor(R.color.backgroundGray)); backgroundSize=TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,30,getResources().getDisplayMetrics()); radius=TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,3,getResources().getDisplayMetrics()); circlePaint=new Paint(); circleTextPaint=new Paint(); circlePaint.setAntiAlias(true); circleTextPaint.setAntiAlias(true); circlePaint.setColor(getResources().getColor(R.color.backgroundGray)); circleTextPaint.setColor(getResources().getColor(R.color.txtGray)); circlePaint.setStyle(Paint.Style.FILL); circlePaint.setStrokeWidth(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,100,getResources().getDisplayMetrics())); circleTextPaint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,30,getResources().getDisplayMetrics())); circleTextPaint.setTextAlign(Paint.Align.CENTER); this.setOnTouchListener(this); } @Override public boolean dispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { height=MeasureSpec.getSize(heightMeasureSpec); paddingHeight=(height-28*textHeight)/29; screenX=getResources().getDisplayMetrics().widthPixels/2; screenY=height/2; int mode=MeasureSpec.getMode(widthMeasureSpec); float size = 0; switch (mode){ case MeasureSpec.EXACTLY: size=MeasureSpec.getSize(widthMeasureSpec); break; case MeasureSpec.AT_MOST: size=backgroundSize; break; } setMeasuredDimension((int) size,height); } @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()){ case MotionEvent.ACTION_DOWN: lastX=event.getX(); lastY=event.getY(); if (lastX>0&&lastX<=backgroundSize){ hasTouch=true; invalidate(); requestLayout(); } break; case MotionEvent.ACTION_MOVE: lastX=event.getX(); lastY=event.getY(); if (lastX>0&&lastX<=backgroundSize){ hasTouch=true; invalidate(); requestLayout(); } break; case MotionEvent.ACTION_UP: if (hasTouch){ hasTouch=false; invalidate(); } break; } return true; } @Override protected void onDraw(Canvas canvas) { char c[]={'↑','A','#'}; float baseY=textHeight; float baseX=(0+backgroundSize)/2; float radius1=TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,100,getResources().getDisplayMetrics())/2; if (hasTouch){ char c1[]={'↑','A','#'}; if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.LOLLIPOP){ canvas.drawRoundRect(0,0,backgroundSize,height,radius,radius,backgroundPaint); }else { canvas.drawRect(0,0,backgroundSize,height,backgroundPaint); } float offsetY=textHeight+paddingHeight; for (int i=0;i<28;i++){ if (lastY>=(i*offsetY)&&lastY<=((i+1)*offsetY)){ canvas.drawCircle(screenX,screenY,radius1,circlePaint); if (i==0){ if (onShowTextListener!=null){ onShowTextListener.showText(String.valueOf(c1[0])); } //canvas.drawText(c1,0,1,screenX,screenY+textHeight,circleTextPaint); }else if (i>0&&i<27){ if (onShowTextListener!=null){ onShowTextListener.showText(String.valueOf(c1[1])); } //canvas.drawText(c1,1,1,screenX,screenY+textHeight,circleTextPaint); }else if (i==27){ if (onShowTextListener!=null){ onShowTextListener.showText(String.valueOf(c1[2])); } //canvas.drawText(c1,2,1,screenX,screenY+textHeight,circleTextPaint); } break; //canvas.drawText(c1,0,1,screenX,screenY+textHeight,circleTextPaint); } if (i>0&&i<27){ c1[1]++; } } }else { if (onShowTextListener!=null){ onShowTextListener.showText(null); } } for (int i=0;i<28;i++){ if (i==0){ canvas.drawText(c,0,1,baseX,baseY,textPaint); }else if (i>0&&i<27){ canvas.drawText(c,1,1,baseX,baseY,textPaint); c[1]++; }else if (i==27){ canvas.drawText(c,2,1,baseX,baseY,textPaint); } baseY+=(paddingHeight+textHeight); } } public interface OnShowTextListener{ void showText(String text); } }
出现在中间的半透明带圆背景字体通过继承TextView实现,代码如下:
public class CircleTextView extends View { private Paint circlePaint,circleTextPaint; private float textHeight; private String text; private float radius; public CircleTextView(Context context) { this(context,null); } public CircleTextView(Context context, AttributeSet attrs) { super(context, attrs); initView(); } private void initView(){ circlePaint=new Paint(); circleTextPaint=new Paint(); circlePaint.setAntiAlias(true); circleTextPaint.setAntiAlias(true); circlePaint.setColor(getResources().getColor(R.color.backgroundGray)); circleTextPaint.setColor(getResources().getColor(R.color.txtGray)); circlePaint.setStyle(Paint.Style.FILL); circlePaint.setStrokeWidth(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,100,getResources().getDisplayMetrics())); circleTextPaint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,30,getResources().getDisplayMetrics())); circleTextPaint.setTextAlign(Paint.Align.CENTER); Paint.FontMetrics metrics=circleTextPaint.getFontMetrics(); textHeight=metrics.bottom-metrics.top; radius=TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,100,getResources().getDisplayMetrics())/2; } public void setText(String text) { this.text = text; invalidate(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension((int)radius*2,(int) radius*2); } @Override protected void onDraw(Canvas canvas) { if (text!=null){ canvas.drawCircle(radius,radius,radius,circlePaint); canvas.drawText(text,radius,radius+textHeight/4,circleTextPaint); } } }
当触摸导航栏时通过监听器OnShowTextListener的showText方法将触摸到的字体的字符传递给CircleTextView显示出来,代码如下:
pinYinSlideView.setOnShowTextListener(new PinYinSlideView.OnShowTextListener() { @Override public void showText(String text) { circleText.setText(text); if (text!=null){ if (!text.equals("↑")){ int position=0; boolean hasPinyin=false; for (int i=0;i<friends.size();i++){ Friend friend=friends.get(i); if (friend.getFirstPinyin().equals(text)){ position=i; hasPinyin=true; break; } } if (hasPinyin){ MainActivity.this.scrollToPosition(position); } }else { MainActivity.this.scrollToPosition(0); } } } });
接下来是重中之重,这里涉及到了RecyclerView的滚动到指定位置问题,当触摸到对应得字母时RecyclerView也要滚动到对应的列表项,这个需要实现RecyclerView的OnScrollListener,还要配合它的scrollToPosition 和 scrollBy 方法来实现:
代码如下:
contactList.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); RecyclerView.LayoutManager layoutManager=recyclerView.getLayoutManager(); if (layoutManager instanceof LinearLayoutManager){ int firstItem=((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition(); Friend friend=contactAdapter.getItem(firstItem); header.setText(friend.getFirstPinyin()); if (move){ move=false; int n=mIndex-firstItem; if (n>=0&&n<contactList.getChildCount()){ int top=contactList.getChildAt(n).getTop(); contactList.scrollBy(0,top); } } } } @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); } });
其中:
onScrolled(RecyclerView recyclerView, int dx, int dy) :
滚动时回调,dx指水平滚动距离,dy指垂直滚动距离
onScrollStateChanged(RecyclerView recyclerView, int newState):
滚动状态变化时回调,dx指水平滚动距离,dy指垂直滚动距离,
newState可分为:
SCROLL_STATE_IDLE:目前RecyclerView不滚动
SCROLL_STATE_DRAGGING:当前RecyclerView正被例如用户触摸这样的事件拖动时的状态
SCROLL_STATE_SETTLING:目前RecyclerView正自动滚动中
滚动到对应position指定位置的函数:
private void scrollToPosition(int position){ if (position>=0&&position<=friends.size()-1){ int firstItem=manager.findFirstVisibleItemPosition(); int lastItem=manager.findLastVisibleItemPosition(); if (position<=firstItem){ contactList.scrollToPosition(position); }else if (position<=lastItem){ int top=contactList.getChildAt(position-firstItem).getTop(); contactList.scrollBy(0,top); }else { contactList.scrollToPosition(position); mIndex=position; move=true; } } }
到这里介绍为止,介绍的不清楚的或者想知道更多的可以到以下地址获取源码:
https://github.com/liberty2015/ContactListView
如果您觉得不错的请点赞,当然源码和文章中的不足之处也请指出,谢谢。