Android Path绘制贝塞尔曲线实现QQ拖拽泡泡

Android Path绘制贝塞尔曲线实现QQ拖拽泡泡,第1张

概述这两天学习了使用Path绘制贝塞尔曲线相关,然后自己动手做了一个类似QQ未读消息可拖拽的小气泡,效果图如下:

这两天学习了使用Path绘制贝塞尔曲线相关,然后自己动手做了一个类似QQ未读消息可拖拽的小气泡,效果图如下:

最终效果图@H_404_8@接下来一步一步的实现整个过程。

基本原理

其实就是使用Path绘制三点的二次方贝塞尔曲线来完成那个妖娆的曲线的。然后根据触摸点不断绘制对应的圆形,根据距离的改变改变原始固定圆形的半径大小。最后就是松手后返回或者爆裂的实现。

Path介绍:

顾名思义,就是一个路径的意思,Path里面有很多的方法,本次设计主要用到的相关方法有

moveto() 移动Path到一个指定的点 quadTo() 绘制二次贝塞尔曲线,接收两个点,第一个是控制弧度的点,第二个是终点。 lineto() 就是连线 close() 闭合Path路径, reset() 重置Path的相关设置

Path入门热身:@H_404_8@

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@H_404_8@

具体实现拆分:

其实整个过程就是绘制了两个贝塞尔二次曲线的的闭合Path路径,然后在上面添加两个圆形。

闭合的Path 路径实现从左上点画二次贝塞尔曲线到左下点,左下点连线到右下点,右下点二次贝塞尔曲线到右上点,最后闭合一下!!@H_404_8@

相关坐标的确定@H_404_8@

这是这次里面的难点之一,因为涉及到了数学里面的一个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;@H_404_8@ //增加一个flag,用于判断使用哪种坐标体系。@H_404_8@

最最重要的来了,绘制相关的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) (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 + 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) (CIRCLEY - Math.cos(angle) * ORIGIN_RAdio));  path.close();  canvas.drawPath(path,paint); }

这里的代码就是把图片上相关的数学公式Java化而已!

到这里,其实主要的工作就完成的差不多了!@H_404_8@

接下来,设置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是万万没法使用到具体项目当中去的!@H_404_8@测量的时候,如果发现不是精准模式,那么都手动去计算出需要的宽度和高度。

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

然后清单文件里面就可以这样配置了:@H_404_8@

<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" 的属性!@H_404_8@这个坑也算是解决了!!

相关状态的确定

我们是不希望它可以无限的拖拽的,就是有一个拖拽的最远距离,还有就是放手后的返回,爆裂。那么对应的,这里需要确定几种状态:

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() 的方法之前已经看过部分了,这次的就是完整的。@H_404_8@这里做的事就是根据拖拽的距离更改相关的状态,并根据百分比来修改原始圆形的半径大小。还有就是之前介绍的确定相关的弧度!

最后放手的时候:

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,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,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 - 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) (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 + 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) (CIRCLEY - Math.cos(angle) * ORIGIN_RAdio));    path.close();    canvas.drawPath(path,paint);   }   if (showCircle) {    canvas.drawCircle(CIRCLEX,paint);//默认的    canvas.drawCircle(startX == 0 ? CIRCLEX : startX,paint);//拖拽的   }   break;  case STATE_DRAG_BREAK://拖拽到了上限,画拖拽的圆:  case STATE_UP_DRAG_BREAK_BACK:   if (showCircle) {    canvas.drawCircle(startX == 0 ? CIRCLEX : startX,paint);//拖拽的   }   break;  case STATE_UP_BREAK://画出爆裂的效果   canvas.drawCircle(startX - 25,startY - 25,10,circlePaint);   canvas.drawCircle(startX + 25,startY + 25,circlePaint);   canvas.drawCircle(startX,startY,18,circlePaint);   canvas.drawCircle(startX - 25,circlePaint);   break; }}

到这里,成品就出来了!!

总结:

1、确定默认圆形的坐标;@H_404_8@2、根据move的情况,实时获取最新的坐标,根据移动的距离(确定出角度),更新相关的状态,画出相关的Path路径。超出上限,不再画Path路径。@H_404_8@3、松手时,根据相关的状态,要么带Path路径执行动画返回,要么不带Path路径直接返回,要么直接爆裂!

以上就是用AndroID Path 绘制贝塞尔曲线的示例,后续继续补充相关文章,谢谢大家对本站的支持!

总结

以上是内存溢出为你收集整理的Android Path绘制贝塞尔曲线实现QQ拖拽泡泡全部内容,希望文章能够帮你解决Android Path绘制贝塞尔曲线实现QQ拖拽泡泡所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/1141194.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-31
下一篇 2022-05-31

发表评论

登录后才能评论

评论列表(0条)

保存