Android自定义View_Path之贝塞尔曲线(学习笔记)

注明: 非常感谢 gcssloop 的博客,以下为我学习时的笔记记录。

更多Android学习笔记,请关注 Android-NoteBook,欢迎Star。

先来看一下常用的方法:

Path 常用方法表:

作用 相关方法 备注
移动起点 moveTo 移动下一次操作的起点位置
设置终点 setLastPoint 重置当前path中最后一个点位置,如果在绘制之前调用,效果和moveTo相同
连接直线 lineTo 添加上一个点到当前点之间的直线到Path
闭合路径 close 连接第一个点连接到最后一个点,形成一个闭合区域
添加内容 addRect, addRoundRect, addOval, addCircle, addPath, addArc, arcTo 添加(矩形, 圆角矩形, 椭圆, 圆, 路径, 圆弧) 到当前Path (注意addArc和arcTo的区别)
是否为空 isEmpty 判断Path是否为空
是否为矩形 isRect 判断path是否是一个矩形
替换路径 set 用新的路径替换到当前路径所有内容
偏移路径 offset 对当前路径之前的操作进行偏移(不会影响之后的操作)
贝塞尔曲线 quadTo, cubicTo 分别为二次和三次贝塞尔曲线的方法
rXxx方法 rMoveTo, rLineTo, rQuadTo, rCubicTo 不带r的方法是基于原点的坐标系(偏移量), rXxx方法是基于当前点坐标系(偏移量)
填充模式 setFillType, getFillType, isInverseFillType, toggleInverseFillType 设置,获取,判断和切换填充模式
提示方法 incReserve 提示Path还有多少个点等待加入**(这个方法貌似会让Path优化存储结构)**
布尔操作(API19) op 对两个Path进行布尔运算(即取交集、并集等操作)
计算边界 computeBounds 计算Path的边界
重置路径 reset, rewind 清除Path中的内容 reset不保留内部数据结构,但会保留FillType.rewind会保留内部的数据结构,但不保留FillType
矩阵操作 transform 矩阵变换

Path详解

在我们以前的学习中,大多都是一些直线,今天我们来看看其中的曲线部分,说到曲线,就不得不说 贝塞尔曲线

贝塞尔曲线能干什么?

贝塞尔曲线的运用是十分广泛的,可以说贝塞尔曲线奠定了计算机绘图的基础(因为它可以将任何复杂的图形用精确的数学语言进行描述),在你不经意间就已经使用过它了。

你会使用Photoshop的话,你可能会注意到里面有一个钢笔工具,这个钢笔工具核心就是贝塞尔曲线。

你说你不会PS? 没关系,你如果看过前面的文章或者用过2D绘图,肯定绘制过圆,圆弧,圆角矩形等这些东西。这里面的圆弧部分全部都是贝塞尔曲线的运用。

贝塞尔曲线作用十分广泛,简单举几个的栗子:

  • QQ小红点拖拽效果
  • 一些炫酷的下拉刷新控件
  • 阅读软件的翻书效果
  • 一些平滑的折线图的制作
  • 很多炫酷的动画效果

开始入门贝塞尔曲线

1. 理解贝塞尔曲线的原理

我们先了解贝塞尔曲线是如何生成。

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

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

一阶曲线原理:

一阶曲线是没有控制点的,仅有两个数据点(A和B),最终效果一个线段。

 

上图表示的是一阶曲线生成过程中的某一个阶段,动态过程可以参照下图。

 

注意:一阶曲线其实就是 lineTo

二阶曲线原理:

二阶曲线由两个数据点(A和C),一个控制点(B)来描述曲线状态,大致如下:

 

上图中红色曲线部分就是传说中的二阶贝塞尔曲线,那么红色曲线是如何生成的呢?我们以其中一个状态分析:

 

连接AB BC,并在AB上取点D,BC上取点E,使其满足条件:

 

连接DE,取点F,使得:

这样获取到的点F就是贝塞尔曲线上的一个点,动态过程如下:

 

三阶曲线原理:

三阶曲线由两个数据点(A 和 D),两个控制点(B 和 C)来描述曲线状态,如下:

 

三阶曲线计算过程与二阶类似,具体可以见下图动态效果:

 

三阶曲线对应的方法是 cubicTo

了解贝塞尔曲线相关函数使用方法

一阶曲线:

一阶曲线是一条线段,比较简单,可以参考Path。

二阶曲线:

我们知道二阶曲线是由两个数据点,一个控制点构成,下面上Demo

首先,两个数据点是控制贝塞尔曲线开始和结束的位置,比较容易理解,而控制点则是控制贝塞尔的弯曲状态。我们现在重点理解贝塞尔曲线弯曲状态与控制点的关系。

 

贝塞尔曲线在动态变化过程中有类似与橡皮筋的弹性效果。因为非常适用于一些弹性效果的实例。

public class CanvasView extends View {
    private Paint paint;
    private int mWidth,mheight;
    //开始点,结束点,实时位置
    private PointF start,end,control;
    public CanvasView(Context context) {
        super(context);
    }

    public CanvasView(Context context,AttributeSet attrs) {
        super(context, attrs);
        paint=new Paint();
        paint.setColor(Color.BLACK);
        paint.setStrokeWidth(8);
        paint.setStyle(Paint.Style.STROKE);
        paint.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) {
        mWidth=w/2;
        mheight=h/2;

        //初始化数据点和控制点的位置
        start.x=mWidth-200;     //起始x
        start.y=mheight;        //起始y
        end.x=mWidth+200;       //结束x
        end.y=mheight;          //结束y
        control.x=mWidth;       //默认实时x为view一半
        control.y=mheight-100;  //默认实时y为view一半-100
        super.onSizeChanged(w, h, oldw, oldh);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //绘制数据点和控制点
        paint.setColor(Color.GRAY);
        paint.setStrokeWidth(20);
        canvas.drawPoint(start.x,start.y,paint);
        canvas.drawPoint(end.x,end.y,paint);
        canvas.drawPoint(control.x,control.y,paint);
        Log.e("Demo","x:"+control.x+"---y"+control.y);

        //绘制辅助线
        paint.setStrokeWidth(4);
        canvas.drawLine(start.x,start.y,control.x,control.y,paint);
        canvas.drawLine(end.x,end.y,control.x,control.y,paint);

        //绘制贝塞尔曲线
        paint.setColor(Color.RED);
        paint.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,paint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //根据触摸位置更新控制点,并提示重绘
        control.x=event.getX();
        control.y=event.getY();
        //刷新位置
        invalidate();
        return true;
    }public class CanvasView extends View {
    private Paint paint;
    private int mWidth,mheight;
    //开始点,结束点,实时位置
    private PointF start,end,control;
    public CanvasView(Context context) {
        super(context);
    }

    public CanvasView(Context context,AttributeSet attrs) {
        super(context, attrs);
        paint=new Paint();
        paint.setColor(Color.BLACK);
        paint.setStrokeWidth(8);
        paint.setStyle(Paint.Style.STROKE);
        paint.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) {
        mWidth=w/2;
        mheight=h/2;

        //初始化数据点和控制点的位置
        start.x=mWidth-200;     //起始x
        start.y=mheight;        //起始y
        end.x=mWidth+200;       //结束x
        end.y=mheight;          //结束y
        control.x=mWidth;       //默认实时x为view一半
        control.y=mheight-100;  //默认实时y为view一半-100
        super.onSizeChanged(w, h, oldw, oldh);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //绘制数据点和控制点
        paint.setColor(Color.GRAY);
        paint.setStrokeWidth(20);
        canvas.drawPoint(start.x,start.y,paint);
        canvas.drawPoint(end.x,end.y,paint);
        canvas.drawPoint(control.x,control.y,paint);
        Log.e("Demo","x:"+control.x+"---y"+control.y);

        //绘制辅助线
        paint.setStrokeWidth(4);
        canvas.drawLine(start.x,start.y,control.x,control.y,paint);
        canvas.drawLine(end.x,end.y,control.x,control.y,paint);

        //绘制贝塞尔曲线
        paint.setColor(Color.RED);
        paint.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,paint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //根据触摸位置更新控制点,并提示重绘
        control.x=event.getX();
        control.y=event.getY();
        //刷新位置
        invalidate();
        return true;
    }
 }


效果如图上所示

三阶曲线
三解曲线由两个数据点和两个控制点来控制曲线状态



public class CanvasView extends View {

    private Paint mPaint;
    //屏幕位置
    private int centerX,centerY;
    //开始点,结束点,辅助线两条
    private PointF start,end,control1,control2;
    private boolean mode=true;


    public CanvasView(Context context) {
        super(context);
        //通过代码添加 addView,所以初始化放在单构造函数里
        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 CanvasView(Context context,AttributeSet attrs) {
        super(context, attrs);

    }
    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使用) 复杂运动状态的动画效果

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

你可能感兴趣的:(Android之路-日常开发,Android初级-夯实基础)