Canvas.drawXXX() 和 Paint 基础

Canvas.drawColor(@ColorInt int color)颜色填充

这是最基本的drawXXX()方法:在整个绘制区域涂上指定的颜色。

canvas.drawColor(Color.BLACK) //纯黑
canvas.drawColor(Color.parseColor("#88880000")) //半透明红色

类似的方法还有drawRGB(int r, int g, int b) 和 drawARGB(int a, int r, in g, int b),它们和drawColor(color)只是使用方式不同,作用都是一样的。

canvas.drawRGB(100, 200, 100);
canvas.drawARGB(100, 100, 200, 100); //第一个参数代表透明度,值越小越透明

drawCircle(float centerX, float centerY, float radius, Paint paint) 画圆

前两个参数centerX、centerY是圆心的坐标,第三个参数radius是圆的半径,单位都是像素

canvas.drawCircle(300f, 300f, 200f, paint);
插播一: Paint.setColor(int color)

例如,你要画一个红色的圆,并不是写成 canvas.drawCircle(300, 300, 200, RED, paint) 这样,而是像下面这样:

paint.color = Color.RED
canvas.drawCircle(300f, 300f, 200f, paint)

插播二: Paint.setStyle(Paint.Style style)

paint.style = Paint.Style.STROKE
canvas.drawCircle(300, 300, 200, paint)

setStyle(Style style) 这个方法设置的是绘制的 StyleStyle 具体来说有三种: FILL, STROKEFILL_AND_STROKEFILL 是填充模式,STROKE 是画线模式(即勾边模式),FILL_AND_STROKE 是两种模式一并使用:既画线又填充。它的默认值是 FILL,填充模式。

插播三: Paint.setStrokeWidth(float width)

STROKEFILL_AND_STROKE 下,还可以使用 paint.setStrokeWidth(float width) 来设置线条的宽度:

paint.style = Paint.Style.STROKE
paint.strokeWidth = 20f
canvas.drawCircle(300f, 300f, 200f, paint)

插播四: 抗锯齿

在绘制的时候,往往需要开启抗锯齿来让图形和文字的边缘更加平滑。开启抗锯齿很简单,只要在 new Paint() 的时候加上一个 ANTI_ALIAS_FLAG 参数就行:

private val paint = Paint(Paint.ANTI_ALIAS_FLAG)

另外,你也可以使用paint.isAntiAlias = true来动态开关抗锯齿。

drawRect(float left, float top, float right, float bottom, Paint paint)

left, top, right, bottom 是矩形四条边的坐标。

paint.style = Paint.Style.FILL
canvas.drawRect(100, 100, 500, 500, paint)
paint.style = Paint.Style.STROKE
canvas.drawRect(700, 100, 1100, 500, paint)
//参数的设置需要满足,right>left,bottom大于top

drawPoint(float x, float y, Paint paint) 画点

xy 是点的坐标。点的大小可以通过 paint.setStrokeWidth(width) 来设置;点的形状可以通过 paint.setStrokeCap(cap) 来设置:ROUND 画出来是圆形的点,SQUAREBUTT 画出来是方形的点。

paint.strokeWidth = 20f
paint.strokeCap = Paint.Cap.ROUND
canvas.drawPoint(50f,50f,paint)

paint.strokeWidth = 20f
paint.strokeCap = Paint.Cap.BUTT
canvas.drawPoint(100f,100f,paint)

paint.strokeWidth = 20f
paint.strokeCap = Paint.Cap.SQUARE
canvas.drawPoint(150f,150f,paint)

drawPoints(float[] pts, int offset, int count, Paint paint) / drawPoints(float[] pts, Paint paint) 画点(批量)

同样是画点,它和 drawPoint() 的区别是可以画多个点。pts 这个数组是点的坐标,每两个成一对;offset 表示跳过数组的前几个数再开始记坐标;count 表示一共要绘制几个点。

val points = floatArrayOf(0f, 0f, 50f, 50f, 50f, 100f, 100f, 50f, 100f, 100f, 150f, 50f, 150f, 100f)
paint.strokeWidth = 20f
canvas.drawPoints(points,2,8,paint)

drawOval(float left, float top, float right, float bottom, Paint paint) 画椭圆

只能绘制横着的或者竖着的椭圆,不能绘制斜的(斜的倒是也可以,但不是直接使用 drawOval(),而是配合几何变换,后面会讲到)。left, top, right, bottom 是这个椭圆的左、上、右、下四个边界点的坐标。

paint.style = Paint.Style.FILL
canvas.drawOval(50f,50f,350f,200f,paint)

paint.style = Paint.Style.STROKE
canvas.drawOval(400f,50f,700f,200f,paint)

drawLine(float startX, float startY, float stopX, float stopY, Paint paint) 画线

startX, startY, stopX, stopY 分别是线的起点和终点坐标。

canvas.drawLine(200f,200f,800f,500f,paint)

由于直线不是封闭图形,所以 setStyle(style) 对直线没有影响。

drawLines(float[] pts, int offset, int count, Paint paint) / drawLines(float[] pts, Paint paint) 画线(批量)

val points = floatArrayOf(20f, 20f, 120f, 20f, 70f, 20f, 70f, 120f, 20f, 120f, 120f, 120f, 150f, 20f, 250f, 20f, 150f, 20f, 150f, 120f, 250f, 20f, 250f, 120f, 150f, 120f, 250f, 120f)
canvas.drawLines(points,paint)

drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, Paint paint) 画圆角矩形

left, top, right, bottom 是四条边的坐标,rxry 是圆角的横向半径和纵向半径。

canvas.drawRoundRect(100f,100f,500f,300f,50f,50f,paint)

drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint) 绘制弧形或扇形

drawArc() 是使用一个椭圆来描述弧形的。left, top, right, bottom 描述的是这个弧形所在的椭圆;startAngle 是弧形的起始角度(x 轴的正向,即正右的方向,是 0 度的位置;顺时针为正角度,逆时针为负角度),sweepAngle 是弧形划过的角度;useCenter 表示是否连接到圆心,如果不连接到圆心,就是弧形,如果连接到圆心,就是扇形。

paint.style = Paint.Style.FILL
canvas.drawArc(200f, 100f, 800f, 500f, -100f, 100f, true, paint)
canvas.drawArc(200f, 100f, 800f, 500f, 20f, 140f, false, paint)
paint.style = Paint.Style.STROKE
canvas.drawArc(200f, 100f,800f, 500f, 180f, 60f, false, paint)

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

drawPath(path) 这个方法是通过描述路径的方式来绘制图形的,它的 path 参数就是用来描述图形路径的对象。path 的类型是 Path ,使用方法大概像下面这样:

path.addArc(200f, 200f, 400f, 400f, -225f, 225f)
path.arcTo(400f,200f,600f,400f,-189f,225f,false)
path.lineTo(400f,542f)
canvas.drawPath(path,paint)

Path 方法第一类:直接描述路径

这一类方法还可以细分为两组:添加子图形和画线(直线或曲线)

第一组: addXxx() ——添加子图形

addCircle(float x, float y, float radius, Direction dir) 添加圆

x, y, radius 这三个参数是圆的基本信息,最后一个参数 dir 是画圆的路径的方向。顺时针CW(clockwise),逆时针(CCW counter-clockwise)。

第二组:xxxTo() ——画线(直线或曲线)

这一组和第一组 addXxx() 方法的区别在于,第一组是添加的完整封闭图形(除了 addPath() ),而这一组添加的只是一条线

lineTo(float x, float y) / rLineTo(float x, float y) 画直线

当前位置向目标位置画一条直线, xy 是目标位置的坐标。这两个方法的区别是,lineTo(x, y) 的参数是绝对坐标,而 rLineTo(x, y) 的参数是相对当前位置的相对坐标 (前缀 r 指的就是 relatively 「相对地」)。

patin.style = Paint.Style.STROKE
path.lineTo(100f, 100f)// 由当前位置 (0, 0) 向 (100, 100) 画一条直线
path.rLineTo(100f, 0f)// 由当前位置 (100, 100) 向正右方 100 像素的位置画一条直线

通过 moveTo(x, y)rMoveTo() 来改变当前位置,从而间接地设置这些方法的起点。

另外,第二组还有两个特殊的方法: arcTo()addArc()。它们也是用来画线的,但并不使用当前位置作为弧线的起点。

arcTo(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo) / arcTo(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean forceMoveTo) / arcTo(RectF oval, float startAngle, float sweepAngle) 画弧形

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

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

paint.style = Paint.Style.STROKE
path.lineTo(100f, 100f)
path.arcTo(100f, 100f, 300f, -90f, 90f, ture)// 强制移动到弧形起点(无痕迹)
paint.style = Paint.Style.STROKE
path.lineTo(100f, 100f)
path.arcTo(100f, 100f, 300f, -90f, 90f, false)// 直接连线连到弧形起点(有痕迹)

addArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle) / addArc(RectF oval, float startAngle, float sweepAngle)

又是一个弧形的方法。一个叫 arcTo ,一个叫 addArc(),都是弧形,区别在哪里?其实很简单: addArc() 只是一个直接使用了 forceMoveTo = true 的简化版 arcTo()

paint.style = Paint.Style.STROKE
path.lineTo(100f, 100f)
path.addArc(100f, 100f, 300f, 300f, -90f, -90f)

close() 封闭当前子图形

它的作用是把当前的子图形封闭,即由当前位置向当前子图形的起点绘制一条直线。

paint.style = Paint.Style.STROKE
path.moveTo(100, 100);
path.lineTo(200, 100);
path.lineTo(150, 150);
path.close(); // 使用 close() 封闭子图形。等价于 path.lineTo(100, 100)

close()lineTo(起点坐标) 是完全等价的。

另外,不是所有的子图形都需要使用 close() 来封闭。当需要填充图形时(即 Paint.StyleFILLFILL_AND_STROKEPath 会自动封闭子图形。

以上就是 Path 的第一类方法:直接描述路径的。

drawBitmap(Bitmap bitmap, float left, float top, Paint paint) 画 Bitmap

绘制 Bitmap 对象,也就是把这个 Bitmap 中的像素内容贴过来。其中 lefttop 是要把 bitmap 绘制到的位置坐标。它的使用非常简单。

drawBitmap(bitmap, 200, 100, paint);

drawText(String text, float x, float y, Paint paint) 绘制文字

canvas.drawText(text, 200, 100, paint);

插播五: Paint.setTextSize(float textSize)

val text = "Hello HenCoder"
paint.textSize = 18f
canvas.drawText(text, 100f, 25f ,paint)
paint.textSize = 36f
canvas.drawText(text,100f,70f, paint)
paint.textSize = 60f
canvas.drawText(text, 100f, 145f, paint)
paint.textSize = 84f
canvas.drawText(text, 100f, 240f, paint)

练习,画一个饼图

class PracticePieChartView: View{

constructor(context: Context): super(context){
init()
}

constructor(context: Context, attributeSet: AttribureSet): super(context, attributeSet){
init()
}

//定义设备名称数组,每个元素代表饼图中一个扇形的对应名称
private val s = arrayOf("Froyo", "GB", "ICS", "JB", "KitKat", "L", "M")
//定义各扇形所占的百分比数组,每个元素对应一个扇形的角度值
private val percent = arrayOf(2, 8, 10, 50, 80, 160, 50)
//定义各扇形对应的颜色数组,每个元素对应一个扇形的填充颜色
private val color = arrayOf(Color.BLACK, Color.BLUE, Color.GRAY, Color.GREEN, Color.RED, Color.LTGRAY, Color.YELLOW)

//存储设备名称的列表
private lateinit var deviceNames: List<String>
//存储颜色值的列表
private lateinit var colorInteger: List<Int>
//存储百分比的列表
private lateinit var percentInteger: List<Int>

//用于绘制饼图的画笔
private lateinit var paintPie: Paint
//用于绘制文本的画笔
private lateinit var paintText: Paint
//用于绘制指示线的画笔
private lateinit var paintLine: Paint

//定义绘制饼图的基础矩形区域,所有扇形都基于此矩形绘制
private lateinit var rectCommon: RectF
//定义绘制突出显示的扇形矩形区域,用于将某个扇形从饼图中突出显示
private lateinit var rectfMove: RectF

private fun init(){

//将设备名称数组转化为列表
deviceNames = s.toList()
//将颜色数组转化为列表
colorInteger = color.toList()
//将百分比数组转化为列表
precentInteger = percent.toList()

//初始化绘制饼图的画笔
paintPie = Paint(Paint.ANTI_ALIAS_FLAG)

//初始化指示线的画笔,设置线条宽度和颜色
paintLine = Paint(Paint.ANTI_ALIAS_FLAG).apply{
strokeWidth = 5f
color = Color.LTGRAY
}

//初始化绘制文本的画笔,设置文本大小和颜色
PaintText = Paint(Paint.ANTI_ALIAS_FLAG).apply{
textSize = 30f
color = Color.LTGRAY
}

//初始化基础矩形区域,用于绘制饼图
rectfCommon = RectF(-300f, -300f, 300f, 300f)
//初始化突出显示矩形区域,用于突出显示某个扇形
rectfMove = RectF(-350f, -350f, 250f, 250f)

}

override fun onDraw(canvas: Canvas){
super.onDraw(canvas)
val width = width
val height = height

//将画布的原点移动到View的中心,方便后续以中心为基准进行绘制
canvas.translate((width/2).toFloat(), (height/2).toFloat())

//初始化饼图扇形的起始高度
var startAngle = 0f
//定义每个扇形的扫描角度
var sweepAngle: Float
//遍历每个扇形,根据数据绘制相应的扇形、指示线和文本
for(i in deviceNames.indices){
// 设置绘制饼图扇形的画笔宽度
paintPie.strokeWidth = 10f
// 设置饼图扇形的画笔颜色,从颜色列表中获取对应颜色
paintPie.color = colorInteger[i]

//获取当前扇形的扫描角度,从百分比列表中获取对应值
sweepAngle = percentInteger[i].toFloat()

//获取当前扇形对应的设备名称,从设备民初列表中获取对应名称
val deviceName = deviceNames[i]

//计算当前扇形的中间角度,用于确定指示线的起始位置
val textAngle = startAngle + precentInteger[i]/2f

//判断是否为需要突出显示的扇形(这里是第五个扇形)
if(i == 5){
//如果是,使用突出显示的矩形区域绘制扇形
canvas.drawArc(rectfMove, startAngle + 1, sweepAngle - 1, true, paintPie)
}else{
//否则,使用基础矩形区域绘制
canvas.drawArc(rectCommon, startAngle + 1, sweepAngle - 1, true ,paintPie)
}

//计算指示线在饼图边缘的起始点的y坐标,使用三角函数根据中间角度计算,要转换成弧度制
val pointY: Float = (sin(textAngle * Math.PI)/ 180 * 300.0).toFloat()
//计算指示线在饼图边缘起始点的x坐标
val pointX: Float = (cos(textAngle * Math.PI)/ 180 * 300.0).toFloat()
//计算指示线终点的y坐标
val lineY: Float = (sin(textAngle * Math.PI)/ 180 * 300.0).toFloat()
//计算指示线终点的x坐标
val lineX: Float = (cos(textAngle * Math.PI)/ 180 * 300.0).toFloat()

//判断是否为需要突出显示的矩形
if(i == 5){
// 如果是,先将画布平移,绘制指示线,再将画布平移回来
canvas.translate(-50f, -50f)
canvas.drawLine(pointX.toFloat(), pointY.toFloat(), lineX, lineY, paintLine)
canvas.translate(50f, 50f)
}else{
canvas.drawLine(pointX, pointY, lineX, lineY, paintLine)
}
//判断指示线的重点在画布的左侧还是右侧
if(lineX < 0){
//如果在左侧,获取设备名称文本的矩形区域,用于确定文本的宽度,因为文本是从左往右绘制
val textRect = getTextRect(deviceName, paintText)
//判断是否为需要突出显示的矩形
if(i == 5){
//如果是,先将画布平移,绘制指示线和文本,再将画布平移回来
canvas.translate(-50f, -50f)
canvas.drawLine(lineX, lineY, -400f, lineY, paint)
canvas.drawText(deviceName, -420f-textRect.width, lineY, paintText)
canvas.translate(50f, 50f)
}else{
canvas.drawLine(lineX, lineY, -400f, lineY, paint)
canvas.drawText(devicenName, -420f-textRect.width, lineY, paintText)
}
}else{
//如果再右侧直接绘制指示线和文本
canvas.drawLine(lineX, lineY, 400f, lineY, paintLine)
canvas.drawText(deviceName, 400f, lineY, paintText)
}
//更新下一个矩形的起始角度,累加当前扇形的扫描角度
startAngle += sweepAngle
}
}

private fun getTextRect(deviceName: String, paint: Paint): Rect{
val mBound = Rect()
// 使用画笔获取的文本的边界信息,存储在 mBound 矩形对象中
paint.getTextBounds(deviceName, 0, deviceName.length, mBound)
return mBound
}
}