自定义View---仿QQ消息拖拽气泡


在这里插入图片描述
至于为什么还有一根水平线,我接下来会说

基本原理

二阶贝塞尔曲线
即Path的quadTo方法:

quadTo(float x1, float y1, float x2, float y2);

需要两条贝塞尔曲线,而且要形成一个闭合的环路才行。
在这里插入图片描述

基本实现

不动的气泡称:stillBubble,移动的气泡称:moveBubble

首先要确定气泡的状态。

a. 默认状态(default)
b. 连接状态(connection)
c. 分离状态(apart)
d. 销毁状态(destroy)

每种状态下气泡需要实现的功能

  • 只要气泡不处于destroy状态的话,我就要绘制出moveBubble及内的文字,此时是不要绘制那个stillBubble的。
		//画移动的气泡和文字
        if (mCurState != bubbleState.BUBBLE_STATE_DESTROY) {
    
    

            canvas.drawCircle(mBubbleMoveCenter.x, mBubbleMoveCenter.y, mBubbleMoveRadius, mBubblePaint);
            mTextPaint.getTextBounds(mTextChars, 0, mTextChars.length(), mTextRect);
            canvas.drawLine(0, mBubbleMoveCenter.y - mTextRect.centerY(), 3000, mBubbleMoveCenter.y - mTextRect.centerY(), mTextPaint);
            Log.d(TAG, "onDraw: " + mBubbleMoveCenter.y + ":" + mTextRect.centerY() + ":" + mTextRect.top + ":" + mTextRect.bottom);
            canvas.drawText(mTextChars, mBubbleMoveCenter.x - mTextRect.width() * 1.0f / 2, mBubbleMoveCenter.y + mTextRect.height() * 1.0f /2, mTextPaint);
        }

注意: 文字是和moveBubble联系在一起的。
drawText这个方法,drawText正确的画法是从基线开始的,而不是从左上角开始的,(有关drawText的绘制),所以会出现上图的那根线。

  • 当气泡处于connection状态时,要绘制出stillBubble,还要计算出各点的坐标,画出两条贝塞尔曲线,并将曲线闭合。
		//画气泡相连的状态,这里要用到贝塞尔曲线
        if (mCurState == bubbleState.BUBBLE_STATE_CONNECT) {
    
    

            //只有连接才会画静止的气泡
            canvas.drawCircle(mBubbleStillCenter.x, mBubbleStillCenter.y, mBubbleStillRadius, mBubblePaint);
            //画两根贝塞尔曲线,并把曲线闭合
            //1.得到点的坐标
            //这个点是控制点
            float iAnchorX = (mBubbleStillCenter.x + mBubbleMoveCenter.x) / 2;
            float iAnchorY = (mBubbleStillCenter.y + mBubbleMoveCenter.y) / 2;
                        //角度
            double atan = Math.atan((mBubbleMoveCenter.y - mBubbleStillCenter.y) / (mBubbleMoveCenter.x - mBubbleStillCenter.x));
            //四个点的坐标
            float bubbleStillStartX = (float) (mBubbleStillCenter.x - mBubbleStillRadius * Math.sin(atan));
            float bubbleStillStartY = (float) (mBubbleStillCenter.y + mBubbleStillRadius * Math.cos(atan));
            float bubbleStillEndX = (float) (mBubbleStillCenter.x + mBubbleStillRadius * Math.sin(atan));
            float bubbleStillEndY =  (float) (mBubbleStillCenter.y - mBubbleStillRadius * Math.cos(atan));
            float bubbleMoveStartX = (float) (mBubbleMoveCenter.x - mBubbleMoveRadius * Math.sin(atan));
            float bubbleMoveStartY = (float) (mBubbleMoveCenter.y + mBubbleMoveRadius * Math.cos(atan));
            float bubbleMoveEndX = (float) (mBubbleMoveCenter.x + mBubbleMoveRadius * Math.sin(atan));
            float bubbleMoveEndY = (float) (mBubbleMoveCenter.y - mBubbleMoveRadius * Math.cos(atan));

            //清除先前绘制的路线,只保留当前的路线
            mBezierPath.reset();
            //画上半弧
            //moveTo用于移动画笔,不会绘制
            mBezierPath.moveTo(bubbleStillStartX, bubbleStillStartY);
            mBezierPath.quadTo(iAnchorX, iAnchorY, bubbleMoveStartX, bubbleMoveStartY);
            //要形成闭合曲线
            //画下半弧
            mBezierPath.lineTo(bubbleMoveEndX, bubbleMoveEndY);
            mBezierPath.quadTo(iAnchorX, iAnchorY, bubbleStillEndX, bubbleStillEndY);
            //闭合曲线
            mBezierPath.close();
            canvas.drawPath(mBezierPath, mBubblePaint);
        }
  • 当气泡处于apart状态时,可能出现气泡复位,也可能会发生气泡爆炸(只是将状态设置为destroy,并获取到爆炸图片的index,并不是真正执行爆炸动画)
if (mCurState == bubbleState.BUBBLE_STATE_APART){
    
    
         if (mDistance < 2 * mBubbleRadius) {
    
    
         		//复位动画
               startBubbleRestAnim();
          } else {
    
    
          		//爆炸动画
               startBubbleBurstAnim();
          }
}
  • 当气泡处于destroy状态时,执行爆炸动画。
        if (mIsBurstAnim) {
    
    
            mBurstRect.set((int)(mBubbleMoveCenter.x - mBubbleMoveRadius), (int)(mBubbleMoveCenter.y - mBubbleMoveRadius),
                    (int) (mBubbleMoveCenter.x + mBubbleMoveRadius) , (int)(mBubbleMoveCenter.y + mBubbleMoveRadius));
            canvas.drawBitmap(mBurstBitmaps[mCurBurstIndex], null, mBurstRect, mBurstPaint);
        }

状态的变化

首先你得明白,onDraw方法用于绘制不同状态下的气泡,onTouchEvent方法用于在拖拽气泡时,得到不同的状态,然后执行invalidate()方法。
流程图:
在这里插入图片描述

应该从onTouchEvent方法入手:

  • MotionEvent.ACTION_DOWN
    按下操作应该判断的是,这个气泡是否具备移动的条件,判断它的状态是否为connect,因为只有connect状态,onDraw方法中才会出现两个气泡相连的情况。
                mDistance = (float) Math.hypot(event.getX() - mBubbleStillCenter.x, event.getY() - mBubbleStillCenter.y);
                if (mDistance < mBubbleRadius + MOVE_OFFSET) {
    
    
                    mCurState = bubbleState.BUBBLE_STATE_CONNECT;
//                    Log.d(TAG, "onTouchEvent: 1");
                } else {
    
    
                    mCurState = bubbleState.BUBBLE_STATE_DEFAULT;
//                    Log.d(TAG, "onTouchEvent: 2");
                }
  • MotionEvent.ACTION_MOVE
    手指进行滑动,总是要保存moveBubble圆心的位置,当到达一个极限距离时,气泡的状态转化为apart,并执行invalidate()方法
if (mCurState != bubbleState.BUBBLE_STATE_DEFAULT) {
    
    

                    mBubbleMoveCenter.x = event.getX();
                    mBubbleMoveCenter.y = event.getY();
                    mDistance = (float) Math.hypot(event.getX() - mBubbleStillCenter.x, event.getY() - mBubbleStillCenter.y);
                    if (mCurState == bubbleState.BUBBLE_STATE_CONNECT) {
    
    
                        if (mDistance < mMaxDistance - MOVE_OFFSET) {
    
    
                            mBubbleStillRadius = mBubbleRadius - mDistance / 8;
                        } else {
    
    
                            //只要移动到了分离状态,就不会画stillBubble了
                            mCurState = bubbleState.BUBBLE_STATE_APART;
                        }
                    }
                    invalidate();
                }
  • MotionEvent.ACTION_UP
    手指抬起时,如果为connect,调用回弹动画,如果状态为apart,如果距离仍小于2倍半径时,调用回弹动画,否则调用爆炸动画。
 if (mCurState == bubbleState.BUBBLE_STATE_CONNECT) {
    
    
     startBubbleRestAnim();
 } else if (mCurState == bubbleState.BUBBLE_STATE_APART) {
    
    
     if (mDistance < 2 * mBubbleRadius) {
    
    
         startBubbleRestAnim();
     } else {
    
    
         startBubbleBurstAnim();
     }
 }

写在结尾

附上完整的代码

猜你喜欢

转载自blog.csdn.net/zk2000416/article/details/108359633
今日推荐