Android运动健康首页混合自定义控件

1,186 阅读2分钟

效果图

微信图片_20210329144146.png

代码


/**
 * 混合图表
 * zrj 2020/9/7
 */
class MixingChart(context: Context, attrs: AttributeSet?) : View(context, attrs) {
    //屏幕宽高
    private var scrWidth = 0f
    private var scrHeight = 0f
    private lateinit var paintPolyline: Paint //心率折线
    private lateinit var paintPolyShadow: Paint //心率折线阴影
    private var mLinePath: Path  //折线路径
    private var mRectF: RectF
    private var mStartAngle = 270F
    private var mSweepAngle = 360F
    private lateinit var mBgCirPaint: Paint
    private var mBgCirWidth = 28f//宽度
    private var mPercent = 0f

    @Type.Project
    private var type = 0  //对应的项目

    init {
        setLayerType(LAYER_TYPE_SOFTWARE, null)
        mRectF = RectF()
        mLinePath = Path()
        initPaint()
    }

    /**
     * 初始化画笔
     */
    private fun initPaint() {
        paintPolyShadow = Paint()
        paintPolyShadow.style = Paint.Style.FILL

        paintPolyline = Paint()
        paintPolyline.style = Paint.Style.FILL
        paintPolyline.strokeWidth = 2f
        paintPolyline.textSize = 12f.sp
        paintPolyline.isAntiAlias = true
        paintPolyline.color = context.colorCompat(R.color.fc355c_fc3159)

        mBgCirPaint = Paint()
        mBgCirPaint.isAntiAlias = true
        mBgCirPaint.style = Paint.Style.STROKE
        mBgCirPaint.strokeWidth = mBgCirWidth
        mBgCirPaint.strokeCap = Paint.Cap.ROUND

    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        scrWidth = width.toFloat()
        scrHeight = height.toFloat()
        ySpacing = scrHeight / 8f //y轴分8份


        val minWidth =
            (w - paddingLeft - paddingRight - 2 * mBgCirWidth).coerceAtMost(h - paddingBottom - paddingTop - 2 * mBgCirWidth)
        val radius = minWidth / 2
        mRectF.left = w / 2 - radius - mBgCirWidth / 2
        mRectF.top = h / 2 - radius - mBgCirWidth / 2
        mRectF.right = w / 2 + radius + mBgCirWidth / 2
        mRectF.bottom = h / 2 + radius + mBgCirWidth / 2
    }

    private var ySpacing = 0f //高度分割份数后间距
    private var xSpacing = 0f //x轴柱子分割份数后间距

    @SuppressLint("DrawAllocation")
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        when (type) {
            Type.EXERCISE_RECORDS -> drawPoints(canvas)
            Type.HEART_RATE -> drawHeart(canvas)
            Type.SLEEP -> drawSleep2(canvas)
            Type.WEIGHT -> {
            }
            Type.SPO2 -> drawSpo2(canvas)
            Type.STRESS -> {
            }
            Type.BLOOD_PRESSURE -> drawBloodPressure(canvas)
            Type.BLOOD_SUGAR -> {
            }
            Type.OTHER -> drawOther(canvas)
            else -> {
            }
        }
    }

    private fun drawSpo2(canvas: Canvas) {
        val paint = Paint()
        paint.style = Paint.Style.FILL
        paint.isAntiAlias = true
        paint.color = context.colorCompat(R.color.secondary_666666_808080)

        val path = Path()
        path.moveTo(scrWidth * (spo2 / 100f) - 10f, scrHeight / 2f - 10f)
        path.lineTo(scrWidth * (spo2 / 100f) - 20f, scrHeight / 2f - 30f)
        path.lineTo(scrWidth * (spo2 / 100f), scrHeight / 2f - 30f)
        path.close()
        canvas.drawPath(path, paint)

        var outerR = floatArrayOf(5f, 5f, 0f, 0f, 0f, 0f, 5f, 5f)
        var mDrawables = ShapeDrawable(RoundRectShape(outerR, null, null))
        mDrawables.paint.color = Color.parseColor("#fc844d")
        mDrawables.setBounds(
            0, scrHeight.toInt() / 2 - 5,
            (scrWidth * 0.7).toInt() - 1,
            scrHeight.toInt() / 2 + 6
        )
        mDrawables.draw(canvas)

        outerR = floatArrayOf(0f, 0f, 5f, 5f, 5f, 5f, 0f, 0f)
        mDrawables = ShapeDrawable(RoundRectShape(outerR, null, null))
        mDrawables.paint.color = Color.parseColor("#54c054")
        mDrawables.setBounds(
            (scrWidth * 0.7).toInt() + 1, scrHeight.toInt() / 2 - 5, scrWidth.toInt(),
            scrHeight.toInt() / 2 + 6
        )
        mDrawables.draw(canvas)
    }

    private fun drawHeart(canvas: Canvas) {
        var x0: Float
        var x1: Float
        var y0: Float
        var y1: Float
        xSpacing = scrWidth / (heartData.size - 1)
        //画折线阴影
        heartData.forEachIndexed { index, i ->
            if (index < heartData.size * mPercent) {
                if (index < heartData.size - 1) {
                    x0 = xSpacing * index
                    y0 = ySpacing * 5f - ySpacing * ((i - 40) / 45f)
                    x1 = xSpacing * (index + 1)
                    y1 = ySpacing * 5f - ySpacing * ((heartData[index + 1] - 40) / 45f)
                    if (i > 0) {
                        if (heartData[index + 1] > 0) {
                            drawHeartShadow(x0, y0, x1, y1, canvas)
                        } else {
                            //单点圆
                            if (index == 0 || heartData[index - 1] < 0) {
                                drawHeartShadow(x0 - 4f, y0, x0 + 4f, y0, canvas)
                            }
                        }
                    }
                }
            }
        }

        paintPolyline.color = context.colorCompat(R.color.fc355c_fc3159)
        //画折线
        heartData.forEachIndexed { index, i ->
            if (index < heartData.size * mPercent) {
                if (index < heartData.size - 1) {
                    x0 = xSpacing * index
                    y0 = ySpacing * 5f - ySpacing * ((i - 40) / 45f)
                    x1 = xSpacing * (index + 1)
                    y1 = ySpacing * 5f - ySpacing * ((heartData[index + 1] - 40) / 45f)
                    if (i > 0) {
                        if (heartData[index + 1] > 0) {
                            canvas.drawLine(x0, y0, x1, y1, paintPolyline)
                        } else {
                            //单点圆
                            if (index == 0 || heartData[index - 1] < 0) {
                                canvas.drawCircle(x0, y0, 2f, paintPolyline)
                            }
                        }
                    }
                }
            }
        }
    }

    private fun drawPoints(canvas: Canvas) {
        val latitudeMax = points.map { it[0] }.maxOrNull() ?: 0.0
        val latitudeMin = points.map { it[0] }.minOrNull() ?: 0.0
        val longitudeMax = points.map { it[1] }.maxOrNull() ?: 0.0
        val longitudeMin = points.map { it[1] }.minOrNull() ?: 0.0
        val temp0 = latitudeMax - latitudeMin
        val temp1 = longitudeMax - longitudeMin
        val pointX = points.map { scrWidth * (1 - (longitudeMax - it[1]) / temp1) }
        val pointY = points.map { scrHeight * (latitudeMax - it[0]) / temp0 }
        val path = Path()
        paintPolyline.color = Color.parseColor("#54c054")
        paintPolyline.style = Paint.Style.STROKE
        //画折线

        var offsetX: Float
        var offsetY: Float

        points.forEachIndexed { index, _ ->
            if (index < points.size * mPercent) {
                if (index < points.size - 1) {
                    path.moveTo(pointX[index].toFloat(), pointY[index].toFloat())
                    path.lineTo(pointX[index + 1].toFloat(), pointY[index + 1].toFloat())
                }
                if (index == 0 || index == points.size - 1) {
                    //单点圆
                    offsetX = if (abs(pointX[index].toFloat() - scrWidth) < 10) -5f else 5f
                    offsetY = if (abs(pointY[index].toFloat() - scrHeight) < 10) -5f else 5f
                    canvas.drawCircle(
                        pointX[index].toFloat() + offsetX,
                        pointY[index].toFloat() + offsetY,
                        4f,
                        paintPolyline
                    )
                }
            }
        }
        canvas.drawPath(path, paintPolyline)
    }

    private fun drawBloodPressure(canvas: Canvas) {
        val mLinearGradient = LinearGradient(
            0f, scrHeight / 2f - 5f, scrWidth, scrHeight / 2f + 5f,
            intArrayOf(
                Color.parseColor("#42cfeb"),
                Color.parseColor("#f1df57"),
                Color.parseColor("#ff634d")
            ), null, Shader.TileMode.MIRROR
        )
        val paintGradientLine = Paint()
        paintGradientLine.style = Paint.Style.FILL
        paintGradientLine.shader = mLinearGradient

        canvas.drawRoundRect(
            RectF(0f, scrHeight / 2f - 5f, scrWidth, scrHeight / 2f + 5f), 5f, 5f, paintGradientLine
        )

        val paint = Paint()
        paint.style = Paint.Style.FILL
        paint.isAntiAlias = true
        paint.color = context.colorCompat(R.color.secondary_666666_808080)

        val path = Path()
        path.moveTo(scrWidth * (sys / 270f), scrHeight / 2f - 10f)
        path.lineTo(scrWidth * (sys / 270f) - 10f, scrHeight / 2f - 30f)
        path.lineTo(scrWidth * (sys / 270f) + 10f, scrHeight / 2f - 30f)
        path.close()
        canvas.drawPath(path, paint)
    }

    private fun drawOther(canvas: Canvas) {
        val mLinearGradient = LinearGradient(
            0f, scrHeight / 2f - 5f, scrWidth, scrHeight / 2f + 5f,
            intArrayOf(
                Color.parseColor("#04d657"),
                Color.parseColor("#ebbd1f"),
                Color.parseColor("#e8361b")
            ), null, Shader.TileMode.MIRROR
        )
        val paintGradientLine = Paint()
        paintGradientLine.style = Paint.Style.FILL
        paintGradientLine.shader = mLinearGradient

        canvas.drawRoundRect(
            RectF(0f, scrHeight / 2f - 5f, scrWidth, scrHeight / 2f + 5f), 5f, 5f, paintGradientLine
        )

        paintPolyline.color = Color.parseColor("#04d657")
        paintPolyline.textAlign = Paint.Align.LEFT
        canvas.drawText(
            "${context.getString(R.string.slowest)} ${min / 60}'${min % 60}''",
            0f,
            scrHeight / 2f - 20f,
            paintPolyline
        )

        paintPolyline.color = Color.parseColor("#e8361b")
        paintPolyline.textAlign = Paint.Align.RIGHT
        canvas.drawText(
            "${context.getString(R.string.fastest)} ${max / 60}'${max % 60}''",
            scrWidth,
            scrHeight / 2f - 20f,
            paintPolyline
        )
    }

    private fun drawHeartShadow(x0: Float, y0: Float, x1: Float, y1: Float, canvas: Canvas) {
        mLinePath.reset()
        mLinePath.moveTo(x0, ySpacing * 7f)
        mLinePath.lineTo(x0, y0)
        mLinePath.lineTo(x1, y1)
        mLinePath.lineTo(x1, ySpacing * 7f)
        mLinePath.lineTo(x0, ySpacing * 7f)
        mLinePath.close()
        val mLinearGradient = LinearGradient(
            0f,
            ySpacing * 5f - ySpacing * ((heartData.maxOrNull()?.minus(40))?.div(45f) ?: 0f),
            0f,
            ySpacing * 7f,
            intArrayOf(
                context.colorCompat(R.color.fecbd5_400c17),
                context.colorCompat(R.color.color_secondary)
            ),
            null,
            Shader.TileMode.MIRROR
        )
        paintPolyShadow.shader = mLinearGradient
        canvas.drawPath(mLinePath, paintPolyShadow)
    }


    private fun drawSleep2(canvas: Canvas) {
        //深睡
        mBgCirPaint.color = Color.WHITE
        mBgCirPaint.strokeWidth = mBgCirWidth
        canvas.drawArc(mRectF, mStartAngle, mSweepAngle, false, mBgCirPaint)
        mBgCirPaint.color = Color.parseColor("#8a2be2")
        mBgCirPaint.strokeWidth = mBgCirWidth - 8f
        canvas.drawArc(mRectF, mStartAngle, mSweepAngle * mPercent, false, mBgCirPaint)
        //浅睡
        mBgCirPaint.color = Color.WHITE
        mBgCirPaint.strokeWidth = mBgCirWidth
        canvas.drawArc(
            mRectF,
            mStartAngle,
            mSweepAngle * (1f - sleep.deep / sleep.total.toFloat()) * mPercent,
            false,
            mBgCirPaint
        )
        mBgCirPaint.color = Color.parseColor("#c64be4")
        mBgCirPaint.strokeWidth = mBgCirWidth - 8f
        canvas.drawArc(
            mRectF,
            mStartAngle,
            mSweepAngle * (1f - sleep.deep / sleep.total.toFloat()) * mPercent,
            false,
            mBgCirPaint
        )
        //rem
        mBgCirPaint.color = Color.WHITE
        mBgCirPaint.strokeWidth = mBgCirWidth
        canvas.drawArc(
            mRectF,
            mStartAngle,
            mSweepAngle * (1f - (sleep.deep + sleep.light) / sleep.total.toFloat()) * mPercent,
            false,
            mBgCirPaint
        )
        mBgCirPaint.color = Color.parseColor("#fd817c")
        mBgCirPaint.strokeWidth = mBgCirWidth - 8f
        canvas.drawArc(
            mRectF,
            mStartAngle,
            mSweepAngle * (1f - (sleep.deep + sleep.light) / sleep.total.toFloat()) * mPercent,
            false,
            mBgCirPaint
        )
        //清醒
        mBgCirPaint.color = Color.WHITE
        mBgCirPaint.strokeWidth = mBgCirWidth
        canvas.drawArc(
            mRectF,
            mStartAngle,
            mSweepAngle * (1f - (sleep.deep + sleep.light + sleep.rem) / sleep.total.toFloat()) * mPercent,
            false,
            mBgCirPaint
        )
        mBgCirPaint.color = Color.parseColor("#fdc221")
        mBgCirPaint.strokeWidth = mBgCirWidth - 8f
        canvas.drawArc(
            mRectF,
            mStartAngle,
            mSweepAngle * (1f - (sleep.deep + sleep.light + sleep.rem) / sleep.total.toFloat()) * mPercent,
            false,
            mBgCirPaint
        )
    }

    private var sys = 0
    fun setBloodPressure(sys: Int) {
        type = Type.BLOOD_PRESSURE
        this.sys = sys
        postInvalidate()
    }

    private lateinit var sleep: Sleep
    fun setSleep(sleep: Sleep) {
        type = Type.SLEEP
        this.sleep = sleep
        startAnim()
    }

    private fun startAnim() {
        val mAnimator = ValueAnimator.ofFloat(0f, 1f)
        mAnimator.duration = 1000L
        mAnimator.addUpdateListener {
            mPercent = it.animatedValue as Float
            postInvalidate()
        }
        mAnimator.start()
    }

    private var spo2 = 0
    fun setSpo2(spo2: Int) {
        type = Type.SPO2
        this.spo2 = spo2
        postInvalidate()
    }

    private var heartData = mutableListOf<Int>()
    fun setHeart(value: List<Int>) {
        type = Type.HEART_RATE
        heartData.clear()
        heartData.addAll(value)
        startAnim()
    }

    private var points = mutableListOf<List<Double>>()
    fun setExercise(value: List<List<Double>>) {
        type = Type.EXERCISE_RECORDS
        points.clear()
        points.addAll(value)
        startAnim()
    }

    private var max = 0
    private var min = 0
    fun setOther(min: Int, max: Int) {
        type = Type.OTHER
        this.min = min
        this.max = max
        postInvalidate()
    }
}