Android运动健康血氧自定义控件

·  阅读 844

效果图

device-2021-03-28-192914.png

代码



/**
 *
 * 日图表
 * zrj 2020/9/1
 */
class BODayChart(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(100, 90, 85, 80, 70)
    private var boData = mutableListOf<Int>()
    private lateinit var paintLine: Paint
    private lateinit var paintGradientLine: 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()


    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)

        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 dispatchTouchEvent(ev: MotionEvent?): Boolean {
        parent.requestDisallowInterceptTouchEvent(true)
        return super.dispatchTouchEvent(ev)
    }

    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 / 10f //y轴分10份

        //底部圆滑块可以滑动的范围
        xWithStart = margin + paintXText.measureText(xData[0]) / 2
        xWithEnd = scrWidth - margin - paintYText.measureText(yData[0].toString()) * 2.5f

        xTextSpacing = (xWithEnd - xWithStart) / (xData.size - 1)
        xSpacing = xTextSpacing / 36 //x轴等分4份   144

    }

    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 * 9) < 60f
            }
            MotionEvent.ACTION_MOVE ->
                if (abs(event.y - mDownY) < abs(event.x - mDownX)) {
                    if (isSlider) {
                        xSlider = event.x
                        invalidate()
                    }
                }

            MotionEvent.ACTION_UP -> {
                if (isSlider) {
                    if (xSlider < xWithStart) {
                        xSlider = xWithStart
                        invalidate()
                    }
                    if (xSlider > xWithEnd) {
                        xSlider = xWithEnd
                        invalidate()
                    }
                    boData.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 {
                        boData.forEachIndexed { index, _ ->
                            val x = xWithStart + xSpacing * index
                            val dis = abs(x - event.x)
                            if (dis < xSpacing) {
                                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 * 9 - 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) {
        boData.forEachIndexed { index, i ->
            if (xSlider < xWithStart + xSpacing * index + xSpacing / 2 && xSlider > xWithStart + xSpacing * index - xSpacing / 2) {
                onDaySelectListener?.invoke(index, i)
            }

            if (i > 89) {
                paintPillar.color = Color.parseColor("#00d656")
            } else {
                paintPillar.color = Color.parseColor("#fdc221")
            }

            if (i > 0) {
                canvas.drawRoundRect(
                    RectF(
                        xWithStart + xSpacing * index - xSpacing / 2,
                        ySpacing + ySpacing * ((100 - i) / 5f) * mPercent,
                        xWithStart + xSpacing * index + xSpacing / 2,
                        ySpacing * 7
                    ), 2f, 2f, paintPillar
                )
            }
        }
    }

    private fun drawY(canvas: Canvas) {
        var k = 1
        for (i in 0..4) {
            if (i > 0) {
                k = i + 2
            }
            if (i == 4) {
                k = 7
            }
            canvas.drawLine(
                margin,
                ySpacing * k,
                scrWidth - margin,
                ySpacing * k,
                paintLine
            )

            canvas.drawText(
                "${yData[i]}%",
                scrWidth - margin,
                ySpacing * k - 10f,
                paintYText
            )
        }
    }

    private fun drawBessel(canvas: Canvas) {
        // 第一条曲线开始点
        mFirstCurveStartPoint[(xSlider - curveCircleRadius * 3).toInt()] = (ySpacing * 9).toInt()
        // 第一条曲线结束点
        mFirstCurveEndPoint[xSlider.toInt()] =
            (ySpacing * 9 - curveCircleRadius - curveCircleRadius / 4).toInt()
        // 第二条开始点
        mSecondCurveStartPoint = mFirstCurveEndPoint
        mSecondCurveEndPoint[(xSlider + curveCircleRadius * 3).toInt()] = (ySpacing * 9).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 * 9)
        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 * 9)
        mPath.lineTo(scrWidth, scrHeight)
        mPath.lineTo(0f, scrHeight)
        mPath.close()

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

    fun setValue(value: MutableList<Int>, time: Int): BODayChart {
        boData.clear()
        boData.addAll(value)
        xSlider = xSpacing * time + xWithStart
        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 * 8,
            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 * 8, paintGradientLine)
        }
    }

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

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

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

    fun setOnDayMoveListener(l: ((index: Boolean) -> Unit)): BODayChart {
        this.onDayMoveListener = l
        return this
    }
}


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