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

·  阅读 852

效果图

device-2021-03-28-192721.png

代码



/**
 *
 * 月图表
 * zrj 2020/7/14
 */
class HeartMonthChart(context: Context, attrs: AttributeSet?) : View(context, attrs) {
    //屏幕宽高
    private var scrWidth = 0f
    private var scrHeight = 0f
    private var xData = mutableListOf("11/11")
    private var yScaleStart = 40f  //初始最低刻度值
    private var yScaleStep = 45 //刻度间隔值

    private var heartData = mutableListOf<String>()
    private var restData = mutableListOf<Int>()
    private lateinit var paintLine: Paint
    private lateinit var paintGradientLine: Paint
    private lateinit var paintPolyline: Paint //心率折线
    private lateinit var paintXText: Paint
    private lateinit var paintYText: Paint
    private lateinit var paintPillar: Paint
    private lateinit var paintRound: Paint
    private lateinit var paintBessel: Paint

    private var animDuration = 500L
    private var anim: ValueAnimator? = null
    private var mPercent = 0f //动画进度
    private var xSlider = 0f //滑块的x轴位置

    private var mPath: Path
    private val curveCircleRadius = 12f.dp

    // the coordinates of the first curve
    private val mFirstCurveStartPoint = Point()
    private val mFirstCurveEndPoint = Point()
    private val mFirstCurveControlPoint1 = Point()
    private val mFirstCurveControlPoint2 = Point()

    //the coordinates of the second curve
    private var mSecondCurveStartPoint = Point()
    private val mSecondCurveEndPoint = Point()
    private val mSecondCurveControlPoint1 = Point()
    private val mSecondCurveControlPoint2 = Point()
    private var isRest = false //是否静息

    init {
        setLayerType(LAYER_TYPE_SOFTWARE, null)
        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

        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 = 4f
        paintPolyline.isAntiAlias = true
        paintPolyline.color = context.colorCompat(R.color.fc355c_fc3159)

        paintPillar = Paint()
        paintPillar.style = Paint.Style.FILL
        paintPillar.isAntiAlias = true
        paintPillar.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(yMarkMax.toString()) * 2f

        xSpacing = (xWithEnd - xWithStart) / (heartData.size - 1)
        xTextSpacing = xSpacing * 7
        xSlider = xSpacing * (day - 1) + 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
                        invalidate()
                        if (isRest) {
                            restData.forEachIndexed { index, i ->
                                val x = xWithStart + xSpacing * index
                                val dis = abs(x - xSlider)
                                if (dis < xSpacing / 2) {
                                    onMonthSelectListener?.invoke(index, i.toString())
                                    return@forEachIndexed
                                }
                            }
                        } else {
                            heartData.forEachIndexed { index, i ->
                                val x = xWithStart + xSpacing * index
                                val dis = abs(x - xSlider)
                                if (dis < xSpacing / 2) {
                                    onMonthSelectListener?.invoke(index, i)
                                    return@forEachIndexed
                                }
                            }
                        }
                    }
                }

            MotionEvent.ACTION_UP -> {
                if (isSlider) {
                    if (xSlider < xWithStart) {
                        xSlider = xWithStart
                        invalidate()
                    }
                    if (xSlider > xWithEnd) {
                        xSlider = xWithEnd
                        invalidate()
                    }

                    if (isRest) {
                        restData.forEachIndexed { index, _ ->
                            val x = xWithStart + xSpacing * index
                            val dis = abs(x - xSlider)
                            if (dis < xSpacing / 2) {
                                xSlider = x
                                invalidate()
                                return@forEachIndexed
                            }
                        }
                    } else {
                        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){
                        onMonthMoveListener?.invoke(event.x > mDownX)
                        return true
                    }
                    if (isRest) {
                        restData.forEachIndexed { index, _ ->
                            val x = xWithStart + xSpacing * index
                            val dis = abs(x - event.x)
                            if (dis < xSpacing / 2) {
                                xSlider = x
                                invalidate()
                                return@forEachIndexed
                            }
                        }
                    } 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)
        //垂直渐变线
        drawGradientLine(canvas)
        //画柱子
        drawPillar(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 drawPillar(canvas: Canvas) {
        if (isRest) {
            //画线
            var x0: Float
            var x1: Float
            var y0: Float
            var y1: Float
            paintPolyline.color = context.colorCompat(R.color.fc355c_fc3159)
            restData.forEachIndexed { index, i ->
                if (index < restData.size - 1) {
                    x0 = xWithStart + xSpacing * index
                    y0 = ySpacing * 5f - ySpacing * ((i - yScaleStart) / yScaleStep) * mPercent
                    x1 = xWithStart + xSpacing * (index + 1)
                    y1 =
                        ySpacing * 5f - ySpacing * ((restData[index + 1] - yScaleStart) / yScaleStep) * mPercent
                    if (i > 0 && restData[index + 1] > 0) {
                        canvas.drawLine(x0, y0, x1, y1, paintPolyline)
                    }
                }
            }
            restData.forEachIndexed { index, i ->
                x0 = xWithStart + xSpacing * index
                y0 = ySpacing * 5f - ySpacing * ((i - yScaleStart) / yScaleStep) * mPercent
                if (i > 0) {
                    //单点圆
                    paintPolyline.color = context.colorCompat(R.color.fc355c_fc3159)
                    canvas.drawCircle(x0, y0, 8f, paintPolyline)
                    paintPolyline.color = context.colorCompat(R.color.f7f7f7_191919)
                    canvas.drawCircle(x0, y0, 4f, paintPolyline)
                    if (xSlider == x0) {
                        canvas.drawCircle(x0, y0, 12f, paintPolyline)
                        paintPolyline.color = context.colorCompat(R.color.fc355c_fc3159)
                        canvas.drawCircle(x0, y0, 8f, paintPolyline)
                    }
                }

                if (xSlider == x0) {
                    onMonthSelectListener?.invoke(index, i.toString())
                }
            }
        } else {
            //画柱子
            heartData.forEachIndexed { index, i ->
                if (xSlider < xWithStart + xSpacing * index + xSpacing / 2 && xSlider > xWithStart + xSpacing * index - xSpacing / 2) {
                    paintPillar.color = context.colorCompat(R.color.fd3a5e_fe3b5f)
                    onMonthSelectListener?.invoke(index, i)
                } else {
                    paintPillar.color = context.colorCompat(R.color.ff7693_ff7894)
                }
                if (i.length > 3) {

                    val data = i.split("-")
                    if (data[1].toFloat() - data[0].toFloat() < 5) {
                        canvas.drawCircle(
                            xWithStart + xSpacing * index,
                            ySpacing * 5f - ySpacing * ((data[1].toFloat() / 2 + data[0].toFloat() / 2 - yScaleStart) / yScaleStep) * mPercent,
                            10f, paintPillar
                        )
                    } else {
                        canvas.drawRoundRect(
                            RectF(
                                xWithStart + xSpacing * index - 5f,
                                ySpacing * 5f - ySpacing * ((data[1].toFloat() - yScaleStart) / yScaleStep) * mPercent,
                                xWithStart + xSpacing * index + 5f,
                                ySpacing * 5f - ySpacing * ((data[0].toFloat() - yScaleStart) / yScaleStep) * mPercent
                            ), 5f, 5f, paintPillar
                        )
                    }
                }
            }
        }
    }

    private fun drawY(canvas: Canvas) {
        for (i in 0..4) {
            canvas.drawLine(
                margin, ySpacing * (i + 1), scrWidth - margin,
                ySpacing * (i + 1), paintLine
            )

            canvas.drawText(
                (yScaleStart + (4 - i) * yScaleStep).toInt().toString(),
                scrWidth - margin,
                ySpacing * (i + 1) - 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 var yMarkMax = 1 //Y轴刻度最大值

    fun setRest(isRest: Boolean) {
        this.isRest = isRest
        yScaleStep = if (isRest) 10 else 45
        yMarkMax = yScaleStart.toInt() + yScaleStep * 4
        startAnimation()
    }

    private var day = 1

    @Suppress("IMPLICIT_CAST_TO_ANY")
    fun setValue(
        heart: MutableList<String>,
        rest: MutableList<Int>, month: Int, day: Int
    ): HeartMonthChart {
        this.day = day
        heartData.clear()
        heartData.addAll(heart)
        restData.clear()
        restData.addAll(rest)
        xData.clear()
        restData.forEachIndexed { index, _ ->
            if (index % 7 == 0) {
                xData.add("${if ((index + 1) < 10) "${if (month < 10) "$month" else month}/${index + 1}" else index + 1}")
            }
        }
        startAnimation()
        return this
    }

    private fun startAnimation() {
        anim = ValueAnimator.ofObject(AngleEvaluator(), 0f, 1f)
        anim?.interpolator = AccelerateDecelerateInterpolator()
        anim?.addUpdateListener { animation ->
            mPercent = animation.animatedValue as Float
            postInvalidate()
        }
        anim?.duration = animDuration
        anim?.start()
    }

    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 onMonthSelectListener: ((index: Int, heart: String) -> Unit)? = null

    fun setOnMonthSelectListener(l: ((index: Int, heart: String) -> Unit)): HeartMonthChart {
        this.onMonthSelectListener = l
        return this
    }

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

    fun setOnMonthMoveListener(l: ((index: Boolean) -> Unit)): HeartMonthChart {
        this.onMonthMoveListener = l
        return this
    }
}

复制代码
分类:
Android
收藏成功!
已添加到「」, 点击更改