Android 中的贝塞尔曲线分析详解

什么是贝塞尔曲线,贝塞尔曲线有什么用?贝塞尔曲线是应用于二维图形应用程序的数学曲线。 曲线定义:数据点(起始点、终止点)、控制点。通过调整控制点,贝塞尔曲线的形状会发生变化。一句话概括贝塞尔曲线:将任意一条曲线转化为精确的数学公式。在我们Android开发的殿堂中,有很多比较炫的效果都是通过贝塞尔曲线来画出来的。下面让我们一起来揭开贝塞尔曲线的神秘面纱!
数学中的贝塞尔曲线
一阶贝塞尔曲线(线性贝塞尔曲线)

给定点P0(起始点)、P1(终止点),线性贝塞尔曲线只是一条两点之间的直线。

这里写图片描述
二阶贝塞尔曲线

二次方贝塞尔曲线的路径由给定点P0(起始点)、P1(控制点)、P2(终止点)的函数贝塞尔曲线追踪:
这里写图片描述
这里写图片描述
三阶贝塞尔曲线

P0(起始点)、P1(控制点)、P2(控制点)、P3(终止点)四个点在平面或在三维空间中定义了三次方贝塞尔曲线。曲线起始于P0走向P1,并从P2的方向来到P3。一般不会经过P1或P2;这两个点只是在那里提供方向资讯。P0和P1之间的间距,决定了曲线在转而趋进P2之前,走向P1方向的“长度有多长”。
这里写图片描述
这里写图片描述
除此之外,还有四阶、五阶、六阶等多阶贝塞尔曲线,由于多阶贝塞尔曲线的效果可以通过降阶转变为低阶贝塞尔曲线来实现,并且Android中的API接口目前只支持到三阶贝塞尔曲线,因此暂时不分析太过高阶的贝塞尔曲线.
Android中的贝塞尔曲线API及其基本使用
数学的一节内容只要有个直观的感受就好,重点主要是Android中的贝塞尔曲线。

  1. Android中二阶贝塞尔曲线的API:
Path.quadTo(float x1, float y1, float x2, float y2)
Path.rQuadTo(float dx1, float dy1, float dx2, float dy2)

这两个API在原理上是可以互相转换的——quadTo是基于绝对坐标,而rQuadTo是基于相对坐标,所以后面我都只以quadTo来进行讲解.该方法前两个参数表示控制点,后两个参数是终止点,那起始点呢?熟悉Path用法的同学应该知道,起始点就是目前Path移到的位置,如果不想让起始点受之前的Path变化的影响,可以调用Path.moveTo(float x, float y)指定某特定点为起始点.
这里写图片描述
具体可以参考: 贝塞尔曲线开发的艺术这篇文章
为了更加容易看出控制点与曲线弯曲程度的关系,上图中绘制出了辅助点和辅助线,从上面的动态图可以看出,贝塞尔曲线在动态变化过程中有类似于橡皮筋一样的弹性效果,因此在制作一些弹性效果的时候很常用。这里可以提供核心的代码:

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        centerX = w/2;
        centerY = h/2;

        // 初始化数据点和控制点的位置
        start.x = centerX-200;
        start.y = centerY;
        end.x = centerX+200;
        end.y = centerY;
        control.x = centerX;
        control.y = centerY-100;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 根据触摸位置更新控制点,并提示重绘
        control.x = event.getX();
        control.y = event.getY();
        invalidate();
        return true;
    }

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

        // 绘制数据点和控制点
        mPaint.setColor(Color.GRAY);
        mPaint.setStrokeWidth(20);
        canvas.drawPoint(start.x,start.y,mPaint);
        canvas.drawPoint(end.x,end.y,mPaint);
        canvas.drawPoint(control.x,control.y,mPaint);

        // 绘制辅助线
        mPaint.setStrokeWidth(4);
        canvas.drawLine(start.x,start.y,control.x,control.y,mPaint);
        canvas.drawLine(end.x,end.y,control.x,control.y,mPaint);

        // 绘制贝塞尔曲线
        mPaint.setColor(Color.RED);
        mPaint.setStrokeWidth(8);

        Path path = new Path();

        path.moveTo(start.x,start.y);
        path.quadTo(control.x,control.y,end.x,end.y);

        canvas.drawPath(path, mPaint);
    }
}

Android中三阶贝塞尔曲线的API:
Path.cubicTo(float x1, float y1, float x2, float y2,float x3, float y3)
Path.rCubicTo(float x1, float y1, float x2, float y2,float x3, float y3)
这两个API在原理上也是可以互相转换的——cubicTo是基于绝对坐标,而rCubicTo是基于相对坐标.方法中前四个参数是两个控制点的坐标,后两个参数是终止点的坐标.那二阶和三阶贝塞尔曲线有什么区别呢?其实就是三阶比二阶多一个控制点,效果就是能得到更加灵活,变化更多的曲线.三阶曲线相比于二阶曲线可以制作更加复杂的形状,但是对于高阶的曲线,用低阶的曲线组合也可达到相同的效果,就是传说中的降阶。因此Android对贝塞尔曲线的封装方法最高只到三阶曲线。

这里写图片描述

 @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        centerX = w / 2;
        centerY = h / 2;

        // 初始化数据点和控制点的位置
        start.x = centerX - 200;
        start.y = centerY;
        end.x = centerX + 200;
        end.y = centerY;
        control1.x = centerX;
        control1.y = centerY - 100;
        control2.x = centerX;
        control2.y = centerY - 100;

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 根据触摸位置更新控制点,并提示重绘
        if (mode) {
            control1.x = event.getX();
            control1.y = event.getY();
        } else {
            control2.x = event.getX();
            control2.y = event.getY();
        }
        invalidate();
        return true;
    }

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

        // 绘制数据点和控制点
        mPaint.setColor(Color.GRAY);
        mPaint.setStrokeWidth(20);
        canvas.drawPoint(start.x, start.y, mPaint);
        canvas.drawPoint(end.x, end.y, mPaint);
        canvas.drawPoint(control1.x, control1.y, mPaint);
        canvas.drawPoint(control2.x, control2.y, mPaint);

        // 绘制辅助线
        mPaint.setStrokeWidth(4);
        canvas.drawLine(start.x, start.y, control1.x, control1.y, mPaint);
        canvas.drawLine(control1.x, control1.y,control2.x, control2.y, mPaint);
        canvas.drawLine(control2.x, control2.y,end.x, end.y, mPaint);

        // 绘制贝塞尔曲线
        mPaint.setColor(Color.RED);
        mPaint.setStrokeWidth(8);

        Path path = new Path();

        path.moveTo(start.x, start.y);
        path.cubicTo(control1.x, control1.y, control2.x,control2.y, end.x, end.y);

        canvas.drawPath(path, mPaint);
    }
}

对于平时开发的过程中,可能我们不能很好地掌握贝塞尔曲线的控件点坐标该怎么确定,这里提供一个在线转换器:
贝塞尔曲线控制点在线转换器

比较好的demo效果推荐几个给大家:

  1. Android高仿path小球刷新效果,类似iOS果冻效果
  2. 三次贝塞尔曲线练习之弹性的圆
  3. Approximate a circle with cubic Bézier curves
    下面我们再来看看贝塞尔曲线的圆效果:
    这里写图片描述
    看到这样子的效果图,我们肯定不能简单得用Android paint画一个圆,而是应该想到这个圆是可以动的,是不规则地拉拽,所以这时我们应该自然而然地想到用贝塞尔曲线画圆实现。对于贝塞尔曲线画圆,这里需要注意几个关键知识点:
    参考这篇文章:http://spencermortensen.com/articles/bezier-circle/
    这需要数学功底了,一般贝塞尔曲线的基本公式:
B(t) = (1-t)^3*P_0 + 3*(1-t)^2*t*P_1 + 3*(1-t)*t^2*P_2 + t^3*P_3, t in [0,1]
P_0 = (0,1), P_1 = (c,1), P_2 = (1,c), P_3 = (1,0)
通过求解这个高阶函数可以得出一个跟为:
c = (4/3)*(sqrt(2) - 1)
即c = 0.5522847498

接下来我们来分析一下这个c值有什么用?看下面这个示意图:
这里写图片描述
看到这个圆周的示意图是不是有种似曾相识的赶脚,没错,就是对称图形嘛,圆既是轴对称图形,也是中心对称图形,所以刚刚我们算出来的那个c就是为了计算圆周各个特殊点的坐标。
,如果我们打算用贝塞尔曲线来画这么一个圆的话,我们需要知道这个圆的半径,以及图中的M的值,知道这两个值的话就能够知道图中12个点的坐标,知道坐标就能够用Path的cubicTo方法来使用贝塞尔曲线画出圆了。圆的半径radius一般都会直接给我们(要不我们画个毛啊),那么p0,p3坐标点就知道了,而M = radius * c;这个c的值就是0.551915024494f.这样p1,p2坐标点也知道了,就可以用画p0到p3这段弧了,下面就画第一段圆弧,其他三段圆弧的画法类似:

mPath.moveTo(p0.x,p0.y);
mPath.cubicTo(p1.x, p1.y, p2.x, p2.y, p3.x,p3.y);

如果有兴趣可以研究这篇文章:三次贝塞尔曲线
最后我们再来分析一下一个开源库的轨迹效果,是利用贝塞尔曲线动画实现的:
这里写图片描述
叶子有两个动画,一个是移动,一个是旋转,旋转很简单,那么移动的动画就需要研究一下了。 首先叶子并不是平移,而是做曲线运动,而且是不规则的曲线运动,一看到曲线二字,立马想到贝塞尔曲线。
既然贝塞尔曲线可以画任意曲线,那么我事先画出一条叶子的运动曲线,然后根据这条运动曲线的坐标来更新叶子的位置,不就实现了吗?
这里写图片描述
有了这么条曲线,接下来就是根据这条曲线来实现叶子动画了. 首先用Path构造出这条曲线:

Path path = new Path();
    path.moveTo(mData[0].x, mData[0].y * mScaleH);
    path.quadTo(mCtrl[0].x * mScaleW, mCtrl[0].y * mScaleH, mData[1].x * mScaleW, mData[1].y * mScaleH);
    path.cubicTo(mCtrl[1].x * mScaleW, mCtrl[1].y * mScaleH, mCtrl[2].x * mScaleW, mCtrl[2].y * mScaleH,
            mData[2].x * mScaleW, mData[2].y * mScaleH);
    path.cubicTo(mCtrl[3].x * mScaleW, mCtrl[3].y * mScaleH, mCtrl[4].x * mScaleW, mCtrl[4].y * mScaleH, mData[3]
            .x * mScaleW, mData[3].y * mScaleH);
    path.quadTo(mCtrl[5].x * mScaleW, mCtrl[5].y * mScaleH, mData[4].x * mScaleW, mData[4].y * mScaleH);

接下来就是如何根据这个Path来更新叶子的位置。 如果是Android 5.0 ,属性动画有一个直接的Api可以根据Path来创建动画:

    ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, "x", "y", path);
    objectAnimator.setRepeatCount(ValueAnimator.INFINITE);
    objectAnimator.setRepeatMode(ValueAnimator.INFINITE);
    objectAnimator.setDuration(5000);
    objectAnimator.start();

注意 ,为了兼容低版本,我们可以利用PathMeasure来实现,如果不懂PathMeasure的可以参考这篇博客:
PathMeasure详解
简单来说就是PathMeasure 把 Path “拉直”,然后给了我们一个接口(getLength)告诉我们path的总长度,然后我们想要知道具体某一点的坐标,只需要用相对的distance去取即可

    final PathMeasure mPathMeasure = new PathMeasure(path, false); //false的意思是不封闭Path,不让Path形成一个环
    final float[] pointF = new float[2];

    ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, mPathMeasure.getLength());
    valueAnimator.setDuration(duration);
    valueAnimator.setStartDelay(startDelay);
    valueAnimator.setInterpolator(interpolator);
    valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
    valueAnimator.setRepeatMode(ValueAnimator.INFINITE);
    valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            float value = (Float) animation.getAnimatedValue();
            // 获取当前点坐标封装到pointF
            mPathMeasure.getPosTan(value, pointF, null);
            view.setX(pointF[0]);
            view.setTranslationY(pointF[1]);
        }
    });
    valueAnimator.start();
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值