学习,从模仿开始。今天就学习一下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值来提供用户进行初始化工作,这就作为一个小需求,留给读者去完成啦~~