Android高仿QQ小紅點

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

上一篇文章分析了QQ側滑欄的實現, 文章地址: http://www.jianshu.com/p/1834ce7a2ce7 ,本篇繼續來實現一下QQ小紅點的功能,閒言少敘,先上效果圖:

qq_point.gif

程式碼已上傳至Github:仿QQ小紅點,如對您有幫助,歡迎star~感謝

#####繪製貝塞爾曲線:

bezier.png

主要是當在一定範圍內拖拽時算出固定圓和拖拽圓的外切直線以及對應的切點,就可以通過path.quadTo()來繪製二階貝塞爾曲線了~

#####整體思路: 1、當小紅點靜止時,什麼都不做,只需要給自定義小紅點QQBezierView(extends TextView)新增一個.9檔案當背景即可 2、當滑動時,通過getRootView()獲得頂級根View,然後new一個DragView ( extends View ) 來繪製各種狀態時的小紅點,並且通過getRootView().addView()的方式把DragView 加進去,這樣DragView 就可以實現全屏滑動了

#####實現過程:

自定義QQBezierView ( extends TextView ) 並複寫onTouchEvent來處理各種情況,程式碼如下:

@Override
public boolean onTouchEvent(MotionEvent event) {
    //獲得根View
    View rootView = getRootView();
    //獲得觸控位置在全屏所在位置
    float mRawX = event.getRawX();
    float mRawY = event.getRawY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            //請求父View不攔截
            getParent().requestDisallowInterceptTouchEvent(true);
            //獲得當前View在螢幕上的位置
            int[] cLocation = new int[2];
            getLocationOnScreen(cLocation);
            if (rootView instanceof ViewGroup) {
                //初始化拖拽時顯示的View
                dragView = new DragView(getContext());
                //設定固定圓的圓心座標
                dragView.setStickyPoint(cLocation[0] + mWidth / 2, cLocation[1] + mHeight / 2, mRawX, mRawY);
                //獲得快取的bitmap,滑動時直接通過drawBitmap繪製出來
                setDrawingCacheEnabled(true);
                Bitmap bitmap = getDrawingCache();
                if (bitmap != null) {
                    dragView.setCacheBitmap(bitmap);
                    //將DragView新增到RootView中,這樣就可以全屏滑動了
                    ((ViewGroup) rootView).addView(dragView);
                    setVisibility(INVISIBLE);
                }
            }
            break;
        case MotionEvent.ACTION_MOVE:
            //請求父View不攔截
            getParent().requestDisallowInterceptTouchEvent(true);
            if (dragView != null) {
                //更新DragView的位置
                dragView.setDragViewLocation(mRawX, mRawY);
            }
            break;
        case MotionEvent.ACTION_UP:
            getParent().requestDisallowInterceptTouchEvent(false);
            if (dragView != null) {
                //手抬起時來判斷各種情況
                dragView.setDragUp();
            }
            break;
    }
    return true;
}
複製程式碼

上面程式碼註釋已經很詳細了,總結一下就是通過內部攔截法來請求父View是否攔截分發事件,並通過event.getRawX()和event.getRawY()來不斷更新DragView的位置,那麼DragView都做了哪些事呢,接下來就看一下DragView,DragView是QQBezierView 的一個內部View類:

 private int mState;//當前紅點的狀態
 private static final int STATE_INIT = 0;//預設靜止狀態
 private static final int STATE_DRAG = 1;//拖拽狀態
 private static final int STATE_MOVE = 2;//移動狀態
 private static final int STATE_DISMISS = 3;//消失狀態
複製程式碼

首先宣告瞭小紅點的四種狀態,靜止狀態,拖拽狀態,移動狀態和消失狀態。 在QQBezierView 的onTouchEvent的DOWN事件中呼叫了setStickyPoint()方法:

/**
 * 設定固定圓的圓心和半徑
 * @param stickyX 固定圓的X座標
 * @param stickyY 固定圓的Y座標
 */
 public void setStickyPoint(float stickyX, float stickyY, float touchX, float touchY) {
     //分別設定固定圓和拖拽圓的座標
     stickyPointF.set(stickyX, stickyY);
     dragPointF.set(touchX, touchY);
     //通過兩個圓點算出圓心距,也是拖拽的距離
     dragDistance = MathUtil.getTwoPointDistance(dragPointF, stickyPointF);
     if (dragDistance <= maxDistance) {
         //如果拖拽距離小於規定最大距離,則固定的圓應該越來越小,這樣看著才符合實際
         stickRadius = (int) (defaultRadius - dragDistance / 10) < 10 ? 10 : (int) (defaultRadius - dragDistance / 10);
         mState = STATE_DRAG;
     } else {
         mState = STATE_INIT;
     }
 }
複製程式碼

接著,在QQBezierView 的onTouchEvent的MOVE事件中呼叫了setDragViewLocation()方法:

 /**
  * 設定拖拽的座標位置
  *
  * @param touchX 拖拽時的X座標
  * @param touchY 拖拽時的Y座標
  */
 public void setDragViewLocation(float touchX, float touchY) {
     dragPointF.set(touchX, touchY);
     //隨時更改圓心距
     dragDistance = MathUtil.getTwoPointDistance(dragPointF, stickyPointF);
     if (mState == STATE_DRAG) {
        if (isInsideRange()) {
             stickRadius = (int) (defaultRadius - dragDistance / 10) < 10 ? 10 : (int) (defaultRadius - dragDistance / 10);
         } else {
             mState = STATE_MOVE;
             if (onDragListener != null) {
                 onDragListener.onMove();
             }
         }
     }
     invalidate();
 }
複製程式碼

最後在QQBezierView 的onTouchEvent的UP事件中呼叫了setDragUp()方法:

public void setDragUp() {
   if (mState == STATE_DRAG && isInsideRange()) {
       //拖拽狀態且在範圍之內
        startResetAnimator();
     } else if (mState == STATE_MOVE) {
         if (isInsideRange()) {
            //在範圍之內 需要RESET
            startResetAnimator();
        } else {
           //在範圍之外 消失動畫
            mState = STATE_DISMISS;
            startExplodeAnim();
        }
    }
}
複製程式碼

最後來看下DragView的onDraw方法,拖拽時的貝塞爾曲線以及拖拽滑動時的狀態都是通過onDraw實現的:

 @Override
 protected void onDraw(Canvas canvas) {
     if (isInsideRange() && mState == STATE_DRAG) {
         mPaint.setColor(Color.RED);
         //繪製固定的小圓
         canvas.drawCircle(stickyPointF.x, stickyPointF.y, stickRadius, mPaint);
         //首先獲得兩圓心之間的斜率
         Float linK = MathUtil.getLineSlope(dragPointF, stickyPointF);
         //然後通過兩個圓心和半徑、斜率來獲得外切線的切點
         PointF[] stickyPoints = MathUtil.getIntersectionPoints(stickyPointF, stickRadius, linK);
         dragRadius = (int) Math.min(mWidth, mHeight) / 2;
         PointF[] dragPoints = MathUtil.getIntersectionPoints(dragPointF, dragRadius, linK);
         mPaint.setColor(Color.RED);
         //二階貝塞爾曲線的控制點取得兩圓心的中點
         controlPoint = MathUtil.getMiddlePoint(dragPointF, stickyPointF);
         //繪製貝塞爾曲線
         mPath.reset();
         mPath.moveTo(stickyPoints[0].x, stickyPoints[0].y);
         mPath.quadTo(controlPoint.x, controlPoint.y, dragPoints[0].x, dragPoints[0].y);
         mPath.lineTo(dragPoints[1].x, dragPoints[1].y);
         mPath.quadTo(controlPoint.x, controlPoint.y, stickyPoints[1].x, stickyPoints[1].y);
         mPath.lineTo(stickyPoints[0].x, stickyPoints[0].y);
         canvas.drawPath(mPath, mPaint);
     }
     if (mCacheBitmap != null && mState != STATE_DISMISS) {
         //繪製快取的Bitmap
         canvas.drawBitmap(mCacheBitmap, dragPointF.x - mWidth / 2,
                        dragPointF.y - mHeight / 2, mPaint);
     }
     if (mState == STATE_DISMISS && explodeIndex < explode_res.length) {
         //繪製小紅點消失時的爆炸動畫
         canvas.drawBitmap(bitmaps[explodeIndex], dragPointF.x - mWidth / 2, dragPointF.y - mHeight / 2, mPaint);
     }
 }
複製程式碼

PS:最開始使用的是 android:clipChildren="false" 這個屬性,如果父View只是一個普通的ViewGroup(如LinearLayout、RelativeLayout等),此時在父View中設定android:clipChildren="false"後,子View就可以超出自己的範圍,在ViewGroup中也可以滑動了,此時也沒問題;但是當是RecycleView時,只要ItemView設定了background屬性,滑動時的DragView就會顯示在background的下面了,好蛋疼~如有知其原因的還望不吝賜教~

最後再貼下原始碼下載地址:仿QQ小紅點

參考: http://blog.csdn.net/qq_31715429/article/details/54386934 http://blog.csdn.net/harvic880925/article/details/51615221

相關文章