Android绘图篇(三)——绘制Path路径及贝塞尔曲线

开篇

不废话,直接开怼。上一篇聊到了文本的绘制:Android绘图篇——绘制文本,这篇来介绍一下canvas中另一个相对重要的api,Path(路径)和贝塞尔曲线相关的知识。

基本绘制

1. 绘制直线
既然要绘制直线,那肯定要有起点和终点了呀,Path中如何设置起点呢?很简单:

void moveTo(float x1,float y1)

好吧,来个小例子:

 Path path = new Path();
 path.moveTo(100,100);

moveTo方法就是来定义Path(路径)的起点,将Path的初始绘制位置定义在(100,100)这个点,如果想绘制直线,则可以调用lineTo(float x, float y)来确定直线的终点,如:

path.lineTo(100,100);

最后调用canvas的drawPath方法,将path绘制出来,完整的代码如下:

  //初始化画笔
  paint = new Paint();

 //这里一定要设置成STROKE,设置成FILL绘制不出来。
 paint.setStyle(Paint.Style.STROKE);
 paint.setStrokeWidth(5);
 paint.setColor(ContextCompat.getColor(getContext(), R.color.colorAccent));
 paint.setAntiAlias(true);


 //定义路径
 Path path = new Path();
 //设置起点
 path.moveTo(100,100);
 //移到200,200,形成一个路径。
 path.lineTo(200,200);
 //绘制这条路径
 canvas.drawPath(path,paint);

结果会绘制一条从(100,100)到(200,200)的直线。lineTo方法可以多次调用,每次到一个点,以此类推,如:

Path path = new Path();
path.moveTo(100,100);
path.lineTo(200,200);
path.lineTo(400,200);
path.lineTo(500,150);
canvas.drawPath(path,paint);

效果如下:


在这里插入图片描述

如果想要形成闭合的路径,则可以在绘制之前调用path.close()方法:

path.close()

效果:


在这里插入图片描述

好吧,绘制线段就这么简单。

2. 矩形路径

void addRect (float left, float top, float right, float bottom, Path.Direction dir)
void addRect (RectF rect, Path.Direction dir)

需要传入一个Rect(矩形)对象,关于这个对象,前面的博客已经说过了。在此不再赘述。还有一个重载的方法,传入矩形左上角的坐标和右下角的坐标。两个方法都需要传递一个Path.Direction参数,表示方向,Path.Direction有两个值,分别如下:

1Path.Direction.CCW:是counter-clockwise缩写,指逆时针方向。
2Path.Direction.CW:是clockwise的缩写,指顺时针方向。

举个栗子:

RectF rect = new RectF(100, 200, 400, 400);
//逆时针矩形
path.addRect(rect, Path.Direction.CCW);
canvas.drawPath(path, paint);

//顺时针矩形
RectF rect1 = new RectF(500, 200, 800, 400);
path.addRect(rect1, Path.Direction.CW);

canvas.drawPath(path,paint);

效果如下:


在这里插入图片描述

两个矩形看起来一样,的确,如果仅仅做展示用的话,两者没有任何的区别。但如果在路径上绘制文字的话,那最后的效果差别就大了:

 String text = "生如夏花之绚烂,死如秋叶之静美";

 Path CCWPath = new Path();
 RectF rect = new RectF(100, 200, 400, 400);
 CCWPath.addRect(rect, Path.Direction.CCW);

 canvas.drawPath(CCWPath, paint);
 canvas.drawTextOnPath(text, CCWPath, 0, 0, paint);


 Path CWPath = new Path();
 RectF rect1 = new RectF(500, 200, 800, 400);
 CWPath.addRect(rect1, Path.Direction.CW);

 canvas.drawPath(CWPath, paint);
 canvas.drawTextOnPath(text, CWPath, 0, 0, paint);

看下结果:


在这里插入图片描述

可以看到矩形是一样的,但是文字的方向却是按照指定的方向来绘制的,左边的文字从从左上角逆时针绘制,右边的文字从左上角顺时针绘制。这就是方向不同带来的一点点不同。

3. 圆角矩形路径

void addRoundRect (RectF rect, float[] radii, Path.Direction dir)
void addRoundRect (RectF rect, float rx, float ry, Path.Direction dir)
  • Rect对象和Path.Direction不说了,第一个方法第二个参数传入一个float数组,用来指定矩形每个角的圆角大小,这个数组的个数必须为8,大于8或小于8都会导致app崩溃。共分四组,分别对应每个角所使用的椭圆的横轴半径和纵轴半径,比如(x1,y1,x2,y2,x3,y3,x4,y4),其中,x1,y1 对应第一个角的(左上角)产生的椭圆的横轴半径和纵轴半径,其他类推。
  • 第二个方法只能指定统一圆角大小。
float rx:所产生圆角的椭圆的横轴半径;
float ry:所产生圆角的椭圆的纵轴半径;

好吧,看个例子好了:

 Path CCWPath = new Path();
 RectF rect = new RectF(100, 200, 400, 400);
 float[] radii = {30, 30, 50, 50, 70, 70, 80, 80};
 //四个角圆角大小不同的矩形。
 CCWPath.addRoundRect(rect, radii, Path.Direction.CCW);
 canvas.drawPath(CCWPath, paint);


 Path CWPath = new Path();
 RectF rect1 = new RectF(500, 200, 800, 400);
 //四个角圆角大小相同的矩形
 CWPath.addRoundRect(rect1, 30, 30, Path.Direction.CW);
 canvas.drawPath(CWPath, paint);

效果如下:


在这里插入图片描述

圆角矩形的绘制到此结束。

4. 圆形路径

void addCircle (float x, float y, float radius, Path.Direction dir)

x和y是圆心的坐标点,radius是半径,没什么好说的,举个例子:

Path path = new Path();
path.addCircle(200,200,100, Path.Direction.CCW);
canvas.drawPath(path, paint);

结果当然是个大大的鹅蛋:


在这里插入图片描述

5. 椭圆路径

void addOval (RectF oval, Path.Direction dir)
  • oval :椭圆的外切矩形,前面文章有提到,一个矩形有且只有一个内切椭圆。
  • dir:逆时针还是顺时针。

举例:

Path path = new Path();
RectF rect = new RectF(100,100,400,300);
path.addOval(rect, Path.Direction.CCW);

结果:


在这里插入图片描述

注意:如果矩形是正方形,则绘制出来的就是圆。

6. 圆弧路径

void addArc (RectF oval, float startAngle, float sweepAngle)
  • 弧是椭圆的一部分,这个参数就是生成椭圆所对应的矩形。
  • float startAngle:开始的角度,X轴正方向为0度。
  • float sweepAngel:持续的度数(方向是顺时针)。
Path path = new Path();
RectF rect = new RectF(100,100,400,300);
path.addArc(rect, 0,90);
canvas.drawPath(path, paint);

如下:


在这里插入图片描述

注:以上凡是需要传入Rect对象的方法,都可以将Rect拆分成左上角和右下角的坐标点传入到方法中,以绘制圆弧举例:

RectF rect = new RectF(100,100,400,300);
path.addArc(rect, 0,90);

可以写成如下形式:

path.addArc(100,100,400,300, 0,90);

贝塞尔曲线

关于贝塞尔曲线,先看下它的简介:


在这里插入图片描述

贝塞尔曲线是计算机图形学中相当重要的参数曲线,至于为什么需要贝塞尔曲线,我们可以看下百科中的介绍:


在这里插入图片描述

大体意思就是人手工很难绘制出平滑的曲线,而贝塞尔曲线就是一种能在计算机上绘制平滑曲线的一种工具。在计算机图形学和各种绘图软件中有广泛的应用,如PS中的钢笔工具,其底层就是采用贝塞尔曲线,通过不同的参数来绘制不同的曲线的,而在移动端,则可以通过贝塞尔曲线去制作各种各样炫酷的动画,例如水波浪效果、弹性的下拉刷新、购物车动画等等,提高人机交互的顺滑性,使得人机交互看起来更加富有弹性。

1. 一阶贝塞尔
贝塞尔曲线都有特定的公式,至于贝塞尔曲线公式的推导,由于我数学不行,在此就不展示公式的推导过程了,感兴趣的可以看这篇博客:贝塞尔曲线公式推导。一阶贝塞尔曲线的公式如下:
在这里插入图片描述

其对应的动画效果如下:


在这里插入图片描述

P0为起点、P1为终点,t表示当前时间,B(t)表示公式的结果值。
注意,曲线的意义就是公式结果B(t)随时间的变化,其取值所形成的轨迹。在动画中,黑色点表示在当前时间t下公式B(t)的取值。而红色的那条线就不在各个时间点下不同取值的B(t)所形成的轨迹。
总而言之:对于一阶贝赛尔曲线,大家可以理解为在起始点和终点形成的这条直线上,匀速移动的点。

好吧,实在不懂,咱们画个图解释下:


在这里插入图片描述

假设这是一个Android屏幕,有一条直线,起点和终点坐标如图所示。假设此时t=0.4,我们将p0和p1的横坐标带入公式发现:

B(t) = 0.6*100+0.4*200=140

带入横坐标时,B(t)就可以理解成A点的横坐标,是完全符合这个公式的,纵坐标同理。在直线上的任意点,其横坐标和纵坐标都满足这个公式。好吧,反正一阶贝塞尔就是一条直线,没什么好说的。

2. 二阶贝塞尔
先看下动画效果:


在这里插入图片描述

这里不去带入公式了,有点复杂,我们采用另一种方法去看二阶贝塞尔是如何形成的,首先看下面的图:


在这里插入图片描述

这是我在二阶贝塞尔曲线动画截取的某一帧,标注了三个点。A、B、S、和原有的P0、P1、P2。A在P0和P1之间匀速运动,B在P1和P2之间匀速运动。各点之间始终满足如下关系。
P0A:P0P1 = P1B:P1P2
连接A和B,在AB上寻找点S满足如下条件:
AS:AB = P0A:P0P1 = P1B:P1P2

当然这只是一个点,在A从P0运动到P1点时,A点位置会不断变化,同时在P1P2上也会有相应的B点和其对应,每次连接AB,在AB上找出符合条件的S点,这些S点形成的路径就是形成的贝塞尔曲线。然后我们以此类推三阶贝塞尔曲线。

3. 三阶贝塞尔


在这里插入图片描述

同样截取其中的一帧来分析:


在这里插入图片描述

由于多了一个点P3,所以会有两条绿色的线,图中绿色的线数量是根据决定贝塞尔曲线的线段的数量来决定的,是线段的数量减去1,比如这里就有两条,二阶贝塞尔只有一条。那三阶贝塞尔怎么去看呢?其实你把两条绿线看成是决定二阶贝塞尔的两条线段,类似于二阶中的P0P1、P1P2那么:
MP:MT = TQ:TN
连接PQ,寻找满足PS:PQ = MP:MT = TQ:TN条件的S点,而满足该条件的众多S点形成的路径就是该三阶贝塞尔曲线。这里有点类似于魔方的降阶公式,把更高的阶层慢慢降成低阶,使得理解起来更容易接受。

4. 四阶贝塞尔


在这里插入图片描述

这里就不去分析了,感兴趣的可以自己去分析下,还是满足刚才的规律的。

5. 五阶贝塞尔


在这里插入图片描述

不得不感叹数学之美,看,这曲线多么优美呀!!

5. Android中的贝塞尔曲线
Android中封装了贝塞尔曲线相关的api,目前只封装了二阶和三阶的api,已经够我们使用了:

//二阶贝赛尔  
public void quadTo(float x1, float y1, float x2, float y2)  
public void rQuadTo(float dx1, float dy1, float dx2, float dy2)  
//三阶贝赛尔  
public void cubicTo(float x1, float y1, float x2, float y2,float x3, float y3)  
public void rCubicTo(float x1, float y1, float x2, float y2,float x3, float y3) 

先看二阶的,传入的x2,y2是终点的坐标,如二阶图中的P2。x1,y1是控制点的坐标,如二阶图中的P1,还有一个起始点该如何指定呢?还记得我们前面说的用moveTo()方法去指定Path的起点么?这里也是一样,贝塞尔曲线的起点都是通过moveTo()方法来指定的,api中最后一个是终点坐标,其他的都是控制点的坐标,比如三阶,(x1,y1) 、(x2,y2)分别对应三阶图中的P1和P2。

二阶的随便玩一下呗:

  protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //设置画笔
        Paint paint = new Paint();
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(5);
        paint.setColor(ContextCompat.getColor(getContext(), R.color.colorAccent));
        paint.setTextSize(50);
        
        //一定要设置抗锯齿哦,不然画出来的不平滑,会有锯齿。
        paint.setAntiAlias(true);

        Path path = new Path();
        //设置起点
        path.moveTo(200,250);

        //二阶贝塞尔。
        path.quadTo(300,120,500,300);

        canvas.drawPath(path,paint);
    }

来,看下效果:


在这里插入图片描述

啧啧,可真好看~我们需要知道如下两点:

  • 整条线的起始点是通过Path.moveTo(x,y)来指定的,如果初始没有调用Path.moveTo(x,y)来指定起始点,则默认以控件左上角(0,0)为起始点。
  • quadTo可以连续调用, 而如果我们连续调用quadTo(),前一个quadTo()的终点,就是下一个quadTo()函数的起点。

好吧,来看下多次调用quadTo的例子,刚我们绘制的是不规则的曲线,如果我们想绘制一种规则的曲线,就像下面这样,该如何绘制呢?


在这里插入图片描述

OK,起点和终点都容易确定,主要是控制点的左边难以确定,只要确定了控制点的坐标,就能绘制出相应的贝塞尔曲线。好吧,先看下面的图:


在这里插入图片描述

这是一条安卓坐标系,向下是正方向。图中的曲线和我们用代码绘制出来的类似。我们想要绘制这样一条曲线,首先可以将这条曲线分成两段,第一段是AC,第二段是CE。
AC段可以用贝塞尔曲线去绘制,起点是A,终点是C,控制点是B,那么可以用如下代码去绘制:

  Path path = new Path();
  //设置起点
  path.moveTo(0,200);
  path.quadTo(100,100,200,200);

CE段同理,由于连续调用quadTo(),前一个quadTo()的终点,就是下一个quadTo()函数的起点。所以CE段的绘制代码如下:

 path.quadTo(300,300,400,200);

所以所有的代码如下:

path.moveTo(0,200);
path.quadTo(100,100,200,200);
path.quadTo(300,300,400,200);
canvas.drawPath(path,paint);

运行后的效果:


在这里插入图片描述

其实规则的曲线寻找控制点还是相对简单的,其遵循如下规则:

  • 控制点往往和曲线的起始方向相同,如上图中的AC段 中的控制点B,曲线起始方向是向上的,因此控制点在A点的上方,又因为形成的AC段贝塞尔曲线是对称的,所以AB的距离应该等于BC的距离。
  • 确定好横坐标后,纵坐标则是控制曲线的坡度,如上图中的AC段,控制点B离A的距离越大,曲线的弧度就越大,反之亦然。
  • 控制点往往控制着波峰的位置,假如修改上面代码:
 Path path = new Path();
 path.moveTo(10,200);
 //修改AC段的控制点B的位置,使其靠近A端
 path.quadTo(40,100,200,200);
 
 path.quadTo(300,300,400,200);

则效果如下:


在这里插入图片描述

可以看到AC端的波峰明显靠近A点。

规则的如此,其实不规则的曲线也可以按照这个规则来寻找控制点,唯一不同的是在不规则曲线的情况下,可能难以一次性精确定位控制点的位置,但按照这个规则,先找方向,然后根据曲线的特性慢慢尝试,很快也能够找到控制点。

好了,说完了贝塞尔曲线的绘制,我们来讲讲贝塞尔曲线在Android中的应用,

贝塞尔曲线在Android中的应用

用贝塞尔曲线来绘制手指轨迹

假设现在有一个需求,当用户手指在屏幕上移动时,要能够绘制出用户手指运动的轨迹。让我们来分析下这个需求:
1、用户手指的运动轨迹是一条路径,我们很自然的想到了Path。
2、用户手指按下屏幕的瞬间,记录下该点的坐标,作为Path的起点。
3、用户每次移动手指的时候,都将path移动到移动后的位置,可以调用path.linnTo()方法来跟踪路径,并重绘ui。

好吧,还是很简单的,全部代码如下:

public class DemoView extends View {
    private Paint mPaint;
    private Path mPath;

    public DemoView(Context context) {
        super(context);
        init();
    }

    public DemoView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public DemoView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        //初始化画笔
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(5);
        mPaint.setColor(ContextCompat.getColor(getContext(), R.color.colorAccent));
        mPaint.setTextSize(50);
        mPaint.setAntiAlias(true);

        //初始化Path。
        mPath = new Path();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        //绘制路径
        canvas.drawPath(mPath, mPaint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //按下时设置Path起点
                mPath.moveTo(event.getX(), event.getY());
                break;
            case MotionEvent.ACTION_MOVE:
                //跟踪用户手指轨迹
                mPath.lineTo(event.getX(), event.getY());

                //重绘,会调用onDraw()方法。
                postInvalidate();
                break;
        }
        return true;
    }
}

好吧,看下最终的效果:


在这里插入图片描述

好吧,请叫我灵魂画师-。-

这种方式很简单,但是有一定的缺陷,我们先将图片放大


在这里插入图片描述

这是放大后其中的一部分,可以看到绘制出来的轨迹表面不是那么圆润和平滑,特别是在有转折的时候,尽管我们设置了抗锯齿。原因在于其实这个轨迹是一条条线段连接起来的,而线段的连接一定会产生一个角度,这个角度就是曲线看起来不平滑的原因。其实运用刚学习的贝塞尔曲线的知识,我们同样能实现相同的效果,而且绘制出来的曲线将更加圆润和平滑。

运用贝塞尔曲线实现此效果:


在这里插入图片描述

假设我们绘制了这样一个路径,不难发现,这个路径是由AB和BC两条线段连接起来的,会形成一个直角,导致绘制出来的曲线不平滑,而想要实现三点之间的平滑过渡,即如下的效果:


在这里插入图片描述

就只能将这两个线段的中间点P1和P2做为起始点和结束点,而将手指的倒数第二个触点B做为控制点。有人可能会说这样在结束的时候,A到P1和P2到C的这段距离岂不是没画进去?是的,如果Path最终没有close的话,这两段距离是被抛弃掉的。但是问题不大,因为手指间滑动时,每两个点间的距离很小,所以A到P1到和P2到C之间的距离可以忽略不计,搞清楚这个,我们就去分析一下控制点的位置和起点终点的位置:
1、起点是前一条线段的中点,终点是后一段线段的中点。
2、控制点是上一个手指触摸的点。

这里解释起来可能有点麻烦,于是在网上找了一个动态图片,出自简书的一篇博客,路径Path绘制以及贝塞尔曲线使用技巧,这个大神分析的挺透彻的:


在这里插入图片描述

这是多条线段连成的一段路径,可以看到每个线段之间都有夹角,即转折,如果用lineTo方法绘制出来并放大则会有明显的折痕。这里在ps中运用钢笔工具,可以将这一段路径以贝塞尔曲线的方式绘制出来。可以看到动图中取前一条线段和后一条线段的中点作为贝塞尔曲线的起点和终点,取上一个手指触摸的点作为控制点,然后绘制出贝塞尔曲线,仅仅会抛弃掉开头的半段和结尾的半段,这一点距离几乎可以忽略不记。于是我们看下代码如何实现:

private void init() {
       //初始化画笔
        paint = new Paint();
        //这里一定要设置成STROKE,设置成FILL绘制不出来。
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(5);
        paint.setColor(ContextCompat.getColor(getContext(), R.color.colorAccent));
        paint.setAntiAlias(true);
        paint.setTextSize(50);
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                eventX = event.getX();
                eventY = event.getY();
                path.moveTo(eventX, eventY);
                break;
            case MotionEvent.ACTION_MOVE:
                float endX = (event.getX() - eventX) / 2 + eventX;
                float endY = (event.getY() - eventY) / 2 + eventY;
                path.quadTo(eventX, eventY, endX, endY);
                eventX = event.getX();
                eventY = event.getY();
                break;

        }
        postInvalidate();
        return true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawPath(path, paint);
    }

好吧,看下效果:


在这里插入图片描述

好像没啥区别啊,放大看:


在这里插入图片描述

是不是圆滑了好多了呢?

总结:关于贝塞尔曲线相关的东西先介绍到这里,当然贝塞尔曲线还有许多其它的应用,如添加购物车的效果,以后有时间会把这个效果实现一下,加深下对贝塞尔曲线的理解。文中有不足或者错误的地方,还请各位批评指正,完~

  • 10
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
在 Qt 中,可以使用鼠标事件和 QPainter 绘图类来实现鼠标连续绘制贝塞尔曲线。下面是一个简单的实现示例: ```cpp #include <QtWidgets> class BezierWidget : public QWidget { public: BezierWidget(QWidget *parent = 0); protected: void mousePressEvent(QMouseEvent *event); void mouseMoveEvent(QMouseEvent *event); void paintEvent(QPaintEvent *event); private: QList<QPointF> m_points; }; BezierWidget::BezierWidget(QWidget *parent) : QWidget(parent) { setMouseTracking(true); } void BezierWidget::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { m_points.append(event->pos()); update(); } } void BezierWidget::mouseMoveEvent(QMouseEvent *event) { if (event->buttons() & Qt::LeftButton) { m_points.append(event->pos()); update(); } } void BezierWidget::paintEvent(QPaintEvent *event) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); if (m_points.size() < 2) { return; } QPainterPath path(m_points.first()); for (int i = 1; i < m_points.size() - 1; ++i) { QPointF p1 = m_points[i - 1]; QPointF p2 = m_points[i]; QPointF p3 = (m_points[i] + m_points[i + 1]) / 2; path.cubicTo(p1, p2, p3); } path.lineTo(m_points.last()); painter.drawPath(path); } int main(int argc, char *argv[]) { QApplication app(argc, argv); BezierWidget widget; widget.resize(400, 400); widget.show(); return app.exec(); } ``` 这个示例程序会在窗口中实时绘制鼠标经过的点和贝塞尔曲线,每次鼠标按下或移动时都会将当前点添加到点列表中,然后使用 QPainterPath 类的 cubicTo() 函数绘制贝塞尔曲线。注意在构造函数中调用 setMouseTracking(true) 可以让窗口接收鼠标移动事件,即使没有按下鼠标键。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值