android开发分享Android 实现自定义折线图控件

前言日前,有一个“折现图”的需求,如下图所示:概述如何自定义折线图?首先将折线图的绘制部分拆分成三部分:原点x轴y轴折线原点第一步,需要定义出“折线图”

上述就是android开发分享Android 实现自定义折线图控件的全部内容,如果对大家有所用处且需要了解更多关于Android学习教程,希望大家多多关注—计算机技术网(www.ctvol.com)!

前言

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

Android 实现自定义折线图控件

概述

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

  • 原点
  • x轴
  • y轴
  • 折线

原点

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

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.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轴的刻度间隔:

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轴刻度计算)

/**   * 根据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

(0)
上一篇 2022年6月27日
下一篇 2022年6月27日

精彩推荐