先看效果圖(本篇實現(xiàn)氣泡拖拽消失,沒有實現(xiàn)回彈和消失時爆炸效果)

觀察效果圖,可以發(fā)現(xiàn)其中有兩個圓(固定圓和拖拽圓)加上中間拖拽的貝塞爾效果,其中固定圓的半徑隨著拉伸不斷變小,小到一定值時消失,手指松開,如果固定圓還存在則回彈,不存在則拖拽圓也消失,本篇沒實現(xiàn)的是回彈時的貝塞爾效果,已經(jīng)拖拽圓消失時爆炸的效果,如果有機會實現(xiàn)了會再進行講解。
先來初始化代碼,我們定義為BubbleView類
public class BubbleView extends View {
private int mFixCircleRadius = 20;//固定圓的半徑,其半徑會隨著拉伸變小,我們設(shè)定最小是6,小于6的時候固定圓消失
private int mDragCircleRadius = 20;//拖拽時顯示的園
private PointF mFixCirclePoint;//固定圓圓心,本文只是演示,具體使用時要自己計算view的大小,再設(shè)置合適的圓心和半徑
private PointF mDragCirclePoint;//拖拽圓的圓心,隨著拖拽不斷變化
private Paint paint;//畫筆
private float distance;//兩個圓心的距離
private boolean isDrag = false;//判斷手指按下時是否落在固定圓的區(qū)域
private boolean isFixCircleShow = true;//固定圓是否顯示
private boolean isUp = false;//手指是否彈起,彈起時如果固定圓顯示這時候要回彈回來,如果固定圓消失了,此時應該執(zhí)行拖拽圓爆炸消失效果,本文演示只做消失效果
public BubbleView(Context context) {
this(context, null);
}
public BubbleView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BubbleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
//初始化畫筆,圓心
private void init() {
mFixCirclePoint = new PointF(300, 300);
mDragCirclePoint = new PointF(300, 300);
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.FILL_AND_STROKE);
paint.setColor(getResources().getColor(R.color.red));
}
}
接著我們監(jiān)聽onTouchEvent事件,在ACTION_DOWN的時候判斷手指是否落在了固定圓的區(qū)域,如果沒有不響應本次拖拽事件,接著在ACTION_MOVE中計算拉伸的距離,并不斷的改變固定圓的大小(拉伸距離越大半徑越?。┮约袄靾A的圓心(手指移動到的點),在ACTION_UP中我們恢復初始值,代碼如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
float downX = event.getX();
float downY = event.getY();
isUp = false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
isDrag(downX, downY);//判斷是否在固定圓區(qū)域,不在不顯示拖拽效果
break;
case MotionEvent.ACTION_MOVE:
if (!isDrag) return true;
//判斷是否出現(xiàn)拉伸效果
if (Math.abs(event.getX() - mFixCirclePoint.x )> mFixCircleRadius || Math.abs(event.getY() - mFixCirclePoint.y) > mFixCircleRadius) {
mDragCirclePoint.x = event.getX();//不斷改變拖拽圓的圓心
mDragCirclePoint.y = event.getY();
float dx = mFixCirclePoint.x - mDragCirclePoint.x;
float dy = mFixCirclePoint.y - mDragCirclePoint.y;
distance = (float) Math.hypot(Math.abs(dx), Math.abs(dy));//計算拖拽的距離
postInvalidate();
}
break;
case MotionEvent.ACTION_UP:
isDrag = false;
isUp = true;
mFixCircleRadius = 20;
mDragCirclePoint.x = event.getX();
mDragCirclePoint.y = event.getY();
postInvalidate();
break;
}
return true;
}
//判斷手指是否落在了固定的小圓內(nèi)
private void isDrag(float x, float y) {
if (x >= mFixCirclePoint.x - mFixCircleRadius-20 && x <= mFixCirclePoint.x + mFixCircleRadius+20 && y >= mFixCirclePoint.y - mFixCircleRadius-20 && y <= mFixCirclePoint.y + mFixCircleRadius+20)
isDrag = true;
else isDrag = false;
}
接下來就是繪制,繪制過程中除了兩個圓,最主要的就是中間的拉伸效果,拉伸效果實際上是一個貝塞爾曲線,如下圖

最主要的就是計算p0,p1,p2,p3的點,其中我們知道c0,c1的位置,然后根據(jù)三角函數(shù)就可以計算出這4個點,然后p0到p1是一個二階貝塞爾曲線,p2到p3是一個貝塞爾曲線,控制點我們選為c0,c1中間的位置,代碼如下:
//繪制拉伸的效果
private void drawBezer(Canvas canvas) {
Path path = new Path();
//計算4個控制點
float dx = mFixCirclePoint.x - mDragCirclePoint.x;
float dy = mFixCirclePoint.y - mDragCirclePoint.y;
if (dx == 0) {
dx = 0.001f;
}
float tan = dy / dx;
// 獲取角a度值
float arcTanA = (float) Math.atan(tan);
// 依次計算 p0 , p1 , p2 , p3 點的位置
float P0X = (float) (mFixCirclePoint.x + mFixCircleRadius * Math.sin(arcTanA));
float P0Y = (float) (mFixCirclePoint.y - mFixCircleRadius * Math.cos(arcTanA));
float P1X = (float) (mDragCirclePoint.x + mDragCircleRadius * Math.sin(arcTanA));
float P1Y = (float) (mDragCirclePoint.y - mDragCircleRadius * Math.cos(arcTanA));
float P2X = (float) (mDragCirclePoint.x - mDragCircleRadius * Math.sin(arcTanA));
float P2Y = (float) (mDragCirclePoint.y + mDragCircleRadius * Math.cos(arcTanA));
float P3X = (float) (mFixCirclePoint.x - mFixCircleRadius * Math.sin(arcTanA));
float P3Y = (float) (mFixCirclePoint.y + mFixCircleRadius * Math.cos(arcTanA));
PointF controlPoint = new PointF(mFixCirclePoint.x + (mDragCirclePoint.x - mFixCirclePoint.x) / 2, (mFixCirclePoint.y + (mDragCirclePoint.y - mFixCirclePoint.y) / 2));
// 整合貝塞爾曲線路徑
path.moveTo(P0X, P0Y);
path.quadTo(controlPoint.x, controlPoint.y, P1X, P1Y);
path.lineTo(P2X, P2Y);
path.quadTo(controlPoint.x, controlPoint.y, P3X, P3Y);
path.close();
canvas.drawPath(path, paint);
}
最后是整個的繪制過程代碼:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (!isUp) {
//手指沒有彈起時
mFixCircleRadius = (int) (20 - distance / 14);
//初始化時只畫固定圓
if (!isDrag)
canvas.drawCircle(mFixCirclePoint.x, mFixCirclePoint.y, mFixCircleRadius, paint);
else {
//拖拽時不斷減小固定圓半徑,如果小于6時,固定圓消失
if (mFixCircleRadius <= 6) {
isFixCircleShow = false;
}
if (mFixCircleRadius > 6 && isFixCircleShow) {
canvas.drawCircle(mFixCirclePoint.x, mFixCirclePoint.y, mFixCircleRadius, paint);
drawBezer(canvas);//繪制拖拽的效果
}
canvas.drawCircle(mDragCirclePoint.x, mDragCirclePoint.y, mDragCircleRadius, paint);
}
} else {
//手指彈起時,如果固定圓沒有消失,顯示回彈效果
if (isFixCircleShow)
canvas.drawCircle(mFixCirclePoint.x, mFixCirclePoint.y, mFixCircleRadius, paint);
}
}
最后貼一下本篇的完整代碼
/**
* Created by liuyong
* Data: 2017/8/9
* Github:https://github.com/MrAllRight
* qq未讀消息拖拽消失
*/
public class BubbleView extends View {
private int mFixCircleRadius = 20;//固定圓的半徑,其半徑會隨著拉伸變小,我們設(shè)定最小是6,小于6的時候固定圓消失
private int mDragCircleRadius = 20;//拖拽時顯示的園
private PointF mFixCirclePoint;//固定圓圓心,本文只是演示,具體使用時要自己計算view的大小,再設(shè)置合適的圓心和半徑
private PointF mDragCirclePoint;//拖拽圓的圓心,隨著拖拽不斷變化
private Paint paint;//畫筆
private float distance;//兩個圓心的距離
private boolean isDrag = false;//判斷手指按下時是否落在固定圓的區(qū)域
private boolean isFixCircleShow = true;//固定圓是否顯示
private boolean isUp = false;//手指是否彈起,彈起時如果固定圓顯示這時候要回彈回來,如果固定圓消失了,此時應該執(zhí)行拖拽圓爆炸消失效果,本文演示只做消失效果
public BubbleView(Context context) {
this(context, null);
}
public BubbleView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BubbleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
//初始化畫筆,圓心
private void init() {
mFixCirclePoint = new PointF(300, 300);
mDragCirclePoint = new PointF(300, 300);
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.FILL_AND_STROKE);
paint.setColor(getResources().getColor(R.color.red));
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float downX = event.getX();
float downY = event.getY();
isUp = false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
isDrag(downX, downY);//判斷是否在固定圓區(qū)域,不在不顯示拖拽效果
break;
case MotionEvent.ACTION_MOVE:
if (!isDrag) return true;
//判斷是否出現(xiàn)拉伸效果
if (Math.abs(event.getX() - mFixCirclePoint.x )> mFixCircleRadius || Math.abs(event.getY() - mFixCirclePoint.y) > mFixCircleRadius) {
mDragCirclePoint.x = event.getX();//不斷改變拖拽圓的圓心
mDragCirclePoint.y = event.getY();
float dx = mFixCirclePoint.x - mDragCirclePoint.x;
float dy = mFixCirclePoint.y - mDragCirclePoint.y;
distance = (float) Math.hypot(Math.abs(dx), Math.abs(dy));//計算拖拽的距離
postInvalidate();
}
break;
case MotionEvent.ACTION_UP:
isDrag = false;
isUp = true;
mFixCircleRadius = 20;
mDragCirclePoint.x = event.getX();
mDragCirclePoint.y = event.getY();
postInvalidate();
break;
}
return true;
}
//判斷手指是否落在了固定的小圓內(nèi)
private void isDrag(float x, float y) {
if (x >= mFixCirclePoint.x - mFixCircleRadius-20 && x <= mFixCirclePoint.x + mFixCircleRadius+20 && y >= mFixCirclePoint.y - mFixCircleRadius-20 && y <= mFixCirclePoint.y + mFixCircleRadius+20)
isDrag = true;
else isDrag = false;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (!isUp) {
//手指沒有彈起時
mFixCircleRadius = (int) (20 - distance / 14);
//初始化時只畫固定圓
if (!isDrag)
canvas.drawCircle(mFixCirclePoint.x, mFixCirclePoint.y, mFixCircleRadius, paint);
else {
//拖拽時不斷減小固定圓半徑,如果小于6時,固定圓消失
if (mFixCircleRadius <= 6) {
isFixCircleShow = false;
}
if (mFixCircleRadius > 6 && isFixCircleShow) {
canvas.drawCircle(mFixCirclePoint.x, mFixCirclePoint.y, mFixCircleRadius, paint);
drawBezer(canvas);//繪制拖拽的效果
}
canvas.drawCircle(mDragCirclePoint.x, mDragCirclePoint.y, mDragCircleRadius, paint);
}
} else {
//手指彈起時,如果固定圓沒有消失,顯示回彈效果
if (isFixCircleShow)
canvas.drawCircle(mFixCirclePoint.x, mFixCirclePoint.y, mFixCircleRadius, paint);
}
}
//繪制拉伸的效果
private void drawBezer(Canvas canvas) {
Path path = new Path();
//計算4個控制點
float dx = mFixCirclePoint.x - mDragCirclePoint.x;
float dy = mFixCirclePoint.y - mDragCirclePoint.y;
if (dx == 0) {
dx = 0.001f;
}
float tan = dy / dx;
// 獲取角a度值
float arcTanA = (float) Math.atan(tan);
// 依次計算 p0 , p1 , p2 , p3 點的位置
float P0X = (float) (mFixCirclePoint.x + mFixCircleRadius * Math.sin(arcTanA));
float P0Y = (float) (mFixCirclePoint.y - mFixCircleRadius * Math.cos(arcTanA));
float P1X = (float) (mDragCirclePoint.x + mDragCircleRadius * Math.sin(arcTanA));
float P1Y = (float) (mDragCirclePoint.y - mDragCircleRadius * Math.cos(arcTanA));
float P2X = (float) (mDragCirclePoint.x - mDragCircleRadius * Math.sin(arcTanA));
float P2Y = (float) (mDragCirclePoint.y + mDragCircleRadius * Math.cos(arcTanA));
float P3X = (float) (mFixCirclePoint.x - mFixCircleRadius * Math.sin(arcTanA));
float P3Y = (float) (mFixCirclePoint.y + mFixCircleRadius * Math.cos(arcTanA));
PointF controlPoint = new PointF(mFixCirclePoint.x + (mDragCirclePoint.x - mFixCirclePoint.x) / 2, (mFixCirclePoint.y + (mDragCirclePoint.y - mFixCirclePoint.y) / 2));
// 整合貝塞爾曲線路徑
path.moveTo(P0X, P0Y);
path.quadTo(controlPoint.x, controlPoint.y, P1X, P1Y);
path.lineTo(P2X, P2Y);
path.quadTo(controlPoint.x, controlPoint.y, P3X, P3Y);
path.close();
canvas.drawPath(path, paint);
}
}
也可到github上下載整個貝塞爾曲線的代碼,其中有水波紋,直播點贊,還有本篇qq氣泡拖拽效果,地址:https://github.com/MrAllRight/BezierView
寫完發(fā)現(xiàn)這個還是參考紅橙Darren的文章,大家可以去他的簡書看一下,里面有自定義view的系列,講解的都不錯,而且還有視頻講解。