手把手帶你擼一個校園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仿魅族通訊錄 等,再次向各位大佬致敬!

如果本文對你有所幫助,還望可以隨手賞一個點贊哈 ~ ~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章