持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第12天,点击查看活动详情
路径Path
在Android中,Path即代表路径,在Canvas中提供了绘制路径的方法:
void drawPath(Path,Paint)
直线路径
通过Path.lineTo(float x,float y)方法可以设置一条直线路径。但是想要将一条直线路径绘制出来还会受到Paint属性的影响,Paint的style属性必须是Paint.Style.Stroke,也就是仅描边才可以。如下代码绘制了一条直线路径:
class TestPathView1 : View {
private val mPaint by lazy {
Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.RED
strokeWidth = 10f
style = Paint.Style.STROKE
}
}
private val mPath by lazy {
Path()
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
val usableWidth = measuredWidth - paddingLeft - paddingRight
val usableHeight = measuredHeight - paddingTop - paddingBottom
mPath.moveTo(paddingLeft.toFloat(),paddingTop.toFloat())
//设置一条直线
mPath.lineTo(paddingLeft + usableWidth / 2f, paddingTop + usableHeight / 2f)
canvas?.drawPath(mPath, mPaint)
}
}
在上面的基础上,我们可以使用直线路径设置一个多边形:
//设置一个三角形
//移动到三角形的顶点
mPath.moveTo(measuredWidth / 2f, paddingTop.toFloat())
//连接到三角形最左边的点
mPath.lineTo(
measuredWidth / 2f - 50,
measuredHeight - paddingBottom.toFloat() - mPaint.strokeWidth / 2f
)
//连接到三角形最右边的点
mPath.lineTo(
measuredWidth / 2f + 50,
measuredHeight - paddingBottom.toFloat() - mPaint.strokeWidth / 2f
)
//关闭三角形
mPath.close()
//绘制三角形
canvas?.drawPath(mPath, mPaint)
通过上面的代码我们就可以绘制出一个三角形,需要注意的是:Path.close()
运行结果如下:
弧线路径
通过arcTo(RectF,float startAngle,float sweepAngle)方法可以设置一个弧线,由于弧线是椭圆的一部分,所以方法中第一个参数RectF表示椭圆所在的矩形,第二个参数startAngle表示弧线起始的角度,sweepAngle表示弧线划过的角度,如下所示:
mPath.moveTo(paddingLeft.toFloat(), paddingTop.toFloat())
//向当前Path设置一条弧线
val right = measuredWidth / 2f
val bottom = measuredHeight - paddingBottom - mPaint.strokeWidth / 2f
mRectF.set(
paddingLeft + mPaint.strokeWidth / 2f,
paddingTop + mPaint.strokeWidth / 2f,
right,
bottom
)
mPath.arcTo(mRectF, -40f, 110f)
//绘制路径
canvas?.drawPath(mPath, mPaint)
从上面的图片可以看出,我们在绘制弧线的同时,同时还绘制出了由路径起点到弧线起点的一条直线。这是由于默认情况下路径都是连续的。
在上面的代码中,我们首先对路径设置了起点,即通过moveTou()方法设置了路径的起始位置,如果不调用这个方法,直接设置弧线,则不会有这个直线。要解决这个问题,有以下三种方式:
- 通过
addXXX()系列重载函数,将直接添加固定形状的路径 - 调用
moveTo()方法改变路径的起始位置 - 使用
arcTo(RectF,float startAngle,float sweepAngle,boolean forceMoveTo)另外的重载函数,可传递boolean forceMoveTo参数,这个参数表示是否强制将弧线的起点作为绘制的起点,设置这个参数为true即可将弧线的起点作为绘制的起点。
addXXX()系列函数
通过前面的学习,我们已经了解,路径一般都是连贯的,而通过addXXX()系列函数可以直接向路径中添加一些曲线,而不必考虑连贯性,如下所示:
//通过addArc方法添加弧线,也可以忽略路径的连续性
mPath.addArc(mRectF,150f,90f)
添加矩形路径
通过下面两个方法可以添加矩形路径:
void addRect(float left,float top,float right,float bottom,Path.Direction dir)
void addRect(RectF rectF,Path.Direction dir)
和上面的addArc()相比较,这里多了一个Path.Direction参数,这个参数可以取下面两个值:
- Path.Direction.CCW: 这是
counter-clockwise的缩写,指创建逆时针方向的矩形路径 - Path.Direction.CW: 这是
clockwise的缩写,指创建顺时针方向的矩形路径。
下面的代码创建了两个大小相同但方向不同的矩形路径:
//左边的矩形
val leftRectLeft = paddingLeft + mPaint.strokeWidth / 2f + usableWidth / 8f
val leftRectTop = paddingTop + mPaint.strokeWidth / 2f + usableHeight / 4f
val leftRectRight = measuredWidth / 2f - mPaint.strokeWidth / 2f - usableWidth / 8f
val leftRectBottom =
measuredHeight - paddingBottom - mPaint.strokeWidth / 2f - usableHeight / 4f
mLeftRectF.set(
leftRectLeft,
leftRectTop,
leftRectRight,
leftRectBottom
)
//右边的矩形
val rightRectLeft = measuredWidth / 2f + usableWidth / 8f + mPaint.strokeWidth / 2f
val rightRectTop = paddingTop + usableHeight / 4f + mPaint.strokeWidth / 2f
val rightRectRight =
measuredWidth - paddingRight - usableWidth / 8f - mPaint.strokeWidth / 2f
val rightRectBottom =
measuredHeight - paddingBottom - usableHeight / 4f - mPaint.strokeWidth / 2f
mRightRectF.set(rightRectLeft, rightRectTop, rightRectRight, rightRectBottom)
//按照顺时针的方向向路径中添加左边的矩形
mPath.addRect(mLeftRectF, Path.Direction.CW)
//按照逆时针的方向向路径中添加右边的矩形
mPath.addRect(mRightRectF, Path.Direction.CCW)
//绘制路径
canvas?.drawPath(mPath, mPaint)
在上面的代码中,我们分别通过顺时针的逆时针的方式向路径中添加了左右两个矩形,并绘制出了这个路径.
从上面的图片可以看出,顺时针和逆时针绘制并没有对最后的结果产生任何影响。而之所以这里要区分方向,是因为在绘制文字的时候,我们可以按照路径绘制文字,如下所示:
val usableWidth = measuredWidth - paddingLeft - paddingRight
val usableHeight = measuredHeight - paddingTop - paddingBottom
//左边的矩形
val leftRectLeft = paddingLeft + mPaint.strokeWidth / 2f + usableWidth / 8f
val leftRectTop = paddingTop + mPaint.strokeWidth / 2f + usableHeight / 4f
val leftRectRight = measuredWidth / 2f - mPaint.strokeWidth / 2f - usableWidth / 8f
val leftRectBottom =
measuredHeight - paddingBottom - mPaint.strokeWidth / 2f - usableHeight / 4f
mLeftRectF.set(
leftRectLeft,
leftRectTop,
leftRectRight,
leftRectBottom
)
//右边的矩形
val rightRectLeft = measuredWidth / 2f + usableWidth / 8f + mPaint.strokeWidth / 2f
val rightRectTop = paddingTop + usableHeight / 4f + mPaint.strokeWidth / 2f
val rightRectRight =
measuredWidth - paddingRight - usableWidth / 8f - mPaint.strokeWidth / 2f
val rightRectBottom =
measuredHeight - paddingBottom - usableHeight / 4f - mPaint.strokeWidth / 2f
mRightRectF.set(rightRectLeft, rightRectTop, rightRectRight, rightRectBottom)
//按照顺时针的方向向路径中添加左边的矩形
mPath.addRect(mLeftRectF, Path.Direction.CW)
//按照逆时针的方向向路径中添加右边的矩形
//mPath.addRect(mRightRectF, Path.Direction.CCW)
//按照逆时针的方式添加矩形
mRightPath.addRect(mRightRectF,Path.Direction.CCW)
//绘制路径
canvas?.drawPath(mPath, mPaint)
canvas?.drawPath(mRightPath,mPaint)
//按照路径绘制文字
val testStr = "测试按路径绘制文字"
canvas?.drawTextOnPath(testStr,mPath,0f,0f,mTextPaint)
canvas?.drawTextOnPath(testStr,mRightPath,0f,0f,mTextPaint)
我们修改了之前的代码,现在又添加了一个路径,之前的路径添加左边的矩形,现在的路径按照逆时针的方向添加右边的矩形,绘制出这两个路径。然后沿着这两个路径绘制文本.
添加圆角矩形路径
通过下面两个方法可以添加圆角矩形路径:
Path.addRoundRect(RectF rect,float[] radii,Path.Direction direction)
Path.addRoundRect(RectF rect,float rx, float ry, Path.Direction direction)
在上面的两个重载函数中,第一个函数需要指定圆角矩形所在的矩形范围,第二个参数一个数组,需要传入八个值,分别代表每一个角的圆角大小,最后传入方向信息。第二个函数子只需要传入x轴和y轴的圆角大小,这样四个角的圆角大小就是一样的。分别使用两个函数绘制两个圆角矩形如下:
class TestPathView4: View {
constructor(context: Context,attrs: AttributeSet?,defStyleAttr: Int): super(context, attrs, defStyleAttr)
constructor(context: Context,attrs: AttributeSet?): this(context,attrs,0)
constructor(context: Context): this(context,null)
//画笔
private val mPaint by lazy {
Paint(Paint.ANTI_ALIAS_FLAG)
}
//路径
private val mPath by lazy {
Path()
}
//左边圆角矩形所在的矩形
private val mLeftRectF by lazy {
RectF()
}
//右边圆角矩形所在的矩形范围
private val mRightRectF by lazy {
RectF()
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.drawColor(Color.argb(0xff,0xee,0xee,0xee))
val usableHeight = measuredHeight - paddingTop - paddingBottom
val usableWidth = measuredWidth - paddingLeft - paddingRight
mPaint.color = Color.argb(0xff,0,0xff,0xff)
//绘制左边的圆角矩形
mLeftRectF.set(
paddingLeft + usableWidth / 8f,
paddingTop + usableHeight / 4f,
measuredWidth / 2f - usableWidth / 8f,
measuredHeight - usableHeight / 4f
)
//四个圆角的数组
val radiusArray = floatArrayOf(
10f,10f,20f,20f,30f,30f,40f,40f
)
//将左边的圆角矩形添加到路径中
mPath.addRoundRect(mLeftRectF,radiusArray,Path.Direction.CW)
//设置右边的圆角矩形的范围
mRightRectF.set(
measuredWidth / 2f + usableWidth / 8f,
paddingTop + usableHeight / 4f,
measuredWidth - usableWidth / 8f,
measuredHeight - usableHeight / 4f
)
//将右边的圆角矩形添加到路径中
mPath.addRoundRect(mRightRectF,10f,10f,Path.Direction.CCW)
//绘制路径
canvas?.drawPath(mPath,mPaint)
}
}
通过上面的方法,我们就可以给一个路径添加圆角矩形。需要注意的是,如果我们需要指定每一个圆角的大小,那么传递进去的圆角数组必须是8个元素,否则会出现如下异常:
java.lang.ArrayIndexOutOfBoundsException: radii[] needs 8 values
- 添加圆形路径
通过下面的方法可以向路径中添加一个圆形:
void addCircle(float x, float y,float radius,Path.Direction dir)
其中float x和float y为圆心的坐标,Path.Direction dir是方向,如下代码所示:
class TestPathView5 : View {
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context) : this(context, null)
//画笔
private val mPaint by lazy {
Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.argb(0xff, 0xbb, 0xff, 0)
strokeWidth = 20f
style = Paint.Style.STROKE
}
}
//路径
private val mPath by lazy {
Path()
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
val usableWidth = measuredWidth - paddingLeft - paddingRight
val usableHeight = measuredHeight - paddingTop - paddingBottom
val radius = min(usableWidth, usableHeight) / 2f - mPaint.strokeWidth / 2f
//向路径中添加一个园
mPath.addCircle(measuredWidth / 2f, measuredHeight / 2f, radius, Path.Direction.CCW)
//绘制路径
canvas?.drawPath(mPath, mPaint)
}
}
- 添加椭圆路径
使用下面的方法可以向路径中添加一个椭圆:
void addOval(RectF rectF,Path.Direction dir)
上面的方法中RectF表示椭圆所在的路径,Path.Direction表示方向.
class TestPathView5 : View {
//设置椭圆所在的矩形
mOvalRectF.set(
paddingLeft + usableWidth / 2f + mPaint.strokeWidth / 2f,
paddingTop + mPaint.strokeWidth / 2f,
measuredWidth - paddingRight - mPaint.strokeWidth / 2f,
measuredHeight - paddingBottom - mPaint.strokeWidth / 2f
)
//向路径中添加椭圆
mPath.addOval(mOvalRectF,Path.Direction.CW)
//绘制路径
canvas?.drawPath(mPath, mPaint)
我们修改了之前的TestPathView5中的代码,在向路径中添加圆形的页面中现在又添加了一个椭圆.
- 添加圆弧
使用下面的方法可以向路径中添加一个圆弧:
void addArc(RecrF rectf,float startAngle,float sweepAngle)
其中RectF表示圆弧所在的椭圆所在的矩形,startAngle表示起始角度,sweepAngle表示弧线扫过的角度。可以看到,这个方法不需要指定路径的方向,这是因为通过起始角度和路径扫过的角度我们就可以确定方向是顺时针还是逆时针(sweepAngle为正表示顺时针,为负表示逆时针),所以不需要指定。
仍然是修改了之前的TestPathView5的代码,如下所示:
//设置圆弧所在的矩形
mArcRectF.set(
paddingLeft + usableWidth / 4f + mPaint.strokeWidth / 2f,
paddingTop + mPaint.strokeWidth / 2f,
measuredWidth - paddingRight - usableWidth / 4f - mPaint.strokeWidth / 2f,
measuredHeight - paddingBottom - mPaint.strokeWidth / 2f
)
//向路径中添加圆弧
mPath.addArc(mArcRectF,-150f,150f)
填充模式
Path的填充模式是指填充Path的哪一部分,使用Path.FillType表示(下面的结论基于默认direction为Path.Direction.CW),有4个枚举值,分别为:
FillType.WINDING: 默认值,绘制两个图形及其相交的部分FillType.EVEN_ODD: 相交的部分不会被绘制,其余部分会被绘制FillType.INVERSE_WINDING:两个图形及其相交的部分不会被绘制,其余整个外部空间会被绘制FillType.INVERSE_EVEN_ODD:整个外部空间和两个图形相交的部分会被绘制,其余部分不会被绘制
下面的代码分别演示了不同填充模式的效果:
class TestPathView6 : View {
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context) : this(context, null)
//画笔
private val mPaint by lazy {
Paint()
}
//第一个矩形
private val mFirstRectF by lazy {
RectF()
}
//第一个路径
private val mFirstPath by lazy {
Path().apply {
fillType = Path.FillType.WINDING
}
}
//第二个路径
private val mSecondPath by lazy {
Path().apply {
fillType = Path.FillType.EVEN_ODD
}
}
//第二个矩形
private val mSecondRectF by lazy {
RectF()
}
//第三个路径
private val mThirdPath by lazy {
Path().apply {
fillType = Path.FillType.INVERSE_WINDING
}
}
//第三个矩形
private val mThirdRectF by lazy {
RectF()
}
//第四个路径
private val mFourthPath by lazy {
Path()
}
//第四个矩形
private val mFourthRectF by lazy {
RectF()
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
mPaint.apply {
this.isAntiAlias = true
color = Color.argb(0xff, 0xff, 0, 0)
style = Paint.Style.FILL
}
//绘制颜色
canvas?.drawColor(Color.argb(0xff, 0xee, 0xee, 0xee))
val usableWidth = measuredWidth - paddingLeft - paddingRight
val usableHeight = measuredHeight - paddingTop - paddingBottom
//将可用的区域分为四个部分,每一部分的可用宽度和高度
val areaWidth = usableWidth / 2f
val areaHeight = usableHeight / 2f
//设置第一个矩形的位置
mFirstRectF.set(
paddingLeft + mPaint.strokeWidth / 2f + areaWidth / 6f,
paddingTop + mPaint.strokeWidth / 2f + areaHeight / 6f,
measuredWidth / 2f - mPaint.strokeWidth / 2f - areaWidth / 6f,
measuredHeight / 2f - mPaint.strokeWidth / 2f - areaHeight / 6f
)
//添加矩形
mFirstPath.addRect(mFirstRectF, Path.Direction.CCW)
//左上角的圆
val firstCircleRadius = min(areaWidth, areaHeight) / 3f - mPaint.strokeWidth / 2f
//添加圆形
mFirstPath.addCircle(
paddingLeft + areaWidth / 3f * 2,
paddingTop + areaHeight / 3f * 2,
firstCircleRadius,
Path.Direction.CCW
)
//绘制第一个路径
canvas?.drawPath(mFirstPath, mPaint)
//设置第二个矩形
mSecondRectF.set(
measuredWidth / 2f + mPaint.strokeWidth / 2f + areaWidth / 6f,
paddingTop + mPaint.strokeWidth / 2f + areaHeight / 6f,
measuredWidth - paddingRight - mPaint.strokeWidth / 2f - areaWidth / 6f,
measuredHeight / 2f - mPaint.strokeWidth / 2f - areaHeight / 6f
)
//向第二个路径中添加矩形和圆形
mSecondPath.fillType = Path.FillType.EVEN_ODD
mSecondPath.addRect(mSecondRectF, Path.Direction.CW)
mSecondPath.addCircle(
measuredWidth - paddingRight - areaWidth / 3f,
paddingTop + areaHeight / 3f * 2,
firstCircleRadius,
Path.Direction.CW
)
//绘制第二个路径
canvas?.drawPath(mSecondPath, mPaint)
//设置第三个矩形
mThirdRectF.set(
paddingLeft + mPaint.strokeWidth / 2f + areaWidth / 6f,
measuredHeight / 2f + mPaint.strokeWidth / 2f + areaHeight / 6f,
measuredWidth / 2f - mPaint.strokeWidth / 2f - areaWidth / 6f,
measuredHeight - paddingBottom - areaHeight / 6f
)
//向第三个路径中添加矩形和圆形
mThirdPath.fillType = Path.FillType.INVERSE_WINDING
mThirdPath.addRect(mThirdRectF, Path.Direction.CW)
mThirdPath.addCircle(
paddingLeft + areaWidth / 3f * 2,
measuredHeight - paddingBottom - areaHeight / 3f,
firstCircleRadius,
Path.Direction.CW
)
//绘制第三个路径
canvas?.drawPath(mThirdPath,mPaint)
//设置第四个矩形的范围
mFourthRectF.set(
measuredWidth / 2f + mPaint.strokeWidth / 2f + areaWidth / 6f,
measuredHeight / 2f + mPaint.strokeWidth / 2f + areaHeight / 6f,
measuredWidth - paddingRight - mPaint.strokeWidth / 2f - areaWidth / 6f,
measuredHeight - paddingBottom - areaHeight / 6f
)
//向第四个路径中添加矩形
mFourthPath.fillType = Path.FillType.INVERSE_EVEN_ODD
mFourthPath.addRect(mFourthRectF, Path.Direction.CW)
//添加第四个圆
mFourthPath.addCircle(
measuredWidth - paddingRight - areaWidth / 3f,
measuredHeight - paddingBottom - areaHeight / 3f,
firstCircleRadius,
Path.Direction.CW
)
//绘制第四个路径
canvas?.drawPath(mFourthPath, mPaint)
}
}
需要注意的是,由于第三个和第四个路径都会绘制外部区域,所以同时执行上面的绘制,则会导致什么都看不到了,可以先注释掉其它部分的绘制,一个一个来显示.
另外,最终的显示效果和Path.Direction参数也有关系.
重置路径
当需要绘制一条全新的路径的时候,开发人员为了重复利用空间,允许重置路径对象。路径对象一旦被重置,其中保存的所有路径都将被清空,这样我们就不需要重新定义一个新的路径对象了。重新定义路径对象的问题在于老对象的回收和新对象的内存分配,这些都会消耗手机性能。
其中Path提供了两个方法来重置路径:
void reset()
void rewind()
这两个函数的共同点是都会清空内部所保存的路径,区别在于:
-
rewind()函数会清除FillType及所有的直线,曲线,点的数据等,但是会保留数据结构。这样可以实现快速重用,提高一定的性能。例如,重复绘制一类线段,它们的点的数量都相等,那么rewind函数可以保留装在点数据的数据结构,效率会更高。但是需要注意的是,只有在重复绘制相同的路径时,这些数据结构才是可以复用的。 -
reset()函数类似于新建一个路径对象,它的所有数据空间都会被回收并重新分配,但不会清除FillType。 -
从整体来讲,
rewind()函数不会清除内存,但会清除FillType,而reset()函数则会清除内存,但不会清除FillType。
下面是两个函数的源码,从源码中也可以看出上面的结论:
public void reset() {
isSimplePath = true;
mLastDirection = null;
if (rects != null) rects.setEmpty();
// We promised not to change this, so preserve it around the native
// call, which does now reset fill type.
final FillType fillType = getFillType();
nReset(mNativePath);
setFillType(fillType);
}
public void rewind() {
isSimplePath = true;
mLastDirection = null;
if (rects != null) rects.setEmpty();
nRewind(mNativePath);
}
下面的代码演示了reset()和rewind()对于FillType不同的处理情况:
class TestPathView7 : View {
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context) : this(context, null)
//画笔
private val mPaint by lazy {
Paint()
}
//左边的路径
private val mLeftPath by lazy {
Path()
}
//右边的路径
private val mRightPath by lazy {
Path()
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
//设置画笔属性
mPaint.let {
it.isAntiAlias = true
it.color = Color.argb(0xff, 0xff, 0x44, 0)
}
//可用的宽度和高度
val usableWidth = measuredWidth - paddingLeft - paddingRight
val usableHeight = measuredHeight - paddingTop - paddingBottom
//计算圆形的可用半径
val radius = min(usableWidth / 2, usableHeight) / 2 - 10
//设置左边路径的属性
mLeftPath.fillType = Path.FillType.INVERSE_WINDING
mLeftPath.reset()
mLeftPath.addCircle(
paddingLeft + usableWidth / 4f,
measuredHeight / 2f,
radius.toFloat(),
Path.Direction.CW
)
//canvas?.drawPath(mLeftPath, mPaint)
//设置右边路径的属性
mRightPath.fillType = Path.FillType.INVERSE_WINDING
mRightPath.rewind()
mRightPath.addCircle(
measuredWidth - paddingLeft - usableWidth / 4f,
measuredHeight / 2f,
radius.toFloat(),
Path.Direction.CW
)
//绘制路径
canvas?.drawPath(mRightPath, mPaint)
}
}
分别绘制左边的路径和右边的路径.
从上面的图片效果来看,在设置了fillType之后,调用reset()函数,然后在对当前路径添加一个圆,绘制出来的仍然是带有fillType效果的路径,而调用rewind()函数,在添加一个圆形,绘制出来就是没有fillType效果的路径。