请选择 进入手机版 | 继续访问电脑版
设为首页收藏本站

houcx

 找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 2719|回复: 0

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

[复制链接]

222

主题

224

帖子

1123

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
1123
发表于 2018-2-22 08:55:54 | 显示全部楼层 |阅读模式

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

        


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

        基本原理

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

        path介绍:

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



  

    1. moveto() 移动path到一个指定的点


  

    2. quadto() 绘制二次贝塞尔曲线,接收两个点,第一个是控制弧度的点,第二个是终点。


  

    3. lineto() 就是连线


  

    4. close() 闭合path路径,


  

    5. reset() 重置path的相关设置


  

    6.

        path入门热身:

[Java] [color=rgb(51, 102, 153) !important]查看源文件 [color=rgb(51, 102, 153) !important]复制代码
[color=rgb(160, 160, 160) !important][backcolor=rgb(212, 208, 200) !important]
[color=#a0a0a0 !important]?

01

02

03

04

05

06

07

08

09

10

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旋转的过程中,角标体系是有两套的,如果就使用一套来画的话,就画出现在旋转的过程中曲线重叠在一起的情况!

        问题已经抛出来了,接下来直接看看代码实现!

        角度确定

        根据贴出来的原理图可以知道,我们可以使用起始圆心坐标和拖拽的圆心坐标,根据反正切函数来得到具体的弧度。

[Java] [color=rgb(51, 102, 153) !important]查看源文件 [color=rgb(51, 102, 153) !important]复制代码
[color=rgb(160, 160, 160) !important][backcolor=rgb(212, 208, 200) !important]
[color=#a0a0a0 !important]?

1

2

3

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路径!

        

[Java] [color=rgb(51, 102, 153) !important]查看源文件 [color=rgb(51, 102, 153) !important]复制代码
[color=rgb(160, 160, 160) !important][backcolor=rgb(212, 208, 200) !important]
[color=#a0a0a0 !important]?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

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 为填充的效果,最后再画两个圆

[Java] [color=rgb(51, 102, 153) !important]查看源文件 [color=rgb(51, 102, 153) !important]复制代码
[color=rgb(160, 160, 160) !important][backcolor=rgb(212, 208, 200) !important]
[color=#a0a0a0 !important]?

1

2

3

paint.setstyle(paint.style.fill)
canvas.drawcircle(circlex, circley, origin_radio, paint);//默认的
canvas.drawcircle(startx == 0




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

        这里不得不再说说ontouch的处理!

[Java] [color=rgb(51, 102, 153) !important]查看源文件 [color=rgb(51, 102, 153) !important]复制代码
[color=rgb(160, 160, 160) !important][backcolor=rgb(212, 208, 200) !important]
[color=#a0a0a0 !important]?

1

2

3

4

5

6

7

case motionevent.action_down://有事件先拦截再说!!
   getparent().requestdisallowintercepttouchevent(true);
   currentstate = state_idle;
   animsetxy.cancel();
   startx = (int) ev.getx();
   starty = (int) ev.getrawy();
   break;




        处理一下事件分发的坑!

        测量和布局

        这样基本过得去了,但是我们的布局什么的还没有处理,math_parent是万万没法使用到具体项目当中去的!
测量的时候,如果发现不是精准模式,那么都手动去计算出需要的宽度和高度。

[Java] [color=rgb(51, 102, 153) !important]查看源文件 [color=rgb(51, 102, 153) !important]复制代码
[color=rgb(160, 160, 160) !important][backcolor=rgb(212, 208, 200) !important]
[color=#a0a0a0 !important]?

01

02

03

04

05

06

07

08

09

10

11

12

13

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




        然后在布局变化时,获取相关坐标,确定初始圆心坐标:

[Java] [color=rgb(51, 102, 153) !important]查看源文件 [color=rgb(51, 102, 153) !important]复制代码
[color=rgb(160, 160, 160) !important][backcolor=rgb(212, 208, 200) !important]
[color=#a0a0a0 !important]?

1

2

3

4

5

6

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




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

[Java] [color=rgb(51, 102, 153) !important]查看源文件 [color=rgb(51, 102, 153) !important]复制代码
[color=rgb(160, 160, 160) !important][backcolor=rgb(212, 208, 200) !important]
[color=#a0a0a0 !important]?

1

2

3

4

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

        相关状态的确定

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

[Java] [color=rgb(51, 102, 153) !important]查看源文件 [color=rgb(51, 102, 153) !important]复制代码
[color=rgb(160, 160, 160) !important][backcolor=rgb(212, 208, 200) !important]
[color=#a0a0a0 !important]?

01

02

03

04

05

06

07

08

09

10

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的时候,就要去做相关判断了:

[Java] [color=rgb(51, 102, 153) !important]查看源文件 [color=rgb(51, 102, 153) !important]复制代码
[color=rgb(160, 160, 160) !important][backcolor=rgb(212, 208, 200) !important]
[color=#a0a0a0 !important]?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

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

        最后放手的时候:

[Java] [color=rgb(51, 102, 153) !important]查看源文件 [color=rgb(51, 102, 153) !important]复制代码
[color=rgb(160, 160, 160) !important][backcolor=rgb(212, 208, 200) !important]
[color=#a0a0a0 !important]?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

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,

[Java] [color=rgb(51, 102, 153) !important]查看源文件 [color=rgb(51, 102, 153) !important]复制代码
[color=rgb(160, 160, 160) !important][backcolor=rgb(212, 208, 200) !important]
[color=#a0a0a0 !important]?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

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

[Java] [color=rgb(51, 102, 153) !important]查看源文件 [color=rgb(51, 102, 153) !important]复制代码
[color=rgb(160, 160, 160) !important][backcolor=rgb(212, 208, 200) !important]
[color=#a0a0a0 !important]?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

@override
protected 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




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

        总结:

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


来自:http://www.apkbus.com/forum.php?mod=viewthread&tid=590450&highlight=贝塞尔


回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|houcx ( 京ICP备15004793号  

GMT+8, 2019-3-22 16:13 , Processed in 0.066661 second(s), 24 queries .

Powered by houcx! X3.2

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表