rendering

code


/**
 *
 * 周图表
 * zrj 2020/7/14
 */
class StepWeekChart(context: Context, attrs: AttributeSet?) : View(context, attrs) {
    //屏幕宽高
    private var scrWidth = 0f
    private var scrHeight = 0f
    private var xData: Array<String> = arrayOf(
        context.getString(R.string.mon),
        context.getString(R.string.tue),
        context.getString(R.string.wed),
        context.getString(R.string.thu),
        context.getString(R.string.fri),
        context.getString(R.string.sat),
        context.getString(R.string.sun)
    )
    private var data = mutableListOf(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f)
    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.blue_7fbeff)

        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])
        xWithEnd = scrWidth - margin - paintYText.measureText(xData[0]) * 2.5f
        xSpacing = (xWithEnd - xWithStart) / (data.size - 1)
        xSlider = xSpacing * (week - 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()
                    }
                }

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

                    data.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) {
                        onWeekMoveListener?.invoke(event.x > mDownX)
                    } else {
                        data.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轴分割份数后间距

    @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 + xSpacing * index
            val dis = abs(x - xSlider)
            var y = ySpacing * 7 - 10f.dp
            if (dis < xSpacing / 2) {
                paintXText.typeface = Typeface.DEFAULT_BOLD
                y -= 10f.dp * (1 - dis / xSpacing)
            } else {
                paintXText.typeface = Typeface.DEFAULT
            }
            canvas.drawText(s, x, y, paintXText)
            if (index == 0) {
                canvas.drawText(monDay, x, y - 12f.dp, paintXText)
            }
        }
    }

    private fun drawPillar(canvas: Canvas) {
        data.forEachIndexed { index, i ->
            if (xSlider < xWithStart + xSpacing * index + xSpacing / 2 && xSlider > xWithStart + xSpacing * index - xSpacing / 2) {
                paintPillar.color = context.colorCompat(R.color.blue_007dff)
                onWeekSelectListener?.invoke(index, i)
            } else {
                paintPillar.color = context.colorCompat(R.color.blue_7fbeff)
            }
            if (i > 0) {
                canvas.drawRoundRect(
                    RectF(
                        xWithStart + xSpacing * index - 15f,
                        ySpacing + ySpacing * 4f * (1 - (i / yMarkMax.toFloat()) * mPercent),
                        xWithStart + xSpacing * index + 15f,
                        ySpacing * 5
                    ), 10f, 10f, paintPillar
                )
            }
        }
    }

    private fun drawY(canvas: Canvas) {
        val y = ySpacing + ySpacing * 4f * (1 - (stepGoal / yMarkMax.toFloat()))

        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 (abs(y - (ySpacing * (i + 1))) > 25) { //目标重叠
                canvas.drawText(
                    (yMarkMax - i * (yMarkMax / 4)).toString(),
                    scrWidth - margin,
                    ySpacing * (i + 1) - 10f,
                    paintYText
                )
            }
        }
        if (stepGoal > 0) {
            paintPillar.color = context.colorCompat(R.color.blue_7fbeff)
            canvas.drawLine(margin, y, scrWidth - margin, y, paintPillar)
            paintYText.color = context.colorCompat(R.color.blue_7fbeff)
            canvas.drawText(stepGoal.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 var yMarkMax = 1 //Y轴刻度最大值(根据设置的数据自动赋值)
    private var monDay = "" //每周一的日期
    private var stepGoal = 0
    private var week = 0
    fun setValue(value: MutableList<Float>, monDay: String, goal: Int, week: Int): StepWeekChart {
        this.monDay = monDay
        this.stepGoal = goal
        this.week = week
        data.clear()
        data.addAll(value)
        xSlider = xSpacing * (week - 1) + xWithStart
        var yMark = (data.maxOrNull() ?: 4).toInt() / 4 + 1   // 4 3 2 1 0  每个刻度间距 5个刻度4个间距
        val mark = yMark.toString().substring(0, 1).toInt() + 1
        when (yMark.toString().length) {
            1 ->
                //yMark = 1、2、5、10
                yMark =
                    if (yMark == 3 || yMark == 4 || yMark == 6 || yMark == 7 || yMark == 8 || yMark == 9) if (yMark == 3 || yMark == 4) 5 else 10 else yMark

            2 -> yMark = mark * 10

            3 -> yMark = mark * 100

            4 -> yMark = mark * 1000

            5 -> yMark = mark * 10000

            6 -> yMark = mark * 100000
        }
        yMarkMax = yMark * 4
        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), Color.parseColor("#0e83ff"),
                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 onWeekSelectListener: ((index: Int, value: Float) -> Unit)? = null

    fun setOnWeekSelectListener(l: ((index: Int, value: Float) -> Unit)): StepWeekChart {
        this.onWeekSelectListener = l
        return this
    }

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

    fun setOnWeekMoveListener(l: ((index: Boolean) -> Unit)): StepWeekChart {
        this.onWeekMoveListener = l
        return this
    }
}


Copy the code