2010年JetBrains推出kotlin语言,次年将其开源,在2017年Google I/O大会上,官宣kotlin成为Android开发第一编程语言。这就像当年Google官宣使用Android Studio成为Android官方支持的IDE一样,刚开始很多人还是继续使用Eclipse,觉得AS不好用,可是慢慢的,随着AS的不断迭代完善,基本上大部分人都转过来使用AS了。
2017/6的时候,也写过一篇kotlin入门博客,Kotlin入门配置与简单实战,不过之后就不了了之了,因为那时候的工作重点在大数据。所以,很惭愧,作为一名Android开发人员,现在才开始真正学习kotlin!
某一天,去图书馆看小说的时候无意中发现书架上有一本书《Android自定义控件开发入门与实战》,拿回去学习了一天后发现还是做出点案例比较实在,正好想到最近在做地图有关的项目,界面上会以数字形式显示实时速度信息,那么能不能做出一个类似汽车码表的自定义View来代替数字显示实时速度呢,答案是肯定的
第一步,画一个半圆,这里我假设这个半圆是一个正方形的圆弧,使用path路径arcTo方法即可完成
第二步,画一个中心白点,就是上面说的那个正方形的圆心,drawCircle搞定
第三步,画半圆上的大刻度,首先我们一眼就能看出这个刻度肯定是用drawLine画出来的,所以只要知道起终点坐标就ok了。
先说起点坐标,起点就是半圆上的点,半圆上那么多点,我只取其中的十三个点,就好像弱水三千我只取一瓢饮一样。那么为什么取十三个点呢,因为我看正常的汽车码表都是从0到240,每个格子20,那就是十二个格子,换成点那就是十三,我们首先可以算出每个格子占用的角度,那就是180/12 = 15,这样就知道每个点的角度,那就是180、195、210、225、240…,然后开始计算坐标,首先要把角度转换为弧度,因为接下来要用的正余弦函数,他们是要使用弧度作为参数的,我们来看下面这张图
B点的坐标(c,d)、圆的半径r、角度m都是已知的,现在要求A点的坐标,我们知道如下公式
cosm = f / r
sinm = e / r
所以A点坐标(x,y)就分别是
x = c + cosm * r
y = d + sinm * r
注意这里的m实际度数是180+m,所以cosm或者sinm算出来的应该是负数,所以最终就可以拿到起点坐标(x,y)了
OK,我们已经拿到了起点坐标,接下来计算终点坐标,这条线其实就是从半径这条线上截取了一小段,所以我们依然可以使用正余弦函数来获取目标值,只不过原来正余弦函数里面的斜边r,换成我们想要的偏移量即可
finalx = x - cosm * 偏移量即刻度的长度
finaly = y - sinm * 偏移量即刻度的长度
最后使用drawLine画出所有大刻度
第四步,画出小刻度,同理上一步,只不过角度和长度不一样而已,此处不再赘述
第五步,画数字,其实和上一步差不多,只不过这里要注意的是,文字不像线条或者点,文字本身是横线扩展的,所以我们要根据索引来动态调整最终的xy坐标
第六步,画单位km/h,也是调用drawText方法,这里就根据270度这个角度来画,这样才会把文字画在中间,同时把偏移量调大一点,最后把文字设置成斜体即可
第七步,画指针,原理和画大小刻度差不多,重点在于把数字速度转换成角度,这样做的目的是为了计算出指针终点的坐标,指针起点坐标就是圆心
(1)定义全局变量
/**
* 当前速度,单位km/h
*/
private var curSpeed: Int = -1
/**
* 画笔
*/
private val paint = Paint()
/**
* 画外层圆弧,路径和椭圆
*/
private var rectF: RectF? = null
private var arcPath: Path? = null
/**
* 中心点坐标、半径
*/
private var centerX: Float? = null
private var centerY: Float? = null
private var radius: Float? = null
/**
* 码表外层RectF
*/
private var left = -1F
private var top = -1F
private var right = -1F
private var bottom = -1F
/**
* 偏移量
*/
private val pointerOffset = 20
private val unitOffset = 140
private val numberOffset = 70
private val smallOffset = 25
private val bigOffset = 40
/**
* 码表表盘数组
*/
private val bigMarkArr: MutableList<Double> = mutableListOf()
private val smallMarkArr: MutableList<Double> = mutableListOf()
private val numberArr: MutableList<String> = mutableListOf()
(2)初始化数组
init {
// 大刻度数组
for (i in 180..360 step 15)
bigMarkArr.add(i.toDouble())
// 小刻度数组
for (i in 187..352 step 15)
smallMarkArr.add(i + 0.5)
// 速度数字数组 12组 需要和刻度数组数量相等
for (i in 0..60 step 5)
numberArr.add(i.toString())
}
(3)获取控件宽高,计算圆心半径
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
centerX = width / 2.toFloat()
centerY = height * 5 / 6.toFloat()
radius = height * 3 / 4.toFloat()
left = centerX!! - radius!!
top = centerY!! - radius!!
right = centerX!! + radius!!
bottom = centerY!! + radius!!
rectF = RectF(left, top, right, bottom)
arcPath = Path()
}
(4)画外层半圆
/**
* 画外层半圆
*/
private fun drawHalf(canvas: Canvas?) {
paint.isAntiAlias = true
paint.color = Color.WHITE
paint.style = Paint.Style.STROKE
paint.strokeWidth = 10F
arcPath!!.rewind() // 清除直线数据,保留数据结构,方便快速重用
arcPath!!.arcTo(rectF, 178F, 184F)// 多截取一点弧会好看点
canvas?.drawPath(arcPath!!, paint)
}
(5)画圆点
/**
* 画圆点
*/
private fun drawCenter(canvas: Canvas?) {
val centerRadius = 3F
paint.color = Color.WHITE
paint.style = Paint.Style.FILL_AND_STROKE
canvas?.drawCircle(centerX!!, centerY!!, centerRadius, paint)
}
(6)画大刻度
/**
* 画大刻度
*/
private fun drawBig(canvas: Canvas?) {
paint.color = Color.WHITE
paint.strokeWidth = 10F
paint.style = Paint.Style.FILL_AND_STROKE
for (item in bigMarkArr) {
val radian = toRadians(item) // 角度转弧度
val firstX = getRoundX(radian).toFloat()
val firstY = getRoundY(radian).toFloat()
val secondX = (firstX - cos(radian) * bigOffset).toFloat()
val secondY = (firstY - sin(radian) * bigOffset).toFloat()
canvas?.drawLine(firstX, firstY, secondX, secondY, paint)
}
}
(7)画小刻度
/**
* 画小刻度
*/
private fun drawSmall(canvas: Canvas?) {
paint.color = Color.WHITE
paint.strokeWidth = 5F
paint.style = Paint.Style.FILL_AND_STROKE
for (item in smallMarkArr) {
val radian = toRadians(item) // 角度转弧度
val firstX = getRoundX(radian).toFloat()
val firstY = getRoundY(radian).toFloat()
val secondX = (firstX - cos(radian) * smallOffset).toFloat()
val secondY = (firstY - sin(radian) * smallOffset).toFloat()
canvas?.drawLine(firstX, firstY, secondX, secondY, paint)
}
}
(8)画数字速度
/**
* 画数字
*/
private fun drawNumber(canvas: Canvas?) {
paint.textSize = 25F
paint.strokeWidth = 5F
paint.textSkewX = 0F // 倾斜度设置为0,就是非斜体
paint.style = Paint.Style.FILL
for (index in numberArr.indices) {
val radian = toRadians(bigMarkArr[index]) // 角度转弧度
val firstX = getRoundX(radian)
val firstY = getRoundY(radian)
val secondX = (firstX - cos(radian) * numberOffset - index * 3).toFloat()//距离微调
val secondY = (firstY - sin(radian) * numberOffset + index).toFloat()//距离微调
canvas?.drawText(numberArr[index], secondX, secondY, paint)
}
}
(9)画单位
/**
* 画单位 km/h
*/
private fun drawUnit(canvas: Canvas?) {
paint.textSize = 20F
paint.textSkewX = -0.25F // 斜体
val one = toRadians(270.0) // 角度转弧度 最顶端的圆点
val testX = getRoundX(one)
val testY = getRoundY(one)
val finalX = (testX - cos(one) * unitOffset - 27).toFloat()//距离微调
val finalY = (testY - sin(one) * unitOffset + 9).toFloat()//距离微调
canvas?.drawText("km/h", finalX, finalY, paint)
}
(10)画指针
/**
* 画指针
*/
private fun drawPointer(canvas: Canvas?) {
paint.color = Color.RED
paint.strokeWidth = 8F
// (180 + 3 * curSpeed)的意义在于把速度转换为角度
val pointerDegree = (180 + 3 * curSpeed).toDouble()
val radian = toRadians(pointerDegree)
val firstX = getRoundX(radian)
val firstY = getRoundY(radian)
val secondX = (firstX - cos(radian) * pointerOffset).toFloat()
val secondY = (firstY - sin(radian) * pointerOffset).toFloat()
canvas?.drawLine(centerX!!, centerY!!, secondX, secondY, paint)
}
代码已开源在github上,可直接查看https://github.com/xmliu/SpeedView,同时代码已发布到jitpack上,可直接gradle依赖使用到项目中去,具体方法可查看项目主页介绍或者Android发布开源控件到jitpack给他人使用使用案例
1、指针动画均匀移动
2、表盘带有3d层次感