项目地址:https://github.com/hgDendi/ContactsList

界面概览:

ContactsListDemo

ContactsListDemo2

概要

如图,主要简单划分为两个部分:

​ 数据源、与界面组件。

​ 数据源主要来自手机的通讯录信息,通过ContentResolver获取。

​ 而界面组件主要有显示列表和侧边栏。而重点在于列表的分组栏的绘制与现实,这就依靠ItemDecoration来进行实现了,这也是难点。

复用方法

FloatingBarItemDecoration传入需要绘制标题栏的position和标题String的map,目前只支持竖项、单列的列表,如需要扩展,请读完此文,明白原理后很容易实现。

IndexBar传入Label的List,通过setListener加入勾子。

FloatingBarItemDecoration

An ItemDecoration allows the application to add a special drawing and layout offset to specific item views from the adapter's data set. This can be useful for drawing dividers between items, highlights, visual grouping boundaries and more.

ItemDecoration主要是用来对RecyclerView进行一些修饰,是对adapter数据集中的数据视图增加修饰或空位。经常被用来画分割线、强调效果、可见的分组边界等。

getItemOffset()

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
int position = ((RecyclerView.LayoutParams))
view.getLayoutParams()).getViewAdapterPosition();
outRect.set(, mList.containsKey(position) ? mTitleHeight : , , );
}

绘制间距,为绘制标题栏空出间隙。主要逻辑是通过当前view的position判断是否需要在上方空出矩形范围。

onDraw()

主要是进行静态标题栏等绘制,即在每组view的上方,即getItemOffset()的区域进行标题栏的绘制。

    @Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
final int left = parent.getPaddingLeft();
final int right = parent.getWidth() - parent.getPaddingRight();
final int childCount = parent.getChildCount();
for (int i = ; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params =
(RecyclerView.LayoutParams) child.getLayoutParams();
int position = params.getViewAdapterPosition();
if (!mList.containsKey(position)) {
continue;
}
drawTitleArea(c, left, right, child, params, position);
}
}

onDrawOver

实现悬浮分组栏,以及悬浮分组栏碰撞效果绘制。

对于整个列表的绘制流程,是遵循如下的顺序:

​ ItemDecoration#onDraw() -> ItemView的绘制 -> ItemDecoration#onDrawOver

故而在onDrawOver中实现可以满足“悬浮”,即在最上层的效果。

@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
final int position = ((LinearLayoutManager) parent.getLayoutManager()).findFirstVisibleItemPosition();
if (position == RecyclerView.NO_POSITION) {
return;
}
View child = parent.findViewHolderForAdapterPosition(position).itemView;
String initial = getTag(position);
if (initial == null) {
return;
} //flag指示当标题栏是否发生碰撞(如开头gif图中指示的)
boolean flag = false;
if (getTag(position + ) != null && !initial.equals(getTag(position + ))) {
if (child.getHeight() + child.getTop() < mTitleHeight) {
//与restore()对应,表示下面translate平移坐标系只对绘制当前标题栏生效
c.save();
flag = true;
//translate使发生碰撞时,两个标题栏紧贴,制造出挤开的效果(dy<0,表示绘制偏下)
c.translate(, child.getHeight() + child.getTop() - mTitleHeight);
}
} c.drawRect(parent.getPaddingLeft(), parent.getPaddingTop(),
parent.getRight() - parent.getPaddingRight(), parent.getPaddingTop() + mTitleHeight, mBackgroundPaint);
c.drawText(initial, child.getPaddingLeft() + mTextStartMargin,
parent.getPaddingTop() + mTitleHeight - (mTitleHeight - mTextHeight) / - mTextBaselineOffset, mTextPaint); if (flag) {
c.restore();
}
}

IndexBar

IndexBar是侧边栏的实现,是采用的自定义View的形式。

FontMatrics

在此之前,介绍一个概念FontMatrics,是表征字体的一个矩阵。

定义BaseLine为Text的起始点(类似英文五线谱的baseline)

drawText传入的纵坐标值也为BaseLine所在的纵坐标,而非矩形区域的左下角的纵坐标(这点很重要,否则在开发者模式中开启布局边界会发现字体和边界错乱)

主要有以下几个属性:

  • Top (<0)

    • Ascent可能的最小值(绝对值最大)
  • Ascent (<0)

    • 字体最高处距BaseLine的距离
  • Descent (>0)
    • 字体最低处距BaseLine的距离
  • Bottom (>0)
    • Descent可能的最大值
  • Leading
    • 间距,用于多行文字显示时的距离

fontMatrics

在此例中我们用来计算每个text的高度,以此作为测量View高度的参数。很多时候可以选择不加leanding值, 因为单行多行时候的leading值都为0.(不知道什么时候可以取到非0的值)

Paint.FontMetrics fm = mPaint.getFontMetrics();
float singleHeight = fm.bottom - fm.top + fm.leading;

onMeasure()

计算View的长宽。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
} private int measureWidth(int widthMeasureSpec) {
int result;
int specMode = MeasureSpec.getMode(widthMeasureSpec);
int specSize = MeasureSpec.getSize(widthMeasureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
result = getSuggestedMinWidth();
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
return result;
} //获取建议的最小宽度,尽量保证不会出现显示不下的情况(极端情况下仍会显示不下)
private int getSuggestedMinWidth() {
String maxLengthTag = "";
for (String tag : mNavigators) {
if (maxLengthTag.length() < tag.length()) {
maxLengthTag = tag;
}
}
return (int) (mPaint.measureText(maxLengthTag) + 0.5);
} private int measureHeight(int heightMeasureSpec) {
int result;
int specMode = MeasureSpec.getMode(heightMeasureSpec);
int specSize = MeasureSpec.getSize(heightMeasureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
Paint.FontMetrics fm = mPaint.getFontMetrics();
float singleHeight = fm.bottom - fm.top + fm.leading;
//这个mLetterSpacingExtra是疏密程度,是自定义属性,默认1.4
mBaseLineHeight = fm.bottom * mLetterSpacingExtra;
result = (int) (mNavigators.size() * singleHeight * mLetterSpacingExtra);
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
return result;
}

onDraw()

负责绘制

protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int height = getHeight();
int width = getWidth();
//高度为0,可能是因为传入参数为空,则不予显示
if (height == ) {
return;
}
int singleHeight = height / mNavigators.size(); //遍历绘制Text
for (int i = ; i < mNavigators.size(); i++) {
float xPos = width / - mPaint.measureText(mNavigators.get(i)) / ;
float yPos = singleHeight * (i + );
if (i == mFocusIndex) {
canvas.drawText(mNavigators.get(i), xPos, yPos - mBaseLineHeight, mFocusPaint);
} else {
canvas.drawText(mNavigators.get(i), xPos, yPos - mBaseLineHeight, mPaint);
}
}
}

DispatchTouchEvent()

处理交互事件,主要是监听UP、CANCEL、DOWN、MOVE,其中以DOWN做为起点,CANCEL、UP做为终点,其他为中间状态。以TAG的焦点变更和事件的开始、结束做为重绘的触发点。

    @Override
public boolean dispatchTouchEvent(MotionEvent event) {
final float y = event.getY();
final int formerFocusIndex = mFocusIndex;
final OnTouchingLetterChangeListener listener = mOnTouchingLetterChangeListener;
final int c = calculateOnClickItemNum(y); switch (event.getAction()) {
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mFocusIndex = -;
invalidate();
listener.onTouchingEnd(mNavigators.get(c));
break;
case MotionEvent.ACTION_DOWN:
listener.onTouchingStart(mNavigators.get(c));
default:
if (formerFocusIndex != c) {
if (c >= && c < mNavigators.size()) {
listener.onTouchingLetterChanged(mNavigators.get(c));
mFocusIndex = c;
invalidate();
}
}
break;
}
return true;
} /**
* @param yPos
* @return the corresponding position in list
*/
private int calculateOnClickItemNum(float yPos) {
int result;
//计算当前触摸点属于哪个TAG,超出边界按照边界值返回(尤其在MOVE的时候很容易滑出边界)
result = (int) (yPos / getHeight() * mNavigators.size());
if (result >= mNavigators.size()) {
result = mNavigators.size() - ;
} else if (result < ) {
result = ;
}
return result;
}

ContactsUtils

主要是负责获得缩写,其中英文字符就直接获得英文字符,中文字符通过比对GB2312得到英文缩写

对于中文获得缩写的核心思想如下,是通过比对GB2312值得到中文中声母,继而获得缩写情况。

//GB2312中简体中文的起止,判断范围
private static int BEGIN = ;
private static int END = ; /**
* 各声母第一个汉字
* {i、u、v} 不做声母
*/
private static char[] chartable = {'啊', '芭', '擦', '搭', '蛾', '发', '噶', '哈', '击', '喀', '垃','妈', '拿', '哦', '啪', '期', '然', '撒', '塌', '挖', '昔', '压', '匝'}; private static char[] initialtable = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K','L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'W', 'X', 'Y', 'Z'}; //此table是各声母对应的起始GB值,与initialtable对应
private static int[] table = new int[chartable.length + ]; static {
for (int i = ; i < chartable.length; i++) {
table[i] = gbValue(chartable[i]);
}
table[chartable.length] = END;
} //计算char对应的gb值
private static int gbValue(char ch) {
String str = "" + ch;
try {
byte[] bytes = str.getBytes("GB2312");
if (bytes.length < ) {
return ;
}
return (bytes[] << & 0xff00) + (bytes[] & 0xff);
} catch (Exception e) {
return ;
}
}

ContactsManager

负责通讯录信息的获取,此处只取了电话号码和联系人名称,使用的是ContentResolver进行查询

@NonNull
public static ArrayList<ShareContactsBean> getPhoneContacts(Context mContext) {
ArrayList<ShareContactsBean> result = new ArrayList<>();
ContentResolver resolver = mContext.getContentResolver();
Cursor phoneCursor = resolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
new String[]{ContactsContract.CommonDataKinds.Phone.NUMBER, ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME}, null, null, null);
if (phoneCursor != null) {
while (phoneCursor.moveToNext()) {
String phoneNumber = phoneCursor.getString().replace(" ", "");
String contactName = phoneCursor.getString();
result.add(new ShareContactsBean(contactName, phoneNumber));
}
phoneCursor.close();
}
//对结果进行排序,这个排序方法写在bean中
Collections.sort(result, new Comparator<ShareContactsBean>() {
@Override
public int compare(ShareContactsBean l, ShareContactsBean r) {
return l.compareTo(r);
}
});
return result;
}
 

Android 使用RecyclerView优雅实现悬浮标题通讯录的更多相关文章

  1. Android上使用RecyclerView实现顶部悬浮标题效果的Sticky Title View

    目前很多的项目都在使用顶部悬浮标题的效果,很明显,这的确是一个比较人性化,用户体验效果比较好的UI交互效果,对于这个效果,有很多种实现方式,如果说要用RecyclerView来实现一个分类信息展示,并 ...

  2. android中RecyclerView控件实现点击事件

    RecyclerView控件实现点击事件跟ListView控件不同,并没有提供类似setOnItemClickListener()这样的注册监听器方法,而是需要自己给子项具体的注册点击事件. 本文的例 ...

  3. android中RecyclerView控件实现瀑布流布局

    本文是在之前文章的基础上做的修改:android中RecyclerView控件的使用 1.修改列表项news_item.xml: <?xml version="1.0" en ...

  4. android中RecyclerView控件的列表项横向排列

    本文是在上一篇文章的基础上做的修改:android中RecyclerView控件的使用 1.修改列表项news_item.xml:我这里是把新闻标题挪到了新闻图片的下面显示 <?xml vers ...

  5. [Android]使用RecyclerView替代ListView(三)

    以下内容为原创,转载请注明: 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/4268097.html  这次来使用RecyclerView实现Pinn ...

  6. [Android]使用RecyclerView替代ListView(二)

    以下内容为原创,转载请注明: 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/4242541.html 以前写过一篇“[Android]使用Adapte ...

  7. [Android]使用RecyclerView替代ListView(一)

    以下内容为原创,欢迎转载,转载请注明 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/4232560.html RecyclerView是一个比List ...

  8. [Android]使用RecyclerView替代ListView(四:SeizeRecyclerView)

    以下内容为原创,欢迎转载,转载请注明 来自天天博客:<> [Android]使用RecyclerView替代ListView(四:SeizeRecyclerView) 在RecyclerV ...

  9. Android Studio移动鼠标显示悬浮提示的设置方法

    欢迎和大家交流技术相关问题: 邮箱: jiangxinnju@163.com 博客园地址: http://www.cnblogs.com/jiangxinnju GitHub地址: https://g ...

随机推荐

  1. [DEBUG]-[org.mybatis.spring.SqlSessionFactoryBean.buildSqlSessionFactory(SqlSessionFactoryBean.java:431)] 一直在创建sqlsession工厂原因

    今天在做开发项目的时候出现一直在控台输出创建sqlsessionfactory'这个代码, tomcat一直在控制台上输出这个内容无法停止下来,那么到底是什么原因呢, 我们可以在输出信息上看到有个wa ...

  2. 【算法笔记】B1007 素数对猜想

    1007 素数对猜想 (20 分) 让我们定义d​n​​为:d​n​​=p​n+1​​−p​n​​,其中p​i​​是第i个素数.显然有d​1​​=1,且对于n>1有d​n​​是偶数.“素数对猜想 ...

  3. Jquery动态绑定事件处理函数 bind / on / delegate

    1.bind方法绑定的事件处理函数不会应用到后来添加到DOM中的新元素.比如你在用bind给页面元素绑定事件之后,又新添加了一些与之前绑定过事件的元素一样的DOM元素,但是这些事件并不能在新的DOM元 ...

  4. 学习掌握oracle外表(external table)

    [转自] http://blog.chinaunix.net/uid-10697776-id-2935685.html 定义 External tables access data in extern ...

  5. os.path模块

    什么是os.path模块 该模块用于处理路径,我们知道python是一门跨平台的语言,二每种操作系统,文件路径是截然不同的,为了使程序可以在不同平台生正确运行,python提供了该模块,使用该模块可以 ...

  6. 【研究】curl测试不安全的HTTP请求

    漏洞名称: 启用了不安全的HTTP方法 安全风险:       可能会在Web 服务器上上载.修改或删除Web 页面.脚本和文件. 可能原因:       Web 服务器或应用程序服务器是以不安全的方 ...

  7. 2019.3.13 final与static

    final 当使用final修饰类的时候,表示类不能被继承(就是extends后面不能再加它了) final 注意事项: 当使用final修饰时,该方法不能被子类重写 当一个方法被标记为private ...

  8. bat mvn Maven中-DskipTests和-Dmaven.test.skip=true的区别 Maven 生命周期

    cd C:\Users\lt32806\git\tempest call mvn clean compile -Dmaven.test.skip=truepause Maven中-DskipTests ...

  9. jsoup: Java HTML Parser

    jsoup  Java HTML Parser jsoup 是一款Java 的HTML解析器,可直接解析某个URL地址.HTML文本内容.它提供了一套非常省力的API,可通过DOM,CSS以及类似于j ...

  10. twitter storm学习 - 安装部署问题汇总

    已经碰到的或者将来碰到的关于安装部署方面的问题以及解决方法,先挖个坑 1.提交的topology在admin界面上看emitted始终都是0,查看日志发现有如下错误: worker [ERROR] E ...