【Android】仿QQ可拖拽气泡

学习,从模仿开始。今天就学习一下QQ自定义的气泡,先上效果
这里写图片描述

这里涉及到一下知识点:

  • Path:画贝塞尔曲线
  • Canvas:画形状和文字以及图片
  • view:触摸事件
  • 属性动画:处理爆炸效果

一、初始化

整个过程可以分为以下4个状态:

  • 静止状态:画一个大圆
  • 相连状态:画两个一大一小的圆,并通过贝塞尔曲线相连, 如图
    这里写图片描述
  • 断开状态:画一个跟随手指移动的圆
  • 爆炸状态:画出爆炸效果,并复原

需要以下对象:

  • paint:画两个圆、文字和贝塞尔曲线
  • PointF:保存两个圆的位置
  • Bitmap数组:保存爆炸效果的图片资源
  • Rect:用于画文字和图片的
  • 设置两个圆的半径和最大移动距离

在构造函数中调用

  private void init() {
        mBubPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBubPaint.setColor(getResources().getColor(R.color.colorAccent));
        mBubPaint.setStyle(Paint.Style.FILL);

        mTextRect = new Rect();
        mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setColor(getResources().getColor(R.color.colorPrimaryDark));
        mTextPaint.setStyle(Paint.Style.FILL);
        mTextPaint.setTextSize(spToPx(15));
        mTextPaint.getTextBounds(mTextStr, 0, mTextStr.length(), mTextRect);

        //贝塞尔曲线
        mBerierPath = new Path();

        mBurstPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBurstPaint.setFilterBitmap(true);
        mBurstRect = new Rect();
        //初始化气泡资源
        mBurstBitMapArray = new Bitmap[mBurstDrawableArray.length];
        for (int i = 0; i < mBurstBitMapArray.length; i++) {
            Bitmap bitmap = BitmapFactory.decodeResource(getResources(), mBurstDrawableArray[i]);
            mBurstBitMapArray[i] = bitmap;
        }

        mBubRadius = Math.max(mTextRect.height(),mTextRect.width());
        mBubStillRadius = mBubRadius;
        mBubMoveRadius = mBubRadius;
        mMaxDist = 8 * mBubRadius;
        MOVE_OFFSET = mMaxDist / 4;
    }

重写onSizeChange 方法

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        initView(w, h);
    }
    private void initView(int w, int h) {
        if (mBubStillCenter == null) {
            mBubStillCenter = new PointF(w / 2, h / 2);
        } else {
            mBubStillCenter.set(w / 2, h / 2);
        }
        if (mBubMoveCenter == null) {
            mBubMoveCenter = new PointF(w / 2, h / 2);
        } else {
            mBubMoveCenter.set(w / 2, h / 2);
        }
    }

这里就将装备工作做好了,接下来就进行最重要的两步之一

二、重写ondraw方法

  • 首先判断气泡状态,如果不处于爆炸状态就画出大圆
  • 再判断气泡是否处于相连状态,如果是,就画出相连的
  • 接着判断是否可以画文字
  • 最后判断气泡是否处于爆炸状态,是就画出爆炸效果

代码如下:

   @Override
        protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Log.i(TAG, "onDraw: ");
        //移动的大圆没有消失
        if (mBubState != BUBBLE_STATE_BOB) {
            canvas.drawCircle(mBubMoveCenter.x, mBubMoveCenter.y, mBubMoveRadius, mBubPaint);
        }
        //处于相连状态
        if(mBubState == BUBBLE_STATE_CONNECT){
            drawConnectState(canvas);
        }
        //画文字
        if(mBubState != BUBBLE_STATE_BOB){
            if (!TextUtils.isEmpty(mTextStr)) {
                canvas.drawText(mTextStr,
                        mBubMoveCenter.x - mTextRect.width() / 2,
                        mBubMoveCenter.y + mTextRect.height() / 2,
                        mTextPaint);
            }
        }
        //开始爆炸,就画爆炸效果
        if (mBubState == BUBBLE_STATE_BOB) {
            mBurstRect.set((int) (mBubMoveCenter.x - mBubMoveRadius), (int) (mBubMoveCenter.y - mBubMoveRadius)
                    , (int) (mBubMoveCenter.x + mBubMoveRadius), (int) (mBubMoveCenter.y + mBubMoveRadius));
            canvas.drawBitmap(mBurstBitMapArray[mCurBitmapIndex], null, mBurstRect, mBurstPaint);
        }

    }

其中相连是最重要的一个状态,也是最难的,这里统一提取出drawConnectState来处理相连状态

画相连状态

1、先画静止的小圆

canvas.drawCircle(mBubStillCenter.x, mBubStillCenter.y, mBubStillRadius, mBubPaint);

2、画贝塞尔曲线
如图所示
这里写图片描述
两个圆的圆心是已知的,我们需要求A,B,C,D以及两个圆心的中点d
显然,d点坐标很简单,如下代码即可得到

 float iAnchorX = (mBubStillCenter.x + mBubMoveCenter.x) / 2;
 float iAnchorY = (mBubStillCenter.y + mBubMoveCenter.y) / 2;

由几何知识知,我们可以先求出角度1的sin 和cos值,进而求出剩余的四个点的坐标,
那么怎么才能求出角度1对应的值呢?
依据直角三角形的定理可知道,先求出两个圆心点的距离,再分别通过d(x),d(y)即可求出

float AbsX = Math.abs(mBubStillCenter.x - mBubMoveCenter.x);
float AbsY = Math.abs(mBubStillCenter.y - mBubMoveCenter.y);

mCenterDist = (float) Math.sqrt(AbsX * AbsX + AbsY * AbsY);

float sin1 = (mBubMoveCenter.y - mBubStillCenter.y) / mCenterDist;
float cos1 = (mBubMoveCenter.x - mBubStillCenter.x) / mCenterDist;

接下来就是求剩下4个点的坐标了

//A
float iBubStillStartX = mBubStillCenter.x - mBubStillRadius * sin1;
float iBubStillStartY = mBubStillCenter.y + mBubStillRadius * cos1;
A = new PointF(iBubStillStartX, iBubStillStartY);
 //B
float iBubMoveEndX = mBubMoveCenter.x - mBubMoveRadius * sin1;
loat iBubMoveEndY = mBubMoveCenter.y + mBubMoveRadius * cos1;
B = new PointF(iBubMoveEndX, iBubMoveEndY);
//C
float iBubMoveStartX = mBubMoveCenter.x + mBubMoveRadius * sin1;
float iBubMoveStartY = mBubMoveCenter.y - mBubMoveRadius * cos1;
C = new PointF(iBubMoveStartX, iBubMoveStartY);
//D
float iBubStillEndX = mBubStillCenter.x + mBubStillRadius * sin1;
float iBubStillEndY = mBubStillCenter.y - mBubStillRadius * cos1;
D = new PointF(iBubStillEndX, iBubStillEndY);

最后通过Path提供的quadTo方法即可以画出贝塞尔曲线图形

mBerierPath.reset();
mBerierPath.moveTo(A.x, A.y);
mBerierPath.quadTo(iAnchorX, iAnchorY, B.x, B.y);//画曲线
mBerierPath.lineTo(C.x, C.y);//画直线
mBerierPath.quadTo(iAnchorX, iAnchorY, D.x, D.y);
mBerierPath.close();//整个曲线闭合
canvas.drawPath(mBerierPath, mBubPaint);

三、重写onTouchEvent方法

1、ACTION_DOWN

先判断气泡是否处于爆炸状态,如果不是就根据触摸坐标判断当前状态是处于相连还是默认状态

if (mBubState != BUBBLE_STATE_BOB) {
  mCenterDist = (float) Math.hypot(event.getX() - mBubStillCenter.x,
                  event.getY() - mBubStillCenter.y);
      if (mCenterDist < mBubMoveRadius) {//点中了圆,处于相连状态
             mBubState = BUBBLE_STATE_CONNECT;
        } else {//没有点中,处于默认状态
              mBubState = BUBBLE_STATE_DEFAULT;
       }
 }

2、ACTION_MOVE

更新移动大圆的位置,并根据位置实时更新当前状态

//如果处于默认状态就直接返回
 if (mBubState == BUBBLE_STATE_DEFAULT) {
     return true;
  }
//更新移动大圆位置
mCenterDist = (float) Math.hypot(event.getX() - mBubStillCenter.x,
                        event.getY() - mBubStillCenter.y);
 mBubMoveCenter.set(event.getX(), event.getY());
if (mBubState == BUBBLE_STATE_CONNECT) {//如果处于连接状态
   if (mCenterDist < mMaxDist - MOVE_OFFSET) {
    //减去MOVE_OFFSET是避免 mBubStillRadius == 0
    //减少小圆半径 mBubStillRadius
    mBubStillRadius = mBubRadius - (mBubRadius * mCenterDist / mMaxDist );
   } else {
//处于分离状态
       mBubState = BUBBLE_STATE_APART;
      }
 }
invalidate();

3、ACTION_UP

判断是否应该还原气泡的状态还是绘制爆炸效果

                if (mBubState == BUBBLE_STATE_CONNECT) {
                    //气泡还原
                    startBubResetAnim();
                } else if (mBubState == BUBBLE_STATE_APART) {
                    if (mCenterDist < 2 * mBubRadius) {
                        //气泡还原
                        startBubResetAnim();
                    } else {
                        //气泡爆炸消失
                        startBubBurstAnim();
                    }
                }
气泡还原动画

通过属性动画不断修改移动小圆的位置

    private void startBubResetAnim() {
        mBubState = BUBBLE_STATE_DEFAULT;

        ValueAnimator valueAnimator = ValueAnimator.ofObject(new PointFEvaluator(),
                new PointF(mBubMoveCenter.x, mBubMoveCenter.y),
                new PointF(mBubStillCenter.x, mBubStillCenter.y));
        valueAnimator.setInterpolator(new OvershootInterpolator(5));
        valueAnimator.setDuration(200);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mBubMoveCenter = (PointF) animation.getAnimatedValue();
                invalidate();
            }
        });
        valueAnimator.start();
    }
气泡爆炸效果
    //启动气泡破的动画
    private void startBubBurstAnim() {
        mBubState = BUBBLE_STATE_BOB;
        ValueAnimator valueAnimator = ValueAnimator.ofInt(0, mBurstBitMapArray.length - 1);
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.setDuration(500);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mCurBitmapIndex = (int) animation.getAnimatedValue();
                invalidate();
            }
        });
        valueAnimator.start();
          //监听动画,及时还原
        valueAnimator.addListener(new Animator.AnimatorListener() {

            @Override
            public void onAnimationStart(Animator animator) {

            }

            @Override
            public void onAnimationEnd(Animator animator) {
                initView(getWidth(), getHeight());
                invalidate();
                mBubState = BUBBLE_STATE_DEFAULT;
            }

            @Override
            public void onAnimationCancel(Animator animator) {

            }

            @Override
            public void onAnimationRepeat(Animator animator) {

            }
        });
    }

四、总结

效果图如开始所示,当然,这样有点不灵活,可以设置自定义的attr值来提供用户进行初始化工作,这就作为一个小需求,留给读者去完成啦~~

点击查看源码

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值