方法簡介
先看一下效果, 這裏是模仿qq未讀消息,清空消息動畫效果,主要也是前面講解了貝塞爾曲線的運用實戰
下面我們來計算一下其中各個點的座標位置
AB,CD這兩條線是通過貝塞爾曲線繪製得出的,
- 繪製AB線,我們需要得到A,B,Anchor三個點的座標
- 繪製CD線,我們需要得到C,D,Anchor三個點的座標
得到AB,CD兩條線以後,我們就可以通過path方式,畫出來了,分別對應代碼中的
// 計算控制點座標,兩個圓心的中點
int iAnchorX = (int) ((mBubStillCenter.x + mBubMoveableCenter.x) / 2);
int iAnchorY = (int) ((mBubStillCenter.y + mBubMoveableCenter.y) / 2);
//∠θ
float cosTheta = (mBubMoveableCenter.x-mBubStillCenter.x) / mDist;
float sinTheta = (mBubStillCenter.y-mBubMoveableCenter.y) / mDist;
//A
float iBubStillStartX = mBubStillCenter.x - mBubStillRadius * sinTheta;
float iBubStillStartY = mBubStillCenter.y - mBubStillRadius * cosTheta;
//D
float iBubStillEndX = mBubStillCenter.x + mBubStillRadius * sinTheta;
float iBubStillEndY = mBubStillCenter.y + mBubStillRadius * cosTheta;
//C
float iBubMoveableStartX = mBubMoveableCenter.x + mBubMoveableRadius * sinTheta;
float iBubMoveableStartY = mBubMoveableCenter.y + mBubMoveableRadius * cosTheta;
//B
float iBubMoveableEndX = mBubMoveableCenter.x - mBubMoveableRadius * sinTheta;
float iBubMoveableEndY = mBubMoveableCenter.y - mBubMoveableRadius * cosTheta;
代碼講解
拖動中分爲4個狀態,根據不同的狀態,繪製不同的畫面
/**
* 氣泡默認狀態--靜止
*/
private final int BUBBLE_STATE_DEFAUL = 0;
/**
* 氣泡相連
*/
private final int BUBBLE_STATE_CONNECT = 1;
/**
* 氣泡分離
*/
private final int BUBBLE_STATE_APART = 2;
/**
* 氣泡消失
*/
private final int BUBBLE_STATE_DISMISS = 3;
處理onTouchEvent事件
處理ACTION_DOWN事件,計算兩個圓圓心距離,當圓形距離小於制定距離以內,設置氣泡爲連接狀態
case MotionEvent.ACTION_DOWN: {
//不是消失狀態
if (mBubbleState != BUBBLE_STATE_DISMISS) {
//兩個圓心之間的距離
mDist = (float) Math.hypot(event.getX() - mBubStillCenter.x, event.getY() - mBubStillCenter.y);
if (mDist < mBubbleRadius + MOVE_OFFSET) {
// 加上MOVE_OFFSET是爲了方便拖拽
mBubbleState = BUBBLE_STATE_CONNECT;
} else {
mBubbleState = BUBBLE_STATE_DEFAUL;
}
}
}
處理ACTION_MOVE事件,將拖動的X,Y作爲拖動圓的圓心,同時修改不動圓的半徑,並不斷繪製
case MotionEvent.ACTION_MOVE: {
if (mBubbleState != BUBBLE_STATE_DEFAUL) {
mBubMoveableCenter.x = event.getX();
mBubMoveableCenter.y = event.getY();
mDist = (float) Math.hypot(event.getX() - mBubStillCenter.x, event.getY() - mBubStillCenter.y);
if (mBubbleState == BUBBLE_STATE_CONNECT) {
// 減去MOVE_OFFSET是爲了讓不動氣泡半徑到一個較小值時就直接消失
// 或者說是進入分離狀態
if (mDist < mMaxDist - MOVE_OFFSET) {
mBubStillRadius = mBubbleRadius - mDist / 8;
} else {
mBubbleState = BUBBLE_STATE_APART;
}
}
invalidate();
}
}
處理ACTION_UP事件,
case MotionEvent.ACTION_UP: {
if (mBubbleState == BUBBLE_STATE_CONNECT) {
startBubbleRestAnim();
} else if (mBubbleState == BUBBLE_STATE_APART) {
if (mDist < 2 * mBubbleRadius) {
startBubbleRestAnim();
} else {
startBubbleBurstAnim();
}
}
}
處理ACTION_UP事件: 當兩個氣泡還是連接狀態,放手以後會有一個還原動畫。當兩個氣泡斷開連接以後,會有一個爆炸動畫
case MotionEvent.ACTION_UP: {
//連接狀態,放手,會有一個還原動畫
if (mBubbleState == BUBBLE_STATE_CONNECT) {
startBubbleRestAnim();
} else if (mBubbleState == BUBBLE_STATE_APART) {
if (mDist < 2 * mBubbleRadius) {
startBubbleRestAnim();
} else {
//氣泡分離,爆炸動畫
startBubbleBurstAnim();
}
}
}
還原動畫
在拖拽範圍內歸位的時候我們設置動畫讓終點圓座標從當前位置逐漸變化到起點位置,設置OvershootInterpolator讓動畫出現跳動效果
private void startBubbleRestAnim() {
ValueAnimator anim = ValueAnimator.ofObject(new PointFEvaluator(),
new PointF(mBubMoveableCenter.x, mBubMoveableCenter.y),
new PointF(mBubStillCenter.x, mBubStillCenter.y));
anim.setDuration(200);
anim.setInterpolator(new OvershootInterpolator(5f));
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mBubMoveableCenter = (PointF) animation.getAnimatedValue();
invalidate();
}
});
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mBubbleState = BUBBLE_STATE_DEFAUL;
}
});
anim.start();
}
爆炸動畫
這裏使用了4張爆炸圖片作爲爆炸動畫
private void startBubbleBurstAnim() {
//氣泡改爲消失狀態
mBubbleState = BUBBLE_STATE_DISMISS;
mIsBurstAnimStart = true;
//做一個int型屬性動畫,從0~mBurstDrawablesArray.length結束
ValueAnimator anim = ValueAnimator.ofInt(0, mBurstDrawablesArray.length);
anim.setInterpolator(new LinearInterpolator());
anim.setDuration(500);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//設置當前繪製的爆炸圖片index
mCurDrawableIndex = (int) animation.getAnimatedValue();
invalidate();
}
});
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
//修改動畫執行標誌
mIsBurstAnimStart = false;
}
});
anim.start();
}
完整代碼
/**
* QQ氣泡效果
*/
public class DragBubbleView extends View {
/**
* 氣泡默認狀態--靜止
*/
private final int BUBBLE_STATE_DEFAUL = 0;
/**
* 氣泡相連
*/
private final int BUBBLE_STATE_CONNECT = 1;
/**
* 氣泡分離
*/
private final int BUBBLE_STATE_APART = 2;
/**
* 氣泡消失
*/
private final int BUBBLE_STATE_DISMISS = 3;
/**
* 氣泡半徑
*/
private float mBubbleRadius;
/**
* 氣泡顏色
*/
private int mBubbleColor;
/**
* 氣泡消息文字
*/
private String mTextStr;
/**
* 氣泡消息文字顏色
*/
private int mTextColor;
/**
* 氣泡消息文字大小
*/
private float mTextSize;
/**
* 不動氣泡的半徑
*/
private float mBubStillRadius;
/**
* 可動氣泡的半徑
*/
private float mBubMoveableRadius;
/**
* 不動氣泡的圓心
*/
private PointF mBubStillCenter;
/**
* 可動氣泡的圓心
*/
private PointF mBubMoveableCenter;
/**
* 氣泡的畫筆
*/
private Paint mBubblePaint;
/**
* 貝塞爾曲線path
*/
private Path mBezierPath;
private Paint mTextPaint;
//文本繪製區域
private Rect mTextRect;
private Paint mBurstPaint;
//爆炸繪製區域
private Rect mBurstRect;
/**
* 氣泡狀態標誌
*/
private int mBubbleState = BUBBLE_STATE_DEFAUL;
/**
* 兩氣泡圓心距離
*/
private float mDist;
/**
* 氣泡相連狀態最大圓心距離
*/
private float mMaxDist;
/**
* 手指觸摸偏移量
*/
private final float MOVE_OFFSET;
/**
* 氣泡爆炸的bitmap數組
*/
private Bitmap[] mBurstBitmapsArray;
/**
* 是否在執行氣泡爆炸動畫
*/
private boolean mIsBurstAnimStart = false;
/**
* 當前氣泡爆炸圖片index
*/
private int mCurDrawableIndex;
/**
* 氣泡爆炸的圖片id數組
*/
private int[] mBurstDrawablesArray = {R.drawable.burst_1, R.drawable.burst_2
, R.drawable.burst_3, R.drawable.burst_4, R.drawable.burst_5};
public DragBubbleView(Context context) {
this(context, null);
}
public DragBubbleView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public DragBubbleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public DragBubbleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
//獲取 XML layout中的屬性值
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.DragBubbleView, defStyleAttr, 0);
mBubbleRadius = array.getDimension(R.styleable.DragBubbleView_bubble_radius, mBubbleRadius);
mBubbleColor = array.getColor(R.styleable.DragBubbleView_bubble_color, Color.RED);
mTextStr = array.getString(R.styleable.DragBubbleView_bubble_text);
mTextSize = array.getDimension(R.styleable.DragBubbleView_bubble_textSize, mTextSize);
mTextColor = array.getColor(R.styleable.DragBubbleView_bubble_textColor, Color.WHITE);
//回收TypedArray
array.recycle();
mBubStillRadius = mBubbleRadius;
mBubMoveableRadius = mBubStillRadius;
mMaxDist = 8 * mBubbleRadius;
MOVE_OFFSET = mMaxDist / 4;
//抗鋸齒
mBubblePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBubblePaint.setColor(mBubbleColor);
mBubblePaint.setStyle(Paint.Style.FILL);
mBezierPath = new Path();
//文本畫筆
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setColor(mTextColor);
mTextPaint.setTextSize(mTextSize);
mTextRect = new Rect();
//爆炸畫筆
mBurstPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBurstPaint.setFilterBitmap(true);
mBurstRect = new Rect();
mBurstBitmapsArray = new Bitmap[mBurstDrawablesArray.length];
for (int i = 0; i < mBurstDrawablesArray.length; i++) {
//將氣泡爆炸的drawable轉爲bitmap
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), mBurstDrawablesArray[i]);
mBurstBitmapsArray[i] = bitmap;
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
initView(w, h);
}
/**
* 初始化氣泡位置
*
* @param w
* @param 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 (mBubMoveableCenter == null) {
mBubMoveableCenter = new PointF(w / 2, h / 2);
} else {
mBubMoveableCenter.set(w / 2, h / 2);
}
mBubbleState = BUBBLE_STATE_DEFAUL;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
//不是消失狀態
if (mBubbleState != BUBBLE_STATE_DISMISS) {
//兩個圓心之間的距離
mDist = (float) Math.hypot(event.getX() - mBubStillCenter.x, event.getY() - mBubStillCenter.y);
if (mDist < mBubbleRadius + MOVE_OFFSET) {
// 加上MOVE_OFFSET是爲了方便拖拽
mBubbleState = BUBBLE_STATE_CONNECT;
} else {
mBubbleState = BUBBLE_STATE_DEFAUL;
}
}
}
break;
case MotionEvent.ACTION_MOVE: {
if (mBubbleState != BUBBLE_STATE_DEFAUL) {
mBubMoveableCenter.x = event.getX();
mBubMoveableCenter.y = event.getY();
mDist = (float) Math.hypot(event.getX() - mBubStillCenter.x, event.getY() - mBubStillCenter.y);
if (mBubbleState == BUBBLE_STATE_CONNECT) {
// 減去MOVE_OFFSET是爲了讓不動氣泡半徑到一個較小值時就直接消失
// 或者說是進入分離狀態
if (mDist < mMaxDist - MOVE_OFFSET) {
mBubStillRadius = mBubbleRadius - mDist / 8;
} else {
mBubbleState = BUBBLE_STATE_APART;
}
}
invalidate();
}
}
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;
}
/**
* 爆炸動畫
*/
private void startBubbleBurstAnim() {
//氣泡改爲消失狀態
mBubbleState = BUBBLE_STATE_DISMISS;
mIsBurstAnimStart = true;
//做一個int型屬性動畫,從0~mBurstDrawablesArray.length結束
ValueAnimator anim = ValueAnimator.ofInt(0, mBurstDrawablesArray.length);
anim.setInterpolator(new LinearInterpolator());
anim.setDuration(500);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//設置當前繪製的爆炸圖片index
mCurDrawableIndex = (int) animation.getAnimatedValue();
invalidate();
}
});
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
//修改動畫執行標誌
mIsBurstAnimStart = false;
}
});
anim.start();
}
/**
* 還原動畫
*/
private void startBubbleRestAnim() {
ValueAnimator anim = ValueAnimator.ofObject(new PointFEvaluator(),
new PointF(mBubMoveableCenter.x, mBubMoveableCenter.y),
new PointF(mBubStillCenter.x, mBubStillCenter.y));
anim.setDuration(200);
anim.setInterpolator(new OvershootInterpolator(5f));
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mBubMoveableCenter = (PointF) animation.getAnimatedValue();
invalidate();
}
});
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mBubbleState = BUBBLE_STATE_DEFAUL;
}
});
anim.start();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 1、畫靜止狀態
// 2、畫相連狀態
// 3、畫分離狀態
// 4、畫消失狀態---爆炸動畫
// 1、畫拖拽的氣泡 和 文字
if (mBubbleState != BUBBLE_STATE_DISMISS) {
canvas.drawCircle(mBubMoveableCenter.x, mBubMoveableCenter.y,
mBubMoveableRadius, mBubblePaint);
mTextPaint.getTextBounds(mTextStr, 0, mTextStr.length(), mTextRect);
canvas.drawText(mTextStr, mBubMoveableCenter.x - mTextRect.width() / 2,
mBubMoveableCenter.y + mTextRect.height() / 2, mTextPaint);
}
// 2、畫相連的氣泡狀態
if (mBubbleState == BUBBLE_STATE_CONNECT) {
// 1、畫靜止氣泡
canvas.drawCircle(mBubStillCenter.x, mBubStillCenter.y,
mBubStillRadius, mBubblePaint);
// 2、畫相連曲線
// 計算控制點座標,兩個圓心的中點
int iAnchorX = (int) ((mBubStillCenter.x + mBubMoveableCenter.x) / 2);
int iAnchorY = (int) ((mBubStillCenter.y + mBubMoveableCenter.y) / 2);
//∠θ
float cosTheta = (mBubMoveableCenter.x-mBubStillCenter.x) / mDist;
float sinTheta = (mBubStillCenter.y-mBubMoveableCenter.y) / mDist;
//A
float iBubStillStartX = mBubStillCenter.x - mBubStillRadius * sinTheta;
float iBubStillStartY = mBubStillCenter.y - mBubStillRadius * cosTheta;
//D
float iBubStillEndX = mBubStillCenter.x + mBubStillRadius * sinTheta;
float iBubStillEndY = mBubStillCenter.y + mBubStillRadius * cosTheta;
//C
float iBubMoveableStartX = mBubMoveableCenter.x + mBubMoveableRadius * sinTheta;
float iBubMoveableStartY = mBubMoveableCenter.y + mBubMoveableRadius * cosTheta;
//B
float iBubMoveableEndX = mBubMoveableCenter.x - mBubMoveableRadius * sinTheta;
float iBubMoveableEndY = mBubMoveableCenter.y - mBubMoveableRadius * cosTheta;
mBezierPath.reset();//清除Path中的內容, reset不保留內部數據結構(重置路徑)
// 畫上半弧
mBezierPath.moveTo(iBubStillStartX, iBubStillStartY);//將路徑的繪製位置定在(x,y)的位置
mBezierPath.quadTo(iAnchorX, iAnchorY, iBubMoveableEndX, iBubMoveableEndY);//二階貝塞爾曲線
// 畫下半弧
mBezierPath.lineTo(iBubMoveableStartX, iBubMoveableStartY);//結束點或者下一次繪製直線路徑的開始點
mBezierPath.quadTo(iAnchorX, iAnchorY, iBubStillEndX, iBubStillEndY);//二階貝塞爾曲線
//連接第一個點連接到最後一個點,形成一個閉合區域
mBezierPath.close();
canvas.drawPath(mBezierPath, mBubblePaint);
}
// 3、畫消失狀態---爆炸動畫
if (mIsBurstAnimStart) {
mBurstRect.set((int) (mBubMoveableCenter.x - mBubMoveableRadius),
(int) (mBubMoveableCenter.y - mBubMoveableRadius),
(int) (mBubMoveableCenter.x + mBubMoveableRadius),
(int) (mBubMoveableCenter.y + mBubMoveableRadius));
canvas.drawBitmap(mBurstBitmapsArray[mCurDrawableIndex], null,
mBurstRect, mBubblePaint);
}
}
public void reset() {
initView(getWidth(), getHeight());
invalidate();
}
}