Path从懵逼到精通(2)——贝塞尔曲线

上一篇我们说了 Path 的基本操作,这一篇让我们来说一下 Path 的进阶用法——贝塞尔曲线。

那什么是贝塞尔曲线?贝塞尔曲线能在 Android 中实现什么效果?以及如何做到的?这篇文章都会告诉你。

什么是贝塞尔曲线?

贝塞尔曲线是由皮埃尔·贝塞尔发表的,他主要应用于汽车的主体进行设计,后来成为计算机图形学相当重要的参数曲线。

贝塞尔曲线由什么组成的?它通常由数据点和控制点两个部分组成的。那什么是数据点和控制点呢?请看下表:

类型 作用
数据点 曲线的起点和终点
控制点 控制曲线的弯曲程度

这样听起来可能还有点抽象,我们直接上图来看看。

一阶贝塞尔曲线:

一阶贝塞尔曲线其实就是一条直线,没有控制点,只有数据点 P0,P1,如下图:

一阶贝塞尔曲线

Android提供方法:lineTo()

二阶贝塞尔曲线:

二阶贝塞尔曲线有一个控制点 P1 和两个数据点 P0,P2。如下图:


二阶贝塞尔曲线

Android 提供方法:quadTo()

三阶贝塞尔曲线:

三阶贝塞尔曲线有两个控制点 P1,P2 和两个数据点 P0,P3。如下图:

三阶贝塞尔曲线

Android 提供方法:cubicTo()

更高阶的曲线 Android 并没有提供 API ,所以在这只会介绍二阶和三阶曲线,如果对更高阶的曲线有兴趣的话,可以去贝塞尔曲线———维基百科贝塞尔曲线动态演示这两个网站多了解一下。

贝塞尔曲线是怎么形成的

那么这条曲线究竟是怎么形成的呢?先从二阶曲线分析一下:

二阶贝塞尔曲线形成原理:

1.连接 A,B 形成 AB 线段,连接 B,C 形成 BC 线段。

连成AB,BC线段

2.在 AB 线段取一个点 D,BC 线段取一个点 E ,使其满足条件: AD/AB = BE/BC,连接 D,E 形成线段 DE。

连接DE

3.在 DE 取一个点 F,使其满足条件:AD/AB = BE/BC = DF/DE。

4.而满足这些条件的所有的 F 点所形成的轨迹就是二阶贝塞尔曲线,动态过程如下:


二阶贝塞尔曲线
三阶贝塞尔曲线形成原理:

1.连接 A,B 形成 AB 线段,连接 B,C 形成 BC 线段,连接 C,D 形成 CD 线段。

2.在AB线段取一个点 E,BC 线段取一个点 F,CD 线段取一个点 G,使其满足条件: AE/AB = BF/BE = CG/CD。连接 E,F 形成线段 EF,连接 F,G 形成线段 FG。

3.在EF线段取一个点 H,FG 线段取一个点 I,使其满足条件: AE/AB = BF/BE = CG/CD = EH/EF = FI/FG。连接 H,I 形成线段 HI。

4.在 HI 线段取一个点 J,使其满足条件: AE/AB = BF/BE = CG/CD = EH/EF = FI/FG = HJ/HI。

5.而满足这些条件的所有的J点所形成的轨迹就是三阶贝塞尔曲线,动态过程如下:


三阶贝塞尔曲线

在 Android 中使用贝塞尔曲线

说了这么多原理,是时候要知道要怎么运用贝塞尔曲线了。这里我会用两个例子来说明二阶和三阶贝塞尔曲线的运用:

二阶曲贝塞尔曲线的应用:
方法预览:
public void quadTo (float x1, float y1, float x2, float y2)
有什么用:

画出二阶贝塞尔曲线

怎么用:

因为二阶贝塞尔曲线需要三个点才能确定,所以 quadTo 方法中的四个参数分别是确定第二,第三的点的。第一个点就是 path 上次操作的点。
现在用一个实例来练习下这个方法:

水波纹效果:
效果图:
水波纹效果
实现思路:
  1. 画出至少两段的波纹
  2. 使用 ValueAnimator 不断获取平移的值 offset
  3. 利用 offset 不断的改变波纹的位置

现在分步骤来说明:

1. 画出至少两段波纹

我们首先要画出两段波纹。一段波纹就包含两条曲线。每条曲线我们可以使用 quadTo() 方法来画。

为了更容易理解,请看下图:

水波纹坐标图

mWL 是一段波纹的长度,mCenterY 是屏幕高度的一半。

  • 画第一段波纹的第一条曲线:
mPath.moveTo(-mWL, mCenterY); //将path操作的起点移动到(-mWL,mCenterY)
mPath.quadTo((-mWL * 3 / 4) , mCenterY + 60, (-mWL / 2), mCenterY); //画出第一段波纹的第一条曲线
  • 画出第一段波纹的第二条曲线:
mPath.quadTo((-mWL / 4) , mCenterY - 60, 0, mCenterY); //画出第一段波纹的第二条曲线
  • 画出第二段波纹的第一条曲线:
mPath.quadTo((mWL /4) , mCenterY + 60, (mWL / 2), mCenterY); //画出第二段波纹的第一条曲线
  • 画出第二段波纹的第二条曲线:
mPath.quadTo((mWL * 3/ 4) , mCenterY - 60, mWL, mCenterY);  //画出第二段波纹的第二条曲线
2. 使用 ValueAnimator 不断获取平移的值 offset

那么现在来想一下应该怎么让这几段波纹动起来呢?我们需要一个 offset 的平移值。这个值应该加在每个点的x坐标上,并且 offset 是不断变化的,这样才会形成一个向右平移的效果。那怎么才能获取到这个变化的offset的值呢?答案就是要使用 ValueAnimator 。用法如下:

   ValueAnimator animator = ValueAnimator.ofInt(0, mWL); //mWL是一段波纹的长度
   animator.setDuration(1000);
   animator.setRepeatCount(ValueAnimator.INFINITE);
   animator.setInterpolator(new LinearInterpolator());
   animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                offset = (Integer) animation.getAnimatedValue(); //offset 的值的范围在[0,mWL]之间。
                postInvalidate();
            }
        });
   animator.start();
    }

这样只要动画开始,offset 就会不断从 0~mWL 变化。

3. 利用offset不断的改变波纹的位置

现在为曲线的所有 X 坐标都加上 offset 值。这样就会产生平移的效果,为了简化代码,这里使用的 for 循环来画曲线。

    mPath.moveTo(-mWL + mOffset, mCenterY);
    for (int i = 0; i < 2; i++) {
        mPath.quadTo((-mWL * 3 / 4) + (i * mWL) + offset, mCenterY + 60, (-mWL / 2) + (i * mWL) + offset, mCenterY);
        mPath.quadTo((-mWL / 4) + (i * mWL) + offset, mCenterY - 60, i * mWL + offset, mCenterY);
      }

接下来为了适配各种屏幕,需要根据手机的宽度来计算出所需要的波纹的数目:

mWaveCount = (int) Math.round(mScreenWidth / mWL + 1.5); //这样就保证波纹能覆盖整个屏幕

上面的 for 循环也可以改为:

    mPath.moveTo(-mWL + mOffset, mCenterY);
    for (int i = 0; i < mWaveCount; i++) {
        mPath.quadTo((-mWL * 3 / 4) + (i * mWL) + offset, mCenterY + 60, (-mWL / 2) + (i * mWL) + offset, mCenterY);
        mPath.quadTo((-mWL / 4) + (i * mWL) + offset, mCenterY - 60, i * mWL + offset, mCenterY);
      }
三阶贝塞尔曲线的应用:
弹性的圆:
效果图:
弹性的圆
实现思路:
  1. 用贝塞尔曲线画出正圆
  2. 通过修改坐标的大小来形成圆收缩的效果
1. 用贝塞尔曲线画出正圆

我们首先要知道怎么使用 cubicTo() 方法来画个半径为 r 的正圆。其实使用 cubicTo() 来画正圆就需要 4 条三阶贝塞尔曲线组合而成。如图所示:


三阶贝塞尔曲线形成的圆

如果要画 P0P3 那道曲线应该怎么画呢?我们就要知道 P0,P1,P2,P3 这四个点的坐标。P0,P3 的坐标我们已经知道了,分别是 (0,-r),(r,0)。那么 P1 和 P2 的坐标是什么呢?其实这里有个论证的过程,这个过程在这篇文章就有:Approximate a circle with cubic Bézier curves,感兴趣的可以看看。这里只说结果,最后得到一个数,这个数就是 c = 0.551915024494。也就是说 P1,P2 的坐标就是 (cr,-r),(r,-cr)。其他点的坐标也是用同样的方法得出的,这里就不细说了。

为了更方便管理这几个点,我将这几个点封装分成两个类。分别是 HorizontalLine 和 VerticalLine 。圆的上下两条线属于 HorizontalLine,圆的左右两条线属于 VerticalLine。

以下是这两个类的代码:

private float c = 0.551915024494f;

class HorizontalLine {
        public PointF left = new PointF(); //P7 P11
        public PointF middle = new PointF(); //P0 P6
        public PointF right = new PointF(); //P1 P5

        public HorizontalLine(float x,float y) {
            left.x = -radius*c;
            left.y = y;
            middle.x = x;
            middle.y = y;
            right.x = radius*c;
            right.y = y;
        }

        public void setY(float y) {
            left.y = y;
            middle.y = y;
            right.y = y;
        }

    }

    class VerticalLine {
        public PointF top = new PointF(); //P2 P10
        public PointF middle = new PointF(); //P3 P9
        public PointF bottom = new PointF(); //P4 P8


        public VerticalLine(float x,float y) {
            top.x = x;
            top.y = -radius*c;
            middle.x = x;
            middle.y = y;
            bottom.x = x;
            bottom.y = radius*c;
        }


        public void setX(float x) {
            top.x = x;
            middle.x = x;
            bottom.x = x;
        }
    }

以下是用cubicTo()方法画圆的代码:

    private Paint mPaint;

    private Path mPath;

    private int mScreenHeight;//屏幕高度

    private int mScreenWidth;//屏幕宽度

    private float radius = 100;

    private void initPaint() {
        mPath = new Path();
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.parseColor("#59c3e2"));
        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
    }

    private void initPoint() {
        mTopLine = new HorizontalLine(0,-radius);
        mBottomLine = new HorizontalLine(0,radius);
        mLeftLine = new VerticalLine(-radius,0);
        mRightLine = new VerticalLine(radius,0);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.reset();
        
        //将画布移动到屏幕正中间
        canvas.translate(mScreenWidth / 2, mScreenHeight / 2); 

        mPath.moveTo(mTopLine.middle.x,mTopLine.middle.y);
        mPath.cubicTo(mTopLine.right.x,mTopLine.right.y,mRightLine.top.x,mRightLine.top.y,
                mRightLine.middle.x,mRightLine.middle.y);
        mPath.cubicTo(mRightLine.bottom.x,mRightLine.bottom.y,mBottomLine.right.x,mBottomLine.right.y,
                mBottomLine.middle.x,mBottomLine.middle.y);
        mPath.cubicTo(mBottomLine.left.x,mBottomLine.left.y,mLeftLine.bottom.x,mLeftLine.bottom.y,
                mLeftLine.middle.x,mLeftLine.middle.y);
        mPath.cubicTo(mLeftLine.top.x,mLeftLine.top.y,mTopLine.left.x,mTopLine.left.y,
                mTopLine.middle.x,mTopLine.middle.y);
        canvas.drawPath(mPath,mPaint);
    }

效果如下:
2.通过修改坐标的大小来形成圆收缩的效果

想要达到圆收缩的效果只要增加和减少某些坐标就可以了。比如我要达成如图的这种效果,应该怎么做呢?

只要增加 P2,P3,P4 的横坐标,就可以达到这种效果。

现在试试把圆收缩起来:

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

        //将画布移动到手机屏幕的正中间
        canvas.translate(mScreenWidth / 2, mScreenHeight / 2); 


        //将右边的线的点的横坐标都增大
        mRightLine.setX(radius * 1.5f); 
        
        mPath.moveTo(mTopLine.middle.x,mTopLine.middle.y);
        mPath.cubicTo(mTopLine.right.x,mTopLine.right.y,mRightLine.top.x,mRightLine.top.y,
                mRightLine.middle.x,mRightLine.middle.y);
        mPath.cubicTo(mRightLine.bottom.x,mRightLine.bottom.y,mBottomLine.right.x,mBottomLine.right.y,
                mBottomLine.middle.x,mBottomLine.middle.y);
        mPath.cubicTo(mBottomLine.left.x,mBottomLine.left.y,mLeftLine.bottom.x,mLeftLine.bottom.y,
                mLeftLine.middle.x,mLeftLine.middle.y);
        mPath.cubicTo(mLeftLine.top.x,mLeftLine.top.y,mTopLine.left.x,mTopLine.left.y,
                mTopLine.middle.x,mTopLine.middle.y);
        canvas.drawPath(mPath,mPaint);
    }
效果如下:

以此类推,如果要达到刚刚那个动图的效果,就要减少上下两条线的点的纵坐标,然后不断平移画布就可以了。具体代码可以下载源码来看。

源码下载:

参考资料:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • 本文主要内容为贝塞尔曲线原理解析并用 SurfaceView 实现其展示动画 关于SurfaceView 的使用,...
    涤生_Woo阅读 13,375评论 5 94
  • 前言 贝塞尔曲线是一个强大而又神秘的东西。今天有时间彻底梳理了一下,神秘的面纱被揭下来了,剩下了只有强大。下面就一...
    chaohx阅读 2,947评论 1 9
  • 最近在做项目的时候,需要用到一个动画,非常简单的动画,简单到就是直接对一个View做平移... 然而虽然动画简单,...
    IAMDAEMON阅读 4,265评论 12 69
  • 谈谈贝塞尔曲线 最近在做项目的时候,需要用到一个动画,非常简单的动画,简单到就是直接对一个View做平移… 然而虽...
    雨润听潮阅读 5,962评论 1 16
  • 前言 近段时间我在工作中实现了一个动画功能,其中涉及到动画元素要按一定的轨迹在屏幕上移动,运动轨迹的生成我使用了P...
    Alexyz123阅读 6,543评论 0 11