<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
日前,有一個“折現圖”的需求,如下圖所示:
如何自定義折線圖?首先將折線圖的繪製部分拆分成三部分:
第一步,需要定義出“折線圖”原點的位置,由圖得:
可以發現,原點的位置由X軸、Y軸所佔空間決定:
OriginX:Y軸寬度
OriginY:View高度 - X軸高度
思路:遍歷Y軸的繪製文字,用畫筆測量其最大寬度,在加上其左右Margin間距即Y軸寬度
Y軸寬度 = Y軸MarginLeft + Y軸最大文字寬度 + Y軸MariginRight
思路:獲取X軸畫筆FontMetrics,根據其top、bottom計算出X軸文字高度,在加上其上下Margin間距即X軸高度
val fontMetrics = xAxisTextPaint.fontMetrics val lineHeight = fontMetrics.bottom - fontMetrics.top xAxisHeight = lineHeight + xAxisOptions.textMarginTop + xAxisOptions.textMarginBottom
第二步,根據原點位置,繪製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軸等分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軸的標準間隔 */ 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自定義折線圖控制元件內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45