文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Android怎么实现自定义折线图控件

2023-07-02 10:21

关注

这篇“Android怎么实现自定义折线图控件”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“Android怎么实现自定义折线图控件”文章吧。

前言

日前,有一个“折现图”的需求,如下图所示:

Android怎么实现自定义折线图控件

概述

如何自定义折线图?首先将折线图的绘制部分拆分成三部分:

原点

第一步,需要定义出“折线图”原点的位置,由图得:

Android怎么实现自定义折线图控件

可以发现,原点的位置由X轴、Y轴所占空间决定:

OriginX:Y轴宽度OriginY:View高度 - X轴高度

计算Y轴宽度

思路:遍历Y轴的绘制文字,用画笔测量其最大宽度,在加上其左右Margin间距即Y轴宽度

Y轴宽度 = Y轴MarginLeft + Y轴最大文字宽度 + Y轴MariginRight

计算X轴高度

思路:获取X轴画笔FontMetrics,根据其top、bottom计算出X轴文字高度,在加上其上下Margin间距即X轴高度

val fontMetrics = xAxisTextPaint.fontMetricsval lineHeight = fontMetrics.bottom - fontMetrics.topxAxisHeight = lineHeight + xAxisOptions.textMarginTop + xAxisOptions.textMarginBottom

X轴

第二步,根据原点位置,绘制X轴轴线、网格线、文本

绘制轴线

绘制轴线比较简单,沿原点向控件右侧画一条直线即可

if (xAxisOptions.isEnableLine) {    xAxisLinePaint.strokeWidth = xAxisOptions.lineWidth    xAxisLinePaint.color = xAxisOptions.lineColor    xAxisLinePaint.pathEffect = xAxisOptions.linePathEffect    canvas.drawLine(originX, originY, width.toFloat(), originY, xAxisLinePaint)}

X轴刻度间隔

在绘制网格线、文本之前需要先计算X轴的刻度间隔:

Android怎么实现自定义折线图控件

这里处理的方式比较随意,直接将X轴等分7份即可(因为需要显示近7天的数据)

xGap = (width - originX) / 7

网格线、文本

网格线:只需要根据X轴的刻度,沿Y轴方向依次向控件顶部,画直线即可

文本:文本需要通过画笔,提前测量出待绘制文本的区域,然后计算出居中位置绘制即可

xAxisTexts.forEachIndexed { index, text ->    val pointX = originX + index * xGap    //刻度线    if (xAxisOptions.isEnableRuler) {        xAxisLinePaint.strokeWidth = xAxisOptions.rulerWidth        xAxisLinePaint.color = xAxisOptions.rulerColor        canvas.drawLine(            pointX, originY,            pointX, originY - xAxisOptions.rulerHeight,            xAxisLinePaint        )    }    //网格线    if (xAxisOptions.isEnableGrid) {        xAxisLinePaint.strokeWidth = xAxisOptions.gridWidth        xAxisLinePaint.color = xAxisOptions.gridColor        xAxisLinePaint.pathEffect = xAxisOptions.gridPathEffect        canvas.drawLine(pointX, originY, pointX, 0f, xAxisLinePaint)    }    //文本    bounds.setEmpty()    xAxisTextPaint.textSize = xAxisOptions.textSize    xAxisTextPaint.color = xAxisOptions.textColor    xAxisTextPaint.getTextBounds(text, 0, text.length, bounds)    val fm = xAxisTextPaint.fontMetrics    val fontHeight = fm.bottom - fm.top    val fontX = originX + index * xGap + (xGap - bounds.width()) / 2f    val fontBaseline = originY + (xAxisHeight - fontHeight) / 2f - fm.top    canvas.drawText(text, fontX, fontBaseline, xAxisTextPaint)}

Y轴

第三步:根据原点位置,绘制Y轴轴线、网格线、文本

计算Y轴分布

个人认为,这里是自定义折线图的一个难点,这里经过查阅资料,使用该文章中的算法:

基于JavaScript实现数值型坐标轴刻度计算算法(echarts的y轴刻度计算)

private fun getYInterval(maxY: Int): Int {    val yIntervalCount = yAxisCount - 1    val rawInterval = maxY / yIntervalCount.toFloat()    val magicPower = floor(log10(rawInterval.toDouble()))    var magic = 10.0.pow(magicPower).toFloat()    if (magic == rawInterval) {        magic = rawInterval    } else {        magic *= 10    }    val rawStandardInterval = rawInterval / magic    val standardInterval = getStandardInterval(rawStandardInterval) * magic    return standardInterval.roundToInt()}private fun getStandardInterval(x: Float): Float {    return when {        x <= 0.1f -> 0.1f        x <= 0.2f -> 0.2f        x <= 0.25f -> 0.25f        x <= 0.5f -> 0.5f        x <= 1f -> 1f        else -> getStandardInterval(x / 10) * 10    }}

刻度间隔、网格线、文本

Y轴的轴线、网格线、文本剩下的内容与X轴的处理方式几乎一致

//绘制Y轴//轴线if (yAxisOptions.isEnableLine) {    yAxisLinePaint.strokeWidth = yAxisOptions.lineWidth    yAxisLinePaint.color = yAxisOptions.lineColor    yAxisLinePaint.pathEffect = yAxisOptions.linePathEffect    canvas.drawLine(originX, 0f, originX, originY, yAxisLinePaint)}yAxisTexts.forEachIndexed { index, text ->    //刻度线    val pointY = originY - index * yGap    if (yAxisOptions.isEnableRuler) {        yAxisLinePaint.strokeWidth = yAxisOptions.rulerWidth        yAxisLinePaint.color = yAxisOptions.rulerColor        canvas.drawLine(            originX,            pointY,            originX + yAxisOptions.rulerHeight,            pointY,            yAxisLinePaint        )    }    //网格线    if (yAxisOptions.isEnableGrid) {        yAxisLinePaint.strokeWidth = yAxisOptions.gridWidth        yAxisLinePaint.color = yAxisOptions.gridColor        yAxisLinePaint.pathEffect = yAxisOptions.gridPathEffect        canvas.drawLine(originX, pointY, width.toFloat(), pointY, yAxisLinePaint)    }    //文本    bounds.setEmpty()    yAxisTextPaint.textSize = yAxisOptions.textSize    yAxisTextPaint.color = yAxisOptions.textColor    yAxisTextPaint.getTextBounds(text, 0, text.length, bounds)    val fm = yAxisTextPaint.fontMetrics    val x = (yAxisWidth - bounds.width()) / 2f    val fontHeight = fm.bottom - fm.top    val y = originY - index * yGap - fontHeight / 2f - fm.top    canvas.drawText(text, x, y, yAxisTextPaint)}

折线

折线的连接,这里使用的是Path,将一个一个坐标点连接,最后将Path绘制,就形成了图中的折线图

//绘制数据path.reset()points.forEachIndexed { index, point ->    val x = originX + index * xGap + xGap / 2f    val y = originY - (point.yAxis.toFloat() / yAxisMaxValue) * (yGap * (yAxisCount - 1))    if (index == 0) {        path.moveTo(x, y)    } else {        path.lineTo(x, y)    }    //圆点    circlePaint.color = dataOptions.circleColor    canvas.drawCircle(x, y, dataOptions.circleRadius, circlePaint)}pathPaint.strokeWidth = dataOptions.pathWidthpathPaint.color = dataOptions.pathColorcanvas.drawPath(path, pathPaint)

值得注意的是:坐标点X根据间隔是相对确定的,而坐标点Y则需要进行百分比换算

代码

折线图LineChart

package com.vander.pool.widget.linechartimport android.content.Contextimport android.graphics.*import android.text.TextPaintimport android.util.AttributeSetimport android.view.Viewimport java.text.DecimalFormatimport kotlin.math.floorimport kotlin.math.log10import kotlin.math.powimport kotlin.math.roundToIntclass LineChart : View {    private var options = ChartOptions()        private val xAxisTextPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)    private val xAxisLinePaint = Paint(Paint.ANTI_ALIAS_FLAG)    private val xAxisTexts = mutableListOf<String>()    private var xAxisHeight = 0f        private val yAxisTextPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)    private val yAxisLinePaint = Paint(Paint.ANTI_ALIAS_FLAG)    private val yAxisTexts = mutableListOf<String>()    private var yAxisWidth = 0f    private val yAxisCount = 5    private var yAxisMaxValue: Int = 0        private var originX = 0f    private var originY = 0f    private var xGap = 0f    private var yGap = 0f        private val pathPaint = Paint(Paint.ANTI_ALIAS_FLAG).also {        it.style = Paint.Style.STROKE    }    private val circlePaint = Paint(Paint.ANTI_ALIAS_FLAG).also {        it.color = Color.parseColor("#79EBCF")        it.style = Paint.Style.FILL    }    private val points = mutableListOf<ChartBean>()    private val bounds = Rect()    private val path = Path()    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)    override fun onDraw(canvas: Canvas) {        super.onDraw(canvas)        if (points.isEmpty()) return        val xAxisOptions = options.xAxisOptions        val yAxisOptions = options.yAxisOptions        val dataOptions = options.dataOptions        //设置原点        originX = yAxisWidth        originY = height - xAxisHeight        //设置X轴Y轴间隔        xGap = (width - originX) / points.size        //Y轴默认顶部会留出一半空间        yGap = originY / (yAxisCount - 1 + 0.5f)        //绘制X轴        //轴线        if (xAxisOptions.isEnableLine) {            xAxisLinePaint.strokeWidth = xAxisOptions.lineWidth            xAxisLinePaint.color = xAxisOptions.lineColor            xAxisLinePaint.pathEffect = xAxisOptions.linePathEffect            canvas.drawLine(originX, originY, width.toFloat(), originY, xAxisLinePaint)        }        xAxisTexts.forEachIndexed { index, text ->            val pointX = originX + index * xGap            //刻度线            if (xAxisOptions.isEnableRuler) {                xAxisLinePaint.strokeWidth = xAxisOptions.rulerWidth                xAxisLinePaint.color = xAxisOptions.rulerColor                canvas.drawLine(                    pointX, originY,                    pointX, originY - xAxisOptions.rulerHeight,                    xAxisLinePaint                )            }            //网格线            if (xAxisOptions.isEnableGrid) {                xAxisLinePaint.strokeWidth = xAxisOptions.gridWidth                xAxisLinePaint.color = xAxisOptions.gridColor                xAxisLinePaint.pathEffect = xAxisOptions.gridPathEffect                canvas.drawLine(pointX, originY, pointX, 0f, xAxisLinePaint)            }            //文本            bounds.setEmpty()            xAxisTextPaint.textSize = xAxisOptions.textSize            xAxisTextPaint.color = xAxisOptions.textColor            xAxisTextPaint.getTextBounds(text, 0, text.length, bounds)            val fm = xAxisTextPaint.fontMetrics            val fontHeight = fm.bottom - fm.top            val fontX = originX + index * xGap + (xGap - bounds.width()) / 2f            val fontBaseline = originY + (xAxisHeight - fontHeight) / 2f - fm.top            canvas.drawText(text, fontX, fontBaseline, xAxisTextPaint)        }        //绘制Y轴        //轴线        if (yAxisOptions.isEnableLine) {            yAxisLinePaint.strokeWidth = yAxisOptions.lineWidth            yAxisLinePaint.color = yAxisOptions.lineColor            yAxisLinePaint.pathEffect = yAxisOptions.linePathEffect            canvas.drawLine(originX, 0f, originX, originY, yAxisLinePaint)        }        yAxisTexts.forEachIndexed { index, text ->            //刻度线            val pointY = originY - index * yGap            if (yAxisOptions.isEnableRuler) {                yAxisLinePaint.strokeWidth = yAxisOptions.rulerWidth                yAxisLinePaint.color = yAxisOptions.rulerColor                canvas.drawLine(                    originX,                    pointY,                    originX + yAxisOptions.rulerHeight,                    pointY,                    yAxisLinePaint                )            }            //网格线            if (yAxisOptions.isEnableGrid) {                yAxisLinePaint.strokeWidth = yAxisOptions.gridWidth                yAxisLinePaint.color = yAxisOptions.gridColor                yAxisLinePaint.pathEffect = yAxisOptions.gridPathEffect                canvas.drawLine(originX, pointY, width.toFloat(), pointY, yAxisLinePaint)            }            //文本            bounds.setEmpty()            yAxisTextPaint.textSize = yAxisOptions.textSize            yAxisTextPaint.color = yAxisOptions.textColor            yAxisTextPaint.getTextBounds(text, 0, text.length, bounds)            val fm = yAxisTextPaint.fontMetrics            val x = (yAxisWidth - bounds.width()) / 2f            val fontHeight = fm.bottom - fm.top            val y = originY - index * yGap - fontHeight / 2f - fm.top            canvas.drawText(text, x, y, yAxisTextPaint)        }        //绘制数据        path.reset()        points.forEachIndexed { index, point ->            val x = originX + index * xGap + xGap / 2f            val y = originY - (point.yAxis.toFloat() / yAxisMaxValue) * (yGap * (yAxisCount - 1))            if (index == 0) {                path.moveTo(x, y)            } else {                path.lineTo(x, y)            }            //圆点            circlePaint.color = dataOptions.circleColor            canvas.drawCircle(x, y, dataOptions.circleRadius, circlePaint)        }        pathPaint.strokeWidth = dataOptions.pathWidth        pathPaint.color = dataOptions.pathColor        canvas.drawPath(path, pathPaint)    }        fun setData(list: List<ChartBean>) {        points.clear()        points.addAll(list)        //设置X轴、Y轴数据        setXAxisData(list)        setYAxisData(list)        invalidate()    }        private fun setXAxisData(list: List<ChartBean>) {        val xAxisOptions = options.xAxisOptions        val values = list.map { it.xAxis }        //X轴文本        xAxisTexts.clear()        xAxisTexts.addAll(values)        //X轴高度        val fontMetrics = xAxisTextPaint.fontMetrics        val lineHeight = fontMetrics.bottom - fontMetrics.top        xAxisHeight = lineHeight + xAxisOptions.textMarginTop + xAxisOptions.textMarginBottom    }        private fun setYAxisData(list: List<ChartBean>) {        val yAxisOptions = options.yAxisOptions        yAxisTextPaint.textSize = yAxisOptions.textSize        yAxisTextPaint.color = yAxisOptions.textColor        val texts = list.map { it.yAxis.toString() }        yAxisTexts.clear()        yAxisTexts.addAll(texts)        //Y轴高度        val maxTextWidth = yAxisTexts.maxOf { yAxisTextPaint.measureText(it) }        yAxisWidth = maxTextWidth + yAxisOptions.textMarginLeft + yAxisOptions.textMarginRight        //Y轴间隔        val maxY = list.maxOf { it.yAxis }        val interval = when {            maxY <= 10 -> getYInterval(10)            else -> getYInterval(maxY)        }        //Y轴文字        yAxisTexts.clear()        for (index in 0..yAxisCount) {            val value = index * interval            yAxisTexts.add(formatNum(value))        }        yAxisMaxValue = (yAxisCount - 1) * interval    }        private fun formatNum(num: Int): String {        val absNum = Math.abs(num)        return if (absNum >= 0 && absNum < 1000) {            return num.toString()        } else {            val format = DecimalFormat("0.0")            val value = num / 1000f            "${format.format(value)}k"        }    }        private fun getYInterval(maxY: Int): Int {        val yIntervalCount = yAxisCount - 1        val rawInterval = maxY / yIntervalCount.toFloat()        val magicPower = floor(log10(rawInterval.toDouble()))        var magic = 10.0.pow(magicPower).toFloat()        if (magic == rawInterval) {            magic = rawInterval        } else {            magic *= 10        }        val rawStandardInterval = rawInterval / magic        val standardInterval = getStandardInterval(rawStandardInterval) * magic        return standardInterval.roundToInt()    }        private fun getStandardInterval(x: Float): Float {        return when {            x <= 0.1f -> 0.1f            x <= 0.2f -> 0.2f            x <= 0.25f -> 0.25f            x <= 0.5f -> 0.5f            x <= 1f -> 1f            else -> getStandardInterval(x / 10) * 10        }    }        fun setOptions(newOptions: ChartOptions) {        this.options = newOptions        setData(points)    }    fun getOptions(): ChartOptions {        return options    }    data class ChartBean(val xAxis: String, val yAxis: Int)}

ChartOptions配置选项:

class ChartOptions {    //X轴配置    var xAxisOptions = AxisOptions()    //Y轴配置    var yAxisOptions = AxisOptions()    //数据配置    var dataOptions = DataOptions()}class AxisOptions {   companion object {     private const val DEFAULT_TEXT_SIZE = 20f       private const val DEFAULT_TEXT_COLOR = Color.BLACK        private const val DEFAULT_TEXT_MARGIN = 20        private const val DEFAULT_LINE_WIDTH = 2f        private const val DEFAULT_RULER_WIDTH = 10f    }        @FloatRange(from = 1.0)    var textSize: Float = DEFAULT_TEXT_SIZE    @ColorInt    var textColor: Int = DEFAULT_TEXT_COLOR        var textMarginTop: Int = DEFAULT_TEXT_MARGIN    var textMarginBottom: Int = DEFAULT_TEXT_MARGIN        var textMarginLeft: Int = DEFAULT_TEXT_MARGIN    var textMarginRight: Int = DEFAULT_TEXT_MARGIN        var lineWidth: Float = DEFAULT_LINE_WIDTH    @ColorInt    var lineColor: Int = DEFAULT_TEXT_COLOR    var isEnableLine = true   var linePathEffect: PathEffect? = null        var rulerWidth = DEFAULT_LINE_WIDTH    var rulerHeight = DEFAULT_RULER_WIDTH    @ColorInt    var rulerColor = DEFAULT_TEXT_COLOR    var isEnableRuler = true        var gridWidth: Float = DEFAULT_LINE_WIDTH    @ColorInt    var gridColor: Int = DEFAULT_TEXT_COLOR    var gridPathEffect: PathEffect? = null    var isEnableGrid = true}class DataOptions {    companion object {        private const val DEFAULT_PATH_WIDTH = 2f        private const val DEFAULT_PATH_COLOR = Color.BLACK        private const val DEFAULT_CIRCLE_RADIUS = 10f        private const val DEFAULT_CIRCLE_COLOR = Color.BLACK    }    var pathWidth = DEFAULT_PATH_WIDTH    var pathColor = DEFAULT_PATH_COLOR    var circleRadius = DEFAULT_CIRCLE_RADIUS    var circleColor = DEFAULT_CIRCLE_COLOR}

Demo样式:

private fun initView() {    val options = binding.chart.getOptions()    //X轴    val xAxisOptions = options.xAxisOptions    xAxisOptions.isEnableLine = false    xAxisOptions.textColor = Color.parseColor("#999999")    xAxisOptions.textSize = dpToPx(12)    xAxisOptions.textMarginTop = dpToPx(12).toInt()    xAxisOptions.textMarginBottom = dpToPx(12).toInt()    xAxisOptions.isEnableGrid = false    xAxisOptions.isEnableRuler = false    //Y轴    val yAxisOptions = options.yAxisOptions    yAxisOptions.isEnableLine = false    yAxisOptions.textColor = Color.parseColor("#999999")    yAxisOptions.textSize = dpToPx(12)    yAxisOptions.textMarginLeft = dpToPx(12).toInt()    yAxisOptions.textMarginRight = dpToPx(12).toInt()    yAxisOptions.gridColor = Color.parseColor("#999999")    yAxisOptions.gridWidth = dpToPx(0.5f)    val dashLength = dpToPx(8f)    yAxisOptions.gridPathEffect = DashPathEffect(floatArrayOf(dashLength, dashLength / 2), 0f)    yAxisOptions.isEnableRuler = false    //数据    val dataOptions = options.dataOptions    dataOptions.pathColor = Color.parseColor("#79EBCF")    dataOptions.pathWidth = dpToPx(1f)    dataOptions.circleColor = Color.parseColor("#79EBCF")    dataOptions.circleRadius = dpToPx(3f)    binding.chart.setOnClickListener {        initChartData()    }    binding.toolbar.setLeftClick {        finish()    }}private fun initChartData() {    val random = 1000    val list = mutableListOf<LineChart.ChartBean>()    list.add(LineChart.ChartBean("05-01", Random.nextInt(random)))    list.add(LineChart.ChartBean("05-02", Random.nextInt(random)))    list.add(LineChart.ChartBean("05-03", Random.nextInt(random)))    list.add(LineChart.ChartBean("05-04", Random.nextInt(random)))    list.add(LineChart.ChartBean("05-05", Random.nextInt(random)))    list.add(LineChart.ChartBean("05-06", Random.nextInt(random)))    list.add(LineChart.ChartBean("05-07", Random.nextInt(random)))    binding.chart.setData(list)    //文本    val text = list.joinToString("\n") {        "x : ${it.xAxis}  y:${it.yAxis}"    }    binding.value.text = text}

以上就是关于“Android怎么实现自定义折线图控件”这篇文章的内容,相信大家都有了一定的了解,希望小编分享的内容对大家有帮助,若想了解更多相关的知识内容,请关注编程网行业资讯频道。

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     807人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     351人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     314人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     433人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     221人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯