美文网首页
绘图基础

绘图基础

作者: code希必地 | 来源:发表于2020-12-07 18:20 被阅读0次

1、Rect与RectF

Rect和RectF所具有的的函数相同,只不过保存的数值类型不同,Rect所保存的数据类型是int型的,而RectF所保存的数据类型是float类型的。

1.1、判断是否包某个点

boolean contains(int x, int y)

该函数用于判断某个点是否在矩形区域内,如果在则返回true,否则返回false。
我们可以利用这个函数判断点击的位置是否在指定的区域内。

1.2、判断是否包含某个矩形

boolean contains(int left, int top, int right, int bottom) 
boolean contains(@RecentlyNonNull Rect r)

根据4个点或一个矩形对象来判断这个矩形是否在当前矩形区域内。

1.3、判断两个矩形是否相交

静态方法判断是否相交

public static boolean intersects(@RecentlyNonNull Rect a, @RecentlyNonNull Rect b)

这是Rect的静态方法,传入两个Rect对象,判断两个矩形是否相交,相交则返回true,否则返回false。

成员方法判断是否相交

 boolean intersects(int left, int top, int right, int bottom)

功能和静态方法相同

判断相交并返回结果

boolean intersect(@RecentlyNonNull Rect r)
boolean intersect(int left, int top, int right, int bottom)

这两个方法不仅会判断是否相交,而且会把相交的结果返给当前对象。如果不相交,当前对象不变。

1.4、合并

合并两个矩形

不论两个矩形是否相交,取两个矩形的最小左上角的点和最大右下角的点,来作为结果矩形的左上角的点和右下角的点。如果合并的矩形有一方为空,则将有值的一方作为最终结果。

void union(@RecentlyNonNull Rect r)
void union(int left, int top, int right, int bottom) 

将合并后的结果赋值给当前对象。

合并矩形与某个点

public void union(int x, int y)

判断当前矩形和点是否相交,注意:如果不相交,则将目标点设置为结果矩形的左上角或右下角。如果当前矩形是一个空矩形,则最后的结果矩形为([0,0],[x,y])

2、路径

使用Canvas绘制路径的方法

canvas.drawPath(path,paint)

2.1、直线路径

画一条直线路径一般设计下面三个函数:

 public void moveTo(float x, float y)

moveTo作用是指将(x,y)作为Path的起始点,如果不指定Path的起始点默认为(0,0)。

public void lineTo(float x, float y)

(x,y)是直线的终点,又是下一次绘制的起点。

 public void close() 

如果连续画几条直线,但是没有形成闭环,调用close()可以将首尾点进行连接形成闭环。

2.2、弧线路径

public void arcTo(@NonNull RectF oval, float startAngle, float sweepAngle)

这是一个画弧线路径的方法,从参数就可看出:弧线路径是从椭圆上截取的一部分。
- RectF oval:生成椭圆的矩形
- float startAngle:弧形开始的角度,以X轴正方向为0度
- float sweepAngle:弧形持续的角度
示例:

 val path=Path()
  path.moveTo(10f,10f)
  val rectF=RectF(100f,10f,200f,100f)
  path.arcTo(rectF,0f,90f)
  canvas?.drawPath(path, mPaint)
arcTo.png

从图中发现我们只画了一个圆弧,为什么最终圆弧还是和起始点(10,10)连接起来?
默认情况下路径都是连贯的,如果不想连接,可以使用Path类下的两个方法:

public void arcTo(@NonNull RectF oval, float startAngle, float sweepAngle,boolean forceMoveTo)
 public void arcTo(float left, float top, float right, float bottom, float startAngle,float sweepAngle, boolean forceMoveTo)

forceMoveTo的含义是:是否强制将圆弧的起点作为绘制的起点。
对上面代码进行修改

 val path=Path()
  path.moveTo(10f,10f)
  val rectF=RectF(100f,10f,200f,100f)
  path.arcTo(rectF,0f,90f,true)
  canvas?.drawPath(path, mPaint)

得到的结果如下图:


image.png

2.3、addXXX系列函数

路径一般都是连贯的,而addXXX函数可以向路径中添加一些曲线,而不必考虑连贯性。
示例:

val path = Path()
path.moveTo(10f, 10f)
path.lineTo(100f, 100f)
val arcRect = RectF(300f, 300f, 600f, 600f)
path.addArc(arcRect, 0f, 180f)
canvas?.drawPath(path, mPaint)

效果如下:


image.png

2.3.1、添加矩形路径

void addRect(@NonNull RectF rect, @NonNull Direction dir) 
void addRect(float left, float top, float right, float bottom, @NonNull Direction dir)

Direction dir的可选值有:CW和CCW

  • CCW:是counter-clockwise,表示创建逆时针方向的路径。
  • CW:是clickwise的缩写,表示创建顺时针方向的路径。
    路径的方向并不会改变矩形的大小,只表示生成矩形时的路径的方向,使用canvas.drawTextOnPath()就可以看出区别,效果图如下:
    image.png

2.3.2、添加圆角矩形路径

void addRoundRect(float left, float top, float right, float bottom, float rx, float ry,@NonNull Direction dir)
addRoundRect(@NonNull RectF rect, @NonNull float[] radii, @NonNull Direction dir)

两个函数都是添加圆角矩形,不同的是第一个函数只能统一设置矩形圆角的大小,第二个函数可以指定每个角圆角的大小。
第一个函数中的参数float rx, float ry就是设置圆角大小的:

  • float rx:设置生成圆角的椭圆的横轴半径
  • float ry:设置生成圆角的椭圆的纵轴的半径
    第二个函数中的参数float[] radii的数组长度必须为8,分别对应每个角所使用的椭圆的横轴和纵轴半径。

2.3.3、添加圆形

void addCircle(float x, float y, float radius, @NonNull Direction dir)

x,y表示圆形的中心点坐标,radius表示圆形的半径大小,dir依然表示路径绘制的方向。

2.3.4、添加椭圆

void addOval(@NonNull RectF oval, @NonNull Direction dir)
void addOval(float left, float top, float right, float bottom, @NonNull Direction dir) 

2.3.5、添加圆弧

void addArc(float left, float top, float right, float bottom, float startAngle,
            float sweepAngle)
void addArc(@NonNull RectF oval, float startAngle, float sweepAngle) 

3、Path的填充模式

3.1、判断是否在图形内部

Path内部填充颜色,就需要判断哪部分在Path的内部,哪部分在Path的外部,判断是否在Path的内部的方法有2种:
PS:此处所有的图形均为封闭图形,不包含图形不封闭的情况

方法 判定条件 解释
奇偶规则 奇数表示在图形内部,偶数表示在图形外部 从任意位置p作一条射线, 若与该射线相交的图形边的数目为奇数,则p是图形内部点,否则是外部点
非零环绕数规则 若环绕数为0表示在图形外,非零表示在图形内 首先使图形的边变为矢量。将环绕数初始化为零。再从任意位置p作一条射线。当从p点沿射线方向移动时,对在每个方向上穿过射线的边计数,每当图形的边从右到左穿过射线时,环绕数加1,从左到右时,环绕数减1。处理完图形的所有相关边之后,若环绕数为非零,则p为内部点,否则,p是外部点。

接下来先了解一下两种判断方法的工作原理:

3.1.1、奇偶规则(Even-Odd Rule)

先看一张简单的图


奇偶规则.jpg

在上图中我们选择了三个点来判断是否在四边形的内部

P1:从P1发出一条射线,发现图形和射线相交的边数为0,偶数,则P1在图形外部。
P2:从P2发出一条射线,发现图形和射线相交的边数为1,奇数,则P2在图形的内部。
P3:从P3发出一条射线,发现图形和射线相交的边数为2,偶数,则P3在图形是外部。

3.1.2、非0环绕数规则(Non-Zero Winding Number Rule)

从上面的学习可知Path是有方向性的。


非0环绕数.jpg

P1:从P1发出一条射线,沿射线方向移动,并没有与边相交的部分,环绕数为0,故P1在图形外部。
P2:从P2发出一条射线,沿射线方向移动,和图形左边相交,该边从左向右穿过这条射线,环绕数为-1,最终环绕数为-1,故P2在图形内部。
P3:从P3发出一条射线,沿射线方向移动,在第一个交点处,底边从右向左穿过这条射线,环绕数+1,在第二个交点处,右边线从左向右穿过这个射线,环绕数-1,最终环绕数为0,故P3在图形外侧。

通常这两种方法判断的结果相同,但是也存在不同的情况,如下面的情况

image.png
图中白色区域表图形外侧,故没有填充颜色。

3.2、Path填充模式

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

3.2.1、填充模式为奇偶规则EVEN_ODD

val path = Path()
val point = PointF((width / 2).toFloat(), (height / 2).toFloat())
val rect = RectF(point.x - 300, point.y - 300, point.x, point.y)
path.addRect(rect, Path.Direction.CCW)
path.addCircle(point.x, point.y, 300f, Path.Direction.CCW)
path.fillType = Path.FillType.EVEN_ODD
canvas?.drawPath(path, mPaint)

从代码可以看出Path是由一个矩形和一个圆形组成的。


image.png

这里我们使用的填充模式为EVEN_ODD,通过奇偶原则可知2和4表示的区域是在图形外部的,1和3表示在图形内部的,所以填充区域应该为1和3,效果如下图:


image.png

3.2.2、填充模式为反奇偶规则INVERSE_EVEN_ODD

INVERSE表示相反,EVEN_ODD填充区域为1和3,则INVERSE_EVEN_ODD填充区域为2和4,效果如下


image.png

3.2.3、填充模式为非零环绕数规则WINDING

如果不设置填充模式,则默认为WINDING。

val path = Path()
val point = PointF((width / 2).toFloat(), (height / 2).toFloat())
val rect = RectF(point.x - 300, point.y - 300, point.x, point.y)
path.addRect(rect, Path.Direction.CCW)
path.addCircle(point.x, point.y, 300f, Path.Direction.CCW)
path.fillType = Path.FillType.WINDING
canvas?.drawPath(path, mPaint)

path依然是有矩形和圆形组成,它们方向均为逆时针的,根据非零环绕数规则判断可知区域1、 2和3都是在图形内部,4在图形外部。


image.png

如果修改矩形路径为顺时针 Path.Direction.CW,根据非零环绕数规则判断可知2和4在图形外部,1和3在图形内部,效果图下:


image.png

3.2.4、填充模式为反非零环绕数规则INVERSEINVERSE_WINDING

对应上面矩形和圆形均为逆时针时的效果图如下:


image.png

对应上面矩形为顺时针,圆形为逆时针效果图如下:


image.png

4、重置路径

系统提供了两个方法实现路径的重置。

void reset()
void rewind()

它们都会清除内部保存的所有路径,但是两者还是有区别的:

  • rewind():会清除FillType以及所有的直线、曲线、点的数据,但是会保留数据结构。这样可以实现快速重用,提高一定的性能,只有绘制相同的路径,这些数据结构才能复用。
  • reset():会清除所有数据,但是** 不会清除FillType**,类似于重新创建一个新的Path。

5、文字

Paint与文字相关的设置方法有如下几个:

 //普通设置
mPaint.setStrokeWidth(5f) //设置画笔的宽度
mPaint.setAntiAlias(true) //设置是否抗锯齿,为true会降低绘制速度
mPaint.setStyle(Paint.Style.FILL) //设置绘图样式 对文字和图形都有效
mPaint.setTextAlign(Paint.Align.LEFT) //设置文字的对齐方式
mPaint.setTextSize(12f) //设置文字的大小

//样式设置
mPaint.setFakeBoldText(true) //设置字体是否为粗体
mPaint.setUnderlineText(true) //设置文字的下划线
mPaint.setStrikeThruText(true) //设置文字的删除线
mPaint.setTextSkewX(0.25f) //设置文字的水平倾斜
//其他设置
mPaint.setTextScaleX(2f) //设置文字水平方向拉伸,高度不变

下面逐个看下每个方法的意义

5.1、填充样式的区别

代码很简单,直接上效果图


image.png

5.2、setTextAlign()函数

  public void setTextAlign(Align align)

用于设置所要绘制字符串和起始点的相对位置,参数Align取值如下:

  • Align.LEFT :指定起始点在文字的最左侧。
  • Align.CENTER:指定起始点在文字的中间
  • Align.RIGHT:指定起始点在文字的最右侧
    效果图如下:
    image.png

5.3、Canvas绘制文本

基本绘制
Canvas绘制文本有如下几个方法:

public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint)

该函数指定绘制文字的起点,参数中(x,y)就是绘制文字的起始点坐标。

public void drawText(@NonNull String text, int start, int end, float x, float y, @NonNull Paint paint)
public void drawText(@NonNull CharSequence text, int start, int end, float x, float y,@NonNull Paint paint)

这两个函数是用于截取字符串中一段文字进行绘制。

  • start:表示起始绘制字符所在字符串中的索引
  • end:表示结束绘制字符所在字符串中的索引
  • x,y:表示文字绘制起始点坐标
 public void drawText(@NonNull char[] text, int index, int count, float x, float y, @NonNull Paint paint)

该函数表示从字符串中截取一段文字进行绘制。

  • index:表示起始绘制字符所在字符串中的索引
  • count:表示从起始起开始绘制几个字符
  • x,y:表示绘制的起始点坐标。
    沿路径绘制
void drawTextOnPath(@NonNull char[] text, int index, int count, @NonNull Path path,float hOffset, float vOffset, @NonNull Paint paint)
void drawTextOnPath(@NonNull String text, @NonNull Path path, float hOffset,float vOffset, @NonNull Paint paint)

这两个函数的区别就在于第一个函数能截取一段字符串进行绘制,这里主要看下float hOffset,float vOffset参数的意义。

  • hOffset:表示绘制文字水平方向的偏移量
  • vOffset:表示绘制文字垂直方向的偏移量

6、Canvas变换

6.1、translate(float dx, float dy)

Canvas.translate(100,100)是平移画布的,在平移之前,坐标系的原点是左上角(0,0),平移之后,画布也会平移,坐标系原点是平移后画布的左上角。

  • float dx:水平方向平移的距离,正数表示向右平移
  • float dy:竖直方向平移的距离,正数表示向下平移
val rect = Rect(0,0,400,400)
canvas?.drawRect(rect,mPaint)
canvas?.translate(400f,400f)
canvas?.drawRect(rect,mPaint)

效果图如下:


image.png

6.2、屏幕显示与Canvas的关系

屏幕显示和Canvas不是一个概念,每次调用Canvas.drawXXX()都会生成一个新的透明图层,然后在图层上进行绘制,绘制完成后覆盖在屏幕上展示,类似于PS中的图层。
下面分析一下上图显示的过程:

  • 1、在调用canvas?.drawRect(rect,mPaint)绘制时,产生一个透明图层,此时未进行平移,画布的坐标为(0,0),在Canvas上绘制完后,覆盖在屏幕上。
  • 2、再次调用canvas?.drawRect(rect,mPaint)绘制时,此时又产生一个新的透明图层,此时画布已经平移,向下和向右平移了400像素,画布坐标发生了变化,变成了(400,400)。

6.3、旋转(Rotate)

画布的旋转默认是围绕着坐标原点进行的,以后在此画布上绘制出来的图形看起来都是旋转的。旋转的方法有两个:

public void rotate(float degrees)
 public final void rotate(float degrees, float px, float py)

degrees是旋转的角度,为正数时是顺时针旋转,负数是逆时针旋转。
不同的是方法一是围绕着坐标原点进行旋转的,方法二是围绕着点(px,py)进行旋转的。
示例:

val rect=Rect(300,300,800,800)
canvas?.drawRect(rect,mPaintGray)
canvas?.rotate(30f,300f,300f)
canvas?.drawRect(rect,mPaintGreen)

效果如下图


image.png

6.4、缩放(Scale)

缩放是用于变更坐标密度,有两个方法:

public void scale(float sx, float sy)
public final void scale(float sx, float sy, float px, float py)

sx是指x轴的缩放比例,sy是指y轴的缩放比例。
第一个函数默认是以坐标原点(0,0)为缩放中心的,第二个函数是以(px,py)为缩放中心的。
示例:

val rect=Rect(300,300,800,800)
canvas?.drawRect(rect,mPaintGray)
canvas?.scale(0.5f,0.5f,300f,300f)
canvas?.drawRect(rect,mPaintGreen)

效果图如下:


image.png

6.5、裁剪画布(clip系列函数)

裁剪画布是指利用clip系列函数,通过与Path、Rect和Region取交、并、差等集合运算来获取新的画布,裁剪画布后是不可逆的,除调用save()和restore()除外。
注意:在使用clip函数需要禁用硬件加速
clip系列函数如下:

boolean clipRect(float left, float top, float right, float bottom)
boolean clipRect(float left, float top, float right, float bottom,@NonNull Region.Op op)
boolean clipRect(@NonNull RectF rect)
boolean clipRect(@NonNull RectF rect, @NonNull Region.Op op)
boolean clipRect(@NonNull Rect rect)
boolean clipRect(@NonNull Rect rect, @NonNull Region.Op op)
boolean clipPath(@NonNull Path path) 

Region.Op有6个选项值

  • DIFFERENCE:表示最终区域为region1和region2不同的区域
  • INTERSECT:表示最终区域为region1和region2相交的区域
  • UNION:最终区域为region1和region2组合成的区域(API28起已弃用)
  • XOR:最终区域为region1和region2相交之外的区域(API28起已弃用
  • REVERSE_DIFFERENCE:最终区域为region2和region1不同的区域(API28起已弃用
  • REPLACE:最终区域为region2的区域(API28起已弃用
    示例:
val rect = Rect(100, 100, 300, 300)
canvas?.clipRect(rect,Region.Op.DIFFERENCE)
canvas?.drawColor(Color.RED)

上面代码就是取region1和region2不同的区域即rect之外的区域进行绘制,效果图如下


image.png

下面看下如何取region1和region2相交的区域进行绘制

val rect = Rect(100, 100, 300, 300)
canvas?.clipRect(rect)
//canvas?.clipRect(rect,Region.Op.INTERSECT)
canvas?.drawColor(Color.RED)

clipRect(rect)等同于clipRect(rect,Region.Op.INTERSECT)
效果如下


image.png

6.6、画布的保存与恢复

由于画布的操作是不可逆的,所以很多时候就需要对画布进行保存和恢复,画布的保存和恢复使用如下两个方法:

public int save() 
public void restore()
  • save() :每次调用save()函数,都会保存当前画布的状态,然后将其存入特定的栈中。
  • restore():每次调用restore()都会从栈顶的画布状态取出来,并按照这个画布恢复当前画布,然后在画布上进行绘制。
    示例:
canvas?.drawColor(Color.GRAY)
val rect = Rect(100, 100, 300, 300)
canvas?.save()
canvas?.clipRect(rect)
canvas?.drawColor(Color.GREEN)
canvas?.restore()
canvas?.drawColor(Color.RED)

在这个例子中首先将画布绘制成灰色的,然后通过save()保存当前画布的状态,调用clipRect()进行裁剪,将裁剪后的画布绘制成绿色,然后调用restore()从栈顶中恢复画布,然后将画布绘制成红色。
虽然最终呈现在屏幕上的是红色,其实是由3个图层重叠在一起的,只不过我们只能看到红色而已。
restoreToCount(int count)
如果调用多次save()函数,在每次调用restore()时只会恢复栈顶层的画布,有时候可能需要到特定的画布,这就需要多次调用restore(),为了解决这个问题,这就需要调用restoreToCount(int count)。
在每次调用save()时都会有一个int类型的返回值,restoreToCount(int count)中的参数count就对应save()的返回值。调用restoreToCount(int count)就是一直出栈,直到指定的索引为止。

相关文章

网友评论

      本文标题:绘图基础

      本文链接:https://www.haomeiwen.com/subject/mbdawktx.html