Android运动健康心率自定义控件(日)

效果图

device-2021-03-28-192622.png

device-2021-03-28-194719.png

代码

/**
 *
 * 日图表
 * zrj 2020/7/9
 */
 
class HeartDayChart(context: Context, attrs: AttributeSet?) : View(context,attrs){

    //屏幕宽高
    private var scrWidth = 0f 
    private var scrHeight = 0f
    private var xData: Array<String> = arrayOf("00:00", "06:00", "12:00", "18:00", "00:00")
    private var yData: Array<Int> = arrayOf(220, 175, 130, 85, 40)
    private var heartData = mutableListOf<Int>()
    private lateinit var paintLine: Paint //y轴线
    private lateinit var paintGradientLine: Paint //指示渐变线
    private lateinit var paintXText: Paint //x轴坐标
    private lateinit var paintYText: Paint //y轴坐标
    private lateinit var paintPolyline: Paint //心率折线
    private lateinit var paintPolyShadow: Paint //心率折线阴影
    private lateinit var paintRound: Paint  //指示滑块圆
    private lateinit var paintBessel: Paint  //滑块底部

    private var xSlider = 0f //滑块的x轴位置

    private var mLinePath: Path  //折线路径

    private val curveCircleRadius = 12f.dp
    private var mPath: Path  //滑块贝塞尔

    //第一条曲线的坐标
    private val mFirstCurveStartPoint = Point()
    private val mFirstCurveEndPoint = Point()
    private val mFirstCurveControlPoint1 = Point()
    private val mFirstCurveControlPoint2 = Point()

    //第二条曲线的坐标
    private var mSecondCurveStartPoint = Point()
    private val mSecondCurveEndPoint = Point()
    private val mSecondCurveControlPoint1 = Point()
    private val mSecondCurveControlPoint2 = Point()

    private var isRest = false //是否静息
    private var restHeart = 0 //静息值

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

    /**
     * 初始化画笔
     */
    private fun initPaint() {

        paintLine = Paint()
        paintLine.style = Paint.Style.STROKE
        paintLine.strokeWidth = 1f
        paintLine.color = context.colorCompat(R.color.e6e6e6_2e2e2e)

        paintGradientLine = Paint()
        paintGradientLine.style = Paint.Style.STROKE
        paintGradientLine.strokeWidth = 2f

        paintPolyShadow = Paint()
        paintPolyShadow.style = Paint.Style.FILL

        paintXText = Paint()
        paintXText.isAntiAlias = true
        paintXText.strokeWidth = 1f
        paintXText.textSize = 12f.sp
        paintXText.textAlign = Paint.Align.CENTER
        paintXText.color = context.colorCompat(R.color.color_on_surface)

        paintYText = Paint()
        paintYText.isAntiAlias = true
        paintYText.textSize = 12f.sp
        paintYText.strokeWidth = 1f
        paintYText.textAlign = Paint.Align.RIGHT
        paintYText.color = context.colorCompat(R.color.secondary_666666_808080)

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

        paintRound = Paint()
        paintRound.style = Paint.Style.FILL
        paintRound.isAntiAlias = true
        paintRound.color = context.colorCompat(R.color.ffffff_6e6e6e)

        paintBessel = Paint()
        paintBessel.style = Paint.Style.FILL
        paintBessel.isAntiAlias = true
        paintBessel.color = context.colorCompat(R.color.f2f2f2_1d1d1d)
    }


    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份

        //底部圆滑块可以滑动的范围
        xWithStart = margin + paintXText.measureText(xData[0]) / 2
        xWithEnd = scrWidth - margin - paintYText.measureText(yData[0].toString()) * 1.5f
        xTextSpacing = (xWithEnd - xWithStart) / (xData.size - 1)
        xSpacing = xTextSpacing / 36    //一个小时6个值
        xSlider = xSpacing * time + xWithStart
    }

    private var mDownX = 0f
    private var mDownY = 0f
    private var isSlider = false

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                mDownX = event.x
                mDownY = event.y
                isSlider = abs(event.x - xSlider) < 60f && abs(event.y - ySpacing * 7) < 60f
            }
            MotionEvent.ACTION_MOVE ->
                if (abs(event.y - mDownY) < abs(event.x - mDownX)) {
                    if (isSlider) {
                        xSlider = event.x
                        heartData.forEachIndexed { index, i ->
                            val x = xWithStart + xSpacing * index
                            val dis = abs(x - xSlider)
                            if (dis < xSpacing / 2) {
                                onDaySelectListener?.invoke(index, i)
                                return@forEachIndexed
                            }
                        }
                        invalidate()
                    }
                }

            MotionEvent.ACTION_UP -> {
                if (isSlider) {
                    if (xSlider < xWithStart) {
                        xSlider = xWithStart
                        invalidate()
                    }
                    if (xSlider > xWithEnd) {
                        xSlider = xWithEnd
                        invalidate()
                    }
                    heartData.forEachIndexed { index, _ ->
                        val x = xWithStart + xSpacing * index
                        val dis = abs(x - xSlider)
                        if (dis < xSpacing / 2) {
                            xSlider = x
                            invalidate()
                            return@forEachIndexed
                        }
                    }
                } else {
                    if (abs(event.x - mDownX) > xSpacing) {
                        onDayMoveListener?.invoke(event.x > mDownX)
                    } else {
                        heartData.forEachIndexed { index, _ ->
                            val x = xWithStart + xSpacing * index
                            val dis = abs(x - event.x)
                            if (dis < xSpacing / 2) {
                                xSlider = x
                                invalidate()
                                return@forEachIndexed
                            }
                        }
                    }
                }
            }
        }
        return true
    }

    private val margin = 20f.dp //左右两边距离
    private var xWithStart = 0f //x轴的起始点
    private var xWithEnd = 0f  //x轴结束点
    private var ySpacing = 0f //高度分割份数后间距
    private var xSpacing = 0f //x轴柱子分割份数后间距
    private var xTextSpacing = 0f //x轴文字分割份数后间距

    @SuppressLint("DrawAllocation")
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        //画y轴方向横线与文字
        drawY(canvas)
        //折线
        drawPolyline(canvas)
        //垂直渐变线
        drawGradientLine(canvas)
        //底部
        drawBessel(canvas)
        //画x轴方向文字
        drawX(canvas)
    }

    private fun drawX(canvas: Canvas) {
        xData.forEachIndexed { index, s ->
            val x = xWithStart + xTextSpacing * index
            val dis = abs(x - xSlider)
            var y = ySpacing * 7 - 10f
            if (dis < xTextSpacing / 2) {
                paintXText.typeface = Typeface.DEFAULT_BOLD
                y -= 40f * (1 - dis / xTextSpacing)
            } else {
                paintXText.typeface = Typeface.DEFAULT
            }
            canvas.drawText(s, x, y, paintXText)
        }
    }

    private fun drawPolyline(canvas: Canvas) {
        var x0: Float
        var x1: Float
        var y0: Float
        var y1: Float

        //画折线阴影
        heartData.forEachIndexed { index, i ->
            if (index < heartData.size - 1) {
                x0 = xWithStart + xSpacing * index
                y0 = ySpacing * 5f - ySpacing * ((i - 40) / 45f)
                x1 = xWithStart + xSpacing * (index + 1)
                y1 = ySpacing * 5f - ySpacing * ((heartData[index + 1] - 40) / 45f)
                if (i > 0) {
                    if (heartData[index + 1] > 0) {
                        drawPolyShadow(x0, y0, x1, y1, canvas)
                    } else {
                        //单点圆
                        if (index == 0 || heartData[index - 1] < 0) {
                            drawPolyShadow(x0 - 4f, y0, x0 + 4f, y0, canvas)
                        }
                    }
                }
            }
        }

        paintPolyline.color =
            if (isRest) context.colorCompat(R.color.cccccc_656565) else context.colorCompat(R.color.fc355c_fc3159)
        //画折线
        heartData.forEachIndexed { index, i ->
            if (index < heartData.size - 1) {
                x0 = xWithStart + xSpacing * index
                y0 = ySpacing * 5f - ySpacing * ((i - 40) / 45f)
                x1 = xWithStart + 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, 4f, paintPolyline)
                        }
                    }
                    if (xSlider == x0 && !isRest) {
                        paintPolyline.color = context.colorCompat(R.color.f7f7f7_191919)
                        canvas.drawCircle(x0, y0, 10f, paintPolyline)
                        paintPolyline.color = context.colorCompat(R.color.fc355c_fc3159)
                        canvas.drawCircle(x0, y0, 7f, paintPolyline)
                    }
                }
                if (xSlider == x0 && !isRest) {
                    onDaySelectListener?.invoke(index, i)
                }
            }
        }

        if (isRest && restHeart > 0) {
            paintPolyline.color = context.colorCompat(R.color.fc355c_fc3159)
            canvas.drawLine(
                margin,
                ySpacing * 5f - ySpacing * ((restHeart - 40) / 45f),
                scrWidth - margin,
                ySpacing * 5f - ySpacing * ((restHeart - 40) / 45f),
                paintPolyline
            )
        }
    }

    private fun drawY(canvas: Canvas) {
        val y = ySpacing * 5f - ySpacing * ((restHeart - 40) / 45f)
        for (i in 0..4) {
            canvas.drawLine(
                margin, ySpacing * (i + 1), scrWidth - margin,
                ySpacing * (i + 1), paintLine
            )
            paintYText.color = context.colorCompat(R.color.secondary_666666_808080)
            if (!isRest || abs(y - (ySpacing * (i + 1))) > 25) { //静息值重叠
                canvas.drawText(
                    yData[i].toString(), scrWidth - margin, ySpacing * (i + 1) - 10f, paintYText
                )
            }
        }

        if (isRest && restHeart > 0) {
            paintYText.color = context.colorCompat(R.color.fc355c_fc3159)
            canvas.drawText(restHeart.toString(), scrWidth - margin, y - 10f, paintYText)
        }
    }

    private fun drawBessel(canvas: Canvas) {
        // 第一条曲线开始点
        mFirstCurveStartPoint[(xSlider - curveCircleRadius * 3).toInt()] = (ySpacing * 7).toInt()
        // 第一条曲线结束点
        mFirstCurveEndPoint[xSlider.toInt()] =
            (ySpacing * 7 - curveCircleRadius - curveCircleRadius / 4).toInt()
        // 第二条开始点
        mSecondCurveStartPoint = mFirstCurveEndPoint
        mSecondCurveEndPoint[(xSlider + curveCircleRadius * 3).toInt()] = (ySpacing * 7).toInt()

        // 第一条控制点
        mFirstCurveControlPoint1[(mFirstCurveStartPoint.x + curveCircleRadius + curveCircleRadius / 4).toInt()] =
            mFirstCurveStartPoint.y
        mFirstCurveControlPoint2[(mFirstCurveEndPoint.x - curveCircleRadius * 2 + curveCircleRadius).toInt()] =
            mFirstCurveEndPoint.y
        // 第二条控制点
        mSecondCurveControlPoint1[(mSecondCurveStartPoint.x + curveCircleRadius * 2 - curveCircleRadius).toInt()] =
            mSecondCurveStartPoint.y
        mSecondCurveControlPoint2[(mSecondCurveEndPoint.x - curveCircleRadius - curveCircleRadius / 4).toInt()] =
            mSecondCurveEndPoint.y
        mPath.reset()
        mPath.moveTo(0f, ySpacing * 7)
        mPath.lineTo(mFirstCurveStartPoint.x.toFloat(), mFirstCurveStartPoint.y.toFloat())
        mPath.cubicTo(
            mFirstCurveControlPoint1.x.toFloat(), mFirstCurveControlPoint1.y.toFloat(),
            mFirstCurveControlPoint2.x.toFloat(), mFirstCurveControlPoint2.y.toFloat(),
            mFirstCurveEndPoint.x.toFloat(), mFirstCurveEndPoint.y.toFloat()
        )
        mPath.cubicTo(
            mSecondCurveControlPoint1.x.toFloat(), mSecondCurveControlPoint1.y.toFloat(),
            mSecondCurveControlPoint2.x.toFloat(), mSecondCurveControlPoint2.y.toFloat(),
            mSecondCurveEndPoint.x.toFloat(), mSecondCurveEndPoint.y.toFloat()
        )
        mPath.lineTo(scrWidth, ySpacing * 7)
        mPath.lineTo(scrWidth, scrHeight)
        mPath.lineTo(0f, scrHeight)
        mPath.close()

        //底部灰色
        canvas.drawPath(mPath, paintBessel)
        //底部滑块
        canvas.drawCircle(xSlider, ySpacing * 7 + 5f, curveCircleRadius, paintRound)
    }

    private fun drawPolyShadow(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(
                if (isRest) context.colorCompat(R.color.f1f1f1_1e1e1e) else context.colorCompat(R.color.fecbd5_400c17),
                context.colorCompat(R.color.color_secondary)
            ),
            null,
            Shader.TileMode.MIRROR
        )
        paintPolyShadow.shader = mLinearGradient
        canvas.drawPath(mLinePath, paintPolyShadow)
    }

    private var time = 0//当前时间
    fun setValue(value: MutableList<Int>, restHeart: Int, time: Int): HeartDayChart {
        this.restHeart = restHeart
        this.time = time
        heartData.clear()
        heartData.addAll(value)
        postInvalidate()
        return this
    }

    fun setRest(isRest: Boolean) {
        this.isRest = isRest
        postInvalidate()
    }

    private fun drawGradientLine(canvas: Canvas) {
        val mLinearGradient = LinearGradient(
            xSlider, ySpacing, xSlider, ySpacing * 6,
            intArrayOf(
                context.colorCompat(R.color.ffffff_262626),
                context.colorCompat(R.color.fc355c_fc3159),
                context.colorCompat(R.color.ffffff_262626)
            ), null, Shader.TileMode.MIRROR
        )
        paintGradientLine.shader = mLinearGradient

        if (ySpacing > 0) {
            canvas.drawLine(xSlider, ySpacing, xSlider, ySpacing * 6, paintGradientLine)
        }
    }

    private var onDaySelectListener: ((index: Int, heart: Int) -> Unit)? = null

    fun setOnDaySelectListener(l: ((index: Int, heart: Int) -> Unit)): HeartDayChart {
        this.onDaySelectListener = l
        return this
    }

    private var onDayMoveListener: ((isPre: Boolean) -> Unit)? = null

    fun setOnDayMoveListener(l: ((index: Boolean) -> Unit)): HeartDayChart {
        this.onDayMoveListener = l
        return this
    }
}
复制代码

猜你喜欢

转载自juejin.im/post/6944668139208572959