使用贝塞尔曲线绘制qq气泡,整个动画过程可分为4个过程:
1、默认状态:此时显示一个气泡和一个消息框。
2、连接状态:一个固定气泡(大小随着拖拽的长度变化而变化),一个移动气泡和它之上的消息框。
3、分离状态,此时固定气泡消失,只有移动气泡(和它的消息框)随手指的移动而移动。
4、消失状态:放开手指后气泡爆炸消失。
将整个过程分割为这几个步骤,再分别实现每个步骤,整个过程就完成了!
首先是默认状态,就是一开始显示的状态。
这个很简单,覆写onDraw方法,只要在对应的位置使用Canvans对象“画”一个实心的圆,然后“画”文字即可。接着是连接状态的描绘。
if(mBubbleState == BUBBLE_STATE_CONNECT){ canvas.drawCircle(mFixedBubbleCenter.x,mFixedBubbleCenter.y,mFixedBubbleRadius,mBubblePaint); mBezierPath.reset(); //画贝塞尔曲线 //PE = p(y) - o(y) float PE = mMovedBubbleCenter.y - mFixedBubbleCenter.y; //OE = p(x) - o(x) float OE = mMovedBubbleCenter.x - mFixedBubbleCenter.x; float sinAngle = PE / mDist; 便宜香港vps float cosAngle = OE / mDist; //G float Gx = (mFixedBubbleCenter.x + mMovedBubbleCenter.x) / 2f; float Gy = (mFixedBubbleCenter.y + mMovedBubbleCenter.y) / 2f; //B float Bx = mMovedBubbleCenter.x + sinAngle * mMovedBubbleRadius; float By = mMovedBubbleCenter.y - cosAngle * mMovedBubbleRadius; //A float Ax = mFixedBubbleCenter.x + mFixedBubbleRadius * sinAngle; float Ay = mFixedBubbleCenter.y - mFixedBubbleRadius * cosAngle; //D float Dx = mFixedBubbleCenter.x - mFixedBubbleRadius * sinAngle; float Dy = mFixedBubbleCenter.y + mFixedBubbleRadius * cosAngle; //C float Cx = mMovedBubbleCenter.x - mMovedBubbleRadius * sinAngle; float Cy = mMovedBubbleCenter.y + mMovedBubbleRadius * cosAngle; mBezierPath.moveTo(Dx,Dy); mBezierPath.quadTo(Gx,Gy,Cx,Cy); mBezierPath.lineTo(Bx,By); mBezierPath.quadTo(Gx,Gy,Ax,Ay); mBezierPath.close(); canvas.drawPath(mBezierPath,mBubblePaint); }首先“画”屏幕中心固定的小圆,接着就是2条贝塞尔曲线,通过下面这张图就可以很明显地看出如何运用贝塞尔曲线:
我们需要做的,就是“画”出AB、CD这2条二阶贝塞尔曲线。而ABCD这个不规则多边形,就是“气泡”,根据贝塞尔曲线的定义,可以发现,O点、P点为已知点,G点作为AB、CD这2条贝塞尔曲线的控制点,而A和B、C和D分别是AB、CD贝塞尔曲线的数据点。因此求出A、B、C、D、G5个点的坐标就可以画出这2条贝塞尔曲线了!
关于坐标的求解,一个个来:
G:坐标很简单,直接O点与P点的x,y相加除以2就可以算出来。
ABCD点,根据高中的数学相关知识:
PE = O的y坐标-P的y坐标
OE = P的x坐标-O的x坐标
sin∠POE = PE / OP
cos∠POE = OE / OP
A坐标:
x = O的x坐标 - sin∠POE * 固定圆半径
y = O的y坐标 - cos∠POE * 固定圆半径
B坐标:
x = P的x坐标 - sin∠POE * 动圆半径
y = P的y坐标 - cos∠POE * 动圆半径
C坐标:
x = P的x坐标 + sin∠POE * 动圆半径
y = P的y坐标 + cos∠POE * 动圆半径
D坐标:
x = O的x坐标 + sin∠POE * 固定圆半径
y = O的y坐标 + cos∠POE * 固定圆半径
接着通过Path类的quadTo画二阶贝塞尔曲线。
mBezierPath.moveTo(Dx,Dy); mBezierPath.quadTo(Gx,Gy,Cx,Cy); mBezierPath.lineTo(Bx,By); mBezierPath.quadTo(Gx,Gy,Ax,Ay); mBezierPath.close(); canvas.drawPath(mBezierPath,mBubblePaint);接着是气泡爆炸效果的绘制:
至此,onDraw方法覆写完成。
接着覆写onTouchEvent方法:
@Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()){ case MotionEvent.ACTION_DOWN: if(mBubbleState != BUBBLE_STATE_DISMISS) { mDist = (float) Math.hypot(event.getX() - mFixedBubbleCenter.x, event.getY() - mFixedBubbleCenter.y); if(mDist < mFixedBubbleRadius + MOVE_OFFSET){ mBubbleState = BUBBLE_STATE_CONNECT; }else{ mBubbleState = BUBBLE_STATE_DEFAULT; } } break; case MotionEvent.ACTION_MOVE: if(mBubbleState != BUBBLE_STATE_DEFAULT){ mDist = (float) Math.hypot(event.getX() - mFixedBubbleCenter.x, event.getY() - mFixedBubbleCenter.y); mMovedBubbleCenter.x = event.getX(); mMovedBubbleCenter.y = event.getY(); if(mBubbleState == BUBBLE_STATE_CONNECT) { if (mDist < mMaxDist - MOVE_OFFSET){ mFixedBubbleRadius = mMovedBubbleRadius - mDist / 8; //固定气泡半径变化,可自定义更改 }else{ mBubbleState = BUBBLE_STATE_APART; } } invalidate(); //调用onDraw方法 } break; case MotionEvent.ACTION_UP: if(mBubbleState == BUBBLE_STATE_CONNECT){ //橡皮筋动画效果 startBubbleRestAnim(); }else if(mBubbleState == BUBBLE_STATE_APART){ if(mDist < 2 * mBubbleRadius){ startBubbleRestAnim(); }else{ startBubbleBurstAnim(); } } break; } return true; }将手指在屏幕上的操作分为3步处理:
ACTION_DOWN:当手指触摸屏幕的一瞬间:
计算固定圆与动圆的圆心距离,如果<固定圆半径+偏移量时,将状态设置为连接状态,否则仍然是默认状态,这里加上偏移量是为了扩大触碰的面积,它表示在超出固定圆外一定面积内仍然是有效的触碰面积。
ACTION_MOVE:当手指在屏幕上滑动时:
代码看似复杂,其实主要的操作就是修改动圆的圆心坐标,以及固定圆的半径大小,接着做个判断:超过一定的距离后(这个距离完全可以自行设置),将状态设置为分离,然后调用invalidate()方法。我们知道ACTION_MOVE在手指的移动中会被调用数次(相当多次),因此这些操作也会被调用数次(相当多次),所以手指移动看似产生拖拽的气泡效果,其实只不过不停的重画2个圆以及贝塞尔曲线而已。
ACTION_UP:当手指从屏幕上离开时:
如果仍然是连接状态,那么产生一个回弹的橡皮筋动画效果。
如果分离状态,那么如果这个动圆与固定圆小于一定距离(这个距离可以自己设置),那么不做爆炸效果而是一样产生一个回弹的橡皮筋动画效果。如果大于这个距离,那么就做爆炸效果。
关于动画的实现:
实现橡皮筋回弹效果:
使用属性动画类ValueAnimator 来实现。
private void startBubbleRestAnim(){ ValueAnimator anim = ValueAnimator.ofObject(new PointFEvaluator(), new PointF(mMovedBubbleCenter.x,mMovedBubbleCenter.y), new PointF(mFixedBubbleCenter.x,mFixedBubbleCenter.y)); anim.setDuration(200); anim.setInterpolator(new OvershootInterpolator(5f)); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { mMovedBubbleCenter = (PointF) valueAnimator.getAnimatedValue(); invalidate(); //反复调用onDraw } }); anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); mBubbleState = BUBBLE_STATE_DEFAULT; } }); anim.start(); }实现爆炸效果:
private void startBubbleBurstAnim(){ mBubbleState = BUBBLE_STATE_DISMISS; ValueAnimator anim = ValueAnimator.ofInt(0,mBurstBitmapsArray.length); anim.setInterpolator(new LinearInterpolator()); anim.setDuration(500); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { mCurrentDrawableIndex = (int) valueAnimator.getAnimatedValue(); invalidate(); } }); anim.start(); }demo展示:https://github.com/lyx19970504/qqBubbleDemo