绘制基础回顾
绘制的基础要素:
- onDraw(Canvas)重写绘制方法
- Canvas实际用来绘制的工具
- Paint 画笔调整风格-颜色、线条粗细样式等
- 坐标系 View所占的坐标
- 尺寸单位-px 像素,不是DP 绘制阶段相当于和屏幕进行对话了,不同分辨率的手机DP是为了适配。自定义view时,一般需要将DP转成像素PX。
以上是Android View绘制的基本要素,必须要掌握
- onDraw方法,自定义View需要继承View类,重写onDraw方法,通过canvas进行绘制
关于onDraw方法执行:
- 第一次创建会执行
onSizeChanged
->onDraw
进行绘制 - 当View尺寸发生改变会调用
onSizeChanged()
重置和初始化的操作,然后才会调用onDraw
方法进行重新绘制 - 调用
invalidate/postInvalidate
方法时会进行重新绘制调用onDraw
方法,不会调用onSizeChanged
方法,一般invalidate用于更新绘制例如进度条的进度等等
- 第一次创建会执行
//当尺寸发生改变会调用onSizeChanged方法进行相应的处理,如初始化或者重置计算的操作
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
}
//绘制
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// //绘制一条线 line
canvas.drawLine(100f, 100f, 200f, 200f, paint)
}
- Android 中的坐标系:Y轴是向下的从中心点0 正向增加,X轴是向右冲中心点0 正向增加。
- 尺寸单位
重点注意 !!! 在进行自定义view的时候一定要注意尺寸单位,一般在XML中写的单位是dp sp不会写px,但是在使用canvas进行绘制时要使用px,这是因为dp单位是为了适配屏幕不同的分辨率的情况,所以在进行尺寸单位的时候需要将dp转换为px真实的像素,这也是为什么当屏幕尺寸发生变化需要重新绘制了因为需要重新计算dp->px适应屏幕
dp转换为px的代码如下
val Float.dp2px
get() = TypedValue.applyDimension(
//Resources.getSystem() 拿到系统的上下文 不用在传递context
TypedValue.COMPLEX_UNIT_DIP, this, Resources.getSystem().displayMetrics
)
- 绘制仪表盘来巩固基础
下面通过仪表盘绘制来巩固下绘制的基础,仪表盘如何绘制:需要圆弧arc、指针line、刻度。这其中的难点就是如何绘制刻度。 绘制圆弧可以使用
drawArc
、绘制指针使用drawLine
、绘制刻度就需要使用PathDashPathEffect
(在绘制的路径上用指定的形状压印,也就是说可以在path
绘制路径上,执行形状) 关于PathEffect 有多个子类,这里不一一介绍,PathDashPathEffect
可以看成虚线的效果PathEffect
表示要为path
做效果,效果为Path并且是一个Dash
虚线。
首先需要初始化paint画笔,设置画笔为描边效果,并且通过path绘制刻度宽2dp,高5dp的矩形
init {
//stroke
paint.strokeWidth = 3f.dp2px
//不要填充
paint.style = Paint.Style.STROKE
//刻度
dash.addRect(0f, 0f, DASH_WIDTH, DASH_HEIGHT, Path.Direction.CCW)
}
在onSizeChange进行设置,当尺寸发生变化可以实现自适应,通过path绘制圆弧开口角度为120度,这里注意需要计算圆弧的其实角度和结束角度,
PathMeasure
可以拿到圆弧的总长度,拿到总长度后我们才可以计算绘制自定义的刻度数。
PathDashPathEffect 参数:
- pathEffect 用刻度来画 调用效果的paint
- advance: 阶段 每空多长的距离画一次 通过pathMeasure来计算间距拿到总长度后计算加入要绘制20个刻度
length - 刻度的宽度/20
一定要减去一个刻度的宽度否则最后一个刻度会出去了不在圆弧上 - phase: 提前 前置量 要不要提前一段距离再画,一般设置为0即可
- style:样式直接看API
如下图计算起始角度和扫过角度:一定要注意角度是从圆弧的中心点位置开始画,并且是顺时针绘制,注意起始角度的计算override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
path.reset()//尺寸发生改变进行重置
path.addArc(
width / 2f - RADIUS, height / 2f - RADIUS, width / 2f + RADIUS,
height / 2f + RADIUS,
90 + OPEN_ANGLE / 2f,//起始角度
360 - OPEN_ANGLE,//扫过角度 结束角度
)
val pathMeasure = PathMeasure(path, false)
//拿到圆弧的总长度
val length = pathMeasure.length
//绘制刻度数
pathEffect = PathDashPathEffect(dash, (length - DASH_WIDTH) / DASH_INTERVAL, 0f, PathDashPathEffect.Style.ROTATE)
}
- 在onDraw中绘制圆弧和刻度
先绘制圆弧,然后设置paint的pathEffect绘制path的效果,然后在绘制一遍圆弧,这个圆弧就是刻度的圆弧。这里是绘制了两遍的圆弧
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
//绘制圆弧
canvas.drawPath(path, paint)
//绘制刻度
//pathEffect 用刻度来画 调用效果的paint
//advance: 阶段 每空多长的距离画一次 通过pathMeasure来设置间距
//phase: 提前 前置量 要不要提前一段距离再画
//style:样式
paint.pathEffect = pathEffect
//虚线的效果 PathEffect表示要为path做效果,效果为Path Dash虚线
//绘制带有刻度的圆弧
canvas.drawPath(path, paint)
//置为空
paint.pathEffect = null
}
为什么要绘制两遍呢?假如直接设置paint的pathEffect绘制圆弧,效果如下:所以需要提前绘制一个带有描边的圆弧,然后在绘制刻度圆弧,这样就组成了一个仪表盘。
- 绘制指针,如何绘制指针呢?指针的绘制主要是如何让指针精准的指到某个刻度上,如何进行计算?
如下图,黄色的为指针,指向了某一个刻度,要绘制这个指针,起始坐标都知道是中心点,那么终止坐标怎么计算呢?可以通过三角函数来进行计算,指针的长度是自己定义的也就是三角的长边是已知的,求出a和b就可以拿到坐标,a和b如何求解呢?a = c * sin(角度)
b = c * cos(角度)
。那么重点就是只要直到了角度就可以求出a和b的坐标。
只要求出一个刻度的角度就可知道指向某一个刻度的角度了,已知有20个刻度,开口角度120° 那么剩余的角度 360 - 120 = 240°,240 / 20 = 12° 也就是说每个刻度角度12°,在Android中的绘制基于坐标系进行绘制,那么角度计算就是:
起始角度:90 + 120 / 2
某个刻度的角度: 12 * n [n 表示第几个刻度]
那么这个三角的角度=起始角度 + 某个刻度的角度 (正负的计算不用考虑Java中提供的Math都已经进行了计算)
获取三角的角度的代码如下:OPEN_ANGLE 为圆弧的开口角度120°,DASH_INTERVAL 为刻度数:20个
fun toRadians(mark: Int = MARK): Double {
return Math.toRadians(((90 + OPEN_ANGLE / 2f) + ((360 - OPEN_ANGLE) / DASH_INTERVAL) * mark).toDouble())
}
那么指针的绘制就很简单了:DASH_LENGTH 是指针的长度,自己定义的
//绘制指针
canvas.drawLine(
width / 2f, height / 2f,
width / 2f + (DASH_LENGTH * cos(toRadians())).toFloat(),
height / 2f + (DASH_LENGTH * sin(toRadians())).toFloat(), paint
)
在自定义View,很多人会觉得好难啊 不会,其实并不难数学知识经常用到的就是三角函数,复习一下三角函数的知识即可。一定要注意在Android的坐标系以及正向绘制还是逆向绘制。
Xfermode 使用解析
Paint类提供了setXfermode(Xfermode xfermode)方法,Xfermode指明了原图像和目标图像的结合方式。Google的文档中提供了详细的解释
Xfermode有多种结合模式:
实现圆形的头像并且带有描边的效果:
private val IMAGE_WIDTH = 200f.dp2px
private val IMAGE_PADDING = 20f.dp2px
class AvatarView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val porterDuff = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
private val bounds = RectF(IMAGE_PADDING, IMAGE_PADDING, IMAGE_PADDING + IMAGE_WIDTH, IMAGE_PADDING + IMAGE_WIDTH)
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
//离屏缓冲 将某一块区域脱离屏幕 进行绘制,范围就是底板的大小 然后再添加回屏幕
val count = canvas.saveLayer(bounds, null)
//绘制圆形通过xfermode 和图像进行结合 作为底板
canvas.drawOval(IMAGE_PADDING, IMAGE_PADDING, IMAGE_PADDING + IMAGE_WIDTH, IMAGE_PADDING + IMAGE_WIDTH, paint)
//xfermode 设置融合方式
paint.xfermode = porterDuff
//绘制bitmap,和底板结合的效果是通过PorterDuff.Mode进行设置
canvas.drawBitmap(getAvatarBitmap(IMAGE_WIDTH.toInt()), IMAGE_PADDING, IMAGE_PADDING, paint)
paint.xfermode = null
//绘制完恢复,恢复到屏幕
canvas.restoreToCount(count)
//绘制描边
paint.strokeWidth = 5f.dp2px
paint.style = Paint.Style.STROKE
canvas.drawOval(IMAGE_PADDING, IMAGE_PADDING, IMAGE_PADDING + IMAGE_WIDTH, IMAGE_PADDING + IMAGE_WIDTH, paint)
}
private fun getAvatarBitmap(width: Int): Bitmap {
val option = BitmapFactory.Options()
option.inJustDecodeBounds = true//true:只读取图片的属性包括尺寸等,decode返回null,允许调用者查询位图信息而不必为其像素分配内存
//读取非常快
BitmapFactory.decodeResource(resources, R.drawable.avatar_rengwuxian, option)
option.inJustDecodeBounds = false//设置为FALSE 读取像素和位图的信息 用于第二次读取图片
option.inDensity = option.outWidth//原图大小
option.inTargetDensity = width//我要的大小
return BitmapFactory.decodeResource(resources, R.drawable.avatar_rengwuxian, option)//返回bitmap
}
}