這個模擬功能的實現主要依靠了PATH和二階貝塞爾曲線。首先上一張圖來簡單看一下:
這個模擬功能有以下幾個特點:
- 在開始的時候點選圓以外的區域不會觸發拖動事件
- 點選圓的時候可以拖拽,此時會有一個拉伸效果,連線大圓和小圓
- 拉伸到一定距離(自己設定)以後兩個圓會斷開,此時即使再拖拽進距離之內的時候也不會再產生已經斷開的連線
- 在距離之內鬆手的時候會回彈會原位置,並伴有一個彈跳動畫
介紹了這麼多,看過我前邊文章的朋友應該會有一個基本思路。
暴露介面
這個模擬功能共分為三部分,一個是那個小圓,固定的位置,一個是那個大圓,可以移動,還有一部分就是中間的連線部分,會跟隨大圓一起延伸。
首先看一下都有哪些介面可以呼叫:
public void setMinR(float minR) {
this.minR = minR;
}
public void setMaxR(float maxR) {
this.maxR = maxR;
}
public void setBrokeDistance(float distance) {
this.brokeDistance = distance;
}複製程式碼
第一個setMinR是設定小圓的半徑,第二個setMaxR是設定大圓的半徑,第三個setBrokeDistance是設定斷開的距離,也就是小圓和大圓的圓心之間的最大連線距離。
初始化
public Buble(Context context) {
super(context);
init();
}
public Buble(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}複製程式碼
簡單的看一下初始化方法。
private void init() {
paint = new Paint();
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.GREEN);
paint.setAntiAlias(true);
}複製程式碼
其實只有一個畫筆,這裡可以為各個區域分別設定畫筆。
繪製圖形
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawOriginalCircle(canvas);
if (!canBroke) {
drawMoveCircle(canvas);
drawBCurve(canvas);
}
}複製程式碼
這三個方法相對簡單,drawOriginalCircle是畫中心的小圓,然後canBroke是一個開關,控制是否執行畫移動的圓和畫弧線。
private void drawOriginalCircle(Canvas canvas) {
canvas.drawCircle(getWidth() / 2, getHeight() / 2, minR, paint);
}
private void drawMoveCircle(Canvas canvas) {
canvas.drawCircle(moveX, moveY, maxR, paint);
}
private void drawBCurve(Canvas canvas) {
canvas.drawPath(path, paint);
}複製程式碼
注意,moveX, moveY和path都是變化的,所以在他們的值發生改變以後千萬不要忘記invalidate。
path的連線
關於path的文章網上一大堆。
Path從懵逼到精通——基本操作,這篇文章算是網上流傳比較多的一篇。
此處的難點主要是大圓和小圓之間的連線。用一張圖簡單表示一下:
基本就是這個樣子,path的路徑就是那個黑色的類似於漏斗一樣的東西。此處要計算的角度需要用到三角函式關係式,簡單表示一下:
圖中標出的兩個角度是相等的
double angle = Math.atan((offsetX - minCircleX) / (offsetY - minCircleY));複製程式碼
求出這個角度(offsetX是移動圓心的座標)。
這樣就可以算出四個點的座標了。
private void setPath(float offsetX, float offsetY) {
float minCircleX = (float) getWidth() / 2;
float minCircleY = (float) getHeight() / 2;
double angle = Math.atan((offsetX - minCircleX) / (offsetY - minCircleY));
float x1 = (float) (minCircleX + Math.cos(angle) * minR);
float y1 = (float) (minCircleY - Math.sin(angle) * minR);
float x2 = (float) (offsetX + Math.cos(angle) * maxR);
float y2 = (float) (offsetY - Math.sin(angle) * maxR);
float x3 = (float) (offsetX - Math.cos(angle) * maxR);
float y3 = (float) (offsetY + Math.sin(angle) * maxR);
float x4 = (float) (minCircleX - Math.cos(angle) * minR);
float y4 = (float) (minCircleY + Math.sin(angle) * minR);
float centerX = minCircleX + (offsetX - minCircleX) / 2;
float centerY = minCircleY + (offsetY - minCircleY) / 2;
path.reset();
path.moveTo(minCircleX, minCircleY);
path.lineTo(x1, y1);
path.quadTo(centerX, centerY, x2, y2);
path.lineTo(x3, y3);
path.quadTo(centerX, centerY, x4, y4);
path.lineTo(minCircleX, minCircleY);
path.close();
}複製程式碼
注意quadTo的四個引數的意義,前兩個是你的錨點的座標,後兩個是你要移動到那個點的位置的座標。
觸控事件
這個直接上程式碼來實現思路吧,沒什麼好講的。
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
this.canBroke = false;
moveX = event.getX();
moveY = event.getY();
touchArea = !setCanBroke(moveX, moveY, maxR);
break;
case MotionEvent.ACTION_MOVE:
if (touchArea) {
moveX = event.getX();
moveY = event.getY();
if (setCanBroke(moveX, moveY, brokeDistance)) {
touchArea = false;
this.canBroke = true;
} else {
setPath(moveX, moveY);
}
invalidate();
}
break;
case MotionEvent.ACTION_UP:
Log.d("aaa", "actionUp" + touchArea);
if (touchArea) {
resetCircle(event.getX(), event.getY());
}
break;
}
return true;複製程式碼
這裡主要說明一下這個setCanBroke:
private boolean setCanBroke(float offsetX, float offsetY, float brokeDistance) {
float minCircleX = (float) getWidth() / 2;
float minCircleY = (float) getHeight() / 2;
return (offsetX - minCircleX) * (offsetX - minCircleX) +
(offsetY - minCircleY) * (offsetY - minCircleY) > brokeDistance * brokeDistance;
}複製程式碼
這個表示是否超出了最大移動距離,超出則返回真,未超出則返回假。同時在touchArea的設定中它也用用到了,主要是判斷點選區域是否在圓圈上。
最後還要講一下這個resetCicle,這個是一個屬性動畫來控制返回原點的彈性動畫:
private void resetCircle(float x, float y) {
valueAnimatorX = ValueAnimator.ofFloat(x, (float) getWidth() / 2);
valueAnimatorY = ValueAnimator.ofFloat(y, (float) getHeight() / 2);
valueAnimatorX.removeAllUpdateListeners();
valueAnimatorY.removeAllUpdateListeners();
valueAnimatorX.setInterpolator(new BounceInterpolator());
valueAnimatorY.setInterpolator(new BounceInterpolator());
valueAnimatorX.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
tempX = (float) animation.getAnimatedValue();
moveX = tempX;
}
});
valueAnimatorY.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
tempY = (float) animation.getAnimatedValue();
moveY = tempY;
setPath(tempX, tempY);
postInvalidate();
}
});
set.playTogether(valueAnimatorX, valueAnimatorY);
set.start();
}複製程式碼
其中的插值器是BounceInterpolator,類似於小球彈跳的動畫,在我前邊的文章中有介紹。
最後來看一下不會斷開的效果,相當有意思:
關於自定義view的文章會暫時到這裡,下一步準備更新自定義viewgroup的文章。相對於自定義view會稍微簡單一點。