Android Path繪制貝塞爾曲線實現(xiàn)QQ拖拽泡泡
這兩天學習了使用Path繪制貝塞爾曲線相關,然后自己動手做了一個類似QQ未讀消息可拖拽的小氣泡,效果圖如下:
最終效果圖
接下來一步一步的實現(xiàn)整個過程。
基本原理
其實就是使用Path繪制三點的二次方貝塞爾曲線來完成那個妖嬈的曲線的。然后根據(jù)觸摸點不斷繪制對應的圓形,根據(jù)距離的改變改變原始固定圓形的半徑大小。最后就是松手后返回或者爆裂的實現(xiàn)。
Path介紹:
顧名思義,就是一個路徑的意思,Path里面有很多的方法,本次設計主要用到的相關方法有
- moveTo() 移動Path到一個指定的點
- quadTo() 繪制二次貝塞爾曲線,接收兩個點,第一個是控制弧度的點,第二個是終點。
- lineTo() 就是連線
- close() 閉合Path路徑,
- reset() 重置Path的相關設置
Path入門熱身:
path.reset(); path.moveTo(200, 200); //第一個坐標是對應的控制的坐標,第二個坐標是終點坐標 path.quadTo(400, 250, 600, 200); canvas.drawPath(path, paint); canvas.translate(0, 200); //調用close,就會首尾閉合連接 path.close(); canvas.drawPath(path, paint);
記得不要在onDraw方法中new Path或者 Paint喲!
Path
具體實現(xiàn)拆分:
其實整個過程就是繪制了兩個貝塞爾二次曲線的的閉合Path路徑,然后在上面添加兩個圓形。
閉合的Path 路徑實現(xiàn)從左上點畫二次貝塞爾曲線到左下點,左下點連線到右下點,右下點二次貝塞爾曲線到右上點,最后閉合一下!!
相關坐標的確定
這是這次里面的難點之一,因為涉及到了數(shù)學里面的一個sin,cos,tan等等,我其實也忘完了,然后又腦補了一下,廢話不多說,
為什么自己要親自去畫一下呢,因為畫了你才知道,在360旋轉的過程中,角標體系是有兩套的,如果就使用一套來畫的話,就畫出現(xiàn)在旋轉的過程中曲線重疊在一起的情況!
問題已經拋出來了,接下來直接看看代碼實現(xiàn)!
角度確定
根據(jù)貼出來的原理圖可以知道,我們可以使用起始圓心坐標和拖拽的圓心坐標,根據(jù)反正切函數(shù)來得到具體的弧度。
int dy = Math.abs(CIRCLEY - startY); int dx = Math.abs(CIRCLEX - startX); angle = Math.atan(dy * 1.0 / dx);
ok,這里的startX,Y就是移動過程中的坐標。angle就是得到的對應的弧度(角度)。
相關Path繪制
前面已經提到在旋轉的過程中有兩套坐標體系,一開始我也很糾結這個坐標體系要怎么確定,后面又恍然大悟,其實相當于就是一三象限正比例增長,二四象限,反比例增長。
flag = (startY - CIRCLEY ) * (startX- CIRCLEX ) <= 0;
//增加一個flag,用于判斷使用哪種坐標體系。
最最重要的來了,繪制相關的Path路徑!
path.reset(); if (flag) { //第一個點 path.moveTo((float) (CIRCLEX - Math.sin(angle) * ORIGIN_RADIO), (float) (CIRCLEY - Math.cos(angle) * ORIGIN_RADIO)); path.quadTo((float) ((startX + CIRCLEX) * 0.5), (float) ((startY + CIRCLEY) * 0.5), (float) (startX - Math.sin(angle) * DRAG_RADIO), (float) (startY - Math.cos(angle) * DRAG_RADIO)); path.lineTo((float) (startX + Math.sin(angle) * DRAG_RADIO), (float) (startY + Math.cos(angle) * DRAG_RADIO)); path.quadTo((float) ((startX + CIRCLEX) * 0.5), (float) ((startY + CIRCLEY) * 0.5), (float) (CIRCLEX + Math.sin(angle) * ORIGIN_RADIO), (float) (CIRCLEY + Math.cos(angle) * ORIGIN_RADIO)); path.close(); canvas.drawPath(path, paint); } else { //第一個點 path.moveTo((float) (CIRCLEX - Math.sin(angle) * ORIGIN_RADIO), (float) (CIRCLEY + Math.cos(angle) * ORIGIN_RADIO)); path.quadTo((float) ((startX + CIRCLEX) * 0.5), (float) ((startY + CIRCLEY) * 0.5), (float) (startX - Math.sin(angle) * DRAG_RADIO), (float) (startY + Math.cos(angle) * DRAG_RADIO)); path.lineTo((float) (startX + Math.sin(angle) * DRAG_RADIO), (float) (startY - Math.cos(angle) * DRAG_RADIO)); path.quadTo((float) ((startX + CIRCLEX) * 0.5), (float) ((startY + CIRCLEY) * 0.5), (float) (CIRCLEX + Math.sin(angle) * ORIGIN_RADIO), (float) (CIRCLEY - Math.cos(angle) * ORIGIN_RADIO)); path.close(); canvas.drawPath(path, paint); }
這里的代碼就是把圖片上相關的數(shù)學公式Java化而已!
到這里,其實主要的工作就完成的差不多了!
接下來,設置paint 為填充的效果,最后再畫兩個圓
paint.setStyle(Paint.Style.FILL) canvas.drawCircle(CIRCLEX, CIRCLEY, ORIGIN_RADIO, paint);//默認的 canvas.drawCircle(startX == 0 ? CIRCLEX : startX, startY == 0 ? CIRCLEY : startY, DRAG_RADIO, paint);//拖拽的
就可以繪制出想要的效果了!
這里不得不再說說onTouch的處理!
case MotionEvent.ACTION_DOWN://有事件先攔截再說??! getParent().requestDisallowInterceptTouchEvent(true); CurrentState = STATE_IDLE; animSetXY.cancel(); startX = (int) ev.getX(); startY = (int) ev.getRawY(); break;
處理一下事件分發(fā)的坑!
測量和布局
這樣基本過得去了,但是我們的布局什么的還沒有處理,math_parent是萬萬沒法使用到具體項目當中去的!
測量的時候,如果發(fā)現(xiàn)不是精準模式,那么都手動去計算出需要的寬度和高度。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int modeWidth = MeasureSpec.getMode(widthMeasureSpec); int modeHeight = MeasureSpec.getMode(heightMeasureSpec); if (modeWidth == MeasureSpec.UNSPECIFIED || modeWidth == MeasureSpec.AT_MOST) { widthMeasureSpec = MeasureSpec.makeMeasureSpec(DEFAULT_RADIO * 2, MeasureSpec.EXACTLY); } if (modeHeight == MeasureSpec.UNSPECIFIED || modeHeight == MeasureSpec.AT_MOST) { heightMeasureSpec = MeasureSpec.makeMeasureSpec(DEFAULT_RADIO * 2, MeasureSpec.EXACTLY); } super.onMeasure(widthMeasureSpec, heightMeasureSpec); }
然后在布局變化時,獲取相關坐標,確定初始圓心坐標:
@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); CIRCLEX = (int) ((w) * 0.5 + 0.5); CIRCLEY = (int) ((h) * 0.5 + 0.5); }
然后清單文件里面就可以這樣配置了:
<com.lovejjfg.circle.DragBubbleView android:id="@+id/dbv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center"/>
這樣之后,又會出現(xiàn)一個問題,那就是wrap_content 之后,這個View能繪制的區(qū)域只有自身那么大了,拖拽了都看不見了!這個坑怎么辦呢,其實很簡單,父布局加上android:clipChildren="false" 的屬性!
這個坑也算是解決了!!
相關狀態(tài)的確定
我們是不希望它可以無限的拖拽的,就是有一個拖拽的最遠距離,還有就是放手后的返回,爆裂。那么對應的,這里需要確定幾種狀態(tài):
private final static int STATE_IDLE = 1;//靜止的狀態(tài) private final static int STATE_DRAG_NORMAL = 2;//正在拖拽的狀態(tài) private final static int STATE_DRAG_BREAK = 3;//斷裂后的拖拽狀態(tài) private final static int STATE_UP_BREAK = 4;//放手后的爆裂的狀態(tài) private final static int STATE_UP_BACK = 5;//放手后的沒有斷裂的返回的狀態(tài) private final static int STATE_UP_DRAG_BREAK_BACK = 6;//拖拽斷裂又返回的狀態(tài) private int CurrentState = STATE_IDLE; private int MIN_RADIO = (int) (ORIGIN_RADIO * 0.4);//最小半徑 private int MAXDISTANCE = (int) (MIN_RADIO * 13);//最遠的拖拽距離
確定好這些之后,在move的時候,就要去做相關判斷了:
case MotionEvent.ACTION_MOVE://移動的時候 startX = (int) ev.getX(); startY = (int) ev.getY(); updatePath(); invalidate(); break; private void updatePath() { int dy = Math.abs(CIRCLEY - startY); int dx = Math.abs(CIRCLEX - startX); double dis = Math.sqrt(dy * dy + dx * dx); if (dis <= MAXDISTANCE) {//增加的情況,原始半徑減小 if (CurrentState == STATE_DRAG_BREAK || CurrentState == STATE_UP_DRAG_BREAK_BACK) { CurrentState = STATE_UP_DRAG_BREAK_BACK; } else { CurrentState = STATE_DRAG_NORMAL; } ORIGIN_RADIO = (int) (DEFAULT_RADIO - (dis / MAXDISTANCE) * (DEFAULT_RADIO - MIN_RADIO)); Log.e(TAG, "distance: " + (int) ((1 - dis / MAXDISTANCE) * MIN_RADIO)); Log.i(TAG, "distance: " + ORIGIN_RADIO); } else { CurrentState = STATE_DRAG_BREAK; } // distance = dis; flag = (startY - CIRCLEY) * (startX - CIRCLEX) <= 0; Log.i("TAG", "updatePath: " + flag); angle = Math.atan(dy * 1.0 / dx); }
updatePath() 的方法之前已經看過部分了,這次的就是完整的。
這里做的事就是根據(jù)拖拽的距離更改相關的狀態(tài),并根據(jù)百分比來修改原始圓形的半徑大小。還有就是之前介紹的確定相關的弧度!
最后放手的時候:
case MotionEvent.ACTION_UP: if (CurrentState == STATE_DRAG_NORMAL) { CurrentState = STATE_UP_BACK; valueX.setIntValues(startX, CIRCLEX); valueY.setIntValues(startY, CIRCLEY); animSetXY.start(); } else if (CurrentState == STATE_DRAG_BREAK) { CurrentState = STATE_UP_BREAK; invalidate(); } else { CurrentState = STATE_UP_DRAG_BREAK_BACK; valueX.setIntValues(startX, CIRCLEX); valueY.setIntValues(startY, CIRCLEY); animSetXY.start(); } break;
自動返回這里使用到的 ValueAnimator,
animSetXY = new AnimatorSet(); valueX = ValueAnimator.ofInt(startX, CIRCLEX); valueY = ValueAnimator.ofInt(startY, CIRCLEY); animSetXY.playTogether(valueX, valueY); valueX.setDuration(500); valueY.setDuration(500); valueX.setInterpolator(new OvershootInterpolator()); valueY.setInterpolator(new OvershootInterpolator()); valueX.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { startX = (int) animation.getAnimatedValue(); Log.e(TAG, "onAnimationUpdate-startX: " + startX); invalidate(); } }); valueY.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { startY = (int) animation.getAnimatedValue(); Log.e(TAG, "onAnimationUpdate-startY: " + startY); invalidate(); } });
最后在看看完整的onDraw方法吧!
@Override protected void onDraw(Canvas canvas) { switch (CurrentState) { case STATE_IDLE://空閑狀態(tài),就畫默認的圓 if (showCircle) { canvas.drawCircle(CIRCLEX, CIRCLEY, ORIGIN_RADIO, paint);//默認的 } break; case STATE_UP_BACK://執(zhí)行返回的動畫 case STATE_DRAG_NORMAL://拖拽狀態(tài) 畫貝塞爾曲線和兩個圓 path.reset(); if (flag) { //第一個點 path.moveTo((float) (CIRCLEX - Math.sin(angle) * ORIGIN_RADIO), (float) (CIRCLEY - Math.cos(angle) * ORIGIN_RADIO)); path.quadTo((float) ((startX + CIRCLEX) * 0.5), (float) ((startY + CIRCLEY) * 0.5), (float) (startX - Math.sin(angle) * DRAG_RADIO), (float) (startY - Math.cos(angle) * DRAG_RADIO)); path.lineTo((float) (startX + Math.sin(angle) * DRAG_RADIO), (float) (startY + Math.cos(angle) * DRAG_RADIO)); path.quadTo((float) ((startX + CIRCLEX) * 0.5), (float) ((startY + CIRCLEY) * 0.5), (float) (CIRCLEX + Math.sin(angle) * ORIGIN_RADIO), (float) (CIRCLEY + Math.cos(angle) * ORIGIN_RADIO)); path.close(); canvas.drawPath(path, paint); } else { //第一個點 path.moveTo((float) (CIRCLEX - Math.sin(angle) * ORIGIN_RADIO), (float) (CIRCLEY + Math.cos(angle) * ORIGIN_RADIO)); path.quadTo((float) ((startX + CIRCLEX) * 0.5), (float) ((startY + CIRCLEY) * 0.5), (float) (startX - Math.sin(angle) * DRAG_RADIO), (float) (startY + Math.cos(angle) * DRAG_RADIO)); path.lineTo((float) (startX + Math.sin(angle) * DRAG_RADIO), (float) (startY - Math.cos(angle) * DRAG_RADIO)); path.quadTo((float) ((startX + CIRCLEX) * 0.5), (float) ((startY + CIRCLEY) * 0.5), (float) (CIRCLEX + Math.sin(angle) * ORIGIN_RADIO), (float) (CIRCLEY - Math.cos(angle) * ORIGIN_RADIO)); path.close(); canvas.drawPath(path, paint); } if (showCircle) { canvas.drawCircle(CIRCLEX, CIRCLEY, ORIGIN_RADIO, paint);//默認的 canvas.drawCircle(startX == 0 ? CIRCLEX : startX, startY == 0 ? CIRCLEY : startY, DRAG_RADIO, paint);//拖拽的 } break; case STATE_DRAG_BREAK://拖拽到了上限,畫拖拽的圓: case STATE_UP_DRAG_BREAK_BACK: if (showCircle) { canvas.drawCircle(startX == 0 ? CIRCLEX : startX, startY == 0 ? CIRCLEY : startY, DRAG_RADIO, paint);//拖拽的 } break; case STATE_UP_BREAK://畫出爆裂的效果 canvas.drawCircle(startX - 25, startY - 25, 10, circlePaint); canvas.drawCircle(startX + 25, startY + 25, 10, circlePaint); canvas.drawCircle(startX, startY - 25, 10, circlePaint); canvas.drawCircle(startX, startY, 18, circlePaint); canvas.drawCircle(startX - 25, startY, 10, circlePaint); break; } }
到這里,成品就出來了!!
總結:
1、確定默認圓形的坐標;
2、根據(jù)move的情況,實時獲取最新的坐標,根據(jù)移動的距離(確定出角度),更新相關的狀態(tài),畫出相關的Path路徑。超出上限,不再畫Path路徑。
3、松手時,根據(jù)相關的狀態(tài),要么帶Path路徑執(zhí)行動畫返回,要么不帶Path路徑直接返回,要么直接爆裂!
以上就是用Android Path 繪制貝塞爾曲線的示例,后續(xù)繼續(xù)補充相關文章,謝謝大家對本站的支持!
相關文章
Android使用phonegap從相冊里面獲取照片(代碼分享)
本文主要介紹了使用phonegap從相冊里面獲取照片的實現(xiàn)方法代碼。具有很好的參考價值,下面跟著小編一起來看下吧2017-03-03詳解Android中OkHttp3的例子和在子線程更新UI線程的方法
本篇文章主要介紹了詳解Android中OkHttp3的例子和在子線程更新UI線程的方法 ,非常具有實用價值,需要的朋友可以參考下2017-05-05android使用PullToRefresh實現(xiàn)下拉刷新和上拉加載
本篇文章主要介紹了android使用PullToRefresh實現(xiàn)下拉刷新和上拉加載,具有一定的參考價值,有興趣的可以了解一下。2016-12-12Android ListView滾動到底后自動加載數(shù)據(jù)
這篇文章主要為大家詳細介紹了Android之ListView滾動到底后自動加載數(shù)據(jù),文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-09-09android實現(xiàn)多線程下載文件(支持暫停、取消、斷點續(xù)傳)
本篇文章主要介紹了androids實現(xiàn)多線程下載文件,主要包括暫停、取消、斷點續(xù)傳等功能,具有一定的參考價值,感興趣的小伙伴們可以參考一下。2017-02-02Android如何給Textview添加菜單項詳解(Java)
TextView是android里面用的最多的控件,TextView類似一般UI中的Label,TextBlock等控件,只是為了單純的顯示一行或多行文本,下面這篇文章主要給大家介紹了關于Android如何給Textview添加菜單項的相關資料,需要的朋友可以參考下2022-01-01