微信朋友怎么分组管理通讯录如何分组

那什么,微信的通讯录能像qq一样分组么?求解答。。。
全部答案(共2个回答)
你好,这是电脑开机黑屏的解决方法:1。试试开机,出完电脑品牌后,按F8,回车,回车,进安全模式里,高级启动选项,最后一次正确配置,回车,回车,按下去试试!2。再...
那是被系统屏蔽的垃圾用户,建议你使用微应用的清理卫士,可以有效删除僵尸粉、垃圾关注、垃圾微博和垃圾收藏,还可以批量删除的,希望可以帮到你。(如果帮到你,请给好评...
分组分解是因式分解的一种复杂的方法,让我们来学习这个知识。分组分解法的原则是分组后可以直接提公因式,或者可以直接运用公式。使用这种方法的关键在于分组适当,而在分...
你可以在左边的邮件账户上点击鼠标邮件,选择“新建邮件夹”,给个名字。然后同样是鼠标右键,选择“过滤器”,设置过滤条件,然后点击“动作”,选择将他们直接转移到指定...
答: 我这是怎么了,怎样才能解除我现
答: 慢慢弄。我最开始只会装游戏;后来中国有了网络慢慢跟朋友上聊天室聊天;后来出了OICQ(现在叫QQ),又用那东西聊;然后上联众玩在线游戏(棋牌类);后来乱七八糟逛...
答: 七十年代的计算机网络X.25 分组交换网:各国的电信部门建设运行各种专用的网络体系结构:SNA,DNAInternet 的前身ARPANET进行实验运行八十年代...
大家还关注
确定举报此问题
举报原因(必选):
广告或垃圾信息
激进时政或意识形态话题
不雅词句或人身攻击
侵犯他人隐私
其它违法和不良信息
报告,这不是个问题
报告原因(必选):
这不是个问题
这个问题分类似乎错了
这个不是我熟悉的地区
相关问答:123456789101112131415祝给予赞赏的伙伴,2017年发大财!|赞赏
收藏已收藏 | 38赞 | 122
分享到微信扫码分享到微信
微信: jiahaifeng1123
17 万阅读总量
热门问题12345678910实现的原理就是解决两个问题:
1)数据主关键字按拼音间分组排序。
2)基于ExpandableListView控件实现内容展示。
例子代码:http://download.csdn.net/detail/topwangpeng/8104735
(一)数据主关键字按拼音间分组排序
首先把主关键字列表的首字按拼音排序,首字的拼音首字母相同的为一组,这里关键是得到主关键字字符串首字的拼音首字符。使用pinyin4j库可以解决此问题。
public String getFirstChar(String value) { &
& & char firstChar = value.charAt(0); &// 首字符 &
& & String first = &// 首字母分类 &
& & String[] print = PinyinHelper.toHanyuPinyinStringArray(firstChar); &// 是否是非汉字 &
& & if (print == null) { &//非中文
& & & & // 将小写字母改成大写 &
& & & & if ((firstChar &= 97 && firstChar &= 122)) { &
& & & & & & firstChar -= 32; &
& & & & } &
& & & & if (firstChar &= 65 && firstChar &= 90) { &
& & & & & & first = String.valueOf((char) firstChar); &
& & & & } else { &
& & & & & & // 认为首字符为数字或者特殊字符 &
& & & & & & first = &#&; &
& & & & } &
& & } else { &
& & & & // 如果是中文,分类大写字母 &
& & & & first = String.valueOf((char)(print[0].charAt(0) -32)); &
& & if (first == null) { &
& & & & first = &?&; &
汉字按拼音排序也是通过pinyin4j库实现的。
class LanguageComparator_CN implements Comparator&String& { &
& & public int compare(String ostr1, String ostr2) { &
& & & & for (int i = 0; i & ostr1.length() && i & ostr2.length(); i++) { &
& & & & & & int codePoint1 = ostr1.charAt(i); &
& & & & & & int codePoint2 = ostr2.charAt(i); &
& & & & & & if (Character.isSupplementaryCodePoint(codePoint1) ||&
& & & & & & & & Character.isSupplementaryCodePoint(codePoint2)) { &
& & & & & & & & i++; &
& & & & & & } &
& & & & & & if (codePoint1 != codePoint2) { &
& & & & & & & & if (Character.isSupplementaryCodePoint(codePoint1) ||&
& & & & & & & & & & Character.isSupplementaryCodePoint(codePoint2)) { &
& & & & & & & & & & return codePoint1 - codePoint2; &
& & & & & & & & } &
& & & & & & & &&
& & & & & & & & String pinyin1 = pinyin((char) codePoint1); &
& & & & & & & & String pinyin2 = pinyin((char) codePoint2); &
& & & & & & & & if (pinyin1 != null && pinyin2 != null) { // 两个字符都是汉字 &
& & & & & & & & & & if (!pinyin1.equals(pinyin2)) { &
& & & & & & & & & & & & pareTo(pinyin2); &
& & & & & & & & & & } &
& & & & & & & & } else { &
& & & & & & & & & & return codePoint1 - codePoint2; &
& & & & & & & & } &
& & & & & & } &
& & & & } &
& & & & return ostr1.length() - ostr2.length(); &
& & // 获得汉字拼音的首字符 &
& & private String pinyin(char c) { &
& & & & String[] pinyins = PinyinHelper.toHanyuPinyinStringArray(c); &
& & & & if (pinyins == null) { &
& & & & & & &
& & & & } &
& & & & return pinyins[0]; &
使用示例:
List&String& names=new ArrayList&String&();
names.add(&天龙八部&);
names.add(&笑傲江湖&);
names.add(&书剑恩仇录&);
names.add(&雪山飞狐&);
names.add(&神雕侠吕&);
names.add(&射雕英雄传&);
names.add(&八仙过海&);
names.add(&西游记&);
names.add(&红楼梦&);
names.add(&三国演义&);
names.add(&水浒传&);
names.add(&Java&);
names.add(&C++&);
names.add(&C#&);
names.add(&Object C&);
names.add(&Javascript&);
Collections.sort(names, new LanguageComparator_CN());
(二)内容展示
1)使用ExpandableListView控件,关于此控件的使用baidu一下就知道了,其实很简单,重写BaseExpandableListAdapter抽象类中的方法后,调用ExpandableListView.setAdapter就可以展示其基本特性了。
2)设置ExpandableListView控件的groupIndicator为空。
&ExpandableListView
& & android:id=&@+id/expand_listview&
& & android:layout_width=&match_parent&
& & android:layout_height=&match_parent&
& & android:groupIndicator=&@null&
3)默认展开所有分组。
mExpandableListView = (ExpandableListView)findViewById(R.id.expand_listview);
List&String& names=new ArrayList&String&();
names.add(&天龙八部&);
names.add(&笑傲江湖&);
names.add(&书剑恩仇录&);
names.add(&雪山飞狐&);
names.add(&神雕侠吕&);
names.add(&射雕英雄传&);
names.add(&八仙过海&);
names.add(&西游记&);
names.add(&红楼梦&);
names.add(&三国演义&);
names.add(&水浒传&);
names.add(&Java&);
names.add(&C++&);
names.add(&C#&);
names.add(&Object C&);
names.add(&Javascript&);
PinyinAdapter adapter = new PinyinAdapter(this,names);
mExpandableListView.setAdapter(adapter);
for (int i = 0, length = adapter.getGroupCount(); i & i++) {
mExpandableListView.expandGroup(i);
4)实现右则的字母导航示图。
& &a)实现一个自定义View。
& &b)在OnDraw中自绘所有字母。
& &c)重写dispatchTouchEvent事件,根据点击的位置定位哪个分类被选中。
public class AssortView extends Button { &
& & public interface OnTouchAssortListener { &
& & & & public void onTouchAssortListener(String s); &
& & & & public void onTouchAssortUP(); &
& & // 分类 &
& & private static final String[] ASSORT_TEXT = { &?&, &#&, &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 mPaint = new Paint(); &
& & private int mSelectIndex = -1; &
& & private OnTouchAssortListener mListener = &
& & public AssortView(Context context) { &
& & & & super(context); &
& & public AssortView(Context context, AttributeSet attrs) { &
& & & & super(context, attrs); &
& & public AssortView(Context context, AttributeSet attrs, int defStyle) { &
& & & & super(context, attrs, defStyle); &
& & public void setOnTouchAssortListener(OnTouchAssortListener listener) { &
& & & & this.mListener = &
& & @Override &
& & protected void onDraw(Canvas canvas) { &
& & & & super.onDraw(canvas);
& & & & int nHeight = getHeight(); &
& & & & int hWidth = getWidth(); &
& & & & int nAssortCount = ASSORT_TEXT.
& & & & int nInterval = nHeight / nAssortC&
& & & & for (int i = 0; i & nAssortC i++) { &
& & & & mPaint.setAntiAlias(true); &// 抗锯齿 &&
& & & & mPaint.setTypeface(Typeface.DEFAULT_BOLD); &// 默认粗体&
& & & & mPaint.setColor(Color.WHITE); &// 白色 &
& & & & & & if (i == mSelectIndex) { &
& & & & & & & & // 被选择的字母改变颜色和粗体 &
& & & & & & mPaint.setColor(Color.parseColor(&#3399ff&)); &
& & & & & & mPaint.setFakeBoldText(true); &
& & & & & & mPaint.setTextSize(30); &
& & & & & & } &
& & & & & &&
& & & & & & float xPos = hWidth / 2 - mPaint.measureText(ASSORT_TEXT[i]) / 2; &// 计算字母的X坐标 &
& & & & & & float yPos = nInterval * i + nI &// 计算字母的Y坐标 &
& & & & & & canvas.drawText(ASSORT_TEXT[i], xPos, yPos, mPaint); &
& & & & & & mPaint.reset(); &
& & & & } &
& & @Override &
& & public boolean dispatchTouchEvent(MotionEvent event) { &
& & & & int nIndex = (int) (event.getY() / getHeight() * ASSORT_TEXT.length); &
& & & & if (nIndex &= 0 && nIndex & ASSORT_TEXT.length) {
& & & & & & switch (event.getAction()) { &
& & & & & & case MotionEvent.ACTION_MOVE: &
& & & & & & & & // 如果滑动改变 &
& & & & & & & & if (mSelectIndex != nIndex) { &
& & & & & & & & mSelectIndex = nI &
& & & & & & & & & & if (mListener != null) { &
& & & & & & & & & & mListener.onTouchAssortListener(ASSORT_TEXT[mSelectIndex]); &
& & & & & & & & & & }
& & & & & & & & } &
& & & & & & & & &
& & & & & & case MotionEvent.ACTION_DOWN: &
& & & & & & mSelectIndex = nI &
& & & & & & & & if (mListener != null) { &
& & & & & & & & mListener.onTouchAssortListener(ASSORT_TEXT[mSelectIndex]); &
& & & & & & & & } &
& & & & & & & & &
& & & & & & case MotionEvent.ACTION_UP: &
& & & & & & & & if (mListener != null) { &
& & & & & & & & mListener.onTouchAssortUP(); &
& & & & & & & & } &
& & & & & & & & mSelectIndex = -1; &
& & & & & & & & &
& & & & & & } &
& & & & } else { &
& & & & mSelectIndex = -1; &
& & & & & & if (mListener != null) { &
& & & & & & mListener.onTouchAssortUP(); &
& & & & & & } &
& & & & } &
& & & & invalidate(); &
&&相关文章推荐
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:10749次
排名:千里之外
原创:21篇
(1)(1)(3)(1)(1)(2)(1)(1)(2)(1)(2)(9)(3)(1)(1)【Android 仿微信通讯录 导航分组列表-下】自定义View为RecyclerView打造... - 简书
【Android 仿微信通讯录 导航分组列表-下】自定义View为RecyclerView打造右侧索引导航栏IndexBar
本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
转载请标明出处: 本文出自: ()代码传送门:喜欢的话,随手点个star。多谢
在上篇文章( 里,我们用ItemDecoration为RecyclerView打造了带悬停头部的分组列表。其实Android版微信的通讯录界面,它的分组title也不是悬停的,我们已经领先了微信一小步(认真脸)~再看看市面上常见的分组列表(例如饿了么点餐商品列表),不仅有悬停头部,悬停头部在切换时,还会伴有切换动画。关于ItemDecoration还有一个问题,简单布局还好,我们可以draw出来,如果是复杂的头部呢?能否写个xml,inflate进来,这样使用起来才简单,即另一种简单使用onDraw和onDrawOver的姿势。so,本文开头我们就先用两节完善一下我们的ItemDecoration。然后进入正题:自定义View实现右侧索引导航栏IndexBar,对数据源的排序字段按照拼音排序,最后将RecyclerView和IndexBar联动起来,触摸IndexBar上相应字母,RecyclerView滚动到相应位置。(在屏幕中间显示的其实就是一个TextView,我们set个体IndexBar即可)
由于大部分使用右侧索引导航栏的场景,都需要这几个固定步骤,对数据源排序,set给IndexBar,和RecyclerView联动等,所以最后再将其封装一把,成一个高度封装,因此扩展性不太高的控件,更方便使用,如果需要扩展的话,反正看完本文再其基础上修改应该很简单~。
最终版预览:
这里写图片描述
用ItemDecoration实现悬停头部切换动画
另一种简单使用onDraw()和onDrawOver()的姿势
自定义View实现右侧索引导航栏IndexBar
使用TinyPinyin对数据源排序
联动IndexBar和RecyclerView。
封装重复步骤,方便二次使用,并可定制导航数据源。
二 悬停头部的“切换动画”
实现了两种,第一种就是仿饿了么点餐时,商品列表的悬停头部切换“动画效果”,如下:
这里写图片描述
第二种是一种头部折叠起来的视效,个人觉得也还不错~如下:(估计没人喜欢)
这里写图片描述
果然比上部残篇里的效果好看多了,那么代码多不多呢,看我的git show 记录:
这里写图片描述
就绿色部分的不到十行代码就搞定~先上这个图是为了让大家安心,代码不多,分分钟看完。下面放上文字版代码,江湖人称 注释张 的我,已经写满了注释,再简单说下吧,滑动时,在判断头部即将切换(当前pos的tag和pos+1的tag不等)的时候,1.计算出当前悬停头部应该上移的位移,利用Canvas的画布移动方法Canvas.translate(),即可实现“饿了么”悬停头部切换效果。2.计算出当前悬停头部应该在屏幕上还剩余的空间高度,作为头部绘制的高度利用Canvas的Canvas.clipRect()方法,剪切画布,即可实现“折叠”的视效。
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {//最后调用 绘制在最上层
int pos = ((LinearLayoutManager) (parent.getLayoutManager())).findFirstVisibleItemPosition();
String tag = mDatas.get(pos).getTag();
//View child = parent.getChildAt(pos);
View child = parent.findViewHolderForLayoutPosition(pos).itemV//出现一个奇怪的bug,有时候child为空,所以将 child = parent.getChildAt(i)。-》 parent.findViewHolderForLayoutPosition(pos).itemView
boolean flag =//定义一个flag,Canvas是否位移过的标志
if ((pos + 1) & mDatas.size()) {//防止数组越界(一般情况不会出现)
if (null != tag && !tag.equals(mDatas.get(pos + 1).getTag())) {//当前第一个可见的Item的tag,不等于其后一个item的tag,说明悬浮的View要切换了
Log.d("zxt", "onDrawOver() called with: c = [" + child.getTop());//当getTop开始变负,它的绝对值,是第一个可见的Item移出屏幕的距离,
if (child.getHeight() + child.getTop() & mTitleHeight) {//当第一个可见的item在屏幕中还剩的高度小于title区域的高度时,我们也该开始做悬浮Title的“交换动画”
c.save();//每次绘制前 保存当前Canvas状态,
//一种头部折叠起来的视效,个人觉得也还不错~
//可与123行 c.drawRect 比较,只有bottom参数不一样,由于 child.getHeight() + child.getTop() & mTitleHeight,所以绘制区域是在不断的减小,有种折叠起来的感觉
//c.clipRect(parent.getPaddingLeft(), parent.getPaddingTop(), parent.getRight() - parent.getPaddingRight(), parent.getPaddingTop() + child.getHeight() + child.getTop());
//类似饿了么点餐时,商品列表的悬停头部切换“动画效果”
//上滑时,将canvas上移 (y为负数) ,所以后面canvas 画出来的Rect和Text都上移了,有种切换的“动画”感觉
c.translate(0, child.getHeight() + child.getTop() - mTitleHeight);
mPaint.setColor(COLOR_TITLE_BG);
c.drawRect(parent.getPaddingLeft(), parent.getPaddingTop(), parent.getRight() - parent.getPaddingRight(), parent.getPaddingTop() + mTitleHeight, mPaint);
mPaint.setColor(COLOR_TITLE_FONT);
mPaint.getTextBounds(tag, 0, tag.length(), mBounds);
c.drawText(tag, child.getPaddingLeft(),
parent.getPaddingTop() + mTitleHeight - (mTitleHeight / 2 - mBounds.height() / 2),
c.restore();//恢复画布到之前保存的状态
这份代码核心处c.translate(0, child.getHeight() + child.getTop() - mTitleHeight);实现的是饿了么效果,被注释掉的
//c.clipRect(parent.getPaddingLeft(), parent.getPaddingTop(), parent.getRight() - parent.getPaddingRight(), parent.getPaddingTop() + child.getHeight() + child.getTop());
实现的是效果二。
三 另一种使用onDraw()和onDrawOver()的姿势
之前我们使用onDraw(),onDrawOver(),都是用canvas的方法活生生的绘制一个出View,这对于很多人(包括我)来说都不容易,xy坐标的确认,尺寸都较难把握,基本上调UI效果时间都很长。尤其是canvas.drawText()方法的y坐标,其实是baseLine的位置,不了解的童鞋肯定要踩很多坑。当我们想要绘制的分类title、悬停头部复杂一点时,我都不敢想象要调试多久了,这个时候我们还敢用ItemDecoration吗。有没有一种方法,就像我们平时使用的那样,在Layout布局xml里画好View,然后inflate出来就可以了呢。这个问题开始确实也把我难住了,难道又要从入门到放弃了吗?于是我又搜寻资料,功夫不负有心人。解决问题的办法就是,View类的:public void draw(Canvas canvas) {方法下面我们就看一个用法Demo吧:布局layout:header_complex.xml(注意有个ProgressBar哦)
&?xml version="1.0" encoding="utf-8"?&
&LinearLayout xmlns:android="/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimaryDark"
android:orientation="vertical"&
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorAccent"
android:text="复杂头部" /&
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="复杂头部"
android:textColor="@color/colorAccent" /&
&ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content" /&
&/LinearLayout&
onDrawOver代码如下:简单讲解下,先inflate这个复杂的Layout,然后拿到它的LayoutParams,利用这个lp拿到宽和高的MeasureSpec,然后依次调用 measure,layout,draw方法,将复杂头部显示在屏幕上。(小安利一下,MeasureSpec不太了解的可以看看我的这篇
View toDrawView = mInflater.inflate(R.layout.header_complex, parent, false);
int toDrawWidthS//用于测量的widthMeasureSpec
int toDrawHeightS//用于测量的heightMeasureSpec
//拿到复杂布局的LayoutParams,如果为空,就new一个。
// 后面需要根据这个lp 构建toDrawWidthSpec,toDrawHeightSpec
ViewGroup.LayoutParams lp = toDrawView.getLayoutParams();
if (lp == null) {
lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);//这里是根据复杂布局layout的width height,new一个Lp
toDrawView.setLayoutParams(lp);
if (lp.width == ViewGroup.LayoutParams.MATCH_PARENT) {
//如果是MATCH_PARENT,则用父控件能分配的最大宽度和EXACTLY构建MeasureSpec。
toDrawWidthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth() - parent.getPaddingLeft() - parent.getPaddingRight(), View.MeasureSpec.EXACTLY);
} else if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
//如果是WRAP_CONTENT,则用父控件能分配的最大宽度和AT_MOST构建MeasureSpec。
toDrawWidthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth() - parent.getPaddingLeft() - parent.getPaddingRight(), View.MeasureSpec.AT_MOST);
//否则则是具体的宽度数值,则用这个宽度和EXACTLY构建MeasureSpec。
toDrawWidthSpec = View.MeasureSpec.makeMeasureSpec(lp.width, View.MeasureSpec.EXACTLY);
//高度同理
if (lp.height == ViewGroup.LayoutParams.MATCH_PARENT) {
toDrawHeightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight() - parent.getPaddingTop() - parent.getPaddingBottom(), View.MeasureSpec.EXACTLY);
} else if (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
toDrawHeightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight() - parent.getPaddingTop() - parent.getPaddingBottom(), View.MeasureSpec.AT_MOST);
toDrawHeightSpec = View.MeasureSpec.makeMeasureSpec(lp.width, View.MeasureSpec.EXACTLY);
//依次调用 measure,layout,draw方法,将复杂头部显示在屏幕上。
toDrawView.measure(toDrawWidthSpec, toDrawHeightSpec);
toDrawView.layout(parent.getPaddingLeft(), parent.getPaddingTop(),
parent.getPaddingLeft() + toDrawView.getMeasuredWidth(), parent.getPaddingTop() + toDrawView.getMeasuredHeight());
toDrawView.draw(c);
这里还有个有趣的地方,某些需要不断调用onDraw()更新绘制自己最新状态的View,例如ProgressBar,由于在屏幕上显示的并不是真正的View,只是我们手动的调用了一次draw方法,进而调用View的onDraw()显示的一次“残影”,所以ProgressBar只会显示onDraw()当时的样子,并不会主动刷新了。看图说话,还是很容易理解的:
这里写图片描述
滑动时,由于会回调onDrawOver() 方法,所以ProgressBar又被手动调用了draw(),开始变化,滑动的快的话,progressBar会有动画效果。停止不动时,ProgressBar也是静止的,保持draw()时绘制的状态。
四 自定义View实现右侧索引导航栏IndexBar
不管是自定义ItemDecoration还是实现右侧索引导航栏,都有大量的自定义View知识在里面 ,这里简单复习一下。(步骤1-4是自定义View的必须套路,步骤5+是IndexBar特殊定制)1 自定义View首先要确定这个View需要在xml里接受哪些属性?在IndexBar里,我们先需要两个属性,每个索引的文字大小和手指按下时整个View的背景,即在attrs.xml如下定义:
&attr name="textSize" format="dimension" /&
&declare-styleable name="IndexBar"&
&attr name="textSize" /&
&attr name="pressBackground" format="color" /&
&/declare-styleable&
2 在View的构造方法中获得我们自定义的属性套路代码如下,都是套路,记得使用完最后要将typeArray对象 recycle()。
int textSize = (int) TypedValue.applyDimension(
PLEX_UNIT_SP, 16, getResources().getDisplayMetrics());//默认的TextSize
mPressedBackground = Color.BLACK;//默认按下是纯黑色
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.IndexBar, defStyleAttr, 0);
int n = typedArray.getIndexCount();
for (int i = 0; i & i++) {
int attr = typedArray.getIndex(i);
switch (attr) {
case R.styleable.IndexBar_textSize:
textSize = typedArray.getDimensionPixelSize(attr, textSize);
case R.styleable.IndexBar_pressBackground:
mPressedBackground = typedArray.getColor(attr, mPressedBackground);
typedArray.recycle();
3 重写onMesure()方法(可选)onMeasure()方法里,主要就是遍历一遍indexDatas,得到index最大宽度和高度。然后根据三种测量模式,分配不同的值给View,EXACLTY就分配具体的测量值(match_parent,确定数值),AT_MOST就分配父控件能给的最大值和自己需要的值之间的最小值。(保证不超过父控件限定的值)UNSPECIFIED则分配自己需要的值。(随心所欲)
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//取出宽高的MeasureSpec
Mode 和Size
int wMode = MeasureSpec.getMode(widthMeasureSpec);
int wSize = MeasureSpec.getSize(widthMeasureSpec);
int hMode = MeasureSpec.getMode(heightMeasureSpec);
int hSize = MeasureSpec.getSize(heightMeasureSpec);
int measureWidth = 0, measureHeight = 0;//最终测量出来的宽高
//得到合适宽度:
Rect indexBounds = new Rect();//存放每个绘制的index的Rect区域
S//每个要绘制的index内容
for (int i = 0; i & mIndexDatas.size(); i++) {
index = mIndexDatas.get(i);
mPaint.getTextBounds(index, 0, index.length(), indexBounds);//测量计算文字所在矩形,可以得到宽高
measureWidth = Math.max(indexBounds.width(), measureWidth);//循环结束后,得到index的最大宽度
measureHeight = Math.max(indexBounds.width(), measureHeight);//循环结束后,得到index的最大高度,然后*size
measureHeight *= mIndexDatas.size();
switch (wMode) {
case MeasureSpec.EXACTLY:
measureWidth = wS
case MeasureSpec.AT_MOST:
measureWidth = Math.min(measureWidth, wSize);//wSize此时是父控件能给子View分配的最大空间
case MeasureSpec.UNSPECIFIED:
//得到合适的高度:
switch (hMode) {
case MeasureSpec.EXACTLY:
measureHeight = hS
case MeasureSpec.AT_MOST:
measureHeight = Math.min(measureHeight, hSize);//wSize此时是父控件能给子View分配的最大空间
case MeasureSpec.UNSPECIFIED:
setMeasuredDimension(measureWidth, measureHeight);
4 重写onDraw()方法整理一下需求和思路:利用index数据源的size,和控件可绘制的高度(高度-paddingTop-paddingBottom),求出每个index区域的高度mGapHeight。每个index在绘制时,都是处于水平居中,竖直方向上在mGapHeight区域高度内居中。思路整理清楚,代码很简单如下:
public static String[] INDEX_STRING = {"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 List&String& mIndexD//索引数据源
private int mGapH//每个index区域的高度
mIndexDatas = Arrays.asList(INDEX_STRING);//数据源
在onSizeChanged方法里,获取控件的宽高,并计算出mGapHeight:
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mGapHeight = (mHeight - getPaddingTop() - getPaddingBottom()) / mIndexDatas.size();
最后在onDraw()方法里绘制,如果对于竖直居中baseLine的计算不太理解可以先放置,这块的确挺绕人,后面应该会写一篇 canvas.drawText()x y坐标计算的小短文.可记住重点就是 Paint默认的TextAlign是Left,即x方向,左对齐,所以x坐标决定绘制文字的左边界。y坐标是绘制文字的baseLine位置。
protected void onDraw(Canvas canvas) {
int t = getPaddingTop();//top的基准点(支持padding)
Rect indexBounds = new Rect();//存放每个绘制的index的Rect区域
S//每个要绘制的index内容
for (int i = 0; i & mIndexDatas.size(); i++) {
index = mIndexDatas.get(i);
mPaint.getTextBounds(index, 0, index.length(), indexBounds);//测量计算文字所在矩形,可以得到宽高
Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();//获得画笔的FontMetrics,用来计算baseLine。因为drawText的y坐标,代表的是绘制的文字的baseLine的位置
int baseline = (int) ((mGapHeight - fontMetrics.bottom - fontMetrics.top) / 2);//计算出在每格index区域,竖直居中的baseLine值
canvas.drawText(index, mWidth / 2 - indexBounds.width() / 2, t + mGapHeight * i + baseline, mPaint);//调用drawText,居中显示绘制index
以上四步基本完成了IndexBar的绘制工作,下面我们为它添加一些行为的响应。
5 重写onTouchEvent()方法我们需要重写onTouchEvent()方法,以便处理手指按下时的View背景变色,抬起时恢复原来颜色并根据手指触摸的落点坐标,判断当前处于哪个index区域,回调给相应的监听器处理(显示当前index的值,滑动RecyclerView至相应区域等。。)代码如下:
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
setBackgroundColor(mPressedBackground);//手指按下时背景变色
//注意这里没有break,因为down时,也要计算落点 回调监听器
case MotionEvent.ACTION_MOVE:
float y = event.getY();
//通过计算判断落点在哪个区域:
int pressI = (int) ((y - getPaddingTop()) / mGapHeight);
//边界处理(在手指move时,有可能已经移出边界,防止越界)
if (pressI & 0) {
pressI = 0;
} else if (pressI &= mIndexDatas.size()) {
pressI = mIndexDatas.size() - 1;
//回调监听器
if (null != mOnIndexPressedListener) {
mOnIndexPressedListener.onIndexPressed(pressI, mIndexDatas.get(pressI));
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
setBackgroundResource(android.R.color.transparent);//手指抬起时背景恢复透明
//回调监听器
if (null != mOnIndexPressedListener) {
mOnIndexPressedListener.onMotionEventEnd();
6 联动IndexBar和RecyclerView具体的操作交由监听器处理,定义和实现如下:值得一提的就是,滑动RecyclerView到指定postion,我们使用的是LinearLayoutManager的scrollToPositionWithOffset(int position, int offset)方法,offset传入0,postion即目标postion即可。如果使用RecyclerView.scrollToPosition();等方法,滑动会很飘~定位不准。mPressedShowTextView 就是在屏幕中间显示的当前处于哪个index的TextView。
* 当前被按下的index的监听器
public interface onIndexPressedListener {
void onIndexPressed(int index, String text);//当某个Index被按下
void onMotionEventEnd();//当触摸事件结束(UP CANCEL)
private onIndexPressedListener mOnIndexPressedL
public void setmOnIndexPressedListener(onIndexPressedListener mOnIndexPressedListener) {
this.mOnIndexPressedListener = mOnIndexPressedL
//设置index触摸监听器
setmOnIndexPressedListener(new onIndexPressedListener() {
public void onIndexPressed(int index, String text) {
if (mPressedShowTextView != null) { //显示hintTexView
mPressedShowTextView.setVisibility(View.VISIBLE);
mPressedShowTextView.setText(text);
if (mLayoutManager != null) {
int position = getPosByTag(text);
if (position != -1) {
mLayoutManager.scrollToPositionWithOffset(position, 0);
public void onMotionEventEnd() {
//隐藏hintTextView
if (mPressedShowTextView != null) {
mPressedShowTextView.setVisibility(View.GONE);
五 封装重复步骤,方便二次使用。
在我个人的理解里,程序过多的封装是会导致扩展性的降低(也是因为我水平有限),然而我们今天要封装的这个IndexBar,由于使用场景和套路还是挺固定的(城市分组列表,商品分类列表)所以值得将相关的操作都聚合起来,二次使用更方便。毕竟,一个项目里同样的代码写第二遍的程序员都不是好的圣斗士。(其实是我的leader不想写第二遍,让我封装一下给他秒用)梳理一下固定的操作:1 都是先对原始数据sourceDatas源按照排序字段拼音排序。2 然后将屏幕中hint的TextView ,以及索引数据源indexDatas(通过sourceDatas获得),通过set方法传给IndexBar。3 联动IndexBar和RecyclerView,使得触摸IndexBar相应区域RecyclerView会滚动(借助sourceDatas获得对应postion)。根据上述,我的设想在使用时,只需要给IndexBar设置 原始数据sourceDatas,HintTextView,和RecyclerView的LinearLayoutManager,在IndexBar内部对sourceDatas排序,并获得索引数据源indexDatas,然后设置一个默认的index触摸监听器,在手指按下滑动时,由于IndexBar持有HintTextView和LayoutManager,则HintTextView的show hide,以及LayoutManager的滚动 都在IndexBar内部完成。最终使用预览:
//使用indexBar
mTvSideBarHint = (TextView) findViewById(R.id.tvSideBarHint);//HintTextView
mIndexBar = (IndexBar) findViewById(R.id.indexBar);//IndexBar
mIndexBar.setmPressedShowTextView(mTvSideBarHint)//设置HintTextView
.setNeedRealIndex(true)//设置需要真实的索引
.setmLayoutManager(mManager)//设置RecyclerView的LayoutManager
.setmSourceDatas(mDatas);//设置数据源
&?xml version="1.0" encoding="utf-8"?&
&FrameLayout xmlns:android="/apk/res/android"
xmlns:app="/apk/res-auto"
xmlns:tools="/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"&
&android.support.v7.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="match_parent"&
&/android.support.v7.widget.RecyclerView&
&mcxtzhang.itemdecorationdemo.IndexBar.widget.IndexBar
android:id="@+id/indexBar"
android:layout_width="24dp"
android:layout_height="match_parent"
android:layout_gravity="right"
app:pressBackground="@color/partTranslucent"
app:textSize="16sp" /&
android:id="@+id/tvSideBarHint"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_gravity="center"
android:background="@drawable/shape_side_bar_bg"
android:gravity="center"
android:textColor="@android:color/white"
android:textSize="48sp"
android:visibility="gone"
tools:text="A"
tools:visibility="visible" /&
&/FrameLayout&
其中,setNeedRealIndex(true)//设置需要真实的索引,是指索引栏的数据不是固定的A-Z,#。而是根据真实的sourceDatas生成。因为链式调用用起来很爽,所以在这些set方法里都return 了 this。
1 抽象两个实体类和一个接口。先把tag抽象出来,放在顶层,这里存放的就是IndexBar显示的每个index值(A-Z,#)(本例是城市的汉语拼音首字母),而且在联动滑动时,根据tag获取postion时,也需要用到tag。它是导航分组列表的基础。
public class BaseIndexTagBean {
private S//所属的分类(城市的汉语拼音首字母)
public String getTag() {
public void setTag(String tag) {
this.tag =
然后抽象一个接口和一个实体类,接口定义一个方法getTarget(),它返回 需要被转化成拼音,并取出首字母 索引排序的 字段。(本例就是城市的名字)实体类继承BaseIndexTagBean,并实现以上接口,且额外存放 需要排序的字段的拼音值,(本例是城市的拼音)。它根据getTarget()返回的值利用TinyPinyin库得到拼音。
public interface IIndexTargetInterface {
String getTarget();//需要被转化成拼音,并取出首字母 索引排序的 字段
public abstract class BaseIndexPinyinBean extends BaseIndexTagBean implements IIndexTargetInterface {
private String pyC//城市的拼音
public String getPyCity() {
return pyC
public void setPyCity(String pyCity) {
this.pyCity = pyC
有了以上两个类一个接口,我们就可以将 对原始数据源sourceDatas按照拼音排序,并取出索引数据源indexDatas的操作封装起来。
2 封装原始数据源初始化(利用TinyPinyin获取全拼音),取出索引数据源indexDatas的操作。使用时,我们先让具体的实体bean,继承自BaseIndexPinyinBean ,在getTarget()方法返回排序目标字段。本例如下:
public class CityBean extends BaseIndexPinyinBean {
private S//城市名字
public CityBean() {
public CityBean(String city) {
this.city =
public String getCity() {
public void setCity(String city) {
this.city =
public String getTarget() {
IndexBar类内代码:使用时会调用IndexBar.setmSourceDatas()方法传入原始数据源,在方法内对数据源初始化,并取出索引数据源。
private List&? extends BaseIndexPinyinBean& mSourceD//Adapter的数据源
public IndexBar setmSourceDatas(List&? extends BaseIndexPinyinBean& mSourceDatas) {
this.mSourceDatas = mSourceD
initSourceDatas();//对数据源进行初始化
* 初始化原始数据源,并取出索引数据源
private void initSourceDatas() {
int size = mSourceDatas.size();
for (int i = 0; i & i++) {
BaseIndexPinyinBean indexPinyinBean = mSourceDatas.get(i);
StringBuilder pySb = new StringBuilder();
String target = indexPinyinBean.getTarget();//取出需要被拼音化的字段
//遍历target的每个char得到它的全拼音
for (int i1 = 0; i1 & target.length(); i1++) {
//利用TinyPinyin将char转成拼音
//查看源码,方法内 如果char为汉字,则返回大写拼音
//如果c不是汉字,则返回String.valueOf(c)
pySb.append(Pinyin.toPinyin(target.charAt(i1)));
indexPinyinBean.setPyCity(pySb.toString());//设置城市名全拼音
//以下代码设置城市拼音首字母
String tagString = pySb.toString().substring(0, 1);
if (tagString.matches("[A-Z]")) {//如果是A-Z字母开头
indexPinyinBean.setTag(tagString);
if (isNeedRealIndex) {//如果需要真实的索引数据源
if (!mIndexDatas.contains(tagString)) {//则判断是否已经将这个索引添加进去,若没有则添加
mIndexDatas.add(tagString);
} else {//特殊字母这里统一用#处理
indexPinyinBean.setTag("#");
if (isNeedRealIndex) {//如果需要真实的索引数据源
if (!mIndexDatas.contains("#")) {
mIndexDatas.add("#");
sortData();
3 封装对原始数据源sourceDatas,索引数据源indexDatas的排序操作。
* 对数据源排序
private void sortData() {
//对右侧栏进行排序 将 # 丢在最后
Collections.sort(mIndexDatas, new Comparator&String&() {
public int compare(String lhs, String rhs) {
if (lhs.equals("#")) {
} else if (rhs.equals("#")) {
return -1;
pareTo(rhs);
//对数据源进行排序
Collections.sort(mSourceDatas, new Comparator&BaseIndexPinyinBean&() {
public int compare(BaseIndexPinyinBean lhs, BaseIndexPinyinBean rhs) {
if (lhs.getTag().equals("#")) {
} else if (rhs.getTag().equals("#")) {
return -1;
return lhs.getPyCity().compareTo(rhs.getPyCity());
4 是否需要真实的索引数据源。相关变量定义:
public static String[] INDEX_STRING = {"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 List&String& mIndexD//索引数据源
private boolean isNeedRealI//是否需要根据实际的数据来生成索引数据源(例如 只有 A B C 三种tag,那么索引栏就 A B C 三项)
初始化init时,判断不需要真实的索引数据源,就用默认值(A-Z,#)
if (!isNeedRealIndex) {//不需要真实的索引数据源
mIndexDatas = Arrays.asList(INDEX_STRING);
使用时,如果如果真实索引数据源,调用这个方法,传入true,一定要在设置数据源setmSourceDatas(List)之前调用。
* 一定要在设置数据源{@link #setmSourceDatas(List)}之前调用
* @param needRealIndex
public IndexBar setNeedRealIndex(boolean needRealIndex) {
isNeedRealIndex = needRealI
if (isNeedRealIndex){
if (mIndexDatas != null) {
mIndexDatas = new ArrayList&&();
在initSourceDatas() 里,会根据这个变量往mIndexDatas里增加index。
5 IndexBar和外部联动的相关(HintTextView,和RecyclerView的LinearLayoutManager)set方法很简单:
public IndexBar setmPressedShowTextView(TextView mPressedShowTextView) {
this.mPressedShowTextView = mPressedShowTextV
public IndexBar setmLayoutManager(LinearLayoutManager mLayoutManager) {
this.mLayoutManager = mLayoutM
它们两最终都是在index触摸监听器里用到,代码上文已提及,只不过这次挪到IndexBar内部init里。init函数如下:
private void init(Context context, AttributeSet attrs, int defStyleAttr) {
if (!isNeedRealIndex) {//不需要真实的索引数据源
mIndexDatas = Arrays.asList(INDEX_STRING);
//设置index触摸监听器
setmOnIndexPressedListener(new onIndexPressedListener() {
public void onIndexPressed(int index, String text) {
if (mPressedShowTextView != null) { //显示hintTexView
mPressedShowTextView.setVisibility(View.VISIBLE);
mPressedShowTextView.setText(text);
if (mLayoutManager != null) {
int position = getPosByTag(text);
if (position != -1) {
mLayoutManager.scrollToPositionWithOffset(position, 0);
public void onMotionEventEnd() {
//隐藏hintTextView
if (mPressedShowTextView != null) {
mPressedShowTextView.setVisibility(View.GONE);
* 根据传入的pos返回tag
* @param tag
private int getPosByTag(String tag) {
if (TextUtils.isEmpty(tag)) {
return -1;
for (int i = 0; i & mSourceDatas.size(); i++) {
if (tag.equals(mSourceDatas.get(i).getTag())) {
return -1;
六 完整代码
思前想后还是放出来吧,三百多行有点长
* 介绍:索引右侧边栏
* 作者:zhangxutong
* CSDN:http://blog.csdn.net/zxt0601
* 时间: 16/09/04.
public class IndexBar extends View {
private static final String TAG = "zxt/IndexBar";
public static String[] INDEX_STRING = {"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 List&String& mIndexD//索引数据源
private boolean isNeedRealI//是否需要根据实际的数据来生成索引数据源(例如 只有 A B C 三种tag,那么索引栏就 A B C 三项)
private int mWidth, mH//View的宽高
private int mGapH//每个index区域的高度
private Paint mP
private int mPressedB//手指按下时的背景色
//以下边变量是外部set进来的
private TextView mPressedShowTextV//用于特写显示正在被触摸的index值
private List&? extends BaseIndexPinyinBean& mSourceD//Adapter的数据源
private LinearLayoutManager mLayoutM
public IndexBar(Context context) {
this(context, null);
public IndexBar(Context context, AttributeSet attrs) {
this(context, attrs, 0);
public IndexBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr);
private void init(Context context, AttributeSet attrs, int defStyleAttr) {
int textSize = (int) TypedValue.applyDimension(
PLEX_UNIT_SP, 16, getResources().getDisplayMetrics());//默认的TextSize
mPressedBackground = Color.BLACK;//默认按下是纯黑色
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.IndexBar, defStyleAttr, 0);
int n = typedArray.getIndexCount();
for (int i = 0; i & i++) {
int attr = typedArray.getIndex(i);
switch (attr) {
case R.styleable.IndexBar_textSize:
textSize = typedArray.getDimensionPixelSize(attr, textSize);
case R.styleable.IndexBar_pressBackground:
mPressedBackground = typedArray.getColor(attr, mPressedBackground);
typedArray.recycle();
if (!isNeedRealIndex) {//不需要真实的索引数据源
mIndexDatas = Arrays.asList(INDEX_STRING);
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setTextSize(textSize);
mPaint.setColor(Color.BLACK);
//设置index触摸监听器
setmOnIndexPressedListener(new onIndexPressedListener() {
public void onIndexPressed(int index, String text) {
if (mPressedShowTextView != null) { //显示hintTexView
mPressedShowTextView.setVisibility(View.VISIBLE);
mPressedShowTextView.setText(text);
if (mLayoutManager != null) {
int position = getPosByTag(text);
if (position != -1) {
mLayoutManager.scrollToPositionWithOffset(position, 0);
public void onMotionEventEnd() {
//隐藏hintTextView
if (mPressedShowTextView != null) {
mPressedShowTextView.setVisibility(View.GONE);
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
protected void onDraw(Canvas canvas) {
int t = getPaddingTop();//top的基准点(支持padding)
Rect indexBounds = new Rect();//存放每个绘制的index的Rect区域
S//每个要绘制的index内容
for (int i = 0; i & mIndexDatas.size(); i++) {
index = mIndexDatas.get(i);
mPaint.getTextBounds(index, 0, index.length(), indexBounds);//测量计算文字所在矩形,可以得到宽高
Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();//获得画笔的FontMetrics,用来计算baseLine。因为drawText的y坐标,代表的是绘制的文字的baseLine的位置
int baseline = (int) ((mGapHeight - fontMetrics.bottom - fontMetrics.top) / 2);//计算出在每格index区域,竖直居中的baseLine值
canvas.drawText(index, mWidth / 2 - indexBounds.width() / 2, t + mGapHeight * i + baseline, mPaint);//调用drawText,居中显示绘制index
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
setBackgroundColor(mPressedBackground);//手指按下时背景变色
//注意这里没有break,因为down时,也要计算落点 回调监听器
case MotionEvent.ACTION_MOVE:
float y = event.getY();
//通过计算判断落点在哪个区域:
int pressI = (int) ((y - getPaddingTop()) / mGapHeight);
//边界处理(在手指move时,有可能已经移出边界,防止越界)
if (pressI & 0) {
pressI = 0;
} else if (pressI &= mIndexDatas.size()) {
pressI = mIndexDatas.size() - 1;
//回调监听器
if (null != mOnIndexPressedListener) {
mOnIndexPressedListener.onIndexPressed(pressI, mIndexDatas.get(pressI));
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
setBackgroundResource(android.R.color.transparent);//手指抬起时背景恢复透明
//回调监听器
if (null != mOnIndexPressedListener) {
mOnIndexPressedListener.onMotionEventEnd();
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mGapHeight = (mHeight - getPaddingTop() - getPaddingBottom()) / mIndexDatas.size();
* 当前被按下的index的监听器
public interface onIndexPressedListener {
void onIndexPressed(int index, String text);//当某个Index被按下
void onMotionEventEnd();//当触摸事件结束(UP CANCEL)
private onIndexPressedListener mOnIndexPressedL
public onIndexPressedListener getmOnIndexPressedListener() {
return mOnIndexPressedL
public void setmOnIndexPressedListener(onIndexPressedListener mOnIndexPressedListener) {
this.mOnIndexPressedListener = mOnIndexPressedL
* 显示当前被按下的index的TextView
public IndexBar setmPressedShowTextView(TextView mPressedShowTextView) {
this.mPressedShowTextView = mPressedShowTextV
public IndexBar setmLayoutManager(LinearLayoutManager mLayoutManager) {
this.mLayoutManager = mLayoutM
* 一定要在设置数据源{@link #setmSourceDatas(List)}之前调用
* @param needRealIndex
public IndexBar setNeedRealIndex(boolean needRealIndex) {
isNeedRealIndex = needRealI
if (mIndexDatas != null) {
mIndexDatas = new ArrayList&&();
public IndexBar setmSourceDatas(List&? extends BaseIndexPinyinBean& mSourceDatas) {
this.mSourceDatas = mSourceD
initSourceDatas();//对数据源进行初始化
* 初始化原始数据源,并取出索引数据源
private void initSourceDatas() {
int size = mSourceDatas.size();
for (int i = 0; i & i++) {
BaseIndexPinyinBean indexPinyinBean = mSourceDatas.get(i);
StringBuilder pySb = new StringBuilder();
String target = indexPinyinBean.getTarget();//取出需要被拼音化的字段
//遍历target的每个char得到它的全拼音
for (int i1 = 0; i1 & target.length(); i1++) {
//利用TinyPinyin将char转成拼音
//查看源码,方法内 如果char为汉字,则返回大写拼音
//如果c不是汉字,则返回String.valueOf(c)
pySb.append(Pinyin.toPinyin(target.charAt(i1)));
indexPinyinBean.setPyCity(pySb.toString());//设置城市名全拼音
//以下代码设置城市拼音首字母
String tagString = pySb.toString().substring(0, 1);
if (tagString.matches("[A-Z]")) {//如果是A-Z字母开头
indexPinyinBean.setTag(tagString);
if (isNeedRealIndex) {//如果需要真实的索引数据源
if (!mIndexDatas.contains(tagString)) {//则判断是否已经将这个索引添加进去,若没有则添加
mIndexDatas.add(tagString);
} else {//特殊字母这里统一用#处理
indexPinyinBean.setTag("#");
if (isNeedRealIndex) {//如果需要真实的索引数据源
if (!mIndexDatas.contains("#")) {
mIndexDatas.add("#");
sortData();
* 对数据源排序
private void sortData() {
//对右侧栏进行排序 将 # 丢在最后
Collections.sort(mIndexDatas, new Comparator&String&() {
public int compare(String lhs, String rhs) {
if (lhs.equals("#")) {
} else if (rhs.equals("#")) {
return -1;
pareTo(rhs);
//对数据源进行排序
Collections.sort(mSourceDatas, new Comparator&BaseIndexPinyinBean&() {
public int compare(BaseIndexPinyinBean lhs, BaseIndexPinyinBean rhs) {
if (lhs.getTag().equals("#")) {
} else if (rhs.getTag().equals("#")) {
return -1;
return lhs.getPyCity().compareTo(rhs.getPyCity());
* 根据传入的pos返回tag
* @param tag
private int getPosByTag(String tag) {
if (TextUtils.isEmpty(tag)) {
return -1;
for (int i = 0; i & mSourceDatas.size(); i++) {
if (tag.equals(mSourceDatas.get(i).getTag())) {
return -1;
不管是自定义ItemDecoration还是实现右侧索引导航栏,其实大量的自定义View知识在里面 ,so 要想自定义ItemDecoration玩得好,自定义View少不了。
对数据源的排序字段按照拼音排序,我们使用TinyPinyin(它的特性很适合Android平台。
生成的拼音不包含声调,也不处理多音字,默认一个汉字对应一个拼音;
拼音均为大写;
无需初始化,执行效率很高(Pinyin4J的4倍);
很低的内存占用(小于30KB)。(介绍来源于其项目github)
其实不仅仅是IndexBar以及它和RecyclerView,HintTextView的联动可以封装在一起。悬停头部ItemDecoration也可以利用 BaseIndexTagBean 类来抽象一下,不与具体的实体类耦合,将
private List&CityBean& mD
private List&?extends BaseIndexPinyinBean& mD
本文起笔于9.5晚八点,项目上线打包期间,每逢打包是非多~你们懂得,结果打包期间出现各种问题,各种bug紧急修复通宵到凌晨,9.6日,两点起床,又续写后面三节。本文篇幅也略长,写到后面自己也有点懵逼(也可能是通宵还没醒导致),总耗时6小时+,希望大家看后觉得有用可以给我刷波66666.
八 代码地址
我不想吐槽CSDN了,上传代码真心慢,稍后补上CSDN地址。csdn传送门:
github地址:欢迎star
Android工程师
/mcxtzhang

我要回帖

更多关于 微信朋友分组怎么设置 的文章

 

随机推荐