android 自定义折线图 30天统计,点击显示详情

91 阅读3分钟

微信图片_20220804154819.jpg

源码

class LineView : View {

    private var mContext: Context

    //坐标系画笔
    private lateinit var xyPaint: Paint

    //坐标文字画笔
    private lateinit var textPaint: Paint

    private var mViewWidth = 0
    private var mViewHeight = 0
    private lateinit var mShader: LinearGradient

    //坐标轴上下左右距离边界的距离
    private var margin = 100f

    //y轴坐标初始值
    private val yNumber = mutableListOf<String>()

    //x轴坐标值
    private val xNumber = mutableListOf<String>()

    //阴影路径
    private var mShaderPath = Path()

    //阴影画笔
    private lateinit var shaderPaint: Paint

    //最大Y轴值
    var maxY = 0

    //数据集合
    private val mDatas = mutableListOf<EverydayBean.DayBean>()

    //坐标点和折线画笔
    private lateinit var linePaint: Paint

    //点击marker选中的坐标点位置
    private var selectPoint = 0

    //marker画笔
    private lateinit var markerPaint: Paint

    //marker透明背景画笔
    private lateinit var tmPaint: Paint

    //marker文字画笔
    private lateinit var markerTextPaint: Paint

    //虚线画笔
    private lateinit var dottedPaint: Paint

    //是否点击了
    private var isClick = false

    constructor(context: Context) : this(context, null)

    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    ) {
        mContext = context
        //初始化画笔
        initPaint()

    }

    private fun initPaint() {
        xyPaint = Paint()
        xyPaint.style = Paint.Style.FILL
        xyPaint.isAntiAlias = true
        xyPaint.strokeWidth = DisplayUtils.dp2px(mContext, 1f).toFloat()
        xyPaint.color = ContextCompat.getColor(mContext, R.color.black_333)

        textPaint = Paint()
        textPaint.textAlign = Paint.Align.CENTER
        textPaint.isAntiAlias = true
        textPaint.color = ContextCompat.getColor(mContext, R.color.black_333)
        textPaint.textSize = 30f

        linePaint = Paint()
        linePaint.textAlign = Paint.Align.CENTER
        linePaint.isAntiAlias = true
        linePaint.color = ContextCompat.getColor(mContext, R.color.blue_007)
        linePaint.strokeWidth = DisplayUtils.dp2px(mContext, 1f).toFloat()

        shaderPaint = Paint()
        textPaint.isAntiAlias = true

        markerPaint = Paint()
        markerPaint.textAlign = Paint.Align.CENTER
        markerPaint.style = Paint.Style.STROKE
        markerPaint.isAntiAlias = true
        markerPaint.color = ContextCompat.getColor(mContext, R.color.gray_ddd)
        markerPaint.strokeWidth = DisplayUtils.dp2px(mContext, 1f).toFloat()
        markerPaint.textSize = DisplayUtils.sp2px(mContext, 10f).toFloat()

        markerTextPaint = Paint()
        markerTextPaint.textAlign = Paint.Align.CENTER
        markerTextPaint.isAntiAlias = true
        markerTextPaint.color = ContextCompat.getColor(mContext, R.color.black_333)
        markerTextPaint.strokeWidth = DisplayUtils.dp2px(mContext, 1f).toFloat()
        markerTextPaint.textSize = DisplayUtils.sp2px(mContext, 10f).toFloat()


        tmPaint = Paint()
        tmPaint.textAlign = Paint.Align.CENTER
        tmPaint.style = Paint.Style.FILL
        tmPaint.isAntiAlias = true
        tmPaint.color = Color.parseColor("#ccffffff")

        dottedPaint = Paint()
        dottedPaint.isAntiAlias = true
        dottedPaint.pathEffect = DashPathEffect(floatArrayOf(4f, 4f), 0f)

    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        setMeasuredDimension(measureSpec(widthMeasureSpec), measureSpec(heightMeasureSpec))
    }

    private fun measureSpec(heightMeasureSpec: Int): Int {
        var result: Int
        val specSize = View.MeasureSpec.getSize(heightMeasureSpec) //获取高的高度 单位 为px
        val specMode = View.MeasureSpec.getMode(heightMeasureSpec)//获取测量的模式
        //如果是精确测量,就将获取View的大小设置给将要返回的测量值
        if (specMode == View.MeasureSpec.EXACTLY) {
            result = specSize
        } else {
            result = 400
            //如果设置成wrap_content时,给高度指定一个值
            if (specMode == View.MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize)
            }
        }
        return result
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        //获取当前View的宽高
        mViewWidth = w
        mViewHeight = h
        //渐变
        mShader = LinearGradient(
            mViewWidth.toFloat(),
            mViewHeight.toFloat(),
            mViewWidth.toFloat(),
            0f,
            intArrayOf(Color.YELLOW, Color.TRANSPARENT),
            null,
            Shader.TileMode.REPEAT
        )
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (xNumber.size == 0 || yNumber.size == 0) {
            return
        }
        //绘制坐标点
        drawPoint(canvas)

        //绘制圆点 坐标轴 坐标轴值
        drawXY(canvas)

        //绘制marker
        if (isClick) {
            drawMarker(canvas)
        }

    }


    private fun drawXY(canvas: Canvas) {

        //绘制圆点
        canvas.drawCircle(margin, (mViewHeight - margin), 6f, xyPaint)
        //绘制x轴
        canvas.drawLine(
            margin,
            (mViewHeight - margin),
            (mViewWidth - margin),
            (mViewHeight - margin),
            xyPaint
        )
        //绘制y轴
        canvas.drawLine(margin, mViewHeight - margin, margin, margin, xyPaint)
        //绘制x轴上坐标点
        //每个点的间隔 和文字
        var xSpace = (mViewWidth - margin * 2) / 29
        var mXBound = Rect()
        var mYBound = Rect()
        for (i in 0..29) {
            if (i == 0 || i == 15 || i == 29) {
                canvas.drawCircle(margin + xSpace * i, (mViewHeight - margin), 6f, xyPaint)
                val xText = xNumber[i]
                textPaint.getTextBounds(xText, 0, xText.length, mXBound)
                canvas.drawText(
                    xText,
                    margin + xSpace * i,
                    mViewHeight - margin + 2 * mXBound.height(),
                    textPaint
                )
            }
        }

        //绘制y轴上坐标点 和文字
        var ySpace = (mViewHeight - margin * 2) / 5

        for (i in 0..5) {
            canvas.drawCircle(margin, (mViewHeight - margin - ySpace * i), 6f, xyPaint)

            canvas.drawLine(
                margin,
                (mViewHeight - margin - ySpace * i),
                mViewWidth - margin ,
                (mViewHeight - margin - ySpace * i),
                dottedPaint
            )

            var yText = yNumber[i]
            textPaint.getTextBounds(yText, 0, yText.length, mYBound)
            canvas.drawText(
                yText,
                margin - mYBound.width() / 2 - 10,
                (mViewHeight - margin - ySpace * i + mYBound.height() / 2),
                textPaint
            )
        }
        canvas.drawText(
            "人数",
            margin - mYBound.width() / 2 - 10,
            margin / 2,
            textPaint
        )
    }


    private fun drawPoint(canvas: Canvas) {
        var xSpace = (mViewWidth - margin * 2) / 29
        var ySpace = (mViewHeight - margin * 2) / maxY

        mShaderPath.moveTo(
            margin,
            (mViewHeight - margin)
        )

        var startX = 0F
        var startY = 0F
        var endX = 0F
        var endY = 0F
        //画折线
        for (i in 0..29) {
            if (startY == 0F) {
                startX = margin + xSpace * i
                startY = mViewHeight - mDatas[i].tote_count * ySpace - margin
            } else {
                startX = endX
                startY = endY
            }
            if (i != 29) {
                endX = margin + xSpace * (i + 1)
                endY = mViewHeight - mDatas[i + 1].tote_count * ySpace - margin
                canvas.drawCircle(startX, startY, 3f, linePaint)
                canvas.drawCircle(endX, endY, 3f, linePaint)
                //绘制线
//                Log.e("TAG", "$startX-----$startY------$endX-----$endY")
                canvas.drawLine(startX, startY, endX, endY, linePaint)
            } else {
                canvas.drawCircle(startX, startY, 3f, linePaint)
            }
            mShaderPath.lineTo(startX, startY)

            if (i == 29) {
                mShaderPath.lineTo(endX, endY)
                mShaderPath.lineTo(endX, mViewHeight - margin)
                val shader = LinearGradient(
                    mViewWidth.toFloat(),
                    mViewHeight.toFloat(),
                    mViewWidth.toFloat(),
                    0f,
                    intArrayOf(Color.YELLOW, Color.TRANSPARENT),
                    null,
                    Shader.TileMode.REPEAT
                )
                shaderPaint.shader = shader
                canvas.drawPath(mShaderPath, shaderPaint)
            }


        }

        startX = 0F
        startY = 0F
        endX = 0F
        endY = 0F
        for (i in 0..29) {
            if (startY == 0F) {
                startX = margin + xSpace * i
                startY = mViewHeight - mDatas[i].tote_count * ySpace - margin
            } else {
                startX = endX
                startY = endY
            }
            if (i != 29) {
                endX = margin + xSpace * (i + 1)
                endY = mViewHeight - mDatas[i + 1].tote_count * ySpace - margin
                //绘制点
                canvas.drawCircle(startX, startY, 3f, linePaint)
                canvas.drawCircle(endX, endY, 3f, linePaint)
            } else {
                canvas.drawCircle(startX, startY, 3f, linePaint)
            }
        }

    }

    private fun drawMarker(canvas: Canvas) {
        var xSpace = (mViewWidth - margin * 2) / 29
        var ySpace = (mViewHeight - margin * 2) / 5
        //1.绘制虚线
        canvas.drawLine(
            margin + xSpace * selectPoint,
            mViewHeight - margin,
            margin + xSpace * selectPoint,
            margin,
            dottedPaint
        )
        //2.先绘制内容边框,如果虚线距离y轴的距离小于内容框的大小,内容框位置不变
        //如果大于内容框的距离,内容框跟着虚线左右移动
        var startX = if (margin + xSpace * selectPoint > 2 * margin + 2 * ySpace) {
            2 * margin + margin + xSpace * selectPoint - 2 * (margin + ySpace) - dpToPx(10)
        } else {
            2 * margin
        }

        var startY = margin + ySpace
        var tmF = RectF(startX, margin, startX + 2 * ySpace, startY)
        canvas.drawRoundRect(tmF, 10f, 10f, tmPaint)
        var rectF = RectF(startX, margin, startX + 2 * ySpace, startY)
        canvas.drawRoundRect(rectF, 10f, 10f, markerPaint)

        //3.绘制日期
        var timeRect = Rect()
        var timeText = mDatas[selectPoint].date_time
        markerPaint.getTextBounds(timeText, 0, timeText.length, timeRect)
        canvas.drawText(
            timeText,
            (startX + ySpace),
            margin + ySpace / 2 - timeRect.height() / 2,
            markerTextPaint
        )
        //4.绘制图例和数据
        var numRect = Rect()
        var numText = "进场:${mDatas[selectPoint].tote_count}人"
        markerPaint.getTextBounds(numText, 0, numText.length, numRect)
        canvas.drawText(
            numText,
            (startX + ySpace),
            margin + ySpace - numRect.height() / 2,
            markerTextPaint
        )
        //图例
        canvas.drawCircle(
            (startX + ySpace - timeRect.width() / 2),
            margin + ySpace - ySpace / 4,
            3f,
            linePaint
        )

    }


    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {

            MotionEvent.ACTION_UP -> {
                //点击事件
                clickAction(event)
            }
        }

        return true
    }

    /**
     * 根据点击位置,计算出离点击位置最近的点
     * 显示该点的详细信息
     */
    private fun clickAction(event: MotionEvent) {
        isClick = true
        var eventX = event.x
        var eventY = event.y

        var xSpace = (mViewWidth - margin * 2) / 30
        var index = 0
        for (i in 0..29) {
            if (i * xSpace >= (eventX - margin)) {
                index = i
                break
            }
        }
        selectPoint =
            if ((index * xSpace - (eventX - margin) > ((eventX - margin) - ((index - 1) * xSpace)))) {
                index - 1
            } else {
                index
            }

        //超出点击范围隐藏弹窗
        if (eventY < margin || eventY > mViewHeight - margin || eventX < margin || eventX > mViewWidth - margin) {
            isClick = false
        }

        invalidate()
    }


    /**
     * 当最大人数小于100时,最大值设置成100
     * 当最大值大于200时,最大值最高为+1,然后拼接最大值位数减1个领,拼接
     * 后的数当最大值
     * Y轴坐标始终为5个
     */
    fun setData(datas: MutableList<EverydayBean.DayBean>) {

        mDatas.clear()
        mDatas.addAll(datas)
        mDatas.reverse()

        datas.forEachIndexed { index, dayBean ->
//            if (index == 0 || index == 14 || index == 29) {
            xNumber.add(dayBean.date_time)
//            }
            if (dayBean.tote_count > maxY) {
                maxY = dayBean.tote_count
            }
        }
        xNumber.reverse()
        if (maxY == 0 || maxY <= 100) {
            yNumber.add("0")
            yNumber.add("20")
            yNumber.add("40")
            yNumber.add("60")
            yNumber.add("80")
            yNumber.add("100")
            maxY = 100
        } else {
            var maxStr = maxY.toString()
            var num = maxStr.substring(0, 1).toInt()
            Log.e("TAG", "max---->$maxY----->$num")
            val sb = StringBuffer()
            sb.append("${num + 1}")
            for (i in maxStr.indices - 1) {
                sb.append("0")
            }
            maxY = sb.toString().toInt()
            yNumber.add("0")
            yNumber.add("${maxY / 5}")
            yNumber.add("${maxY * 2 / 5}")
            yNumber.add("${maxY * 3 / 5}")
            yNumber.add("${maxY * 4 / 5}")
            yNumber.add("$maxY")
        }

        invalidate()
    }

    private fun dpToPx(dp: Int): Int {
        var density = mContext.resources.displayMetrics.density
        return (dp * density + 0.5f * if (dp >= 0) 1 else -1).toInt()
    }
}

bean 类

class DayBean(
    val date_time: String,
    val tote_count: Int
)