貝塞爾曲線掃盲
貝塞爾曲線主要由于三個(gè)部分控制:起點(diǎn),終點(diǎn),中間的輔助控制點(diǎn)。在android自帶的Path類中自帶了方法参滴,可以幫助我們實(shí)現(xiàn)貝塞爾曲線:
二階貝塞爾
/**
* Add a quadratic bezier from the last point, approaching control point
* (x1,y1), and ending at (x2,y2). If no moveTo() call has been made for
* this contour, the first point is automatically set to (0,0).
*
* @param x1 The x-coordinate of the control point on a quadratic curve
* @param y1 The y-coordinate of the control point on a quadratic curve
* @param x2 The x-coordinate of the end point on a quadratic curve
* @param y2 The y-coordinate of the end point on a quadratic curve
*/
public void quadTo(float x1, float y1, float x2, float y2) {
isSimplePath = false;
native_quadTo(mNativePath, x1, y1, x2, y2);
}
quadTo()
方法從上一個(gè)點(diǎn)為起點(diǎn)開始繪制貝塞爾曲線,其中(x1锻弓,y1)為輔助控制點(diǎn)砾赔,(x2,y2)為終點(diǎn)青灼。
Path mPath = new Path();
mPath.moveTo(x0,y0);
mPath.quadTo(x1,y1,x2,y2);
如調(diào)用以上代碼暴心,即繪制起點(diǎn)(x0,y0)聚至,終點(diǎn)(x2酷勺,y2)本橙,輔助控制點(diǎn)(x1扳躬,y1)的貝塞爾曲線。因此甚亭,通過不斷改變這三個(gè)點(diǎn)的位置贷币,我們可以繪制出各種各樣的曲線
三階貝塞爾
/**
* Add a cubic bezier from the last point, approaching control points
* (x1,y1) and (x2,y2), and ending at (x3,y3). If no moveTo() call has been
* made for this contour, the first point is automatically set to (0,0).
*
* @param x1 The x-coordinate of the 1st control point on a cubic curve
* @param y1 The y-coordinate of the 1st control point on a cubic curve
* @param x2 The x-coordinate of the 2nd control point on a cubic curve
* @param y2 The y-coordinate of the 2nd control point on a cubic curve
* @param x3 The x-coordinate of the end point on a cubic curve
* @param y3 The y-coordinate of the end point on a cubic curve
*/
public void cubicTo(float x1, float y1, float x2, float y2,
float x3, float y3) {
isSimplePath = false;
native_cubicTo(mNativePath, x1, y1, x2, y2, x3, y3);
}
cubicTo()
方法從上一個(gè)點(diǎn)為起點(diǎn)開始繪制三階貝塞爾曲線,其中(x1亏狰,y1),( x2, y2 )為輔助控制點(diǎn)役纹,(x3,y3)為終點(diǎn)暇唾。
拖拽氣泡實(shí)踐
思路
拖拽氣泡,其實(shí) 就是這樣的幾個(gè)狀態(tài)
- 1.氣泡靜止?fàn)顟B(tài) --氣泡小球和數(shù)字
- 2.氣泡相連 -- 兩個(gè)相連的氣泡小球(帶黏連效果),和數(shù)字
- 3.氣泡分離 -- 單個(gè)氣泡小球的拖動(dòng)
- 4.氣泡消失 -- 爆炸
我們需要在這幾個(gè)狀態(tài)分別,做出處理.
1.兩個(gè)圓和一個(gè)角度
- 一個(gè)靜止的圓O1
- 一個(gè)動(dòng)的圓O2
- 互補(bǔ)角和內(nèi)錯(cuò)角<@
根據(jù)圓的關(guān)系,和Anchor點(diǎn),計(jì)算出角度<@,用來求貝塞爾曲線的ABCD點(diǎn)坐標(biāo).
2.兩條貝塞爾二階曲線
上半邊: A---Ar---B, 以Ar為錨點(diǎn),A和B重點(diǎn)的二階曲線
下半邊: C---Ar---D,以Ar為錨點(diǎn),C和D重點(diǎn)的二階曲線
具體坐標(biāo)的求法,在上圖中可見.
根據(jù)貝塞爾曲線的要求:
起點(diǎn)促脉,終點(diǎn)辰斋,中間的輔助控制點(diǎn)。用在android自帶的Path類中自帶了方法.
舉個(gè)例子,就可以這么做
上邊線:
//畫上半弧 B-->A---------------
//A點(diǎn)靜坐標(biāo)計(jì)算
float mBubStillAX = mBubStillCenter.x -mBubStillRadius*sinTheta;
float mBubStillAY = mBubStillCenter.y +mBubStillRadius*cosTheta;
//B點(diǎn)動(dòng)坐標(biāo)計(jì)算
float mBubMoveBX = mBubMoveableCenter.x -mBubMoveableRadius*sinTheta;
float mBubMoveBY = mBubMoveableCenter.y +mBubMoveableRadius*cosTheta;
mBezierPath.moveTo(mBubStillAX,mBubStillAY);
mBezierPath.quadTo(iAnchorX,iAnchorY,mBubMoveBX,mBubMoveBY);
3.狀態(tài)判斷OnTouch
在onTouchEvent()
中,對(duì)touch事件進(jìn)行自己處理,即return true
進(jìn)行以下幾個(gè)狀態(tài)切換.
- 按下, 如果 當(dāng)前狀態(tài) 沒有消失, 則計(jì)算 當(dāng)前氣泡距離,和 最大允許偏移量 作比較 ,進(jìn)行狀態(tài)切換
- 移動(dòng) ,改變動(dòng)點(diǎn)的位置, 計(jì)算兩點(diǎn)距離, 改變 靜止點(diǎn)的半徑,切換狀態(tài),并重繪制,調(diào)用ondraw
- 抬起手, 根據(jù) 鏈接 狀態(tài), 開始進(jìn)行 動(dòng)畫
4.繪制不同 OnDraw
根據(jù)上述不同的狀態(tài),繪制不同的view.
- 1當(dāng) 當(dāng)前狀態(tài) != 消失, 則, 畫 移動(dòng)的氣泡 和文字
- 2.相連的氣泡狀態(tài),繪制貝塞爾曲線
- 3.boom,爆炸狀態(tài), 爆炸效果動(dòng)畫
最終效果
實(shí)現(xiàn)源碼
/**
* 仿QQ拖拽氣泡
* Created by chenchangjun on 17/5/25.
*
*/
public class DragBuubbleView extends View {
public Context mContext;
/**
* state 根據(jù) ontouch事件進(jìn)行判斷,
*/
private final int STATE_DEFAULT = 0;// 1.氣泡靜止?fàn)顟B(tài) --氣泡小球和數(shù)字
private final int STATE_CONECTED = 1;//2.氣泡相連 -- 兩個(gè)相連的氣泡小球(帶黏連效果),和數(shù)字
private final int STATE_APART = 2;//3.氣泡分離 -- 單個(gè)氣泡小球的拖動(dòng)
private final int STATE_DISMISS = 3;//4.氣泡消失 -- 爆炸
//default values;
private float default_radius;
private int default_bubble_color;
private String default_bubble_text;
private int default_bubble_textColor;
private float default_bubble_textSize;
private float default_bubble_radius;
private float bubble_radius;//小球半徑
private int bubble_color;//小球顏色
private String bubble_text;//小球顯示字
private int bubble_textColor; //字體顏色
private float bubble_textSize;//字體大小
/**
* 靜止氣泡和動(dòng)態(tài)氣泡相關(guān)
*/
private float mBubStillRadius;//不動(dòng)氣泡的半徑
private float mBubMoveableRadius;//可動(dòng)氣泡的半徑
private PointF mBubStillCenter;//不動(dòng)氣泡的圓心
private PointF mBubMoveableCenter;//可動(dòng)氣泡的圓心
private Paint mBubblePaint;//氣泡的畫筆
/**
* 貝塞爾曲線path相關(guān)
*/
private Path mBezierPath;
private Paint mTextPaint;
private Rect mTextRect;
private Paint mBurstPaint;
private Rect mBurstRect;
private int current_state = STATE_DEFAULT;//當(dāng)前狀態(tài)
private float bubbleDistance; //氣泡間距
private float maxDistance; //氣泡最大間距
private float MOVE_OFFSET = 0;//允許你的 手指觸摸偏移量
private Bitmap[] burstBitmapsArray;//氣泡爆炸bitmap數(shù)組
private boolean isBurstAnimStart = false; //是否執(zhí)行 氣泡爆炸
private int curDrawableIndex; //當(dāng)前氣泡爆炸圖片index
//氣泡爆炸的圖片id數(shù)組
private int[] burstDrawablesArray = {R.drawable.burst_1, R.drawable.burst_2
, R.drawable.burst_3, R.drawable.burst_4, R.drawable.burst_5};
private void init(Context context, AttributeSet attrs) {
this.mContext = context;
if (attrs != null) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.DragBubbleView);
bubble_radius = typedArray.getDimension(R.styleable.DragBubbleView_bubble_radius, default_bubble_radius);
bubble_color = typedArray.getColor(R.styleable.DragBubbleView_bubble_color, default_bubble_color);
bubble_textSize = typedArray.getDimension(R.styleable.DragBubbleView_bubble_textSize, default_bubble_textSize);
bubble_textColor = typedArray.getColor(R.styleable.DragBubbleView_bubble_textColor, default_bubble_textColor);
bubble_text = typedArray.getString(R.styleable.DragBubbleView_bubble_text);
typedArray.recycle();
}
mBubStillRadius = bubble_radius;
mBubMoveableRadius = mBubStillRadius;
maxDistance = 8 * bubble_radius;
MOVE_OFFSET = maxDistance / 4;
//氣泡畫筆
mBubblePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBubblePaint.setColor(bubble_color);
mBubblePaint.setStyle(Paint.Style.FILL);
mBezierPath = new Path();
//文字畫筆
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setColor(bubble_textColor);
mTextPaint.setTextSize(bubble_textSize);
mTextRect = new Rect();
//爆炸畫筆
mBurstPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBurstPaint.setFilterBitmap(true);
mBurstRect = new Rect();
burstBitmapsArray = new Bitmap[burstDrawablesArray.length];
for (int i = 0; i < burstDrawablesArray.length; i++) {
//將氣泡爆炸的drawable轉(zhuǎn)為bitmap
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), burstDrawablesArray[i]);
burstBitmapsArray[i] = bitmap;
}
}
private void initView(int w, int h) {
//設(shè)置兩氣泡圓心初始坐標(biāo)
if (mBubStillCenter == null) {
mBubStillCenter = new PointF(w / 2, h / 2);
} else {
mBubStillCenter.set(w / 2, h / 2);
}
//設(shè)置動(dòng)點(diǎn)坐標(biāo)
if (mBubMoveableCenter == null) {
mBubMoveableCenter = new PointF(w / 2, h / 2);
} else {
mBubMoveableCenter.set(w / 2, h / 2);
}
current_state = STATE_DEFAULT;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//當(dāng) 當(dāng)前狀態(tài) != 消失, 則, 畫 移動(dòng)的氣泡 和文字
if (current_state != STATE_DISMISS) {
canvas.drawCircle(mBubMoveableCenter.x,mBubMoveableCenter.y,
mBubMoveableRadius,mBubblePaint);
mTextPaint.getTextBounds(bubble_text, 0, bubble_text.length(), mTextRect);
canvas.drawText(bubble_text, mBubMoveableCenter.x - mTextRect.width() / 2, mBubMoveableCenter.y + mTextRect.height() / 2, mTextPaint);
}
//2.相連的氣泡狀態(tài),繪制貝塞爾曲線
if (current_state == STATE_CONECTED) {
//1.靜止氣泡
canvas.drawCircle(mBubStillCenter.x, mBubStillCenter.y, mBubStillRadius, mBubblePaint);
//重置path狀態(tài)
mBezierPath.reset();
//2.相連曲線---貝塞爾曲線
//計(jì)算控制點(diǎn)坐標(biāo)---兩個(gè)球圓心的中點(diǎn).
int iAnchorX = (int) ((mBubStillCenter.x + mBubMoveableCenter.x) / 2);
int iAnchorY = (int) ((mBubStillCenter.y + mBubMoveableCenter.y) / 2);
//計(jì)算 三角函數(shù)
float cosTheta = (mBubStillCenter.x - mBubMoveableCenter.x)/bubbleDistance;
float sinTheta = (mBubStillCenter.y - mBubMoveableCenter.y)/bubbleDistance;
//畫上半弧 B-->A---------------
//A點(diǎn)靜坐標(biāo)計(jì)算
float mBubStillAX = mBubStillCenter.x -mBubStillRadius*sinTheta;
float mBubStillAY = mBubStillCenter.y +mBubStillRadius*cosTheta;
//B點(diǎn)動(dòng)坐標(biāo)計(jì)算
float mBubMoveBX = mBubMoveableCenter.x -mBubMoveableRadius*sinTheta;
float mBubMoveBY = mBubMoveableCenter.y +mBubMoveableRadius*cosTheta;
mBezierPath.moveTo(mBubStillAX,mBubStillAY);
mBezierPath.quadTo(iAnchorX,iAnchorY,mBubMoveBX,mBubMoveBY);
//畫下半弧 C-->D-------------
//C點(diǎn)動(dòng)坐標(biāo)計(jì)算
float mBubMoveCX = mBubMoveableCenter.x +mBubMoveableRadius*sinTheta;
float mBubMoveCY = mBubMoveableCenter.y -mBubMoveableRadius*cosTheta;
//D點(diǎn)靜坐標(biāo)計(jì)算
float mBubStillDX = mBubStillCenter.x +mBubStillRadius*sinTheta;
float mBubStillDY = mBubStillCenter.y -mBubStillRadius*cosTheta;
mBezierPath.lineTo(mBubMoveCX,mBubMoveCY);
mBezierPath.quadTo(iAnchorX,iAnchorY,mBubStillDX,mBubStillDY);
//釋放資源,并畫----------
mBezierPath.close();
canvas.drawPath(mBezierPath,mBubblePaint);
}
//boom,爆炸狀態(tài), 爆炸效果動(dòng)畫
if(isBurstAnimStart){
mBurstRect.set((int)(mBubMoveableCenter.x - mBubMoveableRadius),
(int)(mBubMoveableCenter.y - mBubMoveableRadius),
(int)(mBubMoveableCenter.x + mBubMoveableRadius),
(int)(mBubMoveableCenter.y + mBubMoveableRadius));
canvas.drawBitmap(burstBitmapsArray[curDrawableIndex],null,
mBurstRect,mBubblePaint); }
}
/**
* 重置狀態(tài)
*/
public void reset(){
initView(getWidth(),getHeight());
invalidate();
}
/**
* 開始?xì)馀?重置狀態(tài) 的動(dòng)畫
*/
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) {
current_state= STATE_DEFAULT;
}
});
anim.start();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
initView(w, h);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
//按下, 如果 當(dāng)前狀態(tài) 沒有消失, 則計(jì)算 當(dāng)前氣泡距離,和 最大允許偏移量 作比較 ,進(jìn)行狀態(tài)切換
case MotionEvent.ACTION_DOWN:
if(current_state!=STATE_DISMISS){
bubbleDistance=(float) Math.hypot(event.getX()-mBubStillCenter.x,event.getY()-mBubStillCenter.y);
if (bubbleDistance<bubble_radius+MOVE_OFFSET){
current_state=STATE_CONECTED;
}else {
current_state=STATE_DEFAULT;
}
}
break;
//移動(dòng) ,改變動(dòng)點(diǎn)的位置, 計(jì)算兩點(diǎn)距離, 改變 靜止點(diǎn)的半徑,切換狀態(tài),并重繪制,調(diào)用ondraw
case MotionEvent.ACTION_MOVE:
{
if(current_state != STATE_DEFAULT){
mBubMoveableCenter.x = event.getX();
mBubMoveableCenter.y = event.getY();
bubbleDistance = (float) Math.hypot(event.getX() - mBubStillCenter.x,
event.getY() - mBubStillCenter.y);
if(current_state == STATE_CONECTED){
// 減去MOVE_OFFSET是為了讓不動(dòng)氣泡半徑到一個(gè)較小值時(shí)就直接消失
// 或者說是進(jìn)入分離狀態(tài)
if(bubbleDistance < maxDistance - MOVE_OFFSET){
mBubStillRadius = bubble_radius - bubbleDistance / 8;
}else{
current_state = STATE_APART;
}
}
invalidate();
}
}
break;
//抬起手, 根據(jù) 鏈接 狀態(tài), 開始進(jìn)行 動(dòng)畫
case MotionEvent.ACTION_UP:
{
if(current_state == STATE_CONECTED){
startBubbleRestAnim();
}else if(current_state == STATE_APART){
if(bubbleDistance < 2 * bubble_radius){
startBubbleRestAnim();
}else{
startBubbleBurstAnim();
}
}
}
break;
}
return true;
}
/**
* 氣泡消失動(dòng)畫
*/
private void startBubbleBurstAnim() {
//氣泡改為消失狀態(tài)
current_state = STATE_DISMISS;
isBurstAnimStart = true;
//做一個(gè)int型屬性動(dòng)畫瘸味,從0~mBurstDrawablesArray.length結(jié)束
ValueAnimator anim = ValueAnimator.ofInt(0, burstDrawablesArray.length);
anim.setInterpolator(new LinearInterpolator());
anim.setDuration(500);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//設(shè)置當(dāng)前繪制的爆炸圖片index
curDrawableIndex = (int) animation.getAnimatedValue();
invalidate();
}
});
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
//修改動(dòng)畫執(zhí)行標(biāo)志
isBurstAnimStart = false;
}
});
anim.start();
}
public DragBuubbleView(Context context) {
super(context);
init(context, null);
}
public DragBuubbleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public DragBuubbleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public DragBuubbleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs);
}
}