前言

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

概述

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

  • 原点
  • 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折线图控件内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!