手把手带你撸一个校园APP(八):校园通讯录模块

校园通讯录功能,收集校园内的公开联系方式等内容,并进行展示。在日常生活中经常可能需要用到。比如 水电处问题了找水电师傅,招生就业办,学工办的电话等等。

当然,出于隐私考虑,个人手机号不会被收集展示。只展示办公室联系方式等可在网上或校园内查到的号码。

前言

校园通讯录模块旨在打造便捷的校园信息服务平台,完善校园信息服务,给校园信息的管理和维护提供更便捷、更安全的服务系统。
(本文对该模块进行简易实现,仅讲述通讯录列表样式的展示)

系列文章

Github地址: 科师有约校园APP

  1. 手把手带你撸一个校园APP(一):项目简介
  2. 手把手带你撸一个校园APP(二):应用启动和欢迎页面
  3. 手把手带你撸一个校园APP(三):用户模块(登录注册等)
  4. 手把手带你撸一个校园APP(四):APP框架及功能设计
  5. 手把手带你撸一个校园APP(五):新闻页面中心模块
  6. 手把手带你撸一个校园APP(六):失物招领&二手交易模块
  7. 手把手带你撸一个校园APP(七):校园文化模块(社团活动&表白墙&图说校园)
  8. 手把手带你撸一个校园APP(八):校园通讯录模块
  9. 手把手带你撸一个校园APP(九):课程表模块(模拟登陆爬取教务处课程信息)
  10. 手把手带你撸一个校园APP(十):APP通用模块(更新,意见反馈等)

实现效果

校园通讯录:
校园通讯录

分析

  1. 校园通讯录数据部分比较简单,总共 姓名称呼,部门,联系方式 3个主要字段即可;
  2. 通讯录列表的数据需要排序分组后再显示;

排序
按照姓名称呼的首字母拼音排序
==> 需要获取首字的拼音
==> 通过三方库 TinyPinyin 来实现
分组
可以通过自定义 RecycleView 的 ItemDecoration 来实现,需要额外处理悬浮窗的显示情况

  1. 右侧显示字母快速索引列表,并关联 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;
            }
        }
    }
});

至此,校园通讯录模块基本完成。
本文例子中用到多个三方库TextDrawableTinyPinyin仿魅族通讯录 等,再次向各位大佬致敬!

如果本文对你有所帮助,还望可以随手赏一个点赞哈 ~ ~

发布了73 篇原创文章 · 获赞 203 · 访问量 32万+

猜你喜欢

转载自blog.csdn.net/zheng_weichao/article/details/104372075