在自定义View和雷达图两篇博客的基础上,进行了些许修改,这里总结一下我自己的学习心得。
自定义View有如下几种方式
类型 | 定义 |
---|
自定义组合控件 | 多个控件组合成为一个新的控件,方便多处复用 | 继承系统View控件 | 继承自TextView等系统控件,在系统控件的基础功能上进行扩展 | 继承View | 不复用系统控件逻辑,继承View进行功能定义 | 继承系统ViewGroup | 继承自LinearLayout等系统控件,在系统控件的基础功能上进行扩展 | 继承ViewViewGroup | 不复用系统控件逻辑,继承ViewGroup进行功能定义 |
本文是通过继承于View来实现雷达图。
布局文件
在布局文件里加入雷达图控件,就能展示。控件程序在LeiDaMap类里,布局直接在layout文件夹里XML文件里添加:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.example.demo.radarMap.LeiDaMap
android:id="@+id/leiDaMap"
android:layout_width="300dp"
android:layout_height="280dp"
android:layout_gravity="center_horizontal" />
</LinearLayout>
控件文件
完整的代码如下所示,下面详细介绍程序流程。
package com.example.demo.radarMap
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.util.AttributeSet
import android.view.View
import kotlin.math.cos
import kotlin.math.min
import kotlin.math.sin
class LeiDaMap(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) :
View(context, attrs, defStyleAttr) {
private val count = 6
private val num = 4
private val angle = (Math.PI * 2 / count).toFloat()
private var radius = 0f
private var centerX = 0
private var centerY = 0
private var maxValue = 100f
private var data = doubleArrayOf(50.0, 60.0, 70.0, 80.0, 90.0, 100.0)
private var titles = arrayOf("一一", "二二", "三三", "四四", "五五", "六六")
private var mMainPaint: Paint? = null
private var mTextPaint: Paint? = null
private var mValuePaint: Paint? = null
constructor(context: Context?) : this(context, null)
constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)
private fun initPaint() {
mMainPaint = Paint()
mMainPaint?.isAntiAlias = true
mMainPaint?.strokeWidth = 3F
mMainPaint?.style = Paint.Style.STROKE
mMainPaint?.color = Color.BLACK
mTextPaint = Paint()
mTextPaint?.isAntiAlias = true
mTextPaint?.color = Color.BLUE
mTextPaint?.textSize = 60F
mValuePaint = Paint()
mValuePaint?.isAntiAlias = true
mValuePaint?.color = Color.RED
mValuePaint?.style = Paint.Style.FILL_AND_STROKE
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
radius = min(h, w).toFloat() / 2 * 0.7f
centerX = w / 2
centerY = h / 2
postInvalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
drawPolygon(canvas)
drawLines(canvas)
drawText(canvas)
drawRegion(canvas)
}
private fun drawPolygon(canvas: Canvas) {
val path = Path()
val r = radius / num
for (i in 1..num) {
val curR = r * i
path.reset()
for (j in 0 until count) {
if (j == 0) {
path.moveTo(centerX.toFloat(), centerY + curR)
} else {
val x = (centerX + curR * sin((angle * j).toDouble())).toFloat()
val y = (centerY + curR * cos((angle * j).toDouble())).toFloat()
path.lineTo(x, y)
}
}
path.close()
mMainPaint?.let { canvas.drawPath(path, it) }
}
}
private fun drawLines(canvas: Canvas) {
val path = Path()
for (i in 0 until count) {
path.reset()
path.moveTo(centerX.toFloat(), centerY.toFloat())
val x = (centerX + radius * sin((angle * i).toDouble())).toFloat()
val y = (centerY + radius * cos((angle * i).toDouble())).toFloat()
path.lineTo(x, y)
mMainPaint?.let { canvas.drawPath(path, it) }
}
}
private fun drawText(canvas: Canvas) {
val fontMetrics: Paint.FontMetrics = mTextPaint!!.fontMetrics
val fontHeight: Float = fontMetrics.descent - fontMetrics.ascent
for (i in 0 until count) {
val x =
(centerX + (radius + fontHeight / 2) * sin((angle * i).toDouble())).toFloat()
val y =
(centerY + (radius + fontHeight / 2) * cos((angle * i).toDouble())).toFloat()
val dis: Float = mTextPaint!!.measureText(titles[i])
if (i == 1 || i == 2) {
canvas.drawText(titles[i], x, y, mTextPaint!!)
}
else if (i == 4 || i == 5) {
canvas.drawText(titles[i], x - dis, y, mTextPaint!!)
}
else if (i == 0) {
canvas.drawText(titles[i], x - dis / 2, y + dis / 3, mTextPaint!!)
}
else if (i == 3) {
canvas.drawText(titles[i], x - dis / 2, y, mTextPaint!!)
}
}
}
private fun drawRegion(canvas: Canvas) {
val path = Path()
mValuePaint?.alpha = 255
for (i in 0 until count) {
val percent = data[i] / maxValue
val x = (centerX + radius * sin((angle * i).toDouble()) * percent).toFloat()
val y = (centerY + radius * cos((angle * i).toDouble()) * percent).toFloat()
if (i == 0) {
path.moveTo(centerX.toFloat(), y)
} else {
path.lineTo(x, y)
}
mValuePaint?.let { canvas.drawCircle(x, y, 20F, it) }
}
mValuePaint?.alpha = 127
mValuePaint?.let { canvas.drawPath(path, it) }
}
fun setTitles(titles: Array<String>) {
this.titles = titles
}
fun setData(data: DoubleArray) {
this.data = data
}
fun setMaxValue(maxValue: Float) {
this.maxValue = maxValue
}
fun setMainPaintColor(color: Int) {
mMainPaint?.color = color
}
fun setTextPaintColor(color: Int) {
mTextPaint?.color = color
}
fun setValuePaintColor(color: Int) {
mValuePaint?.color = color
}
init {
initPaint()
}
}
运行后: 
前面都是进行变量的初始化,绘图流程如下: 1、找到布局的中心 2、绘制多边形 3、绘制中心到角的连线 4、绘制文本(可能涉及到文字偏移) 5、绘制区域
1、找到布局的中心
重写onSizeChanged函数,找到中心点,以及雷达图的最大半径。为了给文本留有余地,需要乘以一个系数。
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
radius = min(h, w).toFloat() / 2 * 0.7f
centerX = w / 2
centerY = h / 2
postInvalidate()
}
2、画多边形
private fun drawPolygon(canvas: Canvas) {
val path = Path()
val r = radius / num
for (i in 1..num) {
val curR = r * i
path.reset()
for (j in 0 until count) {
if (j == 0) {
path.moveTo(centerX.toFloat(), centerY + curR)
} else {
val x = (centerX + curR * sin((angle * j).toDouble())).toFloat()
val y = (centerY + curR * cos((angle * j).toDouble())).toFloat()
path.lineTo(x, y)
}
}
path.close()
mMainPaint?.let { canvas.drawPath(path, it) }
}
}
3、绘制中心点到角的连线
private fun drawLines(canvas: Canvas) {
val path = Path()
for (i in 0 until count) {
path.reset()
path.moveTo(centerX.toFloat(), centerY.toFloat())
val x = (centerX + radius * sin((angle * i).toDouble())).toFloat()
val y = (centerY + radius * cos((angle * i).toDouble())).toFloat()
path.lineTo(x, y)
mMainPaint?.let { canvas.drawPath(path, it) }
}
}
4、绘制文本
可根据需要,在if选择语句里根据i的值或者根据象限进行判断。
private fun drawText(canvas: Canvas) {
val fontMetrics: Paint.FontMetrics = mTextPaint!!.fontMetrics
val fontHeight: Float = fontMetrics.descent - fontMetrics.ascent
for (i in 0 until count) {
val x =
(centerX + (radius + fontHeight / 2) * sin((angle * i).toDouble())).toFloat()
val y =
(centerY + (radius + fontHeight / 2) * cos((angle * i).toDouble())).toFloat()
val dis: Float = mTextPaint!!.measureText(titles[i])
if (i == 1 || i == 2) {
canvas.drawText(titles[i], x, y, mTextPaint!!)
}
else if (i == 4 || i == 5) {
canvas.drawText(titles[i], x - dis, y, mTextPaint!!)
}
else if (i == 0) {
canvas.drawText(titles[i], x - dis / 2, y + dis / 3, mTextPaint!!)
}
else if (i == 3) {
canvas.drawText(titles[i], x - dis / 2, y, mTextPaint!!)
}
}
}
5、绘制区域
private fun drawRegion(canvas: Canvas) {
val path = Path()
mValuePaint?.alpha = 255
for (i in 0 until count) {
val percent = data[i] / maxValue
val x = (centerX + radius * sin((angle * i).toDouble()) * percent).toFloat()
val y = (centerY + radius * cos((angle * i).toDouble()) * percent).toFloat()
if (i == 0) {
path.moveTo(centerX.toFloat(), y)
} else {
path.lineTo(x, y)
}
mValuePaint?.let { canvas.drawCircle(x, y, 20F, it) }
}
mValuePaint?.alpha = 127
mValuePaint?.let { canvas.drawPath(path, it) }
}
|