kotlin-实现仪表盘view

效果:

思路:

  • 1、首先画一个圆弧;
  • 2、画刻度,就是一个个小矩形,但是有方向。用PathDashPathEffect来画就很简单,把小矩形当做虚线路径;
  • 3、画指针,就是一条线,起点是圆心,终点用三角函数计算
  • 4、加一个pointerProgress属性,并且设置set方法,给属性动画使用,这样指针就可以做动画了
  • ps:给圆弧和刻度的绘画过程也加了动画,只是为了更深刻的体会绘制的过程。
    代码中各种注释都有

自定义view代码

package com.xc.test

import android.animation.Animator
import android.animation.ObjectAnimator
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
import com.xc.test.util.logE
import com.xc.test.util.px
import kotlin.math.cos
import kotlin.math.sin

/**
 * @author: xuchun
 * @time: 2021/4/1 - 16:20
 * @desc: 仪表盘,支持指针转动动画
 * 1、首先画一个圆弧;
 * 2、画刻度,就是一个个小矩形,但是有方向。用PathDashPathEffect来画就很简单,把小矩形当做虚线路径;
 * 3、画指针,就是一条线,起点是圆心,终点用三角函数计算
 * 4、加一个pointerProgress属性,并且设置set方法,给属性动画使用,这样指针就可以做动画了
 *
 * ps:给圆弧和刻度的绘画过程也加了动画,只是为了更深刻的体会绘制的过程
 */
//仪表盘开口角度
const val OPEN_ANGLE = 90f

//仪表盘半径
val RADIUS = 100f.px

//刻度,即一个小矩形的宽高
val DASH_WIDTH = 2f.px
val DASH_LENGTH = 5f.px

//指针的长度
val POINTER_LENGTH = 80f.px

//刻度间隔数
const val DASH_COUNT = 20f

class DashboardView : View {
    constructor(context: Context?) : super(context)
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    )

    /**值区间:0.0 - DASH_COUNT*/
    private var pointerProgress = 0f
        set(value) {
            field = value
            invalidate()
        }

    /**值区间:0.0 - 100*/
    private var arcProgress = 0f
        set(value) {
            field = value
            invalidate()
        }

    /**值区间:0.0 - 100*/
    private var dashProgress = 0f
        set(value) {
            field = value
            invalidate()
        }


    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)

    //整体所在矩形
    private lateinit var rectF: RectF

    //表盘圆弧
    private val tempArc = Path()
    private val pathArc = Path()

    //刻度的圆弧
    private val pathDashArc = Path()

    //刻度线小矩形,虽然叫做path,其实更多是用来画图形的
    private val dash = Path()

    //用来计算path长度
    private lateinit var pathMeasure: PathMeasure

    //路径效果定制器
    private var pathDashPathEffect: PathDashPathEffect? = null

    //指针角度 (用三角函数时需要把角度转成弧度)
    var pointerAngle = (360 - OPEN_ANGLE) / 20f * pointerProgress + 90 + OPEN_ANGLE / 2


    init {
        //因为画笔颜色没变,这里就不用填充了,不然刻度和指针都看不见
        paint.style = Paint.Style.STROKE
        paint.strokeWidth = 3f.px
        //构建刻度的小矩形
        dash.addRect(0f, 0f, DASH_WIDTH, DASH_LENGTH, Path.Direction.CW)
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        pathArc.reset()
        tempArc.reset()
        rectF =
            RectF(width / 2 - RADIUS, height / 2 - RADIUS, width / 2 + RADIUS, height / 2 + RADIUS)
        tempArc.addArc(rectF, 90 + OPEN_ANGLE / 2, (360 - OPEN_ANGLE))

        //动画画圆弧
        val objectAnimator = ObjectAnimator.ofFloat(this@DashboardView, "arcProgress", 0f, 100f)
        objectAnimator.duration = 1000
//        objectAnimator.interpolator = LinearInterpolator()
        objectAnimator.addListener(object : Animator.AnimatorListener {
            override fun onAnimationStart(animation: Animator?) {
            }

            override fun onAnimationCancel(animation: Animator?) {
            }

            override fun onAnimationRepeat(animation: Animator?) {
            }

            override fun onAnimationEnd(animation: Animator?) {
                //圆弧画完 再画其他
                drawArcFinished = true
                preDrawDsh()

                //开始画刻度
                val objectA = ObjectAnimator.ofFloat(this@DashboardView, "dashProgress", 0f, 100f)
                objectA.duration = 2000
                objectA.start()
            }
        })
        objectAnimator.start()

    }

    private var drawArcFinished = false

    private fun preDrawDsh() {
        //假如有20个格子(21个刻度)  那就用弧线总长度除以20 就是间隔
        //计算弧线总长度
        //创建路径效果:用虚线模式  虚线就是刻度小矩形
        pathMeasure = PathMeasure(tempArc, false)
        "pathMeasure.length = ${pathMeasure.length}".logE()
        pathDashPathEffect = PathDashPathEffect(
            dash,
            (pathMeasure.length - DASH_WIDTH) / 20f,
            0f,
            PathDashPathEffect.Style.ROTATE
        )
    }

    override fun onDraw(canvas: Canvas?) {
        /**
         * useCenter = true 可以用来做饼图! =false就是仪表盘了  不过要想用userCenter要用canvas?.drawArc不能用path.addArc
         */
        //1、画圆弧
        pathArc.addArc(rectF, 90 + OPEN_ANGLE / 2, (360 - OPEN_ANGLE) * (arcProgress / 100))
        canvas?.drawPath(pathArc, paint)

        //2、画刻度效果
        drawDashPath(canvas)

        //3、画指针
        drawPointer(canvas)
    }

    private fun drawPointer(canvas: Canvas?) {
        if (!drawArcFinished) return
        //指针的起点是圆心,终点坐标就是三角函数的正余弦,正弦*斜边长度=纵坐标,余弦*斜边长度 = 横坐标,算好坐标再加上起始坐标即可
        //注意:三角函数需要传入的是弧度值,要先把角度转弧度 、并且不用考虑坐标的正负数,因为三角函数自带正负
        //指针的角度计算 = 圆弧的划过角度除以DASH_COUNT乘以progress可以得到指针在圆弧的角度,再加上圆弧的起始角度(90+半个开口角度),最终得到指针真正的角度
        pointerAngle = (360 - OPEN_ANGLE) / DASH_COUNT * pointerProgress + 90 + OPEN_ANGLE / 2
        //        "pointerAngle = $pointerAngle".logE()
        canvas?.drawLine(
            width / 2f,
            height / 2f,
            (cos(Math.toRadians(pointerAngle.toDouble())) * POINTER_LENGTH + width / 2f).toFloat(),
            (sin(Math.toRadians(pointerAngle.toDouble())) * POINTER_LENGTH + height / 2f).toFloat(),
            paint
        )
    }

    private fun drawDashPath(canvas: Canvas?) {
        pathDashPathEffect?.let {
            paint.pathEffect = pathDashPathEffect
            pathDashArc.addArc(
                rectF,
                90 + OPEN_ANGLE / 2,
                (360 - OPEN_ANGLE) * (dashProgress / 100)
            )
            canvas?.drawPath(pathDashArc, paint)
            paint.pathEffect = null
        }
    }
}

工具类代码

val Float.px
    get() = TypedValue.applyDimension(
        TypedValue.COMPLEX_UNIT_DIP,
        this,
        Resources.getSystem().displayMetrics
    )

fun String.logE() {
    Log.e("xc", this)
}

activity中指针动画代码

startButton.setOnClickListener {
            val objectAnimator = ObjectAnimator.ofFloat(dash_view, "pointerProgress", 0f, 20f)
            objectAnimator.duration = 10000
            objectAnimator.interpolator = LinearInterpolator()
            objectAnimator.start()
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容