1.概述
消息氣泡拖拽資料有很多,網(wǎng)上也有開(kāi)源代碼,下載下來(lái)就可以用。為什么還要折騰呢?我想證明一下數(shù)學(xué)已經(jīng)初中畢業(yè),其次像貝塞爾這種效果還是很常見(jiàn)的,雖然目前我只有一個(gè) APP 用了這個(gè)效果。我想一行代碼讓所有的控件都可以拖動(dòng)爆炸,不是為了重復(fù)造輪子而是為了裝B。

2.效果實(shí)現(xiàn)
** 2.1 效果分析 **
看上面的效果感覺(jué)有點(diǎn)麻煩,怎么做到任何控件都可以拖動(dòng)爆炸,我想說(shuō)網(wǎng)上應(yīng)該僅此一家。首先可以不要搞得這么麻煩,比如我再上一張圖片看下:

??上面這個(gè)效果就比較簡(jiǎn)單了,先分析一下實(shí)現(xiàn)方式。我手指在任何一個(gè)位置觸摸拖動(dòng)都會(huì)是如上圖的這個(gè)樣式,這個(gè)實(shí)現(xiàn)的起來(lái)就相對(duì)簡(jiǎn)單許多了:
2.1.1: 手指按下拖動(dòng)的時(shí)候有一個(gè)拖拽的圓這個(gè)圓的半徑是不會(huì)變化的但是位置會(huì)隨著手指移動(dòng);
2.1.2: 手指按下拖動(dòng)的時(shí)候有一個(gè)固定的圓這個(gè)圓的是會(huì)變化的但是位置不會(huì)變化,圓的半徑取決于兩個(gè)圓的距離,兩個(gè)圓的距離越大圓半徑越小,距離越小圓半徑越大;
2.1.2: 兩個(gè)圓之間有一個(gè)不規(guī)則的東西將兩個(gè)圓連在一起感覺(jué)像粘液一樣,這就是大家所說(shuō)的貝塞爾效果。
2.2 效果實(shí)現(xiàn)
2.2.1: 監(jiān)聽(tīng)觸摸繪制兩個(gè)圓
??我們先挑簡(jiǎn)單的寫(xiě),首先監(jiān)聽(tīng)手指觸摸不斷的繪制兩個(gè)圓(固定圓和拖拽圓),如果對(duì)觸摸監(jiān)聽(tīng)事件以及畫(huà)筆使用不是特別熟悉,請(qǐng)留意看看我之前的一些自定義 View 的文章。Android進(jìn)階之旅 - 自定義View篇
/**
* description: 消息氣泡拖拽的 View
* author: Darren on 2017/7/21 10:40
* email: 240336124@qq.com
* version: 1.0
*/
public class MessageBubbleView extends View {
// 拖拽圓的圓心點(diǎn)
private PointF mDragPoint;
// 固定圓的圓心點(diǎn)
private PointF mFixationPoint;
// 拖拽圓的半徑
private int mDragRadius = 10;
// 固定圓的半徑
private int mFixationRadius = 7;
// 固定圓的最小半徑
private int FIXATION_RADIUS_MIN = 3;
// 固定圓的最大半徑
private int FIXATION_RADIUS_MAX = 7;
// 用來(lái)繪制的畫(huà)筆
private Paint mPaint;
public MessageBubbleView(Context context) {
this(context, null);
}
public MessageBubbleView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MessageBubbleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 初始化畫(huà)筆
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setDither(true);
mPaint.setAntiAlias(true);
// 初始化一些距離
mDragRadius = dip2px(mDragRadius);
mFixationRadius = dip2px(mFixationRadius);
FIXATION_RADIUS_MIN = dip2px(FIXATION_RADIUS_MIN);
FIXATION_RADIUS_MAX = dip2px(FIXATION_RADIUS_MAX);
}
private int dip2px(int dip) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,dip,getResources().getDisplayMetrics());
}
@Override
protected void onDraw(Canvas canvas) {
if (mDragPoint == null && mFixationPoint == null) {
return;
}
// 1.繪制拖拽圓
canvas.drawCircle(mDragPoint.x, mDragPoint.y, mDragRadius, mPaint);
// 計(jì)算兩個(gè)圓之間的距離
int distance = BubbleUtils.getDistance(mDragPoint, mFixationPoint);
// 計(jì)算固定圓的半徑,距離越大圓半徑越小
mFixationRadius = FIXATION_RADIUS_MAX - distance / 14;
if (mFixationRadius > FIXATION_RADIUS_MIN) {
// 2.繪制固定圓
canvas.drawCircle(mFixationPoint.x, mFixationPoint.y, mFixationRadius, mPaint);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
initPoint(event.getX(), event.getY());
break;
case MotionEvent.ACTION_MOVE:
updateDragPoint(event.getX(), event.getY());
break;
case MotionEvent.ACTION_UP:
break;
}
invalidate();
return true;
}
/**
* 更新拖拽圓的位置
*
* @param x
* @param y
*/
private void updateDragPoint(float x, float y) {
mDragPoint.x = x;
mDragPoint.y = y;
}
/**
* 初始化圓的位置
*
* @param x
* @param y
*/
private void initPoint(float x, float y) {
mDragPoint = new PointF(x, y);
mFixationPoint = new PointF(x, y);
}
}
2.2.2: 繪制貝塞爾曲線
貝塞爾曲線繪制起來(lái)有點(diǎn)小麻煩,我沒(méi)記錯(cuò)的話是初中的數(shù)學(xué)知識(shí),如果你不是特別了解貝塞爾曲線和三角函數(shù)可以百度一下,這里給兩個(gè)鏈接 貝塞爾曲線 和 三角函數(shù) 文章中我就不做過(guò)多的解釋?zhuān)旅嬷v一下求解思路:

??看上面這張圖畫(huà)得不咋地但純手工,藍(lán)色部分和黑色部分是已知,黃色部分是輔助線是可以利用三角公式求出來(lái)的,紅色部分是未知。我們只要求得角 a,有了角 a 我們就能求 x 和 y 這樣我們就知道了 p0 的位置,依葫蘆畫(huà)瓢求能求得 p0,p1,p2,p3的值,有了四個(gè)點(diǎn)有了控制點(diǎn)自然就能畫(huà)貝塞爾曲線了。
/**
* 獲取 Bezier 曲線
*
* @return
*/
public Path getBezierPath() {
if (mFixationRadius < FIXATION_RADIUS_MIN) {
return null;
}
Path bezierPath = new Path();
// 貝塞爾曲線怎么求?
// 計(jì)算斜率
float dx = mFixationPoint.x - mDragPoint.x;
float dy = mFixationPoint.y - mDragPoint.y;
if (dx == 0) {
dx = 0.001f;
}
float tan = dy / dx;
// 獲取角a度值
float arcTanA = (float) Math.atan(tan);
// 依次計(jì)算 p0 , p1 , p2 , p3 點(diǎn)的位置
float P0X = (float) (mFixationPoint.x + mFixationRadius * Math.sin(arcTanA));
float P0Y = (float) (mFixationPoint.y - mFixationRadius * Math.cos(arcTanA));
float P1X = (float) (mDragPoint.x + mDragRadius * Math.sin(arcTanA));
float P1Y = (float) (mDragPoint.y - mDragRadius * Math.cos(arcTanA));
float P2X = (float) (mDragPoint.x - mDragRadius * Math.sin(arcTanA));
float P2Y = (float) (mDragPoint.y + mDragRadius * Math.cos(arcTanA));
float P3X = (float) (mFixationPoint.x - mFixationRadius * Math.sin(arcTanA));
float P3Y = (float) (mFixationPoint.y + mFixationRadius * Math.cos(arcTanA));
// 求控制點(diǎn) 兩個(gè)點(diǎn)的中心位置作為控制點(diǎn)
PointF controlPoint = BubbleUtils.getPointByPercent(mDragPoint, mFixationPoint, 0.5f);
// 整合貝塞爾曲線路徑
bezierPath.moveTo(P0X, P0Y);
bezierPath.quadTo(controlPoint.x, controlPoint.y, P1X, P1Y);
bezierPath.lineTo(P2X, P2Y);
bezierPath.quadTo(controlPoint.x, controlPoint.y, P3X, P3Y);
bezierPath.close();
return bezierPath;
}
下一節(jié)我們來(lái)實(shí)現(xiàn)一下如何能夠讓任何一個(gè) View 都能拖動(dòng)消失,就像 QQ 的消息氣泡一樣,當(dāng)然到時(shí)可能又免不了源碼分析。
所有分享大綱:Android進(jìn)階之旅 - 自定義View篇