国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁(yè) > 系統(tǒng) > Android > 正文

Android Path繪制貝塞爾曲線實(shí)現(xiàn)QQ拖拽泡泡

2019-12-12 05:22:09
字體:
來(lái)源:轉(zhuǎn)載
供稿:網(wǎng)友

這兩天學(xué)習(xí)了使用Path繪制貝塞爾曲線相關(guān),然后自己動(dòng)手做了一個(gè)類似QQ未讀消息可拖拽的小氣泡,效果圖如下:

最終效果圖
接下來(lái)一步一步的實(shí)現(xiàn)整個(gè)過(guò)程。

基本原理

其實(shí)就是使用Path繪制三點(diǎn)的二次方貝塞爾曲線來(lái)完成那個(gè)妖嬈的曲線的。然后根據(jù)觸摸點(diǎn)不斷繪制對(duì)應(yīng)的圓形,根據(jù)距離的改變改變?cè)脊潭▓A形的半徑大小。最后就是松手后返回或者爆裂的實(shí)現(xiàn)。

Path介紹:

顧名思義,就是一個(gè)路徑的意思,Path里面有很多的方法,本次設(shè)計(jì)主要用到的相關(guān)方法有

  1. moveTo() 移動(dòng)Path到一個(gè)指定的點(diǎn)
  2. quadTo() 繪制二次貝塞爾曲線,接收兩個(gè)點(diǎn),第一個(gè)是控制弧度的點(diǎn),第二個(gè)是終點(diǎn)。
  3. lineTo() 就是連線
  4. close() 閉合Path路徑,
  5. reset() 重置Path的相關(guān)設(shè)置

Path入門熱身:

path.reset(); path.moveTo(200, 200); //第一個(gè)坐標(biāo)是對(duì)應(yīng)的控制的坐標(biāo),第二個(gè)坐標(biāo)是終點(diǎn)坐標(biāo) path.quadTo(400, 250, 600, 200); canvas.drawPath(path, paint); canvas.translate(0, 200); //調(diào)用close,就會(huì)首尾閉合連接 path.close(); canvas.drawPath(path, paint);

記得不要在onDraw方法中new Path或者 Paint喲!

Path

具體實(shí)現(xiàn)拆分:

其實(shí)整個(gè)過(guò)程就是繪制了兩個(gè)貝塞爾二次曲線的的閉合Path路徑,然后在上面添加兩個(gè)圓形。

閉合的Path 路徑實(shí)現(xiàn)從左上點(diǎn)畫(huà)二次貝塞爾曲線到左下點(diǎn),左下點(diǎn)連線到右下點(diǎn),右下點(diǎn)二次貝塞爾曲線到右上點(diǎn),最后閉合一下!!

相關(guān)坐標(biāo)的確定

這是這次里面的難點(diǎn)之一,因?yàn)樯婕暗搅藬?shù)學(xué)里面的一個(gè)sin,cos,tan等等,我其實(shí)也忘完了,然后又腦補(bǔ)了一下,廢話不多說(shuō),

為什么自己要親自去畫(huà)一下呢,因?yàn)楫?huà)了你才知道,在360旋轉(zhuǎn)的過(guò)程中,角標(biāo)體系是有兩套的,如果就使用一套來(lái)畫(huà)的話,就畫(huà)出現(xiàn)在旋轉(zhuǎn)的過(guò)程中曲線重疊在一起的情況!

問(wèn)題已經(jīng)拋出來(lái)了,接下來(lái)直接看看代碼實(shí)現(xiàn)!

角度確定

根據(jù)貼出來(lái)的原理圖可以知道,我們可以使用起始圓心坐標(biāo)和拖拽的圓心坐標(biāo),根據(jù)反正切函數(shù)來(lái)得到具體的弧度。

int dy = Math.abs(CIRCLEY - startY);int dx = Math.abs(CIRCLEX - startX); angle = Math.atan(dy * 1.0 / dx);

ok,這里的startX,Y就是移動(dòng)過(guò)程中的坐標(biāo)。angle就是得到的對(duì)應(yīng)的弧度(角度)。

相關(guān)Path繪制

前面已經(jīng)提到在旋轉(zhuǎn)的過(guò)程中有兩套坐標(biāo)體系,一開(kāi)始我也很糾結(jié)這個(gè)坐標(biāo)體系要怎么確定,后面又恍然大悟,其實(shí)相當(dāng)于就是一三象限正比例增長(zhǎng),二四象限,反比例增長(zhǎng)。

flag = (startY - CIRCLEY  ) * (startX- CIRCLEX ) <= 0;
 //增加一個(gè)flag,用于判斷使用哪種坐標(biāo)體系。

最最重要的來(lái)了,繪制相關(guān)的Path路徑!

 

path.reset(); if (flag) {  //第一個(gè)點(diǎn) 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 {  //第一個(gè)點(diǎn)  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); }

這里的代碼就是把圖片上相關(guān)的數(shù)學(xué)公式Java化而已!

到這里,其實(shí)主要的工作就完成的差不多了!

接下來(lái),設(shè)置paint 為填充的效果,最后再畫(huà)兩個(gè)圓

paint.setStyle(Paint.Style.FILL) canvas.drawCircle(CIRCLEX, CIRCLEY, ORIGIN_RADIO, paint);//默認(rèn)的 canvas.drawCircle(startX == 0 ? CIRCLEX : startX, startY == 0 ? CIRCLEY : startY, DRAG_RADIO, paint);//拖拽的

就可以繪制出想要的效果了!

這里不得不再說(shuō)說(shuō)onTouch的處理!

case MotionEvent.ACTION_DOWN://有事件先攔截再說(shuō)!!   getParent().requestDisallowInterceptTouchEvent(true);   CurrentState = STATE_IDLE;   animSetXY.cancel();   startX = (int) ev.getX();   startY = (int) ev.getRawY();   break;

處理一下事件分發(fā)的坑!

測(cè)量和布局

這樣基本過(guò)得去了,但是我們的布局什么的還沒(méi)有處理,math_parent是萬(wàn)萬(wàn)沒(méi)法使用到具體項(xiàng)目當(dāng)中去的!
測(cè)量的時(shí)候,如果發(fā)現(xiàn)不是精準(zhǔn)模式,那么都手動(dòng)去計(jì)算出需要的寬度和高度。

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

然后在布局變化時(shí),獲取相關(guān)坐標(biāo),確定初始圓心坐標(biāo):

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

這樣之后,又會(huì)出現(xiàn)一個(gè)問(wèn)題,那就是wrap_content 之后,這個(gè)View能繪制的區(qū)域只有自身那么大了,拖拽了都看不見(jiàn)了!這個(gè)坑怎么辦呢,其實(shí)很簡(jiǎn)單,父布局加上android:clipChildren="false" 的屬性!
這個(gè)坑也算是解決了?。?/p>

相關(guān)狀態(tài)的確定

我們是不希望它可以無(wú)限的拖拽的,就是有一個(gè)拖拽的最遠(yuǎn)距離,還有就是放手后的返回,爆裂。那么對(duì)應(yīng)的,這里需要確定幾種狀態(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;//放手后的沒(méi)有斷裂的返回的狀態(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);//最遠(yuǎn)的拖拽距離

確定好這些之后,在move的時(shí)候,就要去做相關(guān)判斷了:

case MotionEvent.ACTION_MOVE://移動(dòng)的時(shí)候   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īng)看過(guò)部分了,這次的就是完整的。
這里做的事就是根據(jù)拖拽的距離更改相關(guān)的狀態(tài),并根據(jù)百分比來(lái)修改原始圓形的半徑大小。還有就是之前介紹的確定相關(guān)的弧度!

最后放手的時(shí)候:

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;

自動(dòng)返回這里使用到的 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://空閑狀態(tài),就畫(huà)默認(rèn)的圓   if (showCircle) {    canvas.drawCircle(CIRCLEX, CIRCLEY, ORIGIN_RADIO, paint);//默認(rèn)的   }   break;  case STATE_UP_BACK://執(zhí)行返回的動(dòng)畫(huà)  case STATE_DRAG_NORMAL://拖拽狀態(tài) 畫(huà)貝塞爾曲線和兩個(gè)圓   path.reset();   if (flag) {    //第一個(gè)點(diǎn)    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 {    //第一個(gè)點(diǎn)    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);//默認(rèn)的    canvas.drawCircle(startX == 0 ? CIRCLEX : startX, startY == 0 ? CIRCLEY : startY, DRAG_RADIO, paint);//拖拽的   }   break;  case STATE_DRAG_BREAK://拖拽到了上限,畫(huà)拖拽的圓:  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://畫(huà)出爆裂的效果   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; }}

到這里,成品就出來(lái)了?。?/p>

總結(jié):

1、確定默認(rèn)圓形的坐標(biāo);
2、根據(jù)move的情況,實(shí)時(shí)獲取最新的坐標(biāo),根據(jù)移動(dòng)的距離(確定出角度),更新相關(guān)的狀態(tài),畫(huà)出相關(guān)的Path路徑。超出上限,不再畫(huà)Path路徑。
3、松手時(shí),根據(jù)相關(guān)的狀態(tài),要么帶Path路徑執(zhí)行動(dòng)畫(huà)返回,要么不帶Path路徑直接返回,要么直接爆裂!

以上就是用Android Path 繪制貝塞爾曲線的示例,后續(xù)繼續(xù)補(bǔ)充相關(guān)文章,謝謝大家對(duì)本站的支持!

發(fā)表評(píng)論 共有條評(píng)論
用戶名: 密碼:
驗(yàn)證碼: 匿名發(fā)表
主站蜘蛛池模板: 新泰市| 江口县| 页游| 顺义区| 龙山县| 周口市| 体育| 镇宁| 唐山市| 连南| 东阳市| 阿坝县| 高雄市| 临朐县| 沙坪坝区| 大厂| 囊谦县| 云南省| 泸定县| 花垣县| 珲春市| 五大连池市| 建湖县| 杭锦后旗| 密山市| 云阳县| 乐东| 东台市| 开封市| 奇台县| 曲阜市| 汕头市| 墨江| 无极县| 屯门区| 金沙县| 如东县| 黑水县| 阿克陶县| 平和县| 工布江达县|