上述就是android开发分享Android 实现自定义折线图控件的全部内容,如果对大家有所用处且需要了解更多关于Android学习教程,希望大家多多关注—计算机技术网(www.ctvol.com)!
前言
日前,有一个“折现图”的需求,如下图所示:
概述
如何自定义折线图?首先将折线图的绘制部分拆分成三部分:
- 原点
- x轴
- y轴
- 折线
原点
第一步,需要定义出“折线图”原点的位置,由图得:
可以发现,原点的位置由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.fontmetrics val lineheight = fontmetrics.bottom - fontmetrics.top xaxisheight = 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轴的刻度间隔:
这里处理的方式比较随意,直接将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轴刻度计算)
/** * 根据y轴最大值、数量获取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.pathwidth pathpaint.color = dataoptions.pathcolor canvas.drawpath(path, pathpaint)
值得注意的是:坐标点x根据间隔是相对确定的,而坐标点y则需要进行百分比换算
代码
折线图linechart
package com.vander.pool.widget.linechart import android.content.context import android.graphics.* import android.text.textpaint import android.util.attributeset import android.view.view import java.text.decimalformat import kotlin.math.floor import kotlin.math.log10 import kotlin.math.pow import kotlin.math.roundtoint class linechart : view { private var options = chartoptions() /** * x轴相关 */ 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 /** * y轴相关 */ 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() } /** * 设置x轴数据 */ 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 } /** * 设置y轴数据 */ 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" } } /** * 根据y轴最大值、数量获取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 } } /** * 重置参数 */ 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 /** * x轴文字内容上下两侧margin */ var textmargintop: int = default_text_margin var textmarginbottom: int = default_text_margin /** * y轴文字内容左右两侧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 实现自定义折线图控件的文章就介绍到这了,更多相关android折线图控件内容请搜索<计算机技术网(www.ctvol.com)!!>以前的文章或继续浏览下面的相关文章希望大家以后多多支持<计算机技术网(www.ctvol.com)!!>!
本文来自网络收集,不代表计算机技术网立场,如涉及侵权请联系管理员删除。
ctvol管理联系方式QQ:251552304
本文章地址:https://www.ctvol.com/addevelopment/1108903.html