Android 贝塞尔曲线解析

相信很多同学都知道“贝塞尔曲线”这个词,我们在很多地方都能经常看到。利用“贝塞尔曲线”可以做出很多好看的UI效果,本篇博客就让我们一起学习“贝塞尔曲线”。

贝塞尔曲线的原理

贝塞尔曲线是用一系列点来控制曲线状态的,这些点简单分为两类:

类型 作用
数据点 确定曲线的起始和结束位置
控制点 确定曲线的弯曲程度

一阶贝塞尔曲线
一阶曲线是没有控制点的,仅有两个数据点(A 和 B),最终效果一个线段。
动态过程可以参照下图(贝塞尔曲线相关的动态演示图片来自维基百科)。
这里写图片描述

一阶曲线其实就是lineTo方法。

二阶贝塞尔曲线
在平面内任选 3 个不共线的点,依次用线段连接。
这里写图片描述

在第一条线段上任选一个点 D。计算该点到线段起点的距离 AD,与该线段总长 AB 的比例。
这里写图片描述

连接这两点 DE。
这里写图片描述

从新的线段 DE 上再次找出相同比例的点 F,使得 DF:DE = AD:AB = BE:BC。

这里写图片描述

到这里,我们就确定了贝塞尔曲线上的一个点 F。接下来,请稍微回想一下中学所学的极限知识,让选取的点 D 在第一条线段上从起点 A 移动到终点 B,找出所有的贝塞尔曲线上的点 F。所有的点找出来之后,我们也得到了这条贝塞尔曲线。

这里写图片描述

动态过程如下:
这里写图片描述

三阶贝塞尔曲线
控制点个数为 4 时,就是三阶的曲线
这里写图片描述

步骤都是相同的,只不过我们每确定一个贝塞尔曲线上的点,要进行三轮取点操作。如图,AE:AB = BF:BC = CG:CD = EH:EF = FI:FG = HJ:HI,其中点 J 就是最终得到的贝塞尔曲线上的一个点。
这里写图片描述

这样我们得到的是一条三次贝塞尔曲线。
这里写图片描述

动态图如下:
这里写图片描述
三阶曲线对应的方法是cubicTo

要绘制更复杂的曲线,控制点的增加也仅仅是线性的。这一特点使其不光在工业设计领域大展拳脚,就连数学基础不好的人也可以比较容易地掌握,比如大多数平面美术设计师们。

学习贝塞尔曲线函数

一阶曲线是一条线段,非常简单,不再进行介绍,都是path的基本用法。

二阶曲线:
首先,两个数据点是控制贝塞尔曲线开始和结束的位置,而控制点则是控制贝塞尔的弯曲状态
这里写图片描述

从上面的动态图可以看出,贝塞尔曲线在动态变化过程中有类似于橡皮筋一样的弹性效果,因此在制作一些弹性效果的时候很常用。

代码如下:

public class Bezier extends View {

    private Paint mPaint;
    private int centerX, centerY;

    private PointF start, end, control;

    public Bessel1(Context context) {
        super(context);
        mPaint = new Paint();
        mPaint.setColor(Color.BLACK);
        mPaint.setStrokeWidth(8);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setTextSize(60);

        start = new PointF(0,0);
        end = new PointF(0,0);
        control = new PointF(0,0);
    }

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

三阶曲线:
三阶曲线由两个数据点和两个控制点来控制曲线状态。
这里写图片描述

public class Bezier2 extends View {

    private Paint mPaint;
    private int centerX, centerY;

    private PointF start, end, control1, control2;
    private boolean mode = true;

    public Bezier2(Context context) {
        this(context, null);

    }

    public Bezier2(Context context, AttributeSet attrs) {
        super(context, attrs);

        mPaint = new Paint();
        mPaint.setColor(Color.BLACK);
        mPaint.setStrokeWidth(8);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setTextSize(60);

        start = new PointF(0, 0);
        end = new PointF(0, 0);
        control1 = new PointF(0, 0);
        control2 = new PointF(0, 0);
    }

    public void setMode(boolean mode) {
        this.mode = mode;
    }

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

三阶曲线相比于二阶曲线可以制作更加复杂的形状,但是对于高阶的曲线,用低阶的曲线组合也可达到相同的效果,就是传说中的降阶。因此我们对贝塞尔曲线的封装方法一般最高只到三阶曲线。

降阶与升阶

类型 释义 变化
降阶 在保持曲线形状与方向不变的情况下,减少控制点数量,即降低曲线阶数 方法变得简单,数据点变多,控制点可能减少,灵活性变弱
升阶 在保持曲线形状与方向不变的情况下,增加控制点数量,即升高曲线阶数 方法更加复杂,数据点不变,控制点增加,灵活性变强

贝塞尔曲线实例

一般使用贝塞尔曲线的情况如下:

序号 内容 用例
1 事先不知道曲线状态,需要实时计算时 方天气预报气温变化的平滑折线图
2 显示状态会根据用户操作改变时 QQ小红点,仿真翻书效果
3 一些比较复杂的运动状态(配合PathMeasure使用) 复杂运动状态的动画效果

至于只需要一个静态的曲线图形的情况,用图片岂不是更好,大量的计算会很不划算。

如果是显示SVG矢量图的话,已经有相关的解析工具了(内部依旧运用的有贝塞尔曲线),不需要手动计算。

贝塞尔曲线的主要优点是可以实时控制曲线状态,并可以通过改变控制点的状态实时让曲线进行平滑的状态变化。

QQ红点的实现效果

qq的红点去除效果,其实就是用了两条贝塞尔曲线。
这里写图片描述

基本理论:只要在拖动的时候 去改变辅助点的Y,和固定圆的半径, 就可以出来效果。

创建画笔


        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);


        mPaint.setColor(Color.RED);

绘制动圆和固定圆

 /**
     * 固定圆  并且初始化
     */
    private PointF mFixedCircle = new PointF(150f, 150f);

    /**
     * 固定圆的半径
     */
    float mFixedRadius = 14f;

    /**
     * 动圆  并且初始化
     */
    private PointF mDragCircle = new PointF(80f, 80f);

    /**
     * 动圆半径
     */
    float mDragRadius = 20f;

    /**
     * 动圆两个焦点的坐标
     */
    private PointF[] mDragPoints;

    /**
     * 固定圆的两个焦点坐标
     */
    private PointF[] mFixedPoints;

    /**
     * 控制焦点
     */
    private PointF mControlPoint;

获取两个圆之间的距离:

/**
     * 获取临时的固定圆的半径
     *
     * @return
     */
    private float getTempFiexdCircle() {
        //获取到两个圆心之间的距离
        float instance = GeometryUtil.getDistanceBetween2Points(mDragCircle, mFixedCircle);

        //这个是在连个圆之间的实际距离和我们定义的距离之间取得最小值
        instance = Math.min(instance, farestDistance);
        //0.0f--->1.0f>>>>>1.0f---》0.0f
        float percent = instance / farestDistance;

        return evaluate(percent, mFixedRadius, mFixedRadius * 0.2);

    }

    /**
     * 估值器
     *
     * @param fraction
     * @param startValue
     * @param endValue
     * @return
     */
    public Float evaluate(float fraction, Number startValue, Number endValue) {
        float startFloat = startValue.floatValue();
        return startFloat + fraction * (endValue.floatValue() - startFloat);
    }

onDraw()方法绘制圆

//根据两个圆的圆心的距离获取固定圆的半径
        float distance = getTempFiexdCircle();


        //计算连接部分
        //1、获取直线与圆的焦点
        float yOffset = mFixedCircle.y - mDragCircle.y;
        float xOffset = mFixedCircle.x - mDragCircle.x;

        /**
         * 获取斜率
         */
        Double lineK = null;
        if (xOffset != 0) {
            lineK = (double) yOffset / xOffset;
        }
        //通过几何工具获取焦点坐标
        this.mFixedPoints = GeometryUtil.getIntersectionPoints(mFixedCircle, distance, lineK);
        this.mDragPoints = GeometryUtil.getIntersectionPoints(mDragCircle, mDragRadius, lineK);
        //2、获取控制点坐标
        this.mControlPoint = GeometryUtil.getMiddlePoint(mDragCircle, mFixedCircle);

//绘制动圆
  canvas.drawCircle(mDragCircle.x, mDragCircle.y, mDragRadius, mPaint);
 //画一个固定圆
                //canvas.drawCircle(150f,150f,14f,mPaint);
   canvas.drawCircle(mFixedCircle.x, mFixedCircle.y, distance, mPaint);


 //canvas.drawCircle(150f,150f,14f,mPaint);
                canvas.drawCircle(mFixedCircle.x, mFixedCircle.y, distance, mPaint);

                //画连接部分   这个是用的那个贝塞尔曲线绘制的连接部分
                Path path = new Path();
                //跳到某个点1
                path.moveTo(mFixedPoints[0].x, mFixedPoints[0].y);
                //画曲线 1--->2
                path.quadTo(mControlPoint.x, mControlPoint.y, mDragPoints[0].x, mDragPoints[0].y);
                //画直线2---->3
                path.lineTo(mDragPoints[1].x, mDragPoints[1].y);
                //画曲线3---->4
                path.quadTo(mControlPoint.x, mControlPoint.y, mFixedPoints[1].x, mFixedPoints[1].y);

                path.close();
                canvas.drawPath(path, mPaint);


  //恢复画布
        canvas.restore();

处理onTouch()方法,让红点随手势动起来

 @Override
    public boolean onTouchEvent(MotionEvent event) {

        float x = 0;
        float y = 0;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //获取到按下的时候的坐标(因为我们已经把画布往上移动了状态栏的高度了,或者是我们在这里做判断)
                x = event.getRawX();
                y = event.getRawY();

                //更新动圆的坐标
                updataDragCircle(x, y);

                break;
            case MotionEvent.ACTION_MOVE:
                //移动的时候获取坐标
                x = event.getRawX();
                y = event.getRawY();
                updataDragCircle(x, y);

                //处理断开
                float distance = GeometryUtil.getDistanceBetween2Points(mDragCircle, mFixedCircle);
                if (distance > farestDistance) {  //如果获取到的距离大于我们定义的最大的距离
                    isOutToRange = true;  //断开设置为true
                    invalidate();  //重绘
                }

                break;
            case MotionEvent.ACTION_UP:
                if (isOutToRange) {  //如果是断开
                    isOutToRange = false;  //设置为false
                    //处理断开
                    float d = GeometryUtil.getDistanceBetween2Points(mDragCircle, mFixedCircle);
                    if (d > farestDistance) {
                        // * a、拖拽超出范围,断开-->松手-->消失
                        //松手还没有放回去
                        isDisappear = true;

                        //重绘一下
                        invalidate();
                    } else {
                        //    * b、拖拽超出范围,断开---->放回去了--->恢复
                        updataDragCircle(mFixedCircle.x, mFixedCircle.y);
                        isDisappear = false;
                    }

                } else {

                    final PointF tempDragCircle = new PointF(mDragCircle.x, mDragCircle.y);

                    //    * c、拖拽没有超出范围,断开--->恢复
                    final ValueAnimator mAnim = ValueAnimator.ofFloat(1.0f);
                    mAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                        @Override
                        public void onAnimationUpdate(ValueAnimator animation) {
                            float percent = mAnim.getAnimatedFraction();
                            PointF p = GeometryUtil.getPointByPercent(tempDragCircle, mFixedCircle, percent);
                            updataDragCircle(p.x, p.y);
                        }
                    });
                    //差之器,这个是设置弹性的
                    mAnim.setInterpolator(new OvershootInterpolator(4));
                    mAnim.setDuration(500);
                    mAnim.start();
                }


                break;
        }
        return true;
    }


    /**
     * 更新拖拽圆的圆心坐标
     *
     * @param rawX
     * @param rawY
     */
    private void updataDragCircle(float rawX, float rawY) {
        //更新的坐标
        mDragCircle.set(rawX, rawY);
        invalidate();
    }

修改onDraw()判断
isOutToRange和isDisappear分别为true和false的情况

@Override
    protected void onDraw(Canvas canvas) {

        //保持当前画布的状态
        canvas.save();
        //移动画布
        canvas.translate(0, -mStatusBarHeight);


        //根据两个圆的圆心的距离获取固定圆的半径
        float distance = getTempFiexdCircle();


        //计算连接部分
        //1、获取直线与圆的焦点
        float yOffset = mFixedCircle.y - mDragCircle.y;
        float xOffset = mFixedCircle.x - mDragCircle.x;

        /**
         * 获取斜率
         */
        Double lineK = null;
        if (xOffset != 0) {
            lineK = (double) yOffset / xOffset;
        }
        //通过几何工具获取焦点坐标
        this.mFixedPoints = GeometryUtil.getIntersectionPoints(mFixedCircle, distance, lineK);
        this.mDragPoints = GeometryUtil.getIntersectionPoints(mDragCircle, mDragRadius, lineK);
        //2、获取控制点坐标
        this.mControlPoint = GeometryUtil.getMiddlePoint(mDragCircle, mFixedCircle);


        if (!isDisappear) {
            //画拖拽圆
            //canvas.drawCircle(80f,80f,20f,mPaint);

            canvas.drawCircle(mDragCircle.x, mDragCircle.y, mDragRadius, mPaint);

            if (!isOutToRange) {
                //画一个固定圆
                //canvas.drawCircle(150f,150f,14f,mPaint);
                canvas.drawCircle(mFixedCircle.x, mFixedCircle.y, distance, mPaint);

                //画连接部分   这个是用的那个贝塞尔曲线绘制的连接部分
                Path path = new Path();
                //跳到某个点1
                path.moveTo(mFixedPoints[0].x, mFixedPoints[0].y);
                //画曲线 1--->2
                path.quadTo(mControlPoint.x, mControlPoint.y, mDragPoints[0].x, mDragPoints[0].y);
                //画直线2---->3
                path.lineTo(mDragPoints[1].x, mDragPoints[1].y);
                //画曲线3---->4
                path.quadTo(mControlPoint.x, mControlPoint.y, mFixedPoints[1].x, mFixedPoints[1].y);

                path.close();
                canvas.drawPath(path, mPaint);
            }
        }

        //恢复
        canvas.restore();

    }

漂浮的心

漂浮轨迹就是一条三阶贝塞尔曲线,结合属性动画中的估值器进行设置。
这里写图片描述

首先定义一个属性动画的估值器

public class BezierEvaluator implements TypeEvaluator<PointF> {

  private PointF mControlP1;
  private PointF mControlP2;

  public BezierEvaluator(PointF controlP1, PointF controlP2) {
      this.mControlP1 = controlP1;
      this.mControlP2 = controlP2;
  }

  @Override
  public PointF evaluate(float time, PointF start, PointF end) {

      float timeLeft = 1.0f - time;
      PointF point = new PointF();

      point.x = timeLeft * timeLeft * timeLeft * (start.x) + 3 * timeLeft * timeLeft * time *
              (mControlP1.x) + 3 * timeLeft * time *
              time * (mControlP2.x) + time * time * time * (end.x);

      point.y = timeLeft * timeLeft * timeLeft * (start.y) + 3 * timeLeft * timeLeft * time *
              (mControlP1.y) + 3 * timeLeft * time *
              time * (mControlP2.y) + time * time * time * (end.y);
      return point;
  }
}

之后自定义一个view可以生成爱心,添加透明度,缩放等动画和根据贝塞尔曲线改变其位置的属性动画。
初始化爱心图片和多个插值器等,到时随即选取

private void init() {

        // 初始化显示的图片
        drawables = new Drawable[3];
        drawables[0] = getResources().getDrawable(R.drawable.red);
        drawables[1] = getResources().getDrawable(R.drawable.yellow);
        drawables[2] = getResources().getDrawable(R.drawable.green);

        // 初始化插补器
        mInterpolators = new Interpolator[4];
        mInterpolators[0] = new LinearInterpolator();// 线性
        mInterpolators[1] = new AccelerateInterpolator();// 加速
        mInterpolators[2] = new DecelerateInterpolator();// 减速
        mInterpolators[3] = new AccelerateDecelerateInterpolator();// 先加速后减速

        // 底部 并且 水平居中
        dWidth = drawables[0].getIntrinsicWidth();
        dHeight = drawables[0].getIntrinsicHeight();
        lp = new LayoutParams(dWidth, dHeight);
        lp.addRule(CENTER_HORIZONTAL, TRUE);// 这里的TRUE 要注意 不是true
        lp.addRule(ALIGN_PARENT_BOTTOM, TRUE);

    }

入场动画

private AnimatorSet getEnterAnimator(final View target) {
        ObjectAnimator alpha = ObjectAnimator.ofFloat(target, View.ALPHA, 0.2f, 1f);
        ObjectAnimator scaleX = ObjectAnimator.ofFloat(target, View.SCALE_X, 0.2f, 1f);
        ObjectAnimator scaleY = ObjectAnimator.ofFloat(target, View.SCALE_Y, 0.2f, 1f);
        AnimatorSet enter = new AnimatorSet();
        enter.setTarget(target);
        enter.setInterpolator(new LinearInterpolator());
        enter.setDuration(500).playTogether(alpha, scaleX, scaleY);
        return enter;
    }

贝塞尔曲线动画

private ValueAnimator getBezierValueAnimator(final View target) {
        // 初始化贝塞尔估值器
        BezierEvaluator evaluator = new BezierEvaluator(getPointF(2), getPointF(1));
        // 起点在底部中心位置,终点在底部随机一个位置
        ValueAnimator animator = ValueAnimator.ofObject(evaluator, new PointF((mWidth - dWidth) /
                2, mHeight - dHeight), new PointF(random.nextInt(getWidth()), 0));
        animator.setTarget(target);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                // 这里获取到贝塞尔曲线计算出来的的x y值 赋值给view 这样就能让爱心随着曲线走啦
                PointF pointF = (PointF) valueAnimator.getAnimatedValue();
                target.setX(pointF.x);
                target.setY(pointF.y);
                // alpha动画
                target.setAlpha(1 - valueAnimator.getAnimatedFraction());
            }
        });

        animator.setDuration(3000);
        return animator;
    }

结合动画添加爱心

public void addHeart() {

        final ImageView imageView = new ImageView(getContext());
        // 随机选一个爱心
        imageView.setImageDrawable(drawables[random.nextInt(3)]);
        imageView.setLayoutParams(lp);
        addView(imageView);

        AnimatorSet finalSet = new AnimatorSet();

        AnimatorSet enterAnimatorSet = getEnterAnimator(imageView);//入场动画
        ValueAnimator bezierValueAnimator = getBezierValueAnimator(imageView);//贝塞尔曲线路径动画

        finalSet.playSequentially(enterAnimatorSet, bezierValueAnimator);
        finalSet.setInterpolator(mInterpolators[random.nextInt(4)]);
        finalSet.setTarget(imageView);

        finalSet.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                removeView((imageView));//删除爱心
            }
        });
        finalSet.start();

    }

弹性的圆

还有一个实例,就是特别出名的弹性的圆

将这个圆的动画效果拆解开看的画,可以分为5个状态。
这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这个动画效果的实现就是不同状态之间的转化加上水平位移的实现。

我们需要先了解一下如何用贝塞尔曲线画一个圆,因为我的做法是通过贝塞尔曲线来实现的。

这里写图片描述

就是所需要的数值c约等于0.551915024494f,具体可以参考这篇文章,http://spencermortensen.com/articles/bezier-circle/,那么这个c的值的作用,就是把图中的1理解为圆的半径,那么对应的另外个值就应该是半径乘以0.551915024494f。

这里写图片描述

坐标轴也就是Android中的坐标轴了,如果我们打算用贝塞尔曲线来画这么一个圆的话,我们需要知道这个圆的半径,以及图中的M的值,知道这两个值的话就能够知道图中12个点的坐标,知道坐标就能够用Path的cubicTo方法来使用贝塞尔曲线画出圆了。

public class BezierDemo3 extends View {
    private static final float C = 0.551915024494f;     // 一个常量,用来计算绘制圆形贝塞尔曲线控制点的位置

    private Paint mPaint;
    private int mCenterX, mCenterY;

    private PointF mCenter = new PointF(0,0);
    private float mCircleRadius = 200;                  // 圆的半径
    private float mDifference = mCircleRadius*C;        // 圆形的控制点与数据点的差值

    private float[] mData = new float[8];               // 顺时针记录绘制圆形的四个数据点
    private float[] mCtrl = new float[16];              // 顺时针记录绘制圆形的八个控制点

    private float mDuration = 1000;                     // 变化总时长
    private float mCurrent = 0;                         // 当前已进行时长
    private float mCount = 100;                         // 将时长总共划分多少份
    private float mPiece = mDuration/mCount;            // 每一份的时长


    public Bezier3(Context context) {
        this(context, null);

    }

    public Bezier3(Context context, AttributeSet attrs) {
        super(context, attrs);

        mPaint = new Paint();
        mPaint.setColor(Color.BLACK);
        mPaint.setStrokeWidth(8);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setTextSize(60);


        // 初始化数据点

        mData[0] = 0;
        mData[1] = mCircleRadius;

        mData[2] = mCircleRadius;
        mData[3] = 0;

        mData[4] = 0;
        mData[5] = -mCircleRadius;

        mData[6] = -mCircleRadius;
        mData[7] = 0;

        // 初始化控制点

        mCtrl[0]  = mData[0]+mDifference;
        mCtrl[1]  = mData[1];

        mCtrl[2]  = mData[2];
        mCtrl[3]  = mData[3]+mDifference;

        mCtrl[4]  = mData[2];
        mCtrl[5]  = mData[3]-mDifference;

        mCtrl[6]  = mData[4]+mDifference;
        mCtrl[7]  = mData[5];

        mCtrl[8]  = mData[4]-mDifference;
        mCtrl[9]  = mData[5];

        mCtrl[10] = mData[6];
        mCtrl[11] = mData[7]-mDifference;

        mCtrl[12] = mData[6];
        mCtrl[13] = mData[7]+mDifference;

        mCtrl[14] = mData[0]-mDifference;
        mCtrl[15] = mData[1];
    }


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


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
         drawCoordinateSystem(canvas);       // 绘制坐标系

        canvas.translate(mCenterX, mCenterY); // 将坐标系移动到画布中央
        canvas.scale(1,-1);                 // 翻转Y轴

        drawAuxiliaryLine(canvas);


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

        Path path = new Path();
        path.moveTo(mData[0],mData[1]);

        path.cubicTo(mCtrl[0],  mCtrl[1],  mCtrl[2],  mCtrl[3],     mData[2], mData[3]);
        path.cubicTo(mCtrl[4],  mCtrl[5],  mCtrl[6],  mCtrl[7],     mData[4], mData[5]);
        path.cubicTo(mCtrl[8],  mCtrl[9],  mCtrl[10], mCtrl[11],    mData[6], mData[7]);
        path.cubicTo(mCtrl[12], mCtrl[13], mCtrl[14], mCtrl[15],    mData[0], mData[1]);

        canvas.drawPath(path, mPaint);

        mCurrent += mPiece;
        if (mCurrent < mDuration){

            mData[1] -= 120/mCount;
            mCtrl[7] += 80/mCount;
            mCtrl[9] += 80/mCount;

            mCtrl[4] -= 20/mCount;
            mCtrl[10] += 20/mCount;

            postInvalidateDelayed((long) mPiece);
        }
    }

    // 绘制辅助线
    private void drawAuxiliaryLine(Canvas canvas) {
        // 绘制数据点和控制点
        mPaint.setColor(Color.GRAY);
        mPaint.setStrokeWidth(20);

        for (int i=0; i<8; i+=2){
            canvas.drawPoint(mData[i],mData[i+1], mPaint);
        }

        for (int i=0; i<16; i+=2){
            canvas.drawPoint(mCtrl[i], mCtrl[i+1], mPaint);
        }


        // 绘制辅助线
        mPaint.setStrokeWidth(4);

        for (int i=2, j=2; i<8; i+=2, j+=4){
            canvas.drawLine(mData[i],mData[i+1],mCtrl[j],mCtrl[j+1],mPaint);
            canvas.drawLine(mData[i],mData[i+1],mCtrl[j+2],mCtrl[j+3],mPaint);
        }
        canvas.drawLine(mData[0],mData[1],mCtrl[0],mCtrl[1],mPaint);
        canvas.drawLine(mData[0],mData[1],mCtrl[14],mCtrl[15],mPaint);
    }

    // 绘制坐标系
    private void drawCoordinateSystem(Canvas canvas) {
        canvas.save();                      // 绘制做坐标系

        canvas.translate(mCenterX, mCenterY); // 将坐标系移动到画布中央
        canvas.scale(1,-1);                 // 翻转Y轴

        Paint fuzhuPaint = new Paint();
        fuzhuPaint.setColor(Color.RED);
        fuzhuPaint.setStrokeWidth(5);
        fuzhuPaint.setStyle(Paint.Style.STROKE);

        canvas.drawLine(0, -2000, 0, 2000, fuzhuPaint);
        canvas.drawLine(-2000, 0, 2000, 0, fuzhuPaint);

        canvas.restore();
    }
}

这样我们就知道如何使用贝塞尔曲线来绘制一个圆了。也就是状态1和状态5我们都会绘制了,接下来看看状态2如何绘制。
这里写图片描述

状态2其实就是把右边的点向右移动点距离
这里写图片描述

状态3的实现就是在状态2的基础上修改了个值,一个是M的值加大,让圆看起来跟肥一点,还有就是圈住的那些点向右移动,做到居中。

实现如下:

public class Ball {
    /**
     * 圆心横坐标
     */
    public float x;
    /**
     * 圆心纵坐标
     */
    public float y;
    /**
     * 半径
     */
    public float radius;

    /**
     * 构造方法
     * @param x
     * @param y
     * @param radius
     */
    public Ball(float x, float y, float radius) {
        this.x = x;
        this.y = y;
        this.radius = radius;
        this.topX = x;
        this.topY = y - radius;
        this.bottomX = x;
        this.bottomY = y + radius;
        this.leftX = x - radius;
        this.leftY = y;
        this.rightX = x + radius;
        this.rightY = y;
    }

    public void refresh(float x, float y, float topX, float topY, float bottomX, float bottomY,
                        float leftX, float leftY, float rightX, float rightY){
        this.x = x;
        this.y = y;
        this.topX = topX;
        this.topY = topY;
        this.bottomX = bottomX;
        this.bottomY = bottomY;
        this.leftX = leftX;
        this.leftY = leftY;
        this.rightX = rightX;
        this.rightY = rightY;
    }

    /**
     * 球左边点的坐标
     */
    public float leftX;
    public float leftY;

    /**
     * 球右边点的坐标
     */
    public float rightX;
    public float rightY;

    /**
     * 球顶点的坐标
     */
    public float topX;
    public float topY;

    /**
     * 球底部点的坐标
     */
    public float bottomX;
    public float bottomY;
}
public class MagicBall 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 long mDuration = 1200;

    /**
     * 偏移值
     */
    private float offsetTop, offsetBottom, offsetLeft, offsetRight;
    /**
     * 运动方向
     */
    private int mDirection;
    /**
     * 动画完成百分比(0~1)
     */
    private float mAnimPercent;
    /**
     * 弹性距离
     */
    private float mElasticDistance;
    /**
     * 弹性比例
     */
    private float mElasticPercent = 0.8f;
    /**
     * 位移距离
     */
    private float mMoveDistance;

    /**
     * 圆形偏移比例
     */
    private float c = 0.551915024494f;

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

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

    /**
     * 构造方法
     *
     * @param x 圆心横坐标
     * @param y 圆心纵坐标
     * @param radius 圆半径
     */
    public MagicBall(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 MagicBall(endPoint.x, endPoint.y, radius);
        this.mStartPoint = new MagicBall(x, y, radius);
        this.mStatusPoint1 = new MagicBall(x, y, radius);
        this.mStatusPoint2 = new MagicBall(x, y, radius);
        this.mStatusPoint3 = new MagicBall(x, y, radius);
        this.mStatusPoint4 = new MagicBall(x, y, radius);
        this.mStatusPoint5 = new MagicBall(x, y, radius);
        this.mElasticBallInterface = elasticBallInterface;
        calculateDirection();
        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(drawMagicCircle(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 calculateDirection() {
        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 drawMagicCircle(
            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));
    }

}
阅读更多
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u012124438/article/details/75949057
文章标签: 贝塞尔曲线 android
个人分类: 自定义控件
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

不良信息举报

Android 贝塞尔曲线解析

最多只允许输入30个字

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭