Carson带你学Android:自定义View Path类全面解析


前言

  • 自定义View是Android开发者必须了解的基础;而Path类的使用在自定义View绘制中发挥着非常重要的作用
  • 网上有大量关于自定义View中Path类的文章,但存在一些问题:内容不全、思路不清晰、简单问题复杂化等等
  • 今天,我将全面总结自定义View中Path类的使用,我能保证这是市面上的最全面、最清晰、最易懂的

Carson带你学Android自定义View文章系列:
Carson带你学Android:自定义View基础
Carson带你学Android:一文梳理自定义View工作流程
Carson带你学Android:自定义View绘制准备-DecorView创建
Carson带你学Android:自定义View Measure过程
Carson带你学Android:自定义View Layout过程
Carson带你学Android:自定义View Draw过程
Carson带你学Android:手把手教你写一个完整的自定义View
Carson带你学Android:Canvas类全面解析
Carson带你学Android:Path类全面解析


目录

目录

1. 简介

  • 定义:路径,即无数个点连起来的线
  • 作用:设置绘制的顺序 & 区域

Path只用于描述顺序 & 区域,单使用Path无法产生效果

  • 应用场景:绘制复杂图形(如心形、五角星等等)

Path类封装了由直线和曲线(2、3次贝塞尔曲线)构成的几何路径。


2. 基础

2.1 开放路径与闭合路径的区别

开放路径 & 闭合路径

2.2 如何判断点在图形内还是图形外

  • 判断方法分为奇偶规则 & 非零环绕规则,具体介绍如下:
示意图
  • 举例说明1:(奇偶规则)
    示意图

由上图知:

  • p1发出的射线与图形相交1个点,即奇数点,所以P1点在图形内
  • p2发出的射线与图形相交2个点,即偶数点,所以P2点在图形内
  • 举例说明2:(非零环绕数规则)
    从上面方法分析到,任何图形都是由点连成线组成的,是具备方向的,看下图:(矩形是顺时针)
    示意图
  • p1发出的射线与图形相交1个点,矩形的右侧线从左边射到右边,环绕数-1,最终环绕数为-1,故p1在图形内部。
  • p2发出的射线与图形相交2个点:矩形的右侧边从左边射到右边
    环绕数-1;矩形的下侧边从右边射到左边,环绕数+1,最终环绕数为0.故p2在图形外部

3. 具体使用

3.1 对象创建

// 使用Path首先要new一个Path对象
// Path的起点默认为坐标为(0,0)
Path path = new Path();
// 特别注意:建全局Path对象,在onDraw()按需修改;尽量不要在onDraw()方法里new对象
// 原因:若View频繁刷新,就会频繁创建对象,拖慢刷新速度。

3.2 具体方法使用

因为path类的方法都是联合使用,所以下面将一组组方法进行介绍。

第一组:设置路径

采用moveTo()、setLastPoint()、lineTo()、close()组合

// 设置当前点位置
// 后面的路径会从该点开始画
moveTo(float x, float y) ;

// 当前点(上次操作结束的点)会连接该点
// 如果没有进行过操作则默认点为坐标原点。
lineTo(float x, float y)  ;

// 闭合路径,即将当前点和起点连在一起
// 注:如果连接了最后一个点和第一个点仍然无法形成封闭图形,则close什么也不做
close() ;
  • 可使用setLastPoint()设置当前点位置(代替moveTo()
  • 二者区别:


    Paste_Image.png

实例介绍:(含setLastPoint()moveTo()


 // 使用moveTo()
 // 起点默认是(0,0)
        //连接点(400,500)
        path.lineTo(400, 500);

        // 将当前点移动到(300, 300)
        path.moveTo(300, 300) ;

        //连接点(900, 800)
        path.lineTo(900, 800);

        // 闭合路径,即连接当前点和起点
        // 即连接(200,700)与起点2(300, 300)
        // 注:此时起点已经进行变换
        path.close();

        // 画出路径
        canvas.drawPath(path, mPaint1);

// 使用setLastPoint()
// 起点默认是(0,0)
        //连接点(400,500)
        path.lineTo(400, 500);

        // 将当前点移动到(300, 300)
        // 会影响之前的操作
        // 但不将此设置为新起点
        path.setLastPoint(300, 300) ;

        //连接点(900,800)
        path.lineTo(900, 800);

        //连接点(200,700)
        path.lineTo(200, 700);

        // 闭合路径,即连接当前点和起点
        // 即连接(200,700)与起点(0,0)
        // 注:起点一直没变化
        path.close();

        // 画出路径
        canvas.drawPath(path, mPaint1);
效果图

关于重置路径

  • 重置Path有两个方法:reset()rewind()
  • 两者区别在于:
方法 是否保留FillType设置 是否保留原有数据结构
Path.reset()
Path.rewind()
  1. FillType影响显示效果;数据结构影响重建速度
  2. 所以一般选择Path.reset()

由于较简单,此处不作过多展示。

第二组: 添加路径

采用addXxx()、arcTo()组合

2.1 添加基本图形

  • 作用:在Path路径中添加基本图形

如圆形路径、圆弧路径等等

  • 具体使用
 
// 添加圆弧
// 方法1
public void addArc (RectF oval, float startAngle, float sweepAngle)

//  startAngle:确定角度的起始位置
//  sweepAngle : 确定扫过的角度
 
    // 方法2
    // 与上面方法唯一不同的是:如果圆弧的起点和上次最后一个坐标点不相同,就连接两个点
    public void arcTo (RectF oval, float startAngle, float sweepAngle)
   
   // 方法3
   // 参数forceMoveTo:是否将之前路径的结束点设置为圆弧起点
   // true:在新的起点画圆弧,不连接最后一个点与圆弧起点,即与之前路径没有交集(同addArc())
  // false:在新的起点画圆弧,但会连接之前路径的结束点与圆弧起点,即与之前路径有交集(同arcTo(3参数))
    public void arcTo (RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo)
// 下面会详细说明


  // 加入圆形路径
  // 起点:x轴正方向的0度
  // 其中参数dir:指定绘制时是顺时针还是逆时针:CW为顺时针,  CCW为逆时针
  // 路径起点变为圆在X轴正方向最大的点
  addCircle(float x, float y, float radius, Path.Direction dir)   

   // 加入椭圆形路径
  // 其中,参数oval作为椭圆的外切矩形区域
  addOval(RectF oval, Path.Direction dir)    

  // 加入矩形路径
  // 路径起点变为矩形的左上角顶点
  addRect(RectF rect, Path.Direction dir)     

  //加入圆角矩形路径
  
  addRoundRect(RectF rect, float rx, float ry, Path.Direction dir)      
 
//  注:添加图形路径后会改变路径的起点

主要说一下dir这个参数:

dir = Direction = 图形的方向,为枚举类型:

  • CW:clockwise,顺时针
  • CCW:counter-clockwise,逆时针

图形的方向影响的是:

  • 添加图形时确定闭合顺序(各个点的记录顺序)
  • 图形的渲染结果(是判断图形渲染的重要条件)

图形绘制的本质:先画点,再将点连接起来。所以,点与点之间是存在一个先后顺序的;顺时针和逆时针用于确定这些点的顺序。

下面实例将说明:

  // 为了方便观察,平移坐标系
        canvas.translate(350, 500);
        // 顺时针
        path.addRect(0, 0, 400, 400, Path.Direction.CW);
        
        // 逆时针
//        path.addRect(0,0,400,400, Path.Direction.CCW);
        canvas.drawPath(path,mPaint1);
效果图

关于加入图形路径后会影响路径的起点,实例如下:

  // 轨迹1
        // 将Canvas坐标系移到屏幕正中
           canvas.translate(400,500);
        
        // 起点是(0,0),连接点(-100,0)
            path.lineTo(-100,0);
        // 连接点(-100,200)
            path.lineTo(-100,200);
        // 连接点(200,200)
            path.lineTo(200,200);
        // 闭合路径,即连接当前点和起点
        // 即连接(200,200)与起点是(0,0)
            path.close();

        // 画出路径
            canvas.drawPath(path,paint);
        // 具体请看下图


// 轨迹2
        // 将Canvas坐标系移到屏幕正中
            canvas.translate(400,500);
        
        // 起点是(0,0),连接点(-100,0)
            path.lineTo(-100,0);
        // 画圆:圆心=(0,0),半径=100px
        // 此时路径起点改变 = (0,100)(记为起点2)
        // 起点改变原则:新画图形在x轴正方向的最后一个坐标
        // 后面路径的变化以这个点继续下去
            path.addCircle(0,0,100, Path.Direction.CCW);

        // 起点为:(0,100),连接 (-100,200)
            path.lineTo(-100,200);
        // 连接 (200,200)
            path.lineTo(200,200);

        // 闭合路径,即连接当前点和起点(注:闭合的是起点2)
        // 即连接(200,200)与起点2(0,100)
            path.close();
        
        // 画出路径
            canvas.drawPath(path,paint);

        // // 具体请看下图

效果图

这里着重说明:添加圆弧路径(addArc与arcTo)

 // addArc
// 直接添加一个圆弧到path中
//  startAngle:确定角度的起始位置
//  sweepAngle : 确定扫过的角度
    public void addArc (RectF oval, float startAngle, float sweepAngle)


    // arcTo
    // 方法1
    // 同样是添加一个圆弧到path
    // 与上面方法唯一不同的是:如果圆弧的起点和上次最后一个坐标点不相同,就连接两个点
    public void arcTo (RectF oval, float startAngle, float sweepAngle)
   
   // 方法2
   // 参数forceMoveTo:是否将之前路径的结束点设置为圆弧起点
   // true:在新的起点画圆弧,不连接最后一个点与圆弧起点,即与之前路径没有交集(同addArc())
  // false:在新的起点画圆弧,但会连接之前路径的结束点与圆弧起点,即与之前路径有交集(同arcTo(3参数))
    public void arcTo (RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo)

具体请看下面实例


// 将一个圆弧路径添加到一条直线路径里

 // 为了方便观察,平移坐标系
        canvas.translate(350, 500);

        // 先将原点(0,0)连接点(100,100)
        path.lineTo(50, 200);

// 添加圆弧路径(2分之1圆弧)

        // 不连接最后一个点与圆弧起点
        path.addArc(new RectF(200, 200, 300, 300), 0, 180);
        // path.arcTo(oval,0,270,true);             // 与上面一句作用等价
        
        // 连接之前路径的结束点与圆弧起点
        path.arcTo(new RectF(200, 200, 300, 300), 0, 180);
        // path.arcTo(oval,0,270,false);             // 与上面一句作用等价
        
        // 画出路径
        canvas.drawPath(path, mPaint1);

效果图

2.2 添加路径

  • 作用:合并路径

即将路径1加到路径2里

  • 具体使用
    // 方法1
    public void addPath (Path src)

    // 方法2
    // 先将src进行(x,y)位移之后再添加到当前path
    public void addPath (Path src, float dx, float dy)

    // 方法3
    // 先将src进行Matrix变换再添加到当前path
    public void addPath (Path src, Matrix matrix)


// 实例:合并矩形路径和圆形路径

     // 为了方便观察,平移坐标系
        canvas.translate(350, 500);
     // 创建路径的对象
        Path pathRect = new Path();
        Path  pathCircle = new Path();
        // 画一个矩形路径
        pathRect.addRect(-200, -200, 200, 200, Path.Direction.CW);
        // 画一个圆形路径
        pathCircle.addCircle(0, 0, 100, Path.Direction.CW);

        // 将圆形路径移动(0,200),再添加到矩形路径里
        pathRect.addPath(pathCircle, 0, 200);

        // 绘制合并后的路径
        canvas.drawPath(pathRect,mPaint1);
效果图

第三组:判断路径属性

  • 采用isEmpty()、 isRect()、isConvex()、 set() 和 offset()组合

  • 具体使用:

// 判断path中是否包含内容
 public boolean isEmpty ()
// 例子:
Path path = new Path();
path.isEmpty();  //返回false

 path.lineTo(100,100); // 返回true


// 判断path是否是一个矩形
// 如果是一个矩形的话,会将矩形的信息存放进参数rect中。
public boolean isRect (RectF rect)

// 实例
path.lineTo(0,400);
        path.lineTo(400,400);
        path.lineTo(400,0);
        path.lineTo(0,0);

        RectF rect = new RectF();
        boolean b = path.isRect(rect);  // b返回ture,
        // rect存放矩形参数,具体如下:
        // rect.left = 0
        // rect.top = 0
        // rect.right = 400
        // rect.bottom = 400



// 将新的路径替代现有路径
 public void set (Path src)

        // 实例
        // 设置一矩形路径
        Path path = new Path();                     
        path.addRect(-200,-200,200,200, Path.Direction.CW);
      
        // 设置一圆形路径
        Path src = new Path();                     
        src.addCircle(0,0,100, Path.Direction.CW);

        // 将圆形路径代替矩形路径
        path.set(src);      

        // 绘制图形
        canvas.drawPath(path,mPaint);


// 平移路径
// 与Canvas.translate ()平移画布类似
                     

// 方法1
// 参数x,y:平移位置
public void offset (float dx, float dy)

// 方法2
// 参数dst:存储平移后的路径状态,但不影响当前path
// 可通过dst参数绘制存储的路径
        public void offset (float dx, float dy, Path dst)



 // 为了方便观察,平移坐标系
        canvas.translate(350, 500);

        // path中添加一个圆形(圆心在坐标原点)
        path = new Path();
        path.addCircle(0, 0, 100, Path.Direction.CW);

        // 平移路径并存储平移后的状态
        Path dst = new Path();
        path.offset(400, 0, dst);                     // 平移

        canvas.drawPath(path, mPaint1);               // 绘制path

        
        // 通过dst绘制平移后的图形(红色)
        mPaint1.setColor(Color.RED);      
        canvas.drawPath(dst,mPaint1);

效果图

第四组:设置路径填充颜色

  • 在Android中,有四种填充模式,具体如下

均封装在Path类中

填充模式 介绍
EVEN_ODD 奇偶规则
INVERSE_EVEN_ODD 反奇偶规则
WINDING 非零环绕数规则
INVERSE_WINDING 反非零环绕数规则

请记住两个填充规律:

从我之前的文章(1)自定义View基础 - 最易懂的自定义View原理系列提到,图形是存在方向的(画图 = 连接点成的线 = 有连接顺序)。

填充规则
  • 具体使用
// 设置填充规则
path.setFillType()
// 可填规则
// 1. EVEN_ODD:奇偶规则
// 2. INVERSE_EVEN_ODD:反奇偶规则
// 3. WINDING :非零环绕数规则
// 4. INVERSE_WINDING:反非零环绕数规则

// 理解奇偶规则和反奇偶规则:填充效果相反
// 举例:对于一个矩形而言,使用奇偶规则会填充矩形内部,而使用反奇偶规则会填充矩形外部(下面会举例说明)

// 获取当前填充规则
path.getFillType()

// 判断是否是反向(INVERSE)规则
path.isInverseFillType()

// 切换填充规则(即原有规则与反向规则之间相互切换)
path.toggleInverseFillType()

实例1:(奇偶规则)


 // 为了方便观察,平移坐标系
        canvas.translate(350, 500);

        // 在Path中添加一个矩形
        path.addRect(-200, -200, 200, 200, Path.Direction.CW);

        // 设置Path填充模式为 奇偶规则
        path.setFillType(Path.FillType.EVEN_ODD);

        // 反奇偶规则
        // path.setFillType(Path.FillType.INVERSE_EVEN_ODD);

        // 画出路径
        canvas.drawPath(path, mPaint1);

举例2:(非零环绕规则)

    // 为了方便观察,平移坐标系
        canvas.translate(550, 550);
        // 在路径中添加大正方形
        // 逆时针
        path.addRect(-400, -400, 400, 400, Path.Direction.CCW);

        // 在路径中添加小正方形
        // 顺时针
//        path.addRect(-200, -200, 200, 200, Path.Direction.CW);
//          设置为逆时针
          path.addRect(-200, -200, 200, 200, Path.Direction.CCW);


        // 设置Path填充模式为非零环绕规则
        path.setFillType(Path.FillType.WINDING);
        // 设置反非零环绕数规则
        // path.setFillType(Path.FillType.INVERSE_WINDING);

        // 绘制Path
        canvas.drawPath(path, mPaint1);               
效果图

第五组:布尔操作

  • 作用:两个路径Path之间的运算
  • 应用场景:用简单的图形通过特定规则合成相对复杂的图形。
  • 具体使用
// 方法1
    boolean op (Path path, Path.Op op)
// 举例
// 对 path1 和 path2 执行布尔运算,运算方式由第二个参数指定
// 运算结果存入到path1中。
    path1.op(path2, Path.Op.DIFFERENCE);


// 方法2
    boolean op (Path path1, Path path2, Path.Op op)
  // 举例
    // 对 path1 和 path2 执行布尔运算,运算方式由第三个参数指定
    // 运算结果存入到path3中。
    path3.op(path1, path2, Path.Op.DIFFERENCE)

之间的运算方式(即Path.Op参数)如下


Paste_Image.png

举例:

   // 为了方便观察,平移坐标系
        canvas.translate(550, 550);

        // 画两个圆
        // 圆1:圆心 = (0,0),半径 = 100
        // 圆2:圆心 = (50,0),半径 = 100
        path1.addCircle(0, 0, 100, Path.Direction.CW);
        path2.addCircle(50, 0,100, Path.Direction.CW);

        // 取两个路径的异或集
        path1.op(path2, Path.Op.XOR);
        // 画出路径
        canvas.drawPath(path1, mPaint1);
效果图

4. 贝赛尔曲线

  • 定义:计算曲线的数学公式
  • 作用:计算并表示曲线

任何一条曲线都可以用贝塞尔曲线表示

  • 具体使用:贝塞尔曲线可通过1数据点和若干个控制点描述
  1. 数据点:指路径的起始点和终止点;
  2. 控制点:决定了路径的弯曲轨迹;
  3. n+1阶贝塞尔曲线 = 有n个控制点;
  4. (1阶 = 一条直线,高阶可以拆解为多条低阶曲线)

Canvas提供了画二阶 & 三阶贝塞尔曲线的方法,下面是具体方法:


// 绘制二阶贝塞尔曲线
//  (x1,y1)为控制点,(x2,y2)为终点
quadTo(float x1, float y1, float x2, float y2)
//  (x1,y1)为控制点距离起点的偏移量,(x2,y2)为终点距离起点的偏移量
rQuadTo(float x1, float y1, float x2, float y2)

// 绘制三阶贝塞尔曲线
// (x1,y1),(x2,y2)为控制点,(x3,y3)为终点
cubicTo(float x1, float y1, float x2, float y2, float x3, float y3)
// (x1,y1),(x2,y2)为控制点距离起点的偏移量,(x3,y3)为终点距离起点的偏移量
rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3)

此处只简单介绍贝塞尔曲线,想详细理解可以参考这篇文章


5. 总结

通过阅读本文,相信你已经全面了解Path类的使用。Carson带你学Android自定义View文章系列:
Carson带你学Android:自定义View基础
Carson带你学Android:一文梳理自定义View工作流程
Carson带你学Android:自定义View绘制准备-DecorView创建
Carson带你学Android:自定义View Measure过程
Carson带你学Android:自定义View Layout过程
Carson带你学Android:自定义View Draw过程
Carson带你学Android:手把手教你写一个完整的自定义View
Carson带你学Android:Canvas类全面解析
Carson带你学Android:Path类全面解析


欢迎关注Carson_Ho的简书

不定期分享关于安卓开发的干货,追求短、平、快,但却不缺深度


请点赞!因为你的鼓励是我写作的最大动力!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容