高级UI<第二十八篇>:贝赛尔曲线

(1)概念

将任意一条曲线转化为精确的数学公式。

(2)组成部分

数据点: 曲线的起点和终点。
控制点: 控制点决定了一条路径的弯曲轨迹,根据控制点的个数,贝塞尔曲线被分为一阶贝塞尔曲线(0个控制点)、二阶贝塞尔曲线(1个控制点)、三阶贝塞尔曲线(2个控制点)等等。

假设现在有n阶贝赛尔曲线,那么,该贝赛尔曲线的控制点数量为:n-1。

(3)一阶贝塞尔曲线

首先,说一说一阶贝赛尔曲线。

Path的lineTorLineTo可以构建一阶贝塞尔曲线,一阶贝赛尔曲线,其实就是一条直线。

假设现在有两点,分别是P0和P1,现在从P0开始向P1勾勒出一阶贝赛尔曲线)(其实就是一条直线),它的计算公式如下:

P(t) = P0 + (P1-P0)t = (1-t)P0 + tP1,t∈[0,1]。

那么,这个公式是怎么来的呢?

这个公式是经过德卡斯特里奥算法推导得出,贝赛尔曲线是从P0到P1勾勒出的若干个点组成,假设P0和P1之间的点为P,那么

t = (P - P0)/ (P1 - P0),整理之后就是上面的公式了。

当参数t由0到1变化时,其过程如下

一阶贝塞尔曲线.gif

这个t值是曲线占总曲线长度的分量,你可以把总曲线长度看作1,当t为0.5时,图中的黑点就在这条曲线的中间(虽然一次贝塞尔曲线实际上就是条直线)

根据这个公式,我们也可以代码实现其变化,代码如下:

public class Bers extends View {

    private Paint mPaint;

    private float t = 0;

    private float startX = 100;
    private float endX = 1000;
    private float startY = 500;
    private float endY = 500;

    public Bers(Context context) {
        this(context, null);
    }

    public Bers(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public Bers(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init(){
        mPaint = new Paint();
        mPaint.setColor(Color.BLUE);
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth(8);
        mPaint.setTextSize(60);
        mPaint.setStyle(Paint.Style.STROKE);
    }



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

        //关闭硬件加速
        setLayerType(LAYER_TYPE_SOFTWARE, mPaint);

        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setColor(Color.GRAY);
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth(8);

        Path path = new Path();
        path.moveTo(100, 500);
        path.lineTo(1000, 500);
        //绘制路径
        canvas.drawPath(path, mPaint);

        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setStrokeWidth(25);
        mPaint.setColor(Color.parseColor("#D81B60"));

        //画起始点
        canvas.drawPoint(100, 500, mPaint);
        //画终点
        canvas.drawPoint(1000, 500, mPaint);

        mPaint.setStrokeWidth(8);

        //这里直接套公式
        //P(t) = P0 + (P1-P0)t = (1-t)P0 + tP1,t∈[0,1]
        //P0(100,500)   P1(1000,500)
        canvas.drawLine(startX, startY, (1 - t) * startX + t * endX, (1 - t) * startY + t * endY, mPaint);

        t = t >= 1 ? 0 : t + 0.002f;

        invalidate();

    }
}

效果如下:

48.gif
(4)二阶贝塞尔曲线

给定点P0、P1,P2,其中P1是控制点,这条曲线由下式给出:

P(t) = (1 - t)2P0 + 2t(1-t)P1 + t2P2,t∈[0,1]。

这个公式是经过德卡斯特里奥算法推导得出,贝赛尔曲线是从P0到P2勾勒出的若干个点组成,其中P1为控制点,它的推导过程我就在本子上体现了,如下:

二阶贝赛尔曲线算法.jpg

当参数t由0到1变化时,其过程如下

二阶贝塞尔曲线.gif

Path的quadTorQuadTo可以构建二阶贝塞尔曲线。

quadTo(float x1, float y1, float x2, float y2)

x1: 控制点x坐标
y1: 控制点y坐标
x2: 终点x坐标
y2: 终点y坐标

三个点可以构成一个二阶贝塞尔曲线,分别是起点控制点终点
起点: 一般而言(0, 0)是贝塞尔曲线的起点,可以使用Path的moveTo来改变;
控制点:quadTo方法的前两个参数决定;
终点:quadTo方法的后两个参数决定;

其中,控制点的位置决定这曲线的曲率。

那么我们来动态画一个曲线,当点击屏幕的某个位置时,控制点随之移动到当前位置。

效果图如下:

55.gif

代码如下:

public class TestView extends View {

    private float mStartX = 300;//曲线开始位置X坐标
    private float mStartY = 1500;//曲线开始位置Y坐标
    private float mEndX = 1100;//曲线终点位置X坐标
    private float mEndY = 1500;//曲线终点位置Y坐标
    private float mMoveX = mStartX;//初始化移动位置X坐标
    private float mMoveY = mStartY;//初始化移动位置Y坐标
    private Paint mPaint;

    public TestView(Context context) {
        this(context, null);
    }

    public TestView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init(){
        mPaint = new Paint();
        mPaint.setColor(Color.BLUE);
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth(8);
        mPaint.setTextSize(60);
        mPaint.setStyle(Paint.Style.STROKE);
    }

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

        //关闭硬件加速
        setLayerType(LAYER_TYPE_SOFTWARE, mPaint);

        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setColor(Color.BLUE);
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth(8);

        Path path = new Path();
        //确定贝塞尔曲线的起始点
        path.moveTo(mStartX, mStartY);
        //二阶贝塞尔曲线(确定贝塞尔曲线的控制点和终点)
        path.quadTo(mMoveX, mMoveY, mEndX, mEndY);
        path.lineTo(mMoveX, mMoveY);
        path.lineTo(mStartX, mStartY);
        //绘制路径
        canvas.drawPath(path, mPaint);


        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setStrokeWidth(25);
        mPaint.setColor(Color.parseColor("#D81B60"));
        //画起始点
        canvas.drawPoint(mStartX, mStartY, mPaint);
        //画终点
        canvas.drawPoint(mEndX, mEndY, mPaint);


        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(Color.BLACK);
        mPaint.setTextAlign(Paint.Align.CENTER);
        mPaint.setStrokeWidth(2);
        Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
        float textHeight = fontMetrics.descent - fontMetrics.ascent;
        canvas.drawText("起点("+mStartX+","+(mStartY+textHeight)+")", mStartX, mStartY+textHeight, mPaint);
        canvas.drawText("终点("+mEndX+","+(mEndY+textHeight)+")", mEndX, mEndY+textHeight, mPaint);
        if(mStartX != mMoveX || mStartY != mMoveY){
            Log.d("aaa", "mStartX:"+mStartX);
            Log.d("aaa", "mMoveX:"+mMoveX);
            Log.d("aaa", "mStartY:"+mStartY);
            Log.d("aaa", "mMoveY:"+mMoveY);
            canvas.drawText("控制点("+mMoveX+","+(mMoveY-textHeight)+")", mMoveX, mMoveY-textHeight, mPaint);
        }

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        float x = event.getX();
        float y = event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE:
                mMoveX = x;
                mMoveY = y;
                Log.d("aaa", "mMoveX:"+mMoveX);
                Log.d("aaa", "mMoveY:"+mMoveY);
                invalidate();
                break;
        }
        return true;
    }
}
(5)三阶贝塞尔曲线

给定点P0、P1,P2,P3,其中P1和P2是控制点,这条曲线由下式给出:

P(t) = P0(1-t)3 + 3P1t(1-t)2 + 3P2t2(1-t) + P3t3,t∈[0,1]

其推导过程,我也写在了本子上,如下:

三阶贝赛尔曲线算法.jpg

如图所示,一个3阶公式被我降成了3个二阶公式。

当参数t由0到1变化时,其过程如下


三阶贝塞尔曲线.gif

Path的cubicTorCubicTo可以构建三阶贝塞尔曲线。

cubicTo(float x1, float y1, float x2, float y2, float x3, float y3)

x1: 第一个控制点x坐标
y1: 第一个控制点y坐标
x2: 第二个控制点x坐标
y2: 第二个控制点y坐标
x2: 终点x坐标
y2: 终点y坐标

四个点可以构成一个三阶贝塞尔曲线,分别是起点、控制点(2个)、终点
起点: 一般而言(0, 0)是贝塞尔曲线的起点,可以使用Path的moveTo来改变;
控制点: 由cubicTo方法的前四个参数决定;
终点: 由cubicTo方法的后两个参数决定;

其中,控制点的位置决定这曲线的曲率。

(6)绘制手指轨迹

Path的lineTo可以实现手指绘制功能,代码也比较简单

public class FingerView extends View {

    private Paint mPaint;
    private Path path;

    public FingerView(Context context) {
        this(context, null);
    }

    public FingerView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FingerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init(){
        mPaint = new Paint();
        mPaint.setColor(Color.BLUE);
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth(8);
        mPaint.setTextSize(60);
        mPaint.setStyle(Paint.Style.STROKE);
        path = new Path();
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //关闭硬件加速
        setLayerType(LAYER_TYPE_SOFTWARE, mPaint);

        canvas.drawPath(path, mPaint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        float x = event.getX();
        float y = event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                path.moveTo(x, y);
                break;
            case MotionEvent.ACTION_MOVE:
                path.lineTo(x, y);
                invalidate();
                break;
        }
        return true;
    }
}

效果如下:

56.gif

使用lineTo虽然可以实现手指绘制功能,但是只是若干个线段连接起来的,必然会知道线条看起来不怎么平滑,使用二阶赛贝尔曲线可以让线条看起来更加平滑一下。

思路:

二阶贝塞尔曲线需要三个点:起点控制点终点

起点: 手指点击时的点;
终点: 手指移动时的点;
控制点: 计算控制点很重要,那么该怎么计算呢?

直接贴一下我的代码吧

public class FingerView extends View {

    private Paint mPaint;
    private Path path;
    private float mStartX = 0;
    private float mStartY = 0;

    public FingerView(Context context) {
        this(context, null);
    }

    public FingerView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FingerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init(){
        mPaint = new Paint();
        mPaint.setColor(Color.BLUE);
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth(8);
        mPaint.setTextSize(60);
        mPaint.setStyle(Paint.Style.STROKE);
        path = new Path();
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //关闭硬件加速
        setLayerType(LAYER_TYPE_SOFTWARE, mPaint);

        canvas.drawPath(path, mPaint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        float eventX = event.getX();
        float eventY = event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                //设置贝塞尔曲线的起点
                mStartX = eventX;
                mStartY = eventY;
                path.moveTo(mStartX, mStartY);
                break;
            case MotionEvent.ACTION_MOVE:
                //eventX和eventY是贝塞尔曲线的终点
                //现在需要做的是计算出贝塞尔曲线的控制点
                float controlX = (eventX + mStartX) / 2;
                float controlY = (eventY + mStartY) / 2;
                path.quadTo(controlX, controlY, eventX, eventY);
                Log.d("aaa", "("+mStartX+","+mStartY+")("+eventX+","+eventY+")("+controlX+","+controlY+")");
                //将上一次绘制的结束点作为下一次绘制的起点
                mStartX = eventX;
                mStartY = eventY;
                invalidate();
                break;
        }
        return true;
    }
}

核心的计算代码都在onTouchEvent里。

演示如下:

57.gif
(7)贝赛尔曲线实现水波纹

代码如下:

public class WaveView extends View {
    private Paint mPaint;
    private int mWidth;
    private int mHeight;
    private int mWaveHeight;
    private int mWaveDx;
    private int dx;

    public WaveView(Context context) {
        this(context, null);
    }

    public WaveView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public WaveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setColor(Color.parseColor("#FF3891"));
        mPaint.setStyle(Paint.Style.FILL);
        //波长的的长度(这里设置为屏幕的宽度)
        mWaveDx = getResources().getDisplayMetrics().widthPixels;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //控件的宽高
        mWidth = MeasureUtils.measureView(widthMeasureSpec, mWaveDx);
        mHeight = MeasureUtils.measureView(heightMeasureSpec, 300);
        //水波的高度
        mWaveHeight = DensityUtil.dip2px(getContext(), 16);

    }

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


    private void drawWave(Canvas canvas) {
        Path path = new Path();
        path.reset();
        path.moveTo(-mWaveDx + dx, mHeight / 2);
        for (int i = -mWaveDx; i < getWidth() + mWaveDx; i += mWaveDx) {
            path.rQuadTo(mWaveDx / 4, -mWaveHeight, mWaveDx / 2, 0);
            path.rQuadTo(mWaveDx / 4, mWaveHeight, mWaveDx / 2, 0);

        }
        path.lineTo(mWidth, mHeight);
        path.lineTo(0, mHeight);
        //path.close() 绘制封闭的区域
        path.close();
        canvas.drawPath(path, mPaint);
    }

    public void startAnimation() {
        ValueAnimator valueAnimator = ValueAnimator.ofInt(0, mWaveDx);
        valueAnimator.setDuration(2000);
        valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                //水平方向的偏移量
                dx = ( int ) animation.getAnimatedValue();
                invalidate();
            }

        });
        valueAnimator.start();

    }
}

DensityUtil.java

public class DensityUtil {

    public static int dip2px(Context var0, float var1) {
        float var2 = var0.getResources().getDisplayMetrics().density;
        return ( int ) (var1 * var2 + 0.5F);
    }

    public static int dp2px(float value) {
        final float scale = Resources.getSystem().getDisplayMetrics().densityDpi;
        return ( int ) (value * (scale / 160) + 0.5f);
    }

    public static int px2dip(Context var0, float var1) {
        float var2 = var0.getResources().getDisplayMetrics().density;
        return ( int ) (var1 / var2 + 0.5F);
    }

    public static int sp2px(Context var0, float var1) {
        float var2 = var0.getResources().getDisplayMetrics().scaledDensity;
        return ( int ) (var1 * var2 + 0.5F);
    }

    public static int px2sp(Context var0, float var1) {
        float var2 = var0.getResources().getDisplayMetrics().scaledDensity;
        return ( int ) (var1 / var2 + 0.5F);
    }

}

MeasureUtils.java

public class MeasureUtils {
    /**
     * 用于View的测量
     *
     * @param measureSpec 测量模式和大小
     * @param defaultSize 默认的大小
     * @return
     */
    public static int measureView(int measureSpec, int defaultSize) {
        int measureSize;
        //获取用户指定的大小以及模式
        int mode = View.MeasureSpec.getMode(measureSpec);
        int size = View.MeasureSpec.getSize(measureSpec);
        //根据模式去返回大小
        if (mode == View.MeasureSpec.EXACTLY) {
            //精确模式(指定大小以及match_parent)直接返回指定的大小
            measureSize = size;
        } else {
            //UNSPECIFIED模式、AT_MOST模式(wrap_content)的话需要提供默认的大小
            measureSize = defaultSize;
            if (mode == View.MeasureSpec.AT_MOST) {
                //AT_MOST(wrap_content)模式下,需要取测量值与默认值的最小值
                measureSize = Math.min(measureSize, size);
            }
        }
        return measureSize;
    }

    private int measureSize(int measureSpec) {
        int result;
        int specMode = View.MeasureSpec.getMode(measureSpec);
        int specSize = View.MeasureSpec.getSize(measureSpec);
        if (specMode == View.MeasureSpec.EXACTLY) {
            result = specSize;
        } else {
            result = 300;
            if (specMode == View.MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize);
            }
        }
        return result;

    }

}

动画效果如下:

41.gif

[本章完...]

你可能感兴趣的:(高级UI<第二十八篇>:贝赛尔曲线)