drawPath(Path path, Paint paint) 画自定义图形

第一类:直接描述路径

addXxx() ——添加子图形

xxxTo() ——画线(直线或曲线)

初始化

当尺寸改变时,初始化path,在 onSizeChange 初始化。

/**  
 * path 初始化  
 */  
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {  
    path.reset()  
    path.addCircle(width/2f , height/2f ,100.px, Path.Direction.CCW)  
}

[[补充知识

FillType 填充类型 EVEN_ODD

CWCCW 的方向无关(这俩随便填) 在这种规则下,从绘图起点到某一点画一条直线,然后回到起点,形成一个闭合图形。

  • 如果该直线穿过图形的奇数次,那么该区域被填充
  • 如果穿过偶数次,那么该区域不被填充

如果需求要画镂空,就用 EVEN_ODD ,避免去计算 WINDING,麻烦。

path.addCircle(200f, 200f, 100f, Path.Direction.CW)  
path.addCircle(300f, 200f, 100f, Path.Direction.CCW)  
path.fillType = Path.FillType.EVEN_ODD

WINDING

这是默认的填充规则。在这个规则下,同样是通过路径的闭合形成图形,但是穿过图形的方向决定了该区域的填充。 如果方向是顺时针,那么该区域被填充;如果是逆时针,那么该区域不被填充

  • CW - clockwise 顺时针
  • CCW - counter-clockwise 逆时针 异向,按照环绕规则判断 WINDING 规则通过计算从路径的起点开始,穿越路径时经过的轮廓方向(顺时针或逆时针)的总和 来确定填充区域。

举个例子,考虑一个简单的闭合路径,从起点到终点,再回到起点,形成一个圆形。

  • 如果路径是顺时针方向的,那么环绕数为 -1
  • 如果路径是逆时针方向的,那么环绕数为 1。 在 WINDING 规则下,这个路径内的点就会被认为是在路径内部,因为环绕数是奇数。 填充规则
  • 当环绕数是0时,点被认为在路径外部不填充
  • 当环绕数≠0时,点被认为在路径内部填充

例1:

path.addCircle(200f, 200f, 100f, Path.Direction.CW)  
path.addCircle(300f, 200f, 100f, Path.Direction.CCW)  
path.fillType = Path.FillType.WINDING

左右两边,一个顺时针,一个逆时针,都是奇数,所以在内部,填充。 中间,经过两个轮廓方向,所以逆时针和顺时针相加,为0,在外部,不填充。

例2:

path.addCircle(200f, 200f, 100f, Path.Direction.CW)  
path.addCircle(300f, 200f, 100f, Path.Direction.CCW)  
path.addCircle(400f, 200f, 100f, Path.Direction.CW)  
path.fillType = Path.FillType.WINDING

分析: 从起点开始,环绕路径经过轮廓的判断:

  • 画第一个圆时,顺时针: 先是只有一个顺时针路线,所以为 [-1] ,在内部,填充 然后经过了一个逆时针图形(圆2),[1 -1=0],在外部,所以不填充;
  • 画第二个圆时,逆时针: 在不相交的部分时,因为没有经过其它图形的方向,所以环绕数为 [1],在内部,填充; 和圆1相交,方向相反,为0,在外部,不填充; 和圆2相交,方向相反,为0,在外部,不填充;
  • 画第三个圆时,顺时针: 起点开始,没有经过其它圆,环绕数为 [-1],内部,填充; 然后经过第二个圆,轮廓方向相加 [1 -1=0],外部,不填充。

例3:

private fun windingContraryDirectionExample(){  
        path.addCircle(200f, 200f, 100f, Path.Direction.CW)  
        path.addCircle(300f, 200f, 100f, Path.Direction.CCW)  
        path.addCircle(250f, 300f, 100f, Path.Direction.CW)  
        path.fillType = Path.FillType.WINDING  
    }

分析: 在下面新增了第三个圆。 圆3从起点出发(起点随便在哪,这里选最底下),一开始只有自己的圆路径,顺时针,为-1;

然后路线经过圆2,和圆2 一顺一逆,相加后为0,外部不填充(由于此时路径在圆2内部,没有经过圆1,所以不考虑圆1方向);

然后到达圆1圆2相交部分,此时有3个路径方向,1 - 1 -1 = -1 ,内部填充;

然后经过圆1,一顺 一顺,-1 - 1 = -2,内部填充。

同向,点在内部

  • 如果两个圆形方向相同,即都是顺时针方向或都是逆时针方向,那么它们的环绕数相加。
  • 例如,两个顺时针方向的圆,其环绕数相加后是偶数,表示这两个圆的内部。
  • 点在内部即为填充。
    path.addCircle(200f, 200f, 100f, Path.Direction.CCW)  
    path.addCircle(300f, 200f, 100f, Path.Direction.CCW)  
    path.fillType = Path.FillType.WINDING
    

path.addCircle(200f, 200f, 100f, Path.Direction.CW)  
path.addCircle(300f, 200f, 100f, Path.Direction.CW)  
path.fillType = Path.FillType.WINDING

INVERSE_WINDING

winding 相反 INVERSE_EVEN_ODD 与 EVEN_ODD 相反



示例:画心形

path.addArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle)

init {  
    path.addArc(200f,200f,400f,400f,-225f,225f)  
}

如图所示,四个点表示离 坐标轴 的 距离,startAnglesweepAnglecanvas.drawArc 类似。

path.arcTo(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean forceMoveTo)

将指定的弧作为新轮廓添加到路径中。如果路径的起点与路径当前的最后一点不同,则会自动添加 lineTo() 以连接当前轮廓和弧的起点。但是,如果路径是空的,我们就会调用 moveTo() 来连接弧线的第一个点。

  • startAngle - 起始角度(单位:度),弧线从这里开始
  • sweepAngle - 扫描角度(单位:度),顺时针测量,取模 360。
  • forceMoveTo - 如果为 true,则始终以弧线开始新的轮廓线
path.arcTo(400f, 200f, 600f, 400f, -180f, 225f, false);

这个方法和 Canvas.drawArc() 比起来,少了一个参数 useCenter ,而多了一个 参数 forceMoveTo

少了 useCenter ,是因为 arcTo() 只用来 画弧形 而不画扇形,所以不再需要 useCenter 参数;而多出来的这个 forceMoveTo 参数的意思是,绘制是要「抬一 下笔移动过去」,还是「直接拖着笔过去」,区别在于是否留下移动的痕迹

path.lineTo(float x, float y)

从 最后一个点 向 指定点 (x,y) 添加一条直线。如果此轮廓没有调用 moveTo(),则 第一个点 会自动设置为 (0,0)

  • x - 直线终点的 x 坐标
  • y - 直线终点的 y 坐标
path.arcTo(400f, 200f, 600f, 400f, -180f, 225f, false);  

// 从上面一个弧形的最后一个点 向指定点 画一条直线
path.lineTo(400f, 542f) // 点坐标在蓝色点位置

lineTo(x, y) 的参数是 绝对坐标, rLineTo(x, y) 的参数是相对当前 位置的 相对坐标

// 左边
path.addArc(200f,200f,400f,400f,-225f,225f)  
path.lineTo(400f, 542f)

整体代码

private val paint = Paint()  
private val path = Path()

init {  
    path.addArc(200f,200f,400f,400f,-225f,225f)  
    path.arcTo(400f, 200f, 600f, 400f, -180f, 225f, false);  
    path.lineTo(400f, 542f)  
}

override fun onDraw(canvas: Canvas) {  
        super.onDraw(canvas)  
  
//        练习内容:使用 canvas.drawPath() 方法画心形    
  
        canvas.drawPath(path, paint)  // 绘制图形
  
        // 标注椭圆左边起始点和右边终点  
        paint.color = Color.RED  
        paint.strokeWidth = 30f  
        canvas.drawPoint(200f, 200f, paint)  
        paint.color = Color.BLUE  
        canvas.drawPoint(400f, 542f, paint)  
        paint.color = Color.YELLOW  
        canvas.drawPoint(400f, 400f, paint)  
  
    }