Android使用RecycleView實現魅族手機通訊錄介面

_小馬快跑_發表於2017-12-15

本篇文章已授權微信公眾號 hongyangAndroid(鴻洋)獨家釋出

本文主要是通過模仿魅族通訊錄,學習一下RecycleView的基本用法,水平有限,如有不當之處,歡迎批評指正,不勝感激!

先看通過RecycleView實現的一個效果圖:

Contacts.gif

完整程式碼見github:仿魅族通訊錄

Recycleview繼承關係:

recycleview.png
RecycleView的三個主要參與者: 1、LayoutManager 2、ItemAnimator 3、ItemDecoration 本文主要用到的是ItemAnimator和ItemDecoration。

  • Item動畫 ItemAnimator

ItemAnimator是個抽象類,ItemAnimator子類用來管理ViewHolder的動畫,官方已經實現了一個DefaultItemAnimator,它繼承自SimpleItemAnimator,而SimpleItemAnimator繼承自ItemAnimator,SimpleItemAnimator是一個包裝類,用來記錄檢視範圍,決定當前ViewHolder是否執行移動、變化、新增和刪除動畫,如果想自定義動畫可以通過繼承SimpleItemAnimator來實現,github上已經有很多優秀的開源動畫了,如: https://github.com/gabrielemariotti/RecyclerViewItemAnimators 這裡只展示一種從左邊進入的動畫效果,其餘大家可以下載下原始碼檢視:

ItemAnimator.gif
注:這裡動畫position=1是寫死了的,主要是為了方便看效果~

  • ItemDecoration

ItemDecoration是RecycleView的一個靜態內部類,通過對每一個ItemView的邊界新增特殊繪製和佈局,從而影響每一個ItemView的邊界,如繪製分割線、繪製懸浮框等等,ItemDecoration中有三個方法: 1、getItemOffsets() 2、onDraw() 3、onDrawOver() 所有的ItemDecorations繪製都是順序執行,即: onDraw() < Item View < onDrawOver(), *onDraw()*可以用來繪製divider,但在此之前必須在getItemOffsets設定了padding範圍,否則onDraw()的繪製是在ItemView的下面導致不可見;*onDrawOver()*是繪製在最上層,所以可以用來繪製懸浮框等,下面來看各個方法:

1、getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) 內部呼叫outRect.set(int left, int top, int right, int bottom)來改變ItemView的邊界,類似於給ItemView設定Padding,預設getItemOffsets不會影響ItemView的邊界,即預設內部呼叫的是outRect.set(0, 0, 0, 0),如果想得到當前正在修飾的ItemView的位置,可以通過**parent.getChildAdapterPosition(view)**來獲取。

2、onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) 在畫布canvas上進行繪製,onDraw()方法是在ItemView被繪製之前執行的,因此onDraw()的繪製是在ItemView下方。

3、onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) 在畫布canvas上進行繪製,onDrawOver()方法是在ItemView被繪製之後執行的,因此onDrawOver()的繪製是在ItemView上方,可用來繪製本例中的懸浮框等。

下面介紹下實現本例魅族通訊錄的主要思路:

1、自定義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()));
            } } 
     }  
   }
複製程式碼

2、繪製右側導航欄:

首先自定義SideBar(SideBar extends 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);
     }
 }

 @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;
 }
複製程式碼

SideBar中繪製了導航字母並在onTouchEvent處理了滑動事件,當手指上下滑動時左側有個圓跟著滑動,這裡用的自定義IndexBar( IndexBar extends ViewGroup,IndexBar包含SideBar )來處理的,當SideBar滑動處於MOVE狀態時通過((IndexBar) getParent()).setDrawData()把一系列位置引數傳到IndexBar中去,:

@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);
     }
 }
複製程式碼

主要是在onLayout中把SideBar排列到最右側,並在onDraw中根據SideBar傳過來的一系列位置引數來不斷改變圓的位置,這裡要注意一下,自定義ViewGroup的onDraw()方法預設是不會呼叫的,如果想執行onDraw方法,可以通過下面兩種方法: 1.設定透明背景: 在建構函式中:setBackgroundColor(Color.TRANSPARENT); 或者在xml中:android:background="@color/transparent" 2.或者可以在建構函式中新增setWillNotDraw(false);

本文例子中用到的三方庫: 1、https://github.com/amulyakhare/TextDrawable 2、https://github.com/promeG/TinyPinyin 3、https://github.com/gabrielemariotti/RecyclerViewItemAnimators

相關文章