效果是这样的,公司需求,画了一天,头皮发麻。
直接把代码贴上吧,懒得讲了,下班。

Android 自定义View 表盘

难点就一个,刻度的渐变效果怎么停下来。我想了半天,最后想到了,到了值,直接把渐变设为null即可。

代码直接Copy是肯定用不了的,只是一种思路,看看就好

View的代码

/**
 * 风速计首页表盘View
 * @author cym
 */
class AneDialView @JvmOverloads constructor(
    mContext: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(mContext, attrs, defStyleAttr) {

    // 画笔
    private val mPaint: Paint = Paint()

    // 下面的渐变蒙版
    private lateinit var mBottomMask: Shader

    // 刻度渐变
    private lateinit var mGraduationShader: Shader

    // 圆心坐标
    private var mCircleX = 0f
    private var mCircleY = 0f
    private var mCircleRadius = 0f

    // 圆矩形
    private lateinit var mRectCircle: RectF

    // 刻度弧矩形
    private lateinit var mRectGraduation: RectF

    // 刻度字矩形
    private lateinit var mRectText: RectF

    // 里面大圆矩形
    private lateinit var mRectBoard: RectF

    // 下面的渐变蒙版矩形
    private lateinit var mRectBottomMask: RectF

    // 弧度,不要改。如果要改,计算渐变的地方也要手动改,懒得判断了
    private var mSweepAngle = 252f

    // 刻度圆的大小
    private var mCircleRadiusSmall = dp2px(1.5f)
    private var mCircleRadiusMiddle = dp2px(3f)
    private var mCircleRadiusLarge = dp2px(4f)

    // 颜色
    private val mColorWhite = ContextCompat.getColor(mContext, R.color.colorWhite)
    private val mColorGray = ContextCompat.getColor(mContext, R.color.colorFontGray)
    private val mColorGraduation = ContextCompat.getColor(mContext, R.color.colorGraduation)
    private val mColorGraduationStart = ContextCompat.getColor(mContext, R.color.colorGraduationStart)
    private val mColorGraduationMiddle = ContextCompat.getColor(mContext, R.color.colorGraduationMiddle)
    private val mColorGraduationEnd = ContextCompat.getColor(mContext, R.color.colorGraduationEnd)
    private val mColorBottomMaskStart = ContextCompat.getColor(mContext, R.color.colorBottomMaskStart)
    private val mColorBottomMaskEnd = ContextCompat.getColor(mContext, R.color.colorBottomMaskEnd)

    // 数字字体
    private val mTypefaceNum: Typeface = Typeface.createFromAsset(context.assets, "Oswald-Regular.ttf")

    // 文本是否亮。懒得写getSet了,直接在外面设置就好
    var mIsShowNum = true
    var mIsShowM = true
    var mIsShowFT = false
    var mIsShowINHG = false
    var mIsShowHPA = false
    var mIsShowMBAR = false
    var mIsShowMS = false
    var mIsShowKMH = false
    var mIsShowFTMIN = true
    var mIsShowKNOTS = false
    var mIsShowMPH = false
    var mIsShowMAX = true
    var mIsShowMIN = false
    var mIsShowAVG = false
    var mIsShowALT = false
    var mIsShowRH = false
    var mIsShowDP = false
    var mIsShowWCL = false
    var mIsShowC = true
    var mIsShowF = false

    // 当前风的级数,风的级数从0~12,小于0代表没连接设备
    private var mCurLv = -1f

    // 初始化
    init {
        mPaint.isAntiAlias = true
        // 随机设置风级
        mCurLv = (-10..140).random().toFloat() / 10f
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        var width = dp2px(50f).toInt()
        var height = dp2px(50f).toInt()

        // 不是wrap
        if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.AT_MOST) {
            width = MeasureSpec.getSize(widthMeasureSpec)
        }
        if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.AT_MOST) {
            height = MeasureSpec.getSize(heightMeasureSpec)
        }
        setMeasuredDimension(width, height)
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        // 圆的半径是不变的
        mCircleRadius = w / 2f - dp2px(1f)
        mCircleX = w / 2f
        mCircleY = w / 2f
        // 圆矩形和圆是一样大的
        mRectCircle = RectF(
            mCircleX - mCircleRadius,
            mCircleY - mCircleRadius,
            mCircleX + mCircleRadius,
            mCircleY + mCircleRadius
        )
        // 刻度矩形
        mRectGraduation = RectF(
            mCircleX - mCircleRadius + dp2px(16f),
            mCircleY - mCircleRadius + dp2px(16f),
            mCircleX + mCircleRadius - dp2px(16f),
            mCircleY + mCircleRadius - dp2px(16f)
        )
        // 刻度矩形
        mRectText = RectF(
            mCircleX - mCircleRadius + dp2px(32f),
            mCircleY - mCircleRadius + dp2px(32f),
            mCircleX + mCircleRadius - dp2px(32f),
            mCircleY + mCircleRadius - dp2px(32f)
        )
        // 里面部分矩形
        mRectBoard = RectF(
            mCircleX - mCircleRadius + dp2px(40f),
            mCircleY - mCircleRadius + dp2px(40f),
            mCircleX + mCircleRadius - dp2px(40f),
            mCircleY + mCircleRadius - dp2px(40f)
        )
        // 底部渐变蒙版矩形
        mRectBottomMask = RectF(
            0f,
            h - dp2px(50f),
            w * 1.0f,
            h * 1.0f
        )
        // 底部渐变蒙版
        mBottomMask = LinearGradient(
            0f,
            mRectBottomMask.top,
            0f,
            mRectBottomMask.bottom,
            mColorBottomMaskStart,
            mColorBottomMaskEnd,
            Shader.TileMode.CLAMP
        )
        // 刻度渐变
        val arrayColor = intArrayOf(
            mColorGraduationEnd,
            mColorGraduationStart,
            mColorGraduationStart,
            mColorGraduationMiddle,
            mColorGraduationEnd
        )
        val arrayPosition = floatArrayOf(0f, 0.25f, (9 * 18f / 360f), 0.75f, 1.0f)
        mGraduationShader = SweepGradient(mCircleX, mCircleY, arrayColor, arrayPosition)
    }

    override fun onDraw(canvas: Canvas) {

        // 左下角的角度
        val startAngle = 180 - (mSweepAngle - 180) / 2f

        // 画圆形圆弧
        mPaint.color = mColorWhite
        mPaint.style = Paint.Style.STROKE
        mPaint.strokeCap = Paint.Cap.ROUND
        mPaint.strokeWidth = dp2px(2f)
        canvas.drawArc(mRectCircle, startAngle, mSweepAngle, false, mPaint)
        // 画刻度圆弧背景
        mPaint.color = mColorGraduation
        mPaint.strokeWidth = dp2px(16f)
        canvas.drawArc(mRectGraduation, startAngle, mSweepAngle, false, mPaint)

        // 设置圆点扫描渐变,到了当前风级清除渐变,就能实现这种效果
        mPaint.shader = mGraduationShader
        // 判断当前级数的度数
        val curAngle = if (mCurLv < 0) {
            // 还没有连上设备
            -1f
        } else {
            // 已经连上设备了
            startAngle + (mCurLv + 1) / 12f * 216f
        }

        // 使用角度算XY坐标画圆点
        mPaint.color = mColorWhite
        mPaint.style = Paint.Style.FILL
        mPaint.strokeWidth = 1f
        val graduationRadius = (mRectGraduation.width() / 2f).toInt()
        for (i in 0 until 71) {
            val angle = startAngle + i * (mSweepAngle / 70f)
            val x = getXByDegrees(mCircleX.toInt(), graduationRadius, angle).toFloat()
            val y = getYByDegrees(mCircleY.toInt(), graduationRadius, angle).toFloat()
            // 判断圆的大小
            val radius = when {
                i % 10 == 0 -> {
                    // 中圆
                    mCircleRadiusMiddle
                }
                i % 5 == 0 -> {
                    // 大圆
                    mCircleRadiusLarge
                }
                else -> {
                    // 小圆
                    mCircleRadiusSmall
                }
            }
            if (angle > curAngle) {
                // 清除渐变
                mPaint.shader = null
            }
            canvas.drawCircle(x, y, radius, mPaint)
        }
        mPaint.shader = null
        
        // 画里面的大圆弧背景
        mPaint.color = mColorGraduation
        mPaint.style = Paint.Style.FILL
        canvas.drawCircle(mCircleX, mCircleY, mRectBoard.width() / 2f, mPaint)

        // 画底部蒙版
        mPaint.color = mColorWhite
        mPaint.strokeWidth = 1f
        mPaint.shader = mBottomMask
        canvas.drawRect(mRectBottomMask, mPaint)
        mPaint.shader = null

        // 画刻度圆点上的文本。旋转画布会让文字方向也旋转,所以不能使用旋转画布
        mPaint.textAlign = Paint.Align.CENTER
        mPaint.textSize = dp2px(12f)
        val textRadius = (mRectText.width() / 2f).toInt()
        for (i in 0..12 step 2) {
            val angle = startAngle + (i + 1) * (mSweepAngle / 14)
            val x = getXByDegrees(mCircleX.toInt(), textRadius, angle).toFloat()
            val y = getYByDegrees(mCircleY.toInt(), textRadius, angle).toFloat()
            canvas.drawText(i.toString(), x, getBaseline(y), mPaint)
        }

        // 画表盘上的文字
        var x = 0f
        var y = 0f

        // Wind Value
        mPaint.typeface = Typeface.DEFAULT
        mPaint.color = mColorWhite
        mPaint.textSize = dp2px(14f)
        x = mCircleX
        y = mRectBoard.top + dp2px(30f)
        canvas.drawText("Wind Value", x, getBaseline(y), mPaint)

        // 中心数字
        mPaint.typeface = mTypefaceNum
        mPaint.color = if (mIsShowNum) mColorWhite else mColorGray
        mPaint.textSize = dp2px(52f)
        x = mCircleX
        y = mCircleY - dp2px(30f)
        canvas.drawText("000.0", x, getBaseline(y), mPaint)

        // M
        mPaint.textAlign = Paint.Align.LEFT
        mPaint.typeface = Typeface.DEFAULT
        mPaint.color = if (mIsShowM) mColorWhite else mColorGray
        mPaint.textSize = dp2px(11f)
        x = mCircleX - mRectBoard.width() / 2f + dp2px(18f)
        y = mCircleY - dp2px(48f)
        canvas.drawText("M", x, getBaseline(y), mPaint)

        // FT
        mPaint.color = if (mIsShowFT) mColorWhite else mColorGray
        x = mCircleX - mRectBoard.width() / 2f + dp2px(30f)
        y = mCircleY - dp2px(48f)
        canvas.drawText("FT", x, getBaseline(y), mPaint)

        // inHg
        mPaint.color = if (mIsShowINHG) mColorWhite else mColorGray
        x = mCircleX - mRectBoard.width() / 2f + dp2px(18f)
        y = mCircleY - dp2px(36f)
        canvas.drawText("inHg", x, getBaseline(y), mPaint)

        // hpa
        mPaint.color = if (mIsShowHPA) mColorWhite else mColorGray
        x = mCircleX - mRectBoard.width() / 2f + dp2px(18f)
        y = mCircleY - dp2px(24f)
        canvas.drawText("hpa", x, getBaseline(y), mPaint)

        // mbar
        mPaint.color = if (mIsShowMBAR) mColorWhite else mColorGray
        x = mCircleX - mRectBoard.width() / 2f + dp2px(18f)
        y = mCircleY - dp2px(12f)
        canvas.drawText("mbar", x, getBaseline(y), mPaint)

        // m/s
        mPaint.color = if (mIsShowMS) mColorWhite else mColorGray
        x = mCircleX + mRectBoard.width() / 2f - dp2px(42f)
        y = mCircleY - dp2px(54f)
        canvas.drawText("m/s", x, getBaseline(y), mPaint)

        // km/h
        mPaint.color = if (mIsShowKMH) mColorWhite else mColorGray
        x = mCircleX + mRectBoard.width() / 2f - dp2px(42f)
        y = mCircleY - dp2px(42f)
        canvas.drawText("km/h", x, getBaseline(y), mPaint)

        // ft/min
        mPaint.color = if (mIsShowFTMIN) mColorWhite else mColorGray
        x = mCircleX + mRectBoard.width() / 2f - dp2px(42f)
        y = mCircleY - dp2px(30f)
        canvas.drawText("ft/min", x, getBaseline(y), mPaint)

        // Knots
        mPaint.color = if (mIsShowKNOTS) mColorWhite else mColorGray
        x = mCircleX + mRectBoard.width() / 2f - dp2px(42f)
        y = mCircleY - dp2px(18f)
        canvas.drawText("Knots", x, getBaseline(y), mPaint)

        // mph
        mPaint.color = if (mIsShowMPH) mColorWhite else mColorGray
        x = mCircleX + mRectBoard.width() / 2f - dp2px(42f)
        y = mCircleY - dp2px(6f)
        canvas.drawText("mph", x, getBaseline(y), mPaint)

        // MAX
        mPaint.textAlign = Paint.Align.CENTER
        mPaint.textSize = dp2px(14f)
        mPaint.color = if (mIsShowMAX) mColorWhite else mColorGray
        x = mRectBoard.left + mRectBoard.width() / 5f * 1
        y = mCircleY + dp2px(18f)
        canvas.drawText("MAX", x, getBaseline(y), mPaint)

        // MIN
        mPaint.color = if (mIsShowMIN) mColorWhite else mColorGray
        x = mRectBoard.left + mRectBoard.width() / 5f * 2
        y = mCircleY + dp2px(18f)
        canvas.drawText("MIN", x, getBaseline(y), mPaint)

        // AVG
        mPaint.color = if (mIsShowAVG) mColorWhite else mColorGray
        x = mRectBoard.left + mRectBoard.width() / 5f * 3
        y = mCircleY + dp2px(18f)
        canvas.drawText("AVG", x, getBaseline(y), mPaint)

        // ALT
        mPaint.color = if (mIsShowALT) mColorWhite else mColorGray
        x = mRectBoard.left + mRectBoard.width() / 5f * 4
        y = mCircleY + dp2px(18f)
        canvas.drawText("ALT", x, getBaseline(y), mPaint)

        // RH%
        mPaint.color = if (mIsShowRH) mColorWhite else mColorGray
        x = mRectBoard.left + mRectBoard.width() / 6f * 1
        y = mCircleY + dp2px(42f)
        canvas.drawText("RH%", x, getBaseline(y), mPaint)

        // DP
        mPaint.color = if (mIsShowDP) mColorWhite else mColorGray
        x = mRectBoard.left + mRectBoard.width() / 6f * 2
        y = mCircleY + dp2px(42f)
        canvas.drawText("DP", x, getBaseline(y), mPaint)

        // WCL
        mPaint.color = if (mIsShowWCL) mColorWhite else mColorGray
        x = mRectBoard.left + mRectBoard.width() / 6f * 3
        y = mCircleY + dp2px(42f)
        canvas.drawText("WCL", x, getBaseline(y), mPaint)

        // ℃
        mPaint.color = if (mIsShowC) mColorWhite else mColorGray
        x = mRectBoard.left + mRectBoard.width() / 6f * 4
        y = mCircleY + dp2px(42f)
        canvas.drawText("℃", x, getBaseline(y), mPaint)

        // ℉
        mPaint.color = if (mIsShowF) mColorWhite else mColorGray
        x = mRectBoard.left + mRectBoard.width() / 6f * 5
        y = mCircleY + dp2px(42f)
        canvas.drawText("℉", x, getBaseline(y), mPaint)

        // 底部温度
        mPaint.typeface = Typeface.DEFAULT
        mPaint.color = mColorWhite
        mPaint.textAlign = Paint.Align.CENTER
        mPaint.textSize = dp2px(18f)
        x = mCircleX
        y = height - dp2px(10f)
        canvas.drawText("温度 --", x, getBaseline(y), mPaint)
    }

    /**
     * dp转px
     * @param dpValue dp
     */
    private fun dp2px(dpValue: Float): Float {
        val scale = resources.displayMetrics.density
        return dpValue * scale + 0.5f
    }

    /**
     * 根据y计算baseline
     * @param y 文本的y
     */
    private fun getBaseline(y: Float): Float {
        return y + (mPaint.fontMetrics.bottom - mPaint.fontMetrics.top) / 2 - mPaint.fontMetrics.bottom
    }

    // 根据圆心X,半径,角度计算X坐标
    private fun getXByDegrees(centerX: Int, radius: Int, degrees: Float): Int {
        return (centerX + radius * cos(degrees * Math.PI / 180)).toInt()
    }

    // 根据圆心Y,半径,角度计算Y坐标
    private fun getYByDegrees(centerY: Int, radius: Int, degrees: Float): Int {
        return (centerY + radius * sin(degrees * Math.PI / 180)).toInt()
    }

    // 根据圆心XY,坐标XY,计算弧度
    private fun getDegreesByXY(centerX: Int, centerY: Int, x: Int, y: Int): Float {
        var degrees = Math.toDegrees(atan2((y - centerY).toDouble(), (x - centerX).toDouble())).toFloat()
        if (degrees < 0) {
            degrees += 360f
        }
        return degrees
    }
}

color.xml

<resources>
    <color name="colorWhite">#ffffff</color>
    <color name="colorMain">#3e82fe</color>
    <color name="colorFontGray">#4fffffff</color>
    <color name="colorGraduation">#2565D6</color>
    <color name="colorGraduationStart">#60ff40</color>
    <color name="colorGraduationMiddle">#ffce57</color>
    <color name="colorGraduationEnd">#ff4d4d</color>
    <color name="colorBottomMaskStart">#003e82fe</color>
    <color name="colorBottomMaskEnd">#ff3e82fe</color>
</resources>

布局xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorMain">

    <!-- 顶部布局 -->
    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/cons_top"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent">

        <!-- 返回按钮 -->
        <ImageView
            android:id="@+id/iv_back"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:scaleType="center"
            android:src="@drawable/back_white"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <!-- 蓝牙状态 -->
        <TextView
            android:id="@+id/tv_connect_state"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/scaning_device_title"
            android:textColor="@color/colorWhite"
            android:textSize="14dp"
            app:layout_constraintBottom_toBottomOf="@id/iv_back"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="@id/iv_back" />

        <!-- 表盘View -->
        <cn.net.aicare.moudleAnemometer.view.AneDialView
            android:id="@+id/ane_dial_view"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:layout_constraintDimensionRatio="w, 615:817"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/iv_back"
            app:layout_constraintWidth_percent="0.838" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

标签: android, 自定义View

已有 2 条评论

  1. abc abc

    这是画出来的??不用贴图嘛

    1. cym cym

      我也想用贴图啊,但是这些文本都要控制亮还是不亮

添加新评论