继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

Android 路径绘制艺术——贝塞尔曲线

慕标5832272
关注TA
已关注
手记 1245
粉丝 229
获赞 1002

webp

 

目录

1)什么是贝塞尔曲线
2)贝塞尔曲线图解
3)Android绘制贝塞尔曲线
4)绘制水波纹效果

 

概述

什么是贝塞尔曲线?

贝塞尔曲线的数学基础是早在 1912 年就广为人知的伯恩斯坦多项式。但直到 1959 年,当时就职于雪铁龙的法国数学家 Paul de Casteljau 才开始对它进行图形化应用的尝试,并提出了一种数值稳定的 de Casteljau 算法。然而贝塞尔曲线的得名,却是由于 1962 年另一位就职于雷诺的法国工程师 Pierre Bézier 的广泛宣传。他使用这种只需要很少的控制点就能够生成复杂平滑曲线的方法,来辅助汽车车体的工业设计。

只要你使用过图像处理工具,肯定对“钢笔”这个词不陌生,它本质上就是运用贝塞尔曲线来作为计算基础绘制出来的路径:

webp

钢笔


 

贝塞尔曲线原理

贝塞尔曲线是怎么描绘出这样一条弧线的呢,其实主要是依靠顶点间的比例来计算,以二阶贝塞尔为例,示意图如下:

webp

二阶计算示意图


可以看到一共有6个点,假设此时AD占AB的25%,那么在BC上也有这么一个点F,使得BF:BC也是25%,连接DF,在DF上面再找出使得DG:DF=25%的点G,以这个为基本公式,绘制出D点从A运动到B的过程中,计算出来一系列的G点形成的弧线,即为二阶贝塞尔曲线的路径,动态效果图如下:


webp

二阶贝塞尔动图


三阶贝塞尔曲线其实就是在二阶的基础上,再增加一条边线,如下:


webp

三阶贝塞尔示意图

可以看到,多出了一条CD线,同样是需要满足AE:AB = BF:BC = CG:CD,计算出E、F、G之后,连接EF和FG,可以得到两条直线,接着就按照二阶的计算方式继续计算,得到点O的位置,可以看出三阶是在二阶的基础上再套一层,所以才称之为三阶贝塞尔,动态效果图如下:


webp

三阶贝塞尔动图

依此类推,还会有四阶、五阶等等更复杂的贝塞尔曲线,但在Android开发中只提供了二阶和三阶的API,因此我们只探讨这两种的绘制方式。

 

绘制贝塞尔曲线

在Android中,Path类提供了四个绘制贝塞尔曲线的方法:

二阶贝塞尔绘制API:
public void quadTo(float x1, float y1, float x2, float y2)
public void rQuadTo(float dx1, float dy1, float dx2, float dy2)
三阶贝塞尔绘制API:
public void quadTo(float x1, float y1, float x2, float y2, float x3, float y3)
public void rQuadTo(float dx1, float dy1, float dx2, float dy2, float dx3, float dy3)

可以看到二阶与三阶的区别就在于多了一组参数,首先看下二阶,刚才已经分析了二阶贝塞尔的绘制一共有3个重要的顶点,可以理解为起始点(示意图中的A),控制点(示意图中的B),终点(示意图中的C),这里传入两个顶点,分别代表着控制点和终点,那么问题来了,起始点呢?起始点就是Path上一次的终点(比如moveTo移动到的点),如果没有指定(即之前还从未移动过Path),则默认以控件左上角为起始点,举个例子,我们绘制一段简单的贝塞尔:

/**
 * Created by YANG on 2019/2/23.
 */public class BezierView extends View {    private Paint mPaint;    private Path mBezierPath;    private Path mPointPath;    private Point mStartPoint;    private Point mControlPoint;    private Point mEndPoint;    public BezierView(Context context) {        this(context, null);
    }    public BezierView(Context context, @Nullable AttributeSet attrs) {        this(context, attrs, 0);
    }    public BezierView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);
        init();
    }    private void init() {
        mPaint = new Paint();
        mPaint.setColor(Color.BLACK);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(5);

        mBezierPath = new Path();
        mPointPath = new Path();

        mStartPoint = new Point();
        mStartPoint.set(100, 300);
        mControlPoint = new Point();
        mControlPoint.set(300, 100);
        mEndPoint = new Point();
        mEndPoint.set(500, 500);
    }    @Override
    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);        //贝塞尔
        mBezierPath.moveTo(mStartPoint.x, mStartPoint.y);
        mBezierPath.quadTo(mControlPoint.x, mControlPoint.y, mEndPoint.x, mEndPoint.y);        //连接线
        mPointPath.moveTo(mStartPoint.x, mStartPoint.y);
        mPointPath.lineTo(mControlPoint.x, mControlPoint.y);
        mPointPath.lineTo(mEndPoint.x, mEndPoint.y);        //绘制起始点、控制点、终点的连线
        canvas.drawPath(mPointPath, mPaint);        //绘制贝塞尔
        mPaint.setColor(Color.RED);
        canvas.drawPath(mBezierPath, mPaint);
    }
}

一共声明了3个点,且首先调用moveTo(mStartPoint.x, mStartPoint.x)将起始点移动到(300,300),接着调用quadTo将控制点和终点传进去,就可以得到一条贝塞尔曲线(红色部分):

webp

二阶绘制


因此quadTo传进去的参数是控制点和终点的坐标位置,那Path的rQuadTo又有什么用呢?其实rQuadTo功能上跟quadTo是一样的,但是传进去的是相对距离,也就是说相对于起始点的位移,比如我们在刚才的例子中再加点东西,追加一段曲线mBezierPath.rQuadTo(200, 300, 400, -200);

@Override
    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);        //贝塞尔
        mBezierPath.moveTo(mStartPoint.x, mStartPoint.y);
        mBezierPath.quadTo(mControlPoint.x, mControlPoint.y, mEndPoint.x, mEndPoint.y);
        mBezierPath.rQuadTo(200, 300, 400, -200);        //连接线
        mPointPath.moveTo(mStartPoint.x, mStartPoint.y);
        mPointPath.lineTo(mControlPoint.x, mControlPoint.y);
        mPointPath.lineTo(mEndPoint.x, mEndPoint.y);        //绘制起始点、控制点、终点的连线
        canvas.drawPath(mPointPath, mPaint);        //绘制贝塞尔
        mPaint.setColor(Color.RED);
        canvas.drawPath(mBezierPath, mPaint);
    }

效果如图:


webp

二阶绘制波浪线

可以看到第二段曲线的起点是第一段曲线的终点,且可以发现,rQuadTo传的控制点(200,300)并非坐标,而是相对于第一段曲线的终点(500,500)来计算,即(500+200, 500+300)才是第二段曲线控制点的真正坐标,同理第二段曲线终点的坐标是(500+40, 500-200)。

三阶贝塞尔曲线的方法的使用方法跟二阶贝塞尔曲线差不多,就不再复述了。

 

绘制水波纹效果

上面的例子绘制了一段简单的波浪线,这其实就是我们绘制水波纹效果的基础,就相当于完成了一个浪,如果有很多个水浪就可以组合成此起彼伏的效果:

public class BezierView extends View{    private Paint paint;    private Path mPath;    private int mItemWidth = 600;    public BezierView2(Context context) {        this(context, null);
    }    public BezierView2(Context context, @Nullable AttributeSet attrs) {        this(context, attrs, 0);
    }    public BezierView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);
        init();
    }    private void init(){
        paint = new Paint();
        paint.setColor(Color.BLACK);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(5);

        mPath = new Path();
    }    @Override
    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);
        mPath.reset();        int halfItem = mItemWidth / 2;
        mPath.moveTo(0, 300);        for(int i=0; i<mItemWidth + getWidth(); i+=mItemWidth){
            mPath.rQuadTo(halfItem/2, -100, halfItem, 0);
            mPath.rQuadTo(halfItem/2, 100, halfItem, 0);
        }
        canvas.drawPath(mPath, paint);
    }
}

我们将每段波浪的宽度定义为600,因此每半段波浪的高度为300,首先将起点移动到(0,300)处,即整个View最左侧的一个点,接着开始遍历绘制后续多段波浪,mPath.rQuadTo(halfItem/2, -100, halfItem, 0);表示右移半个波浪, 并且上移100,即一个浪的最高点,接着mPath.rQuadTo(halfItem/2, 100, halfItem, 0);再右移半个波浪,并且下移100,即一个浪的最低点,这就形成了一段完整的波浪,然后以此循环,直到超过View的最大宽度:
 

webp

静态水波纹


 
静态效果完成了,如何让它动起来呢?就要结合ValueAnimator了,不断改变整个波浪的起始点:

/**
 * Created by YANG on 2019/2/23.
 */public class BezierView2 extends View {    private Paint paint;    private Path mPath;    private int mItemWidth = 600;    private ValueAnimator mAnimator;    private int mOffsetX;    public BezierView2(Context context) {        this(context, null);
    }    public BezierView2(Context context, @Nullable AttributeSet attrs) {        this(context, attrs, 0);
    }    public BezierView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);
        init();
    }    private void init() {
        paint = new Paint();
        paint.setColor(Color.BLACK);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(5);

        mPath = new Path();

        mAnimator = ValueAnimator.ofInt(0, mItemWidth);
        mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mOffsetX = (int) animation.getAnimatedValue();
                invalidate();
            }
        });

        mAnimator.setInterpolator(new LinearInterpolator());

        mAnimator.setDuration(1000);
        mAnimator.setRepeatCount(-1);
        mAnimator.start();
    }    @Override
    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);
        mPath.reset();        int halfItem = mItemWidth / 2;        //必须先减去一个浪的宽度,以便第一遍动画能够刚好位移出一个波浪,形成无限波浪的效果
        mPath.moveTo(-mItemWidth + mOffsetX, halfItem);        for (int i = -mItemWidth; i < mItemWidth + getWidth(); i += mItemWidth) {
            mPath.rQuadTo(halfItem / 2, -100, halfItem, 0);
            mPath.rQuadTo(halfItem / 2, 100, halfItem, 0);
        }
        canvas.drawPath(mPath, paint);
    }
}

注意,起点改为了mPath.moveTo(-mItemWidth + mOffsetX, halfItem),为何不是(mOffsetX, halfItem)呢,因为mOffsetX的变化范围是从0到mItemWidth,如果一开始不减去mItemWidth,就会导致启动动画之后波浪左边总是会露出一段空白区域,整个动画衔接不起来,无法形成无限循环的视觉效果。

最后,再给这个水波纹效果填充上“水”,设置画笔填充模式为Paint.FILL,换个颜色,然后将我们的路径闭合起来:

    @Override
    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);
        mPath.reset();        int halfItem = mItemWidth / 2;        //必须先减去一个浪的宽度,以便第一遍动画能够刚好位移出一个波浪,形成无限波浪的效果
        mPath.moveTo(-mItemWidth + mOffsetX, halfItem);        for (int i = -mItemWidth; i < mItemWidth + getWidth(); i += mItemWidth) {
            mPath.rQuadTo(halfItem / 2, -100, halfItem, 0);
            mPath.rQuadTo(halfItem / 2, 100, halfItem, 0);
        }        //闭合路径波浪以下区域
        mPath.lineTo(getWidth(), getHeight());
        mPath.lineTo(0, getHeight());
        mPath.close();
        
        canvas.drawPath(mPath, paint);
    }

 
最终效果如下:


webp

水波纹效果图

 

总结

之前一直有看到水波纹的效果,没来得及细细研究,这次终于总结在一块了,这里只是水波纹的基础效果,还有很多拓展的方式,比如可以有多条水波纹叠加,水波纹进度球等更炫酷的效果。当然,贝塞尔曲线不单单可以做水波纹效果,还有很多其他的用法,类似于QQ的拖动取消新消息提醒的效果,手写路径优化等等,由于篇幅有限,下次再叙。



作者:Android小Y
链接:https://www.jianshu.com/p/12fcc3fedbbc


打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP