微信扫一扫

028-83195727 , 15928970361
business@forhy.com

一张图带你彻底了解二阶贝塞尔曲线

Android,贝塞尔曲线,原理,自定义view2016-06-10

上一篇自定义View中,贝塞尔曲线出现的频率很高,有小伙伴就问到关于贝塞尔曲线控制点坐标怎么计算的问题。一阶贝塞尔曲线是一条直线,确定起点终点即可,三阶贝塞尔曲线有两个控制点,相对比较复杂。二阶贝塞尔曲线只有一个控制点,在实际开发中使用的很多。今天讨论的就是关于二阶贝塞尔曲线的控制点坐标计算问题。

到底怎样一张图就能够彻底了解二阶贝塞尔曲线呢,往下看就知道了:

设置二阶贝塞尔曲线的方法:
moveTo(float x,float y)
其中x,y的坐标代表图中曲线左边起点的位置坐标
quadTo(float x1, float y1, float x2, float y2 )
其中x1,y1的坐标就是图中小圆点的位置,也就是控制点的坐标
x2,y2的坐标就是图中曲线右边终点的位置坐标

代码中怎么实现的,一起看一下:


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(10);
        path.reset();
        path.moveTo(mWidth * 1 / 8, mHeight * 1 / 5);
        path.quadTo(xWidth, yHeight, mWidth * 7 / 8, mHeight * 1 / 5);
        canvas.drawPath(path, paint);
        paint.setStyle(Paint.Style.FILL);
        //控制点
        canvas.drawCircle(xWidth, yHeight, 10, paint);

    }


    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                xWidth = x;
                yHeight = y;
                postInvalidate();
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return true;
    }

其实就是重写了onTouchEvent(MotionEvent ev)方法,在action为MotionEvent.ACTION_MOVE的时候,得到滑动时候的x,y坐标,然后赋值给 path.quadTo(xWidth, yHeight, mWidth * 7 / 8, mHeight * 1 / 5)方法中的前两个参数,也就是控制点的坐标。然后调用postInvalidate()来重新进行绘制即可。最后记得在onTouchEvent(MotionEvent ev)方法后返回true,表示View消耗当前滑动事件。

哈哈,看完是不是觉得很简单了。基于这张原理图,实际开发中很多应用。我后来做了一些改进,算是小小拓展一下。看看下面的效果图:

是不是一种很熟悉的赶脚,什么清理小火箭呀,弹性球呀,都是换汤不换药的东东。撸代码:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(10);
        path.reset();
        path.moveTo(mWidth * 1 / 8, mHeight * 2 / 5);
        path.quadTo(mWidth * 4 / 8, yHeight, mWidth * 7 / 8, mHeight * 2 / 5);
        canvas.drawPath(path, paint);
        paint.setStyle(Paint.Style.FILL);
        //绘制圆
        canvas.drawCircle(mWidth * 4 / 8, yCircle, mWidth / 10, paint);
    }

与之前第一张图不一样的是,我让path.quadTo()的第一个参数固定住了,这样就保证弧线只会在竖直方向上进行改变。

   @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        int action = ev.getAction();
        int y = (int) ev.getY();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                yDown = y;
                break;
            case MotionEvent.ACTION_MOVE:
                yMove = y;
                int dy = yMove - yDown;
                //如果是从上往下滑动
                if (dy > 0 && Math.abs(dy) > mTouchSlop * 2) {
                    isSlide = true;
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

重写了dispatchTouchEvent(MotionEvent ev)的方法是为了判断是否是从上往下滑动,确保从下往上滑动时无效的。为了使滑动明显一点,让竖直方向上的滑动距离大于默认的滑动距离的两倍,才设置滑动有效。

    @Override
    public boolean onTouchEvent(MotionEvent ev) {

        int action = ev.getAction();
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                if (isSlide) {
                    yHeight = y;
                    postInvalidate();
                    yCircle = (mHeight * 2 / 5 - mWidth / 10) + (yHeight - mHeight * 2 / 5) / 2;
                    if (yHeight > mHeight * 7 / 10) {
                        startAnim();
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return true;
    }

重写onTouchEvent(MotionEvent ev)方法,在action为MotionEvent.ACTION_MOVE时候,如果滑动有效,并且滑动y坐标大于高度的7/10的时候,开启我们的动画。

    private void startAnim() {
        //圆球动画
        ValueAnimator circleYAnimator = ValueAnimator.ofFloat(yCircle, (0 - mWidth / 10));
        circleYAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                yCircle = (float) animation.getAnimatedValue();
                postInvalidate();
            }
        });

        //弧线动画
        ValueAnimator arcYAnimator = ValueAnimator.ofInt(yHeight, mHeight * 2 / 5);
        arcYAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                yHeight = (int) animation.getAnimatedValue();
                postInvalidate();
            }
        });
        animSet.setDuration(3000);
        animSet.play(circleYAnimator).before(arcYAnimator);
        animSet.start();
    }

这里又要提到属性动画的优越性了,不仅可以作用在view上,也可以作用于某个对象上。只需要设置好开始值与结束值,添加一个动画的监听,就能够得到变化的值,再使用postInvalidate()方法,从而调用onDraw()方法来进行数值的改变并重新绘制。最后设置一个组合动画—-AnimatorSet,圆的动画完成以后,再执行弧线的动画。

OK,相信看到这里,你对二阶贝塞尔曲线有了新的认识,下一篇自定义View再见~~