校园通讯录功能,收集校园内的公开联系方式等内容,并进行展示。在日常生活中经常可能需要用到。比如 水电处问题了找水电师傅,招生就业办,学工办的电话等等。
当然,出于隐私考虑,个人手机号不会被收集展示。只展示办公室联系方式等可在网上或校园内查到的号码。
前言
校园通讯录模块旨在打造便捷的校园信息服务平台,完善校园信息服务,给校园信息的管理和维护提供更便捷、更安全的服务系统。
(本文对该模块进行简易实现,仅讲述通讯录列表样式的展示)
系列文章
Github地址: 科师有约校园APP
- 手把手带你撸一个校园APP(一):项目简介
- 手把手带你撸一个校园APP(二):应用启动和欢迎页面
- 手把手带你撸一个校园APP(三):用户模块(登录注册等)
- 手把手带你撸一个校园APP(四):APP框架及功能设计
- 手把手带你撸一个校园APP(五):新闻页面中心模块
- 手把手带你撸一个校园APP(六):失物招领&二手交易模块
- 手把手带你撸一个校园APP(七):校园文化模块(社团活动&表白墙&图说校园)
- 手把手带你撸一个校园APP(八):校园通讯录模块
- 手把手带你撸一个校园APP(九):课程表模块(模拟登陆爬取教务处课程信息)
- 手把手带你撸一个校园APP(十):APP通用模块(更新,意见反馈等)
实现效果
校园通讯录:
分析
- 校园通讯录数据部分比较简单,总共 姓名称呼,部门,联系方式 3个主要字段即可;
- 通讯录列表的数据需要排序分组后再显示;
排序:
按照姓名称呼的首字母拼音排序
==> 需要获取首字的拼音
==> 通过三方库 TinyPinyin 来实现
分组:
可以通过自定义 RecycleView 的 ItemDecoration 来实现,需要额外处理悬浮窗的显示情况
- 右侧显示字母快速索引列表,并关联 RecycleView ,可以关联滑动
通过自定义控件来实现
实现
说明:本模块的实现主要参考 小马快跑 的 仿魅族通讯录 项目,并在其基础上进行了适合本项目的优化,在此向原作者致敬。
Github:仿魅族通讯录
作者: 小马快跑
文章:https://www.jianshu.com/p/7b7b7ee80c44
数据库设计
字段名 | 描述 | 类型 | 是否主键 |
---|---|---|---|
objectId | 唯一标识 | String | 是 |
name | 姓名称呼 | String | - |
department | 部门 | String | - |
tel | 联系方式 | String | - |
安卓实现
下面介绍下校园通讯录模块实现的主要思路:
step 1: 数据排序
依赖 TinyPinyin 库后,对源数据进行加工处理,增加设置 indexTag 字段内容。然后依据该字段进行排序;
public static void sortData(List<Teacher> list) {
if (list == null || list.size() == 0) return;
for (int i = 0; i < list.size(); i++) {
Teacher bean = list.get(i);
String tag = Pinyin.toPinyin(bean.getName().substring(0, 1).charAt(0)).substring(0, 1);
if (tag.matches("[A-Z]")) {
bean.setIndexTag(tag);
} else {
bean.setIndexTag("#");
}
}
Collections.sort(list, new Comparator<Teacher>() {
@Override
public int compare(Teacher o1, Teacher o2) {
if ("#".equals(o1.getIndexTag())) {
return 1;
} else if ("#".equals(o2.getIndexTag())) {
return -1;
} else {
return o1.getIndexTag().compareTo(o2.getIndexTag());
}
}
});
step 2: 数据分组
数据排序完成后,通过自定义 ItemDecoration 来绘制悬浮框及 ItemView 之上的分类Tag
//用来绘制每个ItemView的边距
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
//............省略部分代码............
int position = parent.getChildAdapterPosition(view);
if (position == 0) {
//第一条数据有bar
outRect.set(0, dividerHeight, 0, 0);
} else if (position > 0) {
if (TextUtils.isEmpty(mBeans.get(position).getIndexTag())) return;
//与上一条数据中的tag不同时,该显示bar了
if (!mBeans.get(position).getIndexTag().equals(mBeans.get(position - 1).getIndexTag())) {
outRect.set(0, dividerHeight, 0, 0);
}
}
}
//用来绘制最上面的悬浮框
@Override
public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
int position = ((LinearLayoutManager) (parent.getLayoutManager())).findFirstVisibleItemPosition();
final int bottom = parent.getPaddingTop() + dividerHeight;
mPaint.setColor(Color.WHITE);
//绘制悬浮框的范围
canvas.drawRect(parent.getLeft(), parent.getPaddingTop(), parent.getRight() - parent.getPaddingRight(), parent.getPaddingTop() + dividerHeight, mPaint);
//............省略部分代码............
mPaint.setTextSize(40);
canvas.drawCircle(DpUtil.dp2px(mContext, 42.5f), bottom - dividerHeight / 2, 35, mPaint);
mPaint.setColor(Color.WHITE);
canvas.drawText(mBeans.get(position).getIndexTag(), DpUtil.dp2px(mContext, 42.5f), bottom - dividerHeight / 3, mPaint);
}
//按需绘制ItemView上面的分类Tag
@Override
public void onDraw(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
//............省略部分代码............
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
int position = params.getViewLayoutPosition();
if (position == 0) {
//第一条数据有bar
drawTitleBar(canvas, parent, child, mBeans.get(position), tagsStr.indexOf(mBeans.get(position).getIndexTag()));
} else if (position > 0) {
//与上一条数据中的tag不同时,该显示bar了
if (!mBeans.get(position).getIndexTag().equals(mBeans.get(position - 1).getIndexTag())) {
drawTitleBar(canvas, parent, child, mBeans.get(position), tagsStr.indexOf(mBeans.get(position).getIndexTag()));
} }
}
}
step 3: 自定义右侧索引View
- 绘制右侧导航栏字母:
@Override
protected void onDraw(Canvas canvas) {
//for循环绘制出所有的导航栏字母
for (int i = 0; i < indexStr.length(); i++) {
String textTag = indexStr.substring(i, i + 1);
float xPos = (mWidth - mPaint.measureText(textTag)) / 2;
canvas.drawText(textTag, xPos, singleHeight * (i + 1) + DpUtil.dp2px(mContext, TOP_MARGIN), mPaint);
}
}
- 处理滑动事件:
在onTouchEvent处理了滑动事件,当手指上下滑动时左侧有个圆跟着滑动,这里用的自定义IndexBar( IndexBar extends ViewGroup,IndexBar包含SideBar )来处理的,当SideBar滑动处于MOVE状态时通过((IndexBar) getParent()).setDrawData()把一系列位置参数传到IndexBar中去
@Override
public boolean onTouchEvent(MotionEvent event) {
//处理按下滑动事件
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//按下
mPaint.setColor(Color.BLACK);
invalidate();
case MotionEvent.ACTION_MOVE:
//滑动 event.getY()得到在父View中的Y坐标,通过和总高度的比例再乘以字符个数总长度得到按下的位置
int position = (int) ((event.getY() - getTop() - DpUtil.dp2px(mContext, 80)) / mHeight * indexStr.toCharArray().length);
if (position >= 0 && position < indexStr.length()) {
((IndexBar) getParent()).setDrawData(event.getY(), String.valueOf(indexStr.toCharArray()[position]), position);
if (listener != null) {
listener.indexChanged(indexStr.substring(position, position + 1));
}
}
break;
case MotionEvent.ACTION_UP:
//抬起
((IndexBar) getParent()).setTagStatus(false);
mPaint.setColor(Color.GRAY);
invalidate();
break;
}
return true;
}
主要是在onLayout中把SideBar排列到最右侧,并在onDraw中根据SideBar传过来的一系列位置参数来不断改变圆的位置,
这里要注意一下,自定义ViewGroup的onDraw()方法默认是不会调用的,如果想执行onDraw方法,可以通过下面两种方法:
1.设置透明背景:
在构造函数中:setBackgroundColor(Color.TRANSPARENT);
或者在xml中:android:background="@color/transparent"
2.或者可以在构造函数中添加setWillNotDraw(false);
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childNum = getChildCount();
if (childNum <= 0) return;
//得到SideBar
View childView = getChildAt(0);
childWidth = childView.getMeasuredWidth();
//把SideBar排列到最右侧
childView.layout((mWidth - childWidth), 0, mWidth, mHeight);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (isShowTag) {
//根据位置来不断变换Paint的颜色
ColorUtil.setPaintColor(mPaint, position);
//绘制圆和文字
canvas.drawCircle((mWidth - childWidth) / 2, centerY, circleRadius, mPaint);
mPaint.setColor(Color.WHITE);
mPaint.setTextSize(80);
canvas.drawText(tag, (mWidth - childWidth - mPaint.measureText(tag)) / 2, centerY - (mPaint.ascent() + mPaint.descent()) / 2, mPaint);
}
}
step 4: 关联RecycleView
sideBar.setIndexChangeListener(new SideBar.indexChangeListener() {
@Override
public void indexChanged(String tag) {
if (TextUtils.isEmpty(tag) || contactList.size() <= 0) return;
for (int i = 0; i < contactList.size(); i++) {
if (tag.equals(contactList.get(i).getIndexTag())) {
layoutManager.scrollToPositionWithOffset(i, 0);
return;
}
}
}
});
至此,校园通讯录模块基本完成。
本文例子中用到多个三方库TextDrawable,TinyPinyin ,仿魅族通讯录 等,再次向各位大佬致敬!
如果本文对你有所帮助,还望可以随手赏一个点赞哈 ~ ~