新西兰服务器

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


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

发布时间:2022-06-16 10:03:14 来源:高防服务器网 阅读:99 作者:iii 栏目:开发技术

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

    前言

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

    概述

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

    • 原点

    • 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怎么实现自定义折线图控件”这篇文章的内容,相信大家都有了一定的了解,希望小编分享的内容对大家有帮助,若想了解更多相关的知识内容,请关注高防服务器网行业资讯频道。

    [微信提示:高防服务器能助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。

    [图文来源于网络,不代表本站立场,如有侵权,请联系高防服务器网删除]
    [