Android-贝塞尔曲线

—— 前言

—— 什么是贝塞尔曲线

—— 贝塞尔曲线的分类

—— 贝塞尔曲线代码实现

—— 贝塞尔曲线的应用

前言:

从去年开始了解贝塞尔曲线之后,发现开发中,不管是Android/Ios平台,还是web前端等,都有贝塞尔曲线的应用,通过绘制贝塞尔曲线,可以帮助开发者实现很多效果,例如一段时间内很流行的粘合型的下拉刷新、又如天气曲线图,同时,以贝塞尔曲线为基础的贝塞尔工具是所有绘图软件的最常用最实用的工具。

什么是贝塞尔曲线:

贝塞尔曲线(Bézier curve),又称贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线。一般的矢量图形软件通过它来精确画出曲线,贝兹曲线由线段节点组成,节点是可拖动的支点,线段像可伸缩的皮筋,我们在绘图工具上看到的钢笔工具就是来做这种矢量曲线的。主要结构:起始点、终止点(也称锚点)、控制点。通过调整控制点,贝塞尔曲线的形状会发生变化。

贝塞尔曲线的分类:

一阶贝塞尔曲线(线段):

公式:

意义:由 P0 至 P1 的连续点, 描述的一条线段

二阶贝塞尔曲线(抛物线)

公式:

原理: 由 P0 至 P1 的连续点 Q0,描述一条线段。

由 P1 至 P2 的连续点 Q1,描述一条线段。

由 Q0 至 Q1 的连续点 B(t),描述一条二次贝塞尔曲线。

三阶贝塞尔曲线:

当然还有四阶曲线、五阶曲线......

一篇帮助理解贝塞尔曲线的文章:http://www.html-js.com/article/1628

一个帮助绘制贝塞尔曲线的网址:http://bezier.method.ac/#

贝塞尔曲线代码实现:

我们一般使用的是二阶贝塞尔曲线和三阶贝塞尔曲线,从动态图和公式我们可以看出,贝塞尔曲线主要由于三个部分控制:起点,终点,中间的辅助控制点。如何利用这三个点画出贝塞尔曲线,在android自带的Path类中自带了方法,可以帮助我们实现贝塞尔曲线:

/**
 * Add a quadratic bezier from the last point, approaching control point
 * (x1,y1), and ending at (x2,y2). If no moveTo() call has been made for
 * this contour, the first point is automatically set to (0,0).
 *
 * @param x1 The x-coordinate of the control point on a quadratic curve
 * @param y1 The y-coordinate of the control point on a quadratic curve
 * @param x2 The x-coordinate of the end point on a quadratic curve
 * @param y2 The y-coordinate of the end point on a quadratic curve
 */
public void quadTo(float x1, float y1, float x2, float y2) {
    isSimplePath = false;
    native_quadTo(mNativePath, x1, y1, x2, y2);
}

quadTo()方法从上一个点为起点开始绘制贝塞尔曲线,其中(x1,y1)为辅助控制点,(x2,y2)为终点。

Path mPath = new Path();

mPath.moveTo(x0,y0);

mPath.quadTo(x1,y1,x2,y2);

如调用以上代码,即绘制起点(x0,y0),终点(x2,y2),辅助控制点(x1,y1)的贝塞尔曲线。因此,通过不断改变这三个点的位置,我们可以绘制出各种各样的曲线

/**
 * Add a cubic bezier from the last point, approaching control points
 * (x1,y1) and (x2,y2), and ending at (x3,y3). If no moveTo() call has been
 * made for this contour, the first point is automatically set to (0,0).
 *
 * @param x1 The x-coordinate of the 1st control point on a cubic curve
 * @param y1 The y-coordinate of the 1st control point on a cubic curve
 * @param x2 The x-coordinate of the 2nd control point on a cubic curve
 * @param y2 The y-coordinate of the 2nd control point on a cubic curve
 * @param x3 The x-coordinate of the end point on a cubic curve
 * @param y3 The y-coordinate of the end point on a cubic curve
 */
public void cubicTo(float x1, float y1, float x2, float y2,
                    float x3, float y3) {
    isSimplePath = false;
    native_cubicTo(mNativePath, x1, y1, x2, y2, x3, y3);
}

cubicTo()方法从上一个点为起点开始绘制三阶贝塞尔曲线,其中(x1,y1),( x2, y2 )为辅助控制点,(x3,y3)为终点。

贝塞尔曲线的应用

(1)二阶贝塞尔曲线——波浪

要实现一个波浪不断涌动的效果,这种效果在很多手机应用中可见,例如手机电量,内存剩余等。我们需要绘制带有平滑自然效果的曲线,这时候就需要贝塞尔曲线来辅助了。

动态图:

原理图:

图中的矩阵即为视图的可见范围,也就是我们手机常见的区域。通过属性动画类ValueAnimator不断改变点1的横坐标,随着点1横坐标向右移动,点2,点3,点4,点5,以及四个控制点的坐标随着点1的移动同时位移相同距离,每一次坐标点更新,我们调用一次invalidate()方法,调用draw重新绘制视图,绘制四段贝塞尔曲线。最后点1移动到原先点3的位置,这样就完成了一次动画。

这样,通过循环不断的动画效果,我们就实现了波浪的效果。

onDraw() 代码:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if (!mIsRunning || !mHasInit)
        return;
    mPath.reset();
    mPath.moveTo(mLeft1.x, mLeft1.y);
    mPath.quadTo(mControlLeft1.x, mControlLeft1.y, mLeft2.x, mLeft2.y);
    mPath.quadTo(mControlLeft2.x, mControlLeft2.y, mFirst.x, mFirst.y);
    mPath.quadTo(mControlFirst.x, mControlFirst.y, mSecond.x, mSecond.y);
    mPath.quadTo(mControlSecond.x, mControlSecond.y, mRight.x, mRight.y);
    mPath.lineTo(mRight.x, mHeight);
    mPath.lineTo(mLeft1.x, mHeight);
    mPath.lineTo(mLeft1.x, mLeft1.y);
    canvas.drawPath(mPath, mPaint);
}

(2)二阶贝塞尔曲线——粘连体

利用二阶贝塞尔曲线还可以实现,类似两种物体粘合在一起的效果,比如我们常用的qq,在qq聊天列表上有一个非常有意思的功能,就是当我们用手指移动聊天列表上的未读消息标志的时候,它与聊天列表会产生粘连的效果:

现在,我们来模拟这种效果,利用学到的二阶贝塞尔曲线来绘制。

我们看到原理图,基本构造为两个圆,和两端贝塞尔曲线,绘制贝塞尔曲线,由于这是一个二阶的贝塞尔曲线,我们只需要一个控制点,在这个图里,我们的两条贝塞尔曲线的两个控制点分别为(x1,y1)(x4, y4)的中点,(x2, y2)(x3, y3)的中点。

从图中可以看出,我们的贝塞尔曲线由我们的控制点控制,控制点又是被起点和终点控制着,因此,当两个圆距离越大,曲线越趋于平缓,当两个圆距离越小,曲线的波动度越大,这样,我们想要的粘连的效果就实现了。另外,这里有一个还有角度(图中的45度角)可以用来控制,也可以作为控制曲线波动度的参数。

通过以上分析,我们通过一个方法来绘制两个圆之间的粘连体路径:

/**
 * 画粘连体
 * @param cx1     圆心x1
 * @param cy1     圆心y1
 * @param r1      圆半径r1
 * @param offset1 贝塞尔曲线偏移角度offset1
 * @param cx2     圆心x2
 * @param cy2     圆心y2
 * @param r2      圆半径r2
 * @param offset2 贝塞尔曲线偏移角度offset2
 * @return
 */
public static Path drawAdhesionBody(float cx1, float cy1, float r1, float offset1, float
        cx2, float cy2, float r2, float offset2) {

    /* 求三角函数 */
    float degrees =(float) Math.toDegrees(Math.atan(Math.abs(cy2 - cy1) / Math.abs(cx2 - cx1)));

    /* 根据圆1与圆2的相对位置求四个点 */
    float differenceX = cx1 - cx2;
    float differenceY = cy1 - cy2;

    /* 两条贝塞尔曲线的四个端点 */
    float x1,y1,x2,y2,x3,y3,x4,y4;

    /* 圆1在圆2的下边 */
    if (differenceX == 0 && differenceY > 0) {
        x2 = cx2 - r2 * (float) Math.sin(Math.toRadians(offset2));
        y2 = cy2 + r2 * (float) Math.cos(Math.toRadians(offset2));
        x4 = cx2 + r2 * (float) Math.sin(Math.toRadians(offset2));
        y4 = cy2 + r2 * (float) Math.cos(Math.toRadians(offset2));
        x1 = cx1 - r1 * (float) Math.sin(Math.toRadians(offset1));
        y1 = cy1 - r1 * (float) Math.cos(Math.toRadians(offset1));
        x3 = cx1 + r1 * (float) Math.sin(Math.toRadians(offset1));
        y3 = cy1 - r1 * (float) Math.cos(Math.toRadians(offset1));
    }
    /* 圆1在圆2的上边 */
    else if (differenceX == 0 && differenceY < 0) {
        x2 = cx2 - r2 * (float) Math.sin(Math.toRadians(offset2));
        y2 = cy2 - r2 * (float) Math.cos(Math.toRadians(offset2));
        x4 = cx2 + r2 * (float) Math.sin(Math.toRadians(offset2));
        y4 = cy2 - r2 * (float) Math.cos(Math.toRadians(offset2));
        x1 = cx1 - r1 * (float) Math.sin(Math.toRadians(offset1));
        y1 = cy1 + r1 * (float) Math.cos(Math.toRadians(offset1));
        x3 = cx1 + r1 * (float) Math.sin(Math.toRadians(offset1));
        y3 = cy1 + r1 * (float) Math.cos(Math.toRadians(offset1));
    }
    /* 圆1在圆2的右边 */
    else if (differenceX > 0 && differenceY == 0) {
        x2 = cx2 + r2 * (float) Math.cos(Math.toRadians(offset2));
        y2 = cy2 + r2 * (float) Math.sin(Math.toRadians(offset2));
        x4 = cx2 + r2 * (float) Math.cos(Math.toRadians(offset2));
        y4 = cy2 - r2 * (float) Math.sin(Math.toRadians(offset2));
        x1 = cx1 - r1 * (float) Math.cos(Math.toRadians(offset1));
        y1 = cy1 + r1 * (float) Math.sin(Math.toRadians(offset1));
        x3 = cx1 - r1 * (float) Math.cos(Math.toRadians(offset1));
        y3 = cy1 - r1 * (float) Math.sin(Math.toRadians(offset1));
    }
    /* 圆1在圆2的左边 */
    else if (differenceX < 0 && differenceY == 0 ) {
        x2 = cx2 - r2 * (float) Math.cos(Math.toRadians(offset2));
        y2 = cy2 + r2 * (float) Math.sin(Math.toRadians(offset2));
        x4 = cx2 - r2 * (float) Math.cos(Math.toRadians(offset2));
        y4 = cy2 - r2 * (float) Math.sin(Math.toRadians(offset2));
        x1 = cx1 + r1 * (float) Math.cos(Math.toRadians(offset1));
        y1 = cy1 + r1 * (float) Math.sin(Math.toRadians(offset1));
        x3 = cx1 + r1 * (float) Math.cos(Math.toRadians(offset1));
        y3 = cy1 - r1 * (float) Math.sin(Math.toRadians(offset1));
    }
    /* 圆1在圆2的右下角 */
    else if (differenceX > 0 && differenceY > 0) {
        x2 = cx2 - r2 * (float) Math.cos(Math.toRadians(180 - offset2 - degrees));
        y2 = cy2 + r2 * (float) Math.sin(Math.toRadians(180 - offset2 - degrees));
        x4 = cx2 + r2 * (float) Math.cos(Math.toRadians(degrees - offset2));
        y4 = cy2 + r2 * (float) Math.sin(Math.toRadians(degrees - offset2));
        x1 = cx1 - r1 * (float) Math.cos(Math.toRadians(degrees - offset1));
        y1 = cy1 - r1 * (float) Math.sin(Math.toRadians(degrees - offset1));
        x3 = cx1 + r1 * (float) Math.cos(Math.toRadians(180 - offset1 - degrees));
        y3 = cy1 - r1 * (float) Math.sin(Math.toRadians(180 - offset1 - degrees));
    }
    /* 圆1在圆2的左上角 */
    else if (differenceX < 0 && differenceY < 0) {
        x2 = cx2 - r2 * (float) Math.cos(Math.toRadians(degrees - offset2));
        y2 = cy2 - r2 * (float) Math.sin(Math.toRadians(degrees - offset2));
        x4 = cx2 + r2 * (float) Math.cos(Math.toRadians(180 - offset2 - degrees));
        y4 = cy2 - r2 * (float) Math.sin(Math.toRadians(180 - offset2 - degrees));
        x1 = cx1 - r1 * (float) Math.cos(Math.toRadians(180 - offset1 - degrees));
        y1 = cy1 + r1 * (float) Math.sin(Math.toRadians(180 - offset1 - degrees));
        x3 = cx1 + r1 * (float) Math.cos(Math.toRadians(degrees - offset1));
        y3 = cy1 + r1 * (float) Math.sin(Math.toRadians(degrees - offset1));
    }
    /* 圆1在圆2的左下角 */
    else if (differenceX < 0 && differenceY > 0) {
        x2 = cx2 - r2 * (float) Math.cos(Math.toRadians(degrees - offset2));
        y2 = cy2 + r2 * (float) Math.sin(Math.toRadians(degrees - offset2));
        x4 = cx2 + r2 * (float) Math.cos(Math.toRadians(180 - offset2 - degrees));
        y4 = cy2 + r2 * (float) Math.sin(Math.toRadians(180 - offset2 - degrees));
        x1 = cx1 - r1 * (float) Math.cos(Math.toRadians(180 - offset1 - degrees));
        y1 = cy1 - r1 * (float) Math.sin(Math.toRadians(180 - offset1 - degrees));
        x3 = cx1 + r1 * (float) Math.cos(Math.toRadians(degrees - offset1));
        y3 = cy1 - r1 * (float) Math.sin(Math.toRadians(degrees - offset1));
    }
    /* 圆1在圆2的右上角 */
    else {
        x2 = cx2 - r2 * (float) Math.cos(Math.toRadians(180 - offset2 - degrees));
        y2 = cy2 - r2 * (float) Math.sin(Math.toRadians(180 - offset2 - degrees));
        x4 = cx2 + r2 * (float) Math.cos(Math.toRadians(degrees - offset2));
        y4 = cy2 - r2 * (float) Math.sin(Math.toRadians(degrees - offset2));
        x1 = cx1 - r1 * (float) Math.cos(Math.toRadians(degrees - offset1));
        y1 = cy1 + r1* (float) Math.sin(Math.toRadians(degrees - offset1));
        x3 = cx1 + r1 * (float) Math.cos(Math.toRadians(180 - offset1 - degrees));
        y3 = cy1 + r1 * (float) Math.sin(Math.toRadians(180 - offset1 - degrees));
    }

    /* 贝塞尔曲线的控制点 */
    float anchorX1,anchorY1,anchorX2,anchorY2;

    /* 圆1大于圆2 */
    if (r1 > r2) {
        anchorX1 = (x2 + x3) / 2;
        anchorY1 = (y2 + y3) / 2;
        anchorX2 = (x1 + x4) / 2;
        anchorY2 = (y1 + y4) / 2;
    }
    /* 圆1小于或等于圆2 */
    else {
        anchorX1 = (x1 + x4) / 2;
        anchorY1 = (y1 + y4) / 2;
        anchorX2 = (x2 + x3) / 2;
        anchorY2 = (y2 + y3) / 2;
    }

    /* 画粘连体 */
    Path path = new Path();
    path.reset();
    path.moveTo(x1, y1);
    path.quadTo(anchorX1, anchorY1, x2, y2);
    path.lineTo(x4, y4);
    path.quadTo(anchorX2, anchorY2, x3, y3);
    path.lineTo(x1, y1);
    return path;
}

再来看仿QQ聊天列表的粘连效果,我们已经实现了粘连体的绘制,接下来,我们需要实现以上的基本效果,我们给控件设置一个粘连的最大距离,即如果两个圆之间的距离超过这个值,则不再绘制粘连体。

好了,我们看效果图:

粘连体除了在类似QQ上这种效果,其实还可以做很多事,比如,如果我们用它来实现一个加载页面的效果呢。

(3)三阶贝塞尔曲线——弹性球

三阶贝塞尔曲线,就是有两个控制点,公式太复杂,我也不是很理解,不过通过之前给出的那个网址来理解,还是比较好明白的,它相比二阶曲线的优点是,由于控制点的增加,它能够更加轻松地绘制出更平滑更自然的曲线。

先来看一个web前端的效果:

真的是很酷炫......

如何绘制类似这种,看起来具有弹性球的滑动球,我们需要使用三阶贝塞尔曲线,那么首先如何用三阶贝塞尔曲线绘制出一个圆,这里有一篇文章,是关于如何用贝塞尔曲线绘制圆:http://spencermortensen.com/articles/bezier-circle/ ,大概意思是讲,我们需要一个值就是c = 0.552284749,如下图,要绘制右上角的圆弧,我们需要两个控制点,其中B就是一个控制点,我们需要保证AB = c *r,即可以画出1/4的圆弧,以此类推,连续画四段这样的圆弧,就可以画出一个标准的圆。

接下来我们观察弹性球的运动,大概可以分为以下几个阶段:

1)开始启动,此时右边点位移,其他点不动

2)开始加速

3)减速

4)到达终点

5)回弹效果

弹性球代码:

 package com.zero.bezier.widget.elastic;

import android.animation.Animator;
import android.animation.ValueAnimator;
import android.graphics.Path;
import android.graphics.PointF;
import android.view.animation.AccelerateDecelerateInterpolator;

/**
 * 弹性球
 * @author linzewu
 * @date 2016/6/1
 */
public class ElasticBall extends Ball {
    /**
     * 向上运动
     */
    private static final int DIRECTION_UP = 1;
    /**
     * 向下运动
     */
    private static final int DIRECTION_DOWN = 2;
    /**
     * 向左运动
     */
    private static final int DIRECTION_LEFT = 3;
    /**
     * 向右运动
     */
    private static final int DIRECTION_RIGHT = 4;
    /**
     * 运动方向
     */
    private int mDirection;
    /**
     * 动画完成百分比(0~1)
     */
    private float mAnimPercent;
    /**
     * 弹性距离
     */
    private float mElasticDistance;
    /**
     * 弹性比例
     */
    private float mElasticPercent = 0.8f;
    /**
     * 位移距离
     */
    private float mMoveDistance;
    /**
     * 动画消费时间
     */
    private long mDuration = 1500;

    /**
     * 偏移值
     */
    private float offsetTop, offsetBottom, offsetLeft, offsetRight;
    /**
     * 圆形偏移比例
     */
    private float c = 0.551915024494f;

    private float c2 = 0.65f;
    /**
     * 动画开始点
     */
    private Ball mStartPoint;

    /**
     * 动画结束点
     */
    private Ball mEndPoint;

    /**
     * 构造方法
     *
     * @param x 圆心横坐标
     * @param y 圆心纵坐标
     * @param radius 圆半径
     */
    public ElasticBall(float x, float y, float radius) {
        super(x, y, radius);
        init();
    }

    private void init() {
        mElasticDistance = mElasticPercent * radius;
        offsetTop = c * radius;
        offsetBottom = c * radius;
        offsetLeft = c * radius;
        offsetRight = c * radius;
    }

    public interface ElasticBallInterface{
        void onChange(Path path);
        void onFinish();
    }

    private ElasticBallInterface mElasticBallInterface;

    /**
     * 对外公布方法,设置弹性比例 (0~1)
     * @param elasticPercent
     */
    public void setElasticPercent(float elasticPercent) {

    }
    /**
     * 对外公布方法,设置动画时间
     * @param duration
     */
    public void setDuration(long duration) {
        this.mDuration = duration;
    }

    /**
     * 对外公布方法, 开启动画
     * @param endPoint
     */
    public void startElasticAnim(PointF endPoint, ElasticBallInterface elasticBallInterface) {
        this.mEndPoint = new ElasticBall(endPoint.x, endPoint.y, radius);
        this.mStartPoint = new ElasticBall(x, y, radius);
        this.mStatusPoint1 = new ElasticBall(x, y, radius);
        this.mStatusPoint2 = new ElasticBall(x, y, radius);
        this.mStatusPoint3 = new ElasticBall(x, y, radius);
        this.mStatusPoint4 = new ElasticBall(x, y, radius);
        this.mStatusPoint5 = new ElasticBall(x, y, radius);
        this.mElasticBallInterface = elasticBallInterface;
        judgeDirection();
        mMoveDistance = getDistance(mStartPoint.x, mStatusPoint1.y, endPoint.x, endPoint.y);
        animStatus0();
        ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
        valueAnimator.setDuration(mDuration);
        valueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
        valueAnimator.start();
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mAnimPercent = (float) animation.getAnimatedValue();
                if(mAnimPercent>=0 && mAnimPercent <= 0.2){
                    animStatus1();
                }
                else if(mAnimPercent > 0.2 && mAnimPercent <= 0.5){
                    animStatus2();
                }
                else if(mAnimPercent > 0.5 && mAnimPercent <= 0.8){
                    animStatus3();
                }
                else if(mAnimPercent > 0.8 && mAnimPercent <= 0.9){
                    animStatus4();
                }
                else if(mAnimPercent > 0.9&&mAnimPercent <= 1){
                    animStatus5();
                }
                if (mElasticBallInterface != null) {
                    mElasticBallInterface.onChange(drawElasticCircle(topX, topY, offsetTop, offsetTop,
                            bottomX, bottomY, offsetBottom, offsetBottom,
                            leftX, leftY, offsetLeft, offsetLeft,
                            rightX, rightY, offsetRight, offsetRight));
                }
            }
        });
        valueAnimator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {

            }

            @Override
            public void onAnimationEnd(Animator animation) {
                if (mElasticBallInterface != null) {
                    mElasticBallInterface.onFinish();
                }
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });
    }

    private void judgeDirection() {
        if (mEndPoint.x - mStartPoint.x > 0) {
            mDirection = DIRECTION_RIGHT;
        }else if (mEndPoint.x - mStartPoint.x < 0) {
            mDirection = DIRECTION_LEFT;
        }else if (mEndPoint.y - mStartPoint.x > 0) {
            mDirection = DIRECTION_DOWN;
        }else if (mEndPoint.y - mStartPoint.y < 0){
            mDirection = DIRECTION_UP;
        }
    }

    /**
     * 动画状态0 (初始状态:圆形)
     */
    private void animStatus0() {
        offsetTop = c * radius;
        offsetBottom = c * radius;
        offsetLeft = c * radius;
        offsetRight = c * radius;
    }

    private Ball mStatusPoint1;

    /**
     * 动画状态1 (0~0.2)
     */
    private void animStatus1() {
        float percent = mAnimPercent * 5f;
        if (mDirection == DIRECTION_LEFT) {
            leftX = mStartPoint.leftX - percent * mElasticDistance;
        } else if (mDirection == DIRECTION_RIGHT) {
            rightX = mStartPoint.rightX + percent * mElasticDistance;
        } else if (mDirection == DIRECTION_UP) {
            topY = mStartPoint.topY - percent * mElasticDistance;
        } else if (mDirection == DIRECTION_DOWN) {
            bottomY = mStartPoint.bottomY + percent * mElasticDistance;
        }
        mStatusPoint1.refresh(x, y, topX, topY, bottomX, bottomY,
                leftX, leftY, rightX, rightY);
    }

    private Ball mStatusPoint2;

    /**
     * 动画状态2 (0.2~0.5)
     */
    private void animStatus2() {
        float percent = (float) ((mAnimPercent - 0.2) * (10f / 3));
        if (mDirection == DIRECTION_LEFT) {
            leftX = mStatusPoint1.leftX - percent * (mMoveDistance / 2 - mElasticDistance / 2 );
            x = mStatusPoint1.x - percent * (mMoveDistance / 2);
            rightX = mStatusPoint1.rightX - percent * (mMoveDistance / 2 - mElasticDistance / 2 );
            topX = x;
            bottomX = x;
            //偏移值稍作变化
            offsetTop = radius * c + radius * ( c2 - c ) * percent;
            offsetBottom = radius * c + radius * ( c2 - c ) * percent;
        } else if (mDirection == DIRECTION_RIGHT) {
            rightX = mStatusPoint1.rightX + percent * (mMoveDistance / 2 - mElasticDistance / 2 );
            x = mStatusPoint1.x + percent * (mMoveDistance / 2);
            leftX = mStatusPoint1.leftX + percent * (mMoveDistance / 2 - mElasticDistance / 2 );
            topX = x;
            bottomX = x;
            //偏移值稍作变化
            offsetTop = radius * c + radius * ( c2 - c ) * percent;
            offsetBottom = radius * c + radius * ( c2 - c ) * percent;
        } else if (mDirection == DIRECTION_UP) {
            topY = mStatusPoint1.topY - percent * (mMoveDistance / 2 - mElasticDistance / 2 );
            y = mStatusPoint1.y - percent * (mMoveDistance / 2);
            bottomY = mStatusPoint1.bottomY - percent * (mMoveDistance / 2 - mElasticDistance / 2 );
            leftY = y;
            rightY = y;
            //偏移值稍作变化
            offsetLeft = radius * c + radius * ( c2 - c ) * percent;
            offsetRight = radius * c + radius * ( c2 - c ) * percent;
        } else if (mDirection == DIRECTION_DOWN) {
            bottomY = mStatusPoint1.bottomY + percent * (mMoveDistance / 2 - mElasticDistance / 2 );
            y = mStatusPoint1.y + percent * (mMoveDistance / 2);
            topY = mStatusPoint1.topY + percent * (mMoveDistance / 2 - mElasticDistance / 2 );
            leftY = y;
            rightY = y;
            //偏移值稍作变化
            offsetLeft = radius * c + radius * ( c2 - c ) * percent;
            offsetRight = radius * c + radius * ( c2 - c ) * percent;
        }
        mStatusPoint2.refresh(x, y, topX, topY, bottomX, bottomY,
                leftX, leftY, rightX, rightY);
    }

    private Ball mStatusPoint3;

    /**
     * 动画状态3 (0.5~0.8)
     */
    private void animStatus3() {
        float percent = (mAnimPercent - 0.5f) * (10f / 3f);
        if (mDirection == DIRECTION_LEFT) {
            leftX = mStatusPoint2.leftX - Math.abs(percent * (mEndPoint.rightX - mStatusPoint2
                    .rightX));
            x = mStatusPoint2.x - Math.abs(percent * (mEndPoint.x - mStatusPoint2.x));
            rightX = mStatusPoint2.rightX - Math.abs(percent * (mEndPoint.x - mStatusPoint2.x));
            topX = x;
            bottomX = x;
            //偏移值稍作变化
            offsetTop = radius * c2 - radius * ( c2 - c ) * percent;
            offsetBottom = radius * c2 - radius * ( c2 - c ) * percent;
        } else if (mDirection == DIRECTION_RIGHT) {
            rightX = mStatusPoint2.rightX + percent * (mEndPoint.rightX - mStatusPoint2.rightX);
            x = mStatusPoint2.x + percent * (mEndPoint.x - mStatusPoint2.x);
            leftX = mStatusPoint2.leftX + percent * (mEndPoint.x - mStatusPoint2.x);
            topX = x;
            bottomX = x;
            //偏移值稍作变化
            offsetTop = radius * c2 - radius * ( c2 - c ) * percent;
            offsetBottom = radius * c2 - radius * ( c2 - c ) * percent;
        } else if (mDirection == DIRECTION_UP) {
            topY = mStatusPoint2.topY - Math.abs(percent * (mEndPoint.topY - mStatusPoint2
                    .topY));
            y = mStatusPoint2.y - Math.abs(percent * (mEndPoint.y - mStatusPoint2.y));
            bottomY = mStatusPoint2.bottomY - Math.abs(percent * (mEndPoint.y - mStatusPoint2.y));
            leftY = y;
            rightY = y;
            //偏移值稍作变化
            offsetLeft = radius * c2 - radius * ( c2 - c ) * percent;
            offsetRight = radius * c2 - radius * ( c2 - c ) * percent;
        } else if (mDirection == DIRECTION_DOWN) {
            bottomY = mStatusPoint2.bottomY + percent * (mEndPoint.bottomY - mStatusPoint2
                    .bottomY);
            y = mStatusPoint2.y + percent * (mEndPoint.y - mStatusPoint2.y);
            topY = mStatusPoint2.topY + percent * (mEndPoint.y - mStatusPoint2.y);
            leftY = y;
            rightY = y;
            //偏移值稍作变化
            offsetLeft = radius * c2 - radius * ( c2 - c ) * percent;
            offsetRight = radius * c2 - radius * ( c2 - c ) * percent;
        }
        mStatusPoint3.refresh(x, y, topX, topY, bottomX, bottomY,
                leftX, leftY, rightX, rightY);
    }

    private Ball mStatusPoint4;

    /**
     * 动画状态4 (0.8~0.9)
     */
    private void animStatus4() {
        float percent = (float) (mAnimPercent - 0.8) * 10;
        if (mDirection == DIRECTION_LEFT) {
            rightX = mStatusPoint3.rightX - percent * (Math.abs(mEndPoint.rightX - mStatusPoint3
                    .rightX) + mElasticDistance/2);
            //再做一次赋值,防止和终点不重合
            leftX = mEndPoint.leftX;
            x = mEndPoint.x;
            bottomX = mEndPoint.bottomX;
            topX = mEndPoint.topX;
        } else if (mDirection == DIRECTION_RIGHT) {
            leftX = mStatusPoint3.leftX + percent * (mEndPoint.leftX - mStatusPoint3.leftX +
                    mElasticDistance/2);
            //再做一次赋值,防止和终点不重合
            rightX = mEndPoint.rightX;
            x = mEndPoint.x;
            bottomX = mEndPoint.bottomX;
            topX = mEndPoint.topX;
        } else if (mDirection == DIRECTION_UP) {
            bottomY = mStatusPoint3.bottomY - percent * (Math.abs(mEndPoint.bottomY - mStatusPoint3
                    .bottomY) + mElasticDistance/2);
            //再做一次赋值,防止和终点不重合
            topY = mEndPoint.topY;
            y = mEndPoint.y;
            leftY = mEndPoint.leftY;
            rightY = mEndPoint.rightY;
        } else if (mDirection == DIRECTION_DOWN) {
            topY = mStatusPoint3.topY + percent * (mEndPoint.topY - mStatusPoint3
                    .topY + mElasticDistance/2);
            //再做一次赋值,防止和终点不重合
            bottomY = mEndPoint.bottomY;
            y = mEndPoint.y;
            leftY = mEndPoint.leftY;
            rightY = mEndPoint.rightY;
        }
        mStatusPoint4.refresh(x, y, topX, topY, bottomX, bottomY,
                leftX, leftY, rightX, rightY);
    }

    private Ball mStatusPoint5;

    /**
     * 动画状态5 (0.9~1)回弹
     */
    private void animStatus5() {
        float percent = (float) (mAnimPercent - 0.9) * 10;
        if (mDirection == DIRECTION_LEFT) {
            rightX = mStatusPoint4.rightX + percent * (mEndPoint.rightX - mStatusPoint4.rightX);
        } else if (mDirection == DIRECTION_RIGHT) {
            leftX = mStatusPoint4.leftX + percent * (mEndPoint.leftX - mStatusPoint4.leftX);
        } else if (mDirection == DIRECTION_UP) {
            bottomY = mStatusPoint4.bottomY + percent * (mEndPoint.bottomY - mStatusPoint4.bottomY);
        } else if (mDirection == DIRECTION_DOWN) {
            topY = mStatusPoint4.topY + percent * (mEndPoint.topY - mStatusPoint4.topY);
        }
        mStatusPoint5.refresh(x, y, topX, topY, bottomX, bottomY,
                leftX, leftY, rightX, rightY);
    }

    /**
     * 绘制弹性圆
     * 通过绘制四段三阶贝塞尔曲线,来实现有弹性变化的圆
     * @param topX
     * @param topY
     * @param offsetTop1
     * @param offsetTop2
     * @param bottomX
     * @param bottomY
     * @param offsetBottom1
     * @param offsetBottom2
     * @param leftX
     * @param leftY
     * @param offsetLeft1
     * @param offsetLeft2
     * @param rightX
     * @param rightY
     * @param offsetRight1
     * @param offsetRight2
     * @return
     */
    private Path drawElasticCircle(
            float topX, float topY, float offsetTop1, float offsetTop2,
            float bottomX, float bottomY, float offsetBottom1, float offsetBottom2,
            float leftX, float leftY, float offsetLeft1, float offsetLeft2,
            float rightX, float rightY, float offsetRight1, float offsetRight2
    ) {
        /**
         * 绘制每一段三阶贝塞尔曲线需要两个控制点
         */
        PointF controlTop1, controlTop2, controlBottom1, controlBottom2,
                controlLeft1, controlLeft2, controlRight1, controlRight2;
        controlTop1 = new PointF();
        controlTop1.x = topX - offsetTop1;
        controlTop1.y = topY;
        controlTop2 = new PointF();
        controlTop2.x = topX + offsetTop2;
        controlTop2.y = topY;
        controlBottom1 = new PointF();
        controlBottom1.x = bottomX - offsetBottom1;
        controlBottom1.y = bottomY;
        controlBottom2 = new PointF();
        controlBottom2.x = bottomX + offsetBottom2;
        controlBottom2.y = bottomY;
        controlLeft1 = new PointF();
        controlLeft1.x = leftX;
        controlLeft1.y = leftY - offsetLeft1;
        controlLeft2 = new PointF();
        controlLeft2.x = leftX;
        controlLeft2.y = leftY + offsetLeft2;
        controlRight1 = new PointF();
        controlRight1.x = rightX;
        controlRight1.y = rightY - offsetRight1;
        controlRight2 = new PointF();
        controlRight2.x = rightX;
        controlRight2.y = rightY + offsetRight2;

        Path path = new Path();
        /**
         * 绘制top到left的圆弧
         */
        path.moveTo(topX, topY);
        path.cubicTo(controlTop1.x, controlTop1.y, controlLeft1.x, controlLeft1.y, leftX, leftY);
        /**
         * 绘制left到bottom的圆弧
         */
        path.cubicTo(controlLeft2.x ,controlLeft2.y, controlBottom1.x, controlBottom1.y, bottomX,
                bottomY);
        /**
         * 绘制bottom到right的圆弧
         */
        path.cubicTo(controlBottom2.x, controlBottom2.y, controlRight2.x, controlRight2.y,
                rightX, rightY);
        /**
         * 绘制right到top的圆弧
         */
        path.cubicTo(controlRight1.x, controlRight1.y, controlTop2.x, controlTop2.y, topX, topY);
        return path;
    }

    /**
     * 求两点之间的距离
     * @param x1 第一个点的横坐标
     * @param y1 第一个点的纵坐标
     * @param x2 第二个点的横坐标
     * @param y2 第二个点的纵坐标
     * @return 两点距离
     */
    private float getDistance(float x1, float y1, float x2, float y2) {
        return (float) Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
    }

}

上面完成了一个弹性球的封装,可以实现四个方向的运动,然后我们实现一个弹性球的loader:

贝塞尔曲线还有很多应用的地方,或者说在各个领域都有。

去年开始在黄同学的影响下,慢慢地去实现一些利用贝塞尔曲线实现的效果,源码有相当一部分代码也是来自于黄同学,也得益于网络上大多数技术博客无私的分享,希望自己能够通过学习这样一个开发的绘图曲线,有所提高。

参考:

http://www.jianshu.com/p/791d3a791ec2

http://spencermortensen.com/articles/bezier-circle/

源码:

时间: 2024-11-05 22:46:31

Android-贝塞尔曲线的相关文章

Android 贝塞尔曲线库

最近做的一个小项目需要绘制一些折线图,AChartEngine其实里面包含很多图,虽然是开源的,但毕竟不是自己写的,而且项目稍有点庞大,有些东西修改起来还是得花点时间的,所以最后打算自己写一个,参考了多看阅读的阅读时间曲线效果,先看效果图: 下面这张是没有中间"平均3小时/天"那个小条的效果 已经开源了,代码放在github,地址是https://github.com/Steven-Luo/android-bezier-curve-chart 用法很简单 布局文件少不了: 1 <

Android 贝塞尔曲线实现QQ拖拽清除效果

纯属好奇心驱动写的一个学习性Demo,效果如下: 这个小功能最重要的点在于起始点和触摸点之间的连接线绘制,它并不是一条单纯的直线,而是中间细两头粗的一条不规则的Path,而这个中间向内弯曲的效果正是一条贝塞尔曲线,中间这个Path是由两条贝塞尔曲线和两条直线组成.看下图: 两个带圆弧的线就是由三点确认的一个贝塞尔曲线: 在Android已经有提供画贝塞尔曲线的接口,三个点传进去,效果就出来了. 贝塞尔曲线是用三个或多个点来确定的一条曲线,它在图形图像学中有相当重要的地位,Path中也提供了一些方

Android 贝塞尔曲线的浅析

博客也开了挺长时间了,一直都没有来写博客,主要原因是自己懒---此篇博客算是给2017年一个好的开始,同时也给2016年画上一个句点,不留遗憾. 那就让我们正式进入今天的主题:贝塞尔曲线. 首先,让我们来了解下什么是贝塞尔曲线. 贝塞尔曲线(Bézier curve),又称贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线.贝塞尔曲线于1962,由法国工程师皮埃尔·贝塞尔(Pierre Bézier)所广泛发表,他运用贝塞尔曲线来为汽车的主体进行设计.贝塞尔曲线最初由Paul de Cas

Android贝塞尔曲线应用-跳动的水滴

主要通过6个控制点实现. val startPoint = PointF() val endPoint = PointF() val control1 = PointF() val control2 = PointF() val control3 = PointF() val control4 = PointF() 绘制过程: private fun drawWater(canvas: Canvas) { waterPath.apply { reset() moveTo(startPoint)

从Android动画到贝塞尔曲线

基础知识: 动画通过连续播放一系列画面,给视觉造成连续变化的图画.很通俗的一种解释.也很好理解.那么我们先来一个案例看看. 动画案例:百度贴吧小熊奔跑 效果: topic.gif 代码: <?xml version="1.0" encoding="utf-8"?> <animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:

Android 利用二次贝塞尔曲线模仿购物车添加物品抛物线动画

0.首先,先给出一张效果gif图. 1.贝塞尔曲线原理及相关公式参考:http://www.jianshu.com/p/c0d7ad796cee 作者:许方镇. 2.原理:计算被点击 view.购物车view 以及他们所在父容器相对于屏幕的坐标. 3.在呗点击View坐标位置 父容器通过addView 增加需要完成动画的imgview. 4.自定义估值器 通过二次贝塞尔曲线公式(2个数据点,一个控制点)完成抛物线路径上的点xy坐标计算. 5.利用属性动画 +自定义估值器 完成imgview在父容

Android 自定义View高级特效,神奇的贝塞尔曲线

效果图 效果图中我们实现了一个简单的随手指滑动的二阶贝塞尔曲线,还有一个复杂点的,穿越所有已知点的贝塞尔曲线.学会使用贝塞尔曲线后可以实现例如QQ红点滑动删除啦,360动态球啦,bulabulabula~ 什么是贝塞尔曲线? 贝赛尔曲线(Bézier曲线)是电脑图形学中相当重要的参数曲线.更高维度的广泛化贝塞尔曲线就称作贝塞尔曲面,其中贝塞尔三角是一种特殊的实例.贝塞尔曲线于1962年,由法国工程师皮埃尔·贝塞尔(Pierre Bézier)所广泛发表,他运用贝塞尔曲线来为汽车的主体进行设计.贝

Android自定义View进阶 - 贝塞尔曲线

Path之贝塞尔曲线 作者微博: @GcsSloop [本系列相关文章] 在上一篇文章Path之基本图形中我们了解了Path的基本使用方法,本次了解Path中非常非常非常重要的内容-贝塞尔曲线. 一.Path常用方法表 为了兼容性(偷懒) 本表格中去除了在API21(即安卓版本5.0)以上才添加的方法.忍不住吐槽一下,为啥看起来有些顺手就能写的重载方法要等到API21才添加上啊.宝宝此刻内心也是崩溃的. 作用 相关方法 备注 移动起点 moveTo 移动下一次操作的起点位置 设置终点 setLa

Android日常学习:OpenGL 实践之贝塞尔曲线绘制

说到贝塞尔曲线,大家肯定都不陌生,网上有很多关于介绍和理解贝塞尔曲线的优秀文章和动态图. 以下两个是比较经典的动图了. 二阶贝塞尔曲线: 三阶贝塞尔曲线: 由于在工作中经常要和贝塞尔曲线打交道,所以简单说一下自己的理解: 现在假设我们要在坐标系中绘制一条直线,直线的方程很简单,就是 y=x ,很容易得到下图: 现在我们限制一下 x 的取值范围为 0~1 的闭区间,那么可以得出 y 的取值范围也是 0~1. 而在 0~1 的区间范围内,x 能取的数有多少个呢?答案当然是无数个了. 同理,y 的取值

【开源项目解析】QQ“一键下班”功能实现解析——学习Path及贝塞尔曲线的基本使用

早在很久很久以前,QQ就实现了"一键下班"功能.何为"一键下班"?当你QQ有信息时,下部会有信息数量提示红点,点击拖动之后,就会出现"一键下班"效果.本文将结合github上关于此功能的一个简单实现,介绍这个功能的基本实现思路. 项目地址 https://github.com/chenupt/BezierDemo 最终实现效果 实现原理解析 我个人感觉,这个效果实现的很漂亮啊!那么咱们就来看看实现原理是什么~ 注:下面内容请参照项目源码观看. 其