
分析:首先是在指定某個位置畫一個圓出來,手指按到這個圓的時候再繪制一個可以根據(jù)手指位置移動的圓,隨著手指的移動兩個圓逐漸分離,分離的過程中兩圓中間出現(xiàn)連接帶,隨著兩圓圓心距的增大,半徑也是根據(jù)某一比例系數(shù)擴大或縮小,當(dāng)超過臨界點的時候起始圓消失,只剩手指所在位置的圓,然后手指松開圓消失。
根據(jù)上面的分析我們得出繪制步驟:
1、在指定位置繪制起始圓(圓中間可以帶數(shù)字)
2、使用貝塞爾曲線繪制兩圓之間的連接帶
3、處理onTouchEvent事件(down、move、up)
4、添加一些動畫特效
1、繪制起始圓
當(dāng)然我們要實現(xiàn)定義一些常量,畫筆等的初始化代碼我就不再展示了
//是否可拖拽
private boolean mIsCanDrag = false;
//是否超過最大距離
private boolean isOutOfRang = false;
//最終圓是否消失
private boolean disappear = false;
//兩圓相離最大距離
private float maxDistance;
//貝塞爾曲線需要的點
private PointF pointA;
private PointF pointB;
private PointF pointC;
private PointF pointD;
//控制點坐標(biāo)
private PointF pointO;
//起始位置點
private PointF pointStart;
//拖拽位置點
private PointF pointEnd;
//根據(jù)滑動位置動態(tài)改變圓的半徑
private float currentRadiusStart;
private float currentRadiusEnd;
private Rect textRect = new Rect();
//消息數(shù)
private int msgCount = 0;
畫圓大家應(yīng)該都不陌生,一行代碼搞定,傳入圓心坐標(biāo),半徑,畫筆即可
/**
* 畫起始小球
*
* @param canvas 畫布
* @param pointF 點坐標(biāo)
* @param radius 半徑
*/
private void drawStartBall(Canvas canvas, PointF pointF, float radius) {
canvas.drawCircle(pointF.x, pointF.y, radius, circlePaint);
}
/**
* 畫拖拽結(jié)束的小球
*
* @param canvas 畫布
* @param pointF 點坐標(biāo)
* @param radius 半徑
*/
private void drawEndBall(Canvas canvas, PointF pointF, float radius) {
canvas.drawCircle(pointF.x, pointF.y, radius, circlePaint);
}
初始化一些常量,我們demo演示以屏幕中心為圓心
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
startX = w / 2;
startY = h / 2;
maxDistance = dp2px(100);
radiusStart = dp2px(15);
radiusEnd = dp2px(15);
currentRadiusEnd = radiusEnd;
currentRadiusStart = radiusStart;
}
這樣我們就在屏幕中心處繪制了一個圓
2、根據(jù)貝塞爾曲線繪制連接帶
這是本文的重點,計算過程會講解的非常詳細(xì),通俗易懂
我們先看下畫出了是什么樣的再去分析

兩個圓我們知道怎么畫的了,現(xiàn)在就來分析一下連接帶的實現(xiàn),可以看到是兩段平滑的過渡,這樣的弧度使用貝塞爾再好不過了,我們在簡單回顧一下貝塞爾曲線的樣子

看到這個效果是不是會心一笑,這TM就是我們要的效果
下邊看下我畫的一個分析圖,可以說是目前網(wǎng)上最詳細(xì)的圖文解釋了(配上驕傲的表情)

注意:圖中有一個角度描述錯了 tanEAS1應(yīng)該是tanESS1
由于帶撇的點無法在MD語法中標(biāo)示出來 故用1代替撇,例如A`=A1
為了加深理解我在描述一下圖中的意思:
起點圓我們定義為圓S(start的縮寫),對應(yīng)的圓心坐標(biāo)為S(Sx,Sy),可拖拽圓也就是終點圓定義為圓E(end的縮寫),圓心坐標(biāo)為E(Ex,Ey)。連接帶的路徑可以從圖上看出來是:A-->O-->B-->C-->O-->D-->A,其中O為AOB和COD這兩段二階貝塞爾曲線的控制點,圖中綠線標(biāo)注了五個角度,這五個角度是相等的,可以根據(jù)三角形的相關(guān)定理得出,為了充分說明我們是史上最詳細(xì)的解釋,我就舉個例子說明一下為什么角度相等,數(shù)學(xué)不錯的伙伴可以跳過這段啦,角ASA1+ A1SE=90度=A1SE+ESD1可以推出角ASA1=ESD1,同理可以的出其余標(biāo)示角度相等,我們定義為角A,后邊我們就是根據(jù)角度計算各個點的
已知起點圓心S(Sx,Sy),終點圓心E(Ex,Ey),E就是手指滑動所在的位置,可以根據(jù)event.getX()和event.getY()取到
我們以角ESS1為例進行計算:
tanESS1=tanA=S1E/SS1=(Ex-Sx)/(Ey-Sy)=rate,rate就是這個角的斜率,然后根據(jù)反正切得出角A,A=arctan(rate),這是反正切公式,忘記的可以去百度百科溫故一下哦。
知道了角度A就可以根據(jù)角度加上正余弦函數(shù)算出各個點的坐標(biāo)了,這個計算推倒過程我已寫在圖上了,下邊就把上述計算過程用代碼實現(xiàn)一下
/**
* 設(shè)置貝塞爾曲線的相關(guān)點坐標(biāo) 計算方式參照結(jié)算圖即可看明白
* (ps為了畫個清楚這個圖花了不少功夫哦)
*/
private void setABCDOPoint() {
//控制點坐標(biāo)
pointO.set((pointStart.x + pointEnd.x) / 2.0f, (pointStart.y + pointEnd.y) / 2.0f);
float x = pointEnd.x - pointStart.x;
float y = pointEnd.y - pointStart.y;
//斜率 tanA=rate
double rate;
rate = x / y;
//角度 根據(jù)反正切函數(shù)算角度
float angle = (float) Math.atan(rate);
pointA.x = (float) (pointStart.x + Math.cos(angle) * currentRadiusStart);
pointA.y = (float) (pointStart.y - Math.sin(angle) * currentRadiusStart);
pointB.x = (float) (pointEnd.x + Math.cos(angle) * currentRadiusEnd);
pointB.y = (float) (pointEnd.y - Math.sin(angle) * currentRadiusEnd);
pointC.x = (float) (pointEnd.x - Math.cos(angle) * currentRadiusEnd);
pointC.y = (float) (pointEnd.y + Math.sin(angle) * currentRadiusEnd);
pointD.x = (float) (pointStart.x - Math.cos(angle) * currentRadiusStart);
pointD.y = (float) (pointStart.y + Math.sin(angle) * currentRadiusStart);
}
至此關(guān)于貝塞爾曲線這部分就介紹完了,下邊把圓個弧度代碼串聯(lián)起來就ok了,還費什么話先看看效果咋樣,先把終點圓坐標(biāo)定死在一個位置看下效果,為了方便看到繪制的路徑我們把畫筆樣式設(shè)為STROKE

3、處理onTouchEvent事件
3.1、處理ACTION_DOWN事件
手指按下的時候我們要判斷手指所在位置是不是在起點圓上,只有按到起點圓上之后拖拽才有效,還記得我們文章開始的時候定義的變量mIsCanDrag吧
case MotionEvent.ACTION_DOWN:
setIsCanDrag(event);
break;
/**
* 判斷是否可以拖拽
*
* @param event event
*/
private void setIsCanDrag(MotionEvent event) {
Rect rect = new Rect();
rect.left = (int) (startX - radiusStart);
rect.top = (int) (startY - radiusStart);
rect.right = (int) (startX + radiusStart);
rect.bottom = (int) (startY + radiusStart);
//觸摸點是否在圓的坐標(biāo)域內(nèi)
mIsCanDrag = rect.contains((int) event.getX(), (int) event.getY());
}
3.2、處理ACTION_MOVE事件
手指按在起點圓是可move的前提,然后根據(jù)手指滑動取出移動點位置的坐標(biāo),這就是可拖拽的終點圓的坐標(biāo),
if (mIsCanDrag) {
currentX = event.getX();
currentY = event.getY();
//設(shè)置拖拽圓的坐標(biāo)
pointEnd.set(currentX, currentY);
}
然后知道了起點圓的坐標(biāo)和終點圓的坐標(biāo)就可以得出所需要的各個點的坐標(biāo)了,其中兩圓圓心距也可以計算出來,然后根據(jù)圓心距與可拖拽最大距離的比例系數(shù)去設(shè)置兩個圓的半徑,當(dāng)拖拽距離超過了最大距離我們通過改變狀態(tài)去控制只繪制拖拽圓,否則繪制出兩圓和中間的連接帶,下面代碼注釋的很清楚了
/**
* 設(shè)置當(dāng)前計算的到的半徑
*/
private void setCurrentRadius() {
//兩個圓心之間的距離
float distance = (float) Math.sqrt(Math.pow(pointStart.x - pointEnd.x, 2) + Math.pow(pointStart.y - pointEnd.y, 2));
//拖拽距離在設(shè)置的最大值范圍內(nèi)才繪制貝塞爾圖形
if (distance <= maxDistance) {
//比例系數(shù) 控制兩圓半徑縮放
float percent = distance / maxDistance;
//之所以*0.6和0.2只為了設(shè)置拖拽過程圓變化的過大和過小這個系數(shù)是多次嘗試的出的
//你也可以適當(dāng)調(diào)整系數(shù)達到自己想要的效果
currentRadiusStart = (1 - percent * 0.6f) * radiusStart;
currentRadiusEnd = (1 + percent * 0.2f) * radiusEnd;
isOutOfRang = false;
} else {
isOutOfRang = true;
currentRadiusStart = radiusStart;
currentRadiusEnd = radiusEnd;
}
}
看下寫到這一步的時候的效果

我們發(fā)現(xiàn)手指松開的時候圓并沒有消失或者重置,因為我們還沒出來up事件。
3.3、處理ACTION_UP事件
手指抬起的時候我們要判斷抬起的時候終點圓所在位置和起點圓的圓心距是否超過設(shè)置最大距離,如果沒有超過就還原拖拽狀態(tài),只保留一個起點圓,如果超過了最大距離就讓圓消失
if (mIsCanDrag) {
if (isOutOfRang) {
//消失動畫
disappear = true;
invalidate();
} else {
disappear = false;
//歸位,重置各個點的坐標(biāo)為開始狀態(tài)
pointEnd.set(pointStart.x,pointStart.y);
setCurrentRadius();
setABCDOPoint();
invalidate();
}
}

看到這里核心的代碼基本已經(jīng)完成了,但是總感覺哪里不是很完美,哦,動畫,少了一些動畫效果看上去好生硬,我們就在手指離開的時候出來歸位的動畫
4、動畫效果,錦上添花
在拖拽范圍內(nèi)歸位的時候我們設(shè)置動畫讓終點圓坐標(biāo)從當(dāng)前位置逐漸變化到起點位置,設(shè)置BounceInterpolator讓動畫出現(xiàn)跳動效果。并且在超過可拖拽范圍并且釋放消失的時候加上回調(diào)方法,我們可以在消失的時候出來自己的業(yè)務(wù)邏輯
case MotionEvent.ACTION_UP:
if (mIsCanDrag) {
if (isOutOfRang) {
//消失動畫
disappear = true;
if (onDragBallListener != null) {
onDragBallListener.onDisappear();
}
invalidate();
} else {
disappear = false;
//回彈動畫
final float a = (pointEnd.y - pointStart.y) / (pointEnd.x - pointStart.x);
ValueAnimator valueAnimator = ValueAnimator.ofFloat(pointEnd.x, pointStart.x);
valueAnimator.setDuration(500);
valueAnimator.setInterpolator(new BounceInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float x = (float) animation.getAnimatedValue();
float y = pointStart.y + a * (x - pointStart.x);
pointEnd.set(x, y);
setCurrentRadius();
setABCDOPoint();
invalidate();
}
});
valueAnimator.start();
}
}
break;

這樣看著也不是很爽,就把畫筆模式調(diào)成FILL_AND_STROKE再來看下

模擬器顯示效果不是很好,真機效果很好看哦
我們可以繼續(xù)完善一下,在圓中間添加數(shù)字實現(xiàn)消息效果
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
pointStart.set(startX, startY);
if (isOutOfRang) {
if (!disappear) {
drawEndBall(canvas, pointEnd, currentRadiusEnd);
}
} else {
drawStartBall(canvas, pointStart, currentRadiusStart);
if (mIsCanDrag) {
drawEndBall(canvas, pointEnd, currentRadiusEnd);
drawBezier(canvas);
}
}
if (!disappear) {
if (msgCount > 0) {
drawText(canvas, msgCount, pointEnd);
}
}
}
帶數(shù)字消息的效果
追求完美的人看到這里肯定會說消失的時候少個動畫,對,QQ上消失的時候有個氣泡破裂的感覺,這個用幾張不同狀態(tài)的圖,加上幀動畫順序播放就可以實現(xiàn),由于我這沒有圖片資源就不演示這個了,幀動畫的寫法比屬性動畫簡單多了哦。