Kotlin编写Android自定义View之码表

一、前言

  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来代替数字显示实时速度呢,答案是肯定的

三、自定义View

1、效果图展示

在这里插入图片描述

2、思路分析

  第一步,画一个半圆,这里我假设这个半圆是一个正方形的圆弧,使用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度这个角度来画,这样才会把文字画在中间,同时把偏移量调大一点,最后把文字设置成斜体即可
  第七步,画指针,原理和画大小刻度差不多,重点在于把数字速度转换成角度,这样做的目的是为了计算出指针终点的坐标,指针起点坐标就是圆心

3、代码实现

(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)
    }

4、代码发布

代码已开源在github上,可直接查看https://github.com/xmliu/SpeedView,同时代码已发布到jitpack上,可直接gradle依赖使用到项目中去,具体方法可查看项目主页介绍或者Android发布开源控件到jitpack给他人使用使用案例

四、后续优化

1、指针动画均匀移动
2、表盘带有3d层次感

猜你喜欢

转载自blog.csdn.net/diyangxia/article/details/106598452