Android Path繪製貝茲路徑實現QQ拖拽泡泡_Android

來源:互聯網
上載者:User

這兩天學習了使用Path繪製貝茲路徑相關,然後自己動手做了一個類似QQ未讀訊息可拖拽的小氣泡,效果圖如下:

最終效果圖
接下來一步一步的實現整個過程。

基本原理

其實就是使用Path繪製三點的二次方貝茲路徑來完成那個妖嬈的曲線的。然後根據觸摸點不斷繪製對應的圓形,根據距離的改變改變原始固定圓形的半徑大小。最後就是鬆手後返回或者爆裂的實現。

Path介紹:

顧名思義,就是一個路徑的意思,Path裡面有很多的方法,本次設計主要用到的相關方法有

  1. moveTo() 移動Path到一個指定的點
  2. quadTo() 繪製二次方貝茲曲線,接收兩個點,第一個是控制弧度的點,第二個是終點。
  3. lineTo() 就是連線
  4. close() 閉合Path路徑,
  5. 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

具體實現拆分:

其實整個過程就是繪製了兩個貝塞爾二次曲線的的閉合Path路徑,然後在上面添加兩個圓形。

閉合的Path 路徑實現從左上點畫二次方貝茲曲線到左下點,左下點連線到右下點,右下點二次方貝茲曲線到右上點,最後閉合一下!!

相關座標的確定

這是這次裡面的痛點之一,因為涉及到了數學裡面的一個sin,cos,tan等等,我其實也忘完了,然後又腦補了一下,廢話不多說,

為什麼自己要親自去畫一下呢,因為畫了你才知道,在360旋轉的過程中,角標體系是有兩套的,如果就使用一套來畫的話,就畫出現在旋轉的過程中曲線重疊在一起的情況!

問題已經拋出來了,接下來直接看看代碼實現!

角度確定

根據貼出來的原理圖可以知道,我們可以使用起始圓心座標和拖拽的圓心座標,根據反正切函數來得到具體的弧度。

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); }

這裡的代碼就是把圖片上相關的數學公式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;

處理一下事件分發的坑!

測量和布局

這樣基本過得去了,但是我們的布局什麼的還沒有處理,math_parent是萬萬沒法使用到具體項目當中去的!
測量的時候,如果發現不是精準模式,那麼都手動去計算出需要的寬度和高度。

@Overrideprotected 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);}

然後在布局變化時,擷取相關座標,確定初始圓心座標:

@Overrideprotected 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"/>

這樣之後,又會出現一個問題,那就是wrap_content 之後,這個View能繪製的地區只有自身那麼大了,拖拽了都看不見了!這個坑怎麼辦呢,其實很簡單,父布局加上android:clipChildren="false" 的屬性!
這個坑也算是解決了!!

相關狀態的確定

我們是不希望它可以無限的拖拽的,就是有一個拖拽的最遠距離,還有就是放手後的返回,爆裂。那麼對應的,這裡需要確定幾種狀態:

private final static int STATE_IDLE = 1;//靜止的狀態 private final static int STATE_DRAG_NORMAL = 2;//正在拖拽的狀態 private final static int STATE_DRAG_BREAK = 3;//斷裂後的拖拽狀態 private final static int STATE_UP_BREAK = 4;//放手後的爆裂的狀態 private final static int STATE_UP_BACK = 5;//放手後的沒有斷裂的返回的狀態 private final static int STATE_UP_DRAG_BREAK_BACK = 6;//拖拽斷裂又返回的狀態 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() 的方法之前已經看過部分了,這次的就是完整的。
這裡做的事就是根據拖拽的距離更改相關的狀態,並根據百分比來修改原始圓形的半徑大小。還有就是之前介紹的確定相關的弧度!

最後放手的時候:

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方法吧!

@Overrideprotected void onDraw(Canvas canvas) { switch (CurrentState) {  case STATE_IDLE://空閑狀態,就畫預設的圓   if (showCircle) {    canvas.drawCircle(CIRCLEX, CIRCLEY, ORIGIN_RADIO, paint);//預設的   }   break;  case STATE_UP_BACK://執行返回的動畫  case STATE_DRAG_NORMAL://拖拽狀態 畫貝茲路徑和兩個圓   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、根據move的情況,即時擷取最新的座標,根據移動的距離(確定出角度),更新相關的狀態,畫出相關的Path路徑。超出上限,不再畫Path路徑。
3、鬆手時,根據相關的狀態,要麼帶Path路徑執行動畫返回,要麼不帶Path路徑直接返回,要麼直接爆裂!

以上就是用Android Path 繪製貝茲路徑的樣本,後續繼續補充相關文章,謝謝大家對本站的支援!

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.