Android自定义View实现三角到八角的属性分布图-雷达图(蜘蛛网图)
前言
刚开始学习自定义view,简单完成了一个属性分布器,可以实现三条到八条属性的分布图,依次是三角形到正八边形,可以在xml和代码中设置属性个数和显示层数,并没有用到什么复杂的知识点,简单的canvas绘制就可以完成,复杂一点点可能就是绘制坐标的计算,我是使用的kotlin进行的编写,也是一个小新手,没使用太多的简化写法。这篇文章主要做一个记录,如果能帮到大家也自然是极好。废话不多说,开始我的炸弹秀,不好意思跑题了,开始
自定义View的关键点
自定义View分为两种,一是自定义单一View(不包含子View),二是自定义ViewGroup(包含子View),本例是一个单一的View。
首先写一个类继承于View,有四个构造方法
class MyView : View {
// 自定义View有四个构造函数
// 如果View是在Java代码里面new的,则调用第一个构造函数
constructor(context: Context) : super(context)
// 如果View是在.xml里声明的,则调用第二个构造函数
// 自定义属性是从AttributeSet参数传进来的
constructor(context: Context, attributeSet: AttributeSet) : this(context, attributeSet, 0)
// 不会自动调用
// 一般是在第二个构造函数里主动调用
// 如View有style属性时
// 这个方法里的内容是自定义属性的内容
constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) : super(context, attributeSet, defStyleAttr) {
val typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.MyView)
if (typedArray != null) {
//MyView_line_color线条颜色
lineColor = typedArray.getColor(R.styleable.MyView_line_color, Color.BLACK)
//MyView_line_size线条宽度
paintSize = typedArray.getDimension(R.styleable.MyView_line_size, 5f)
//MyView_floor_count绘制层数(不得小于各元素分配的比例)
floorCount = typedArray.getInt(R.styleable.MyView_floor_count, 3)
//MyView_angle_count绘制元素个数
angleCount = typedArray.getInt(R.styleable.MyView_angle_count, 3)
//MyView_text_color文本颜色
textColor = typedArray.getColor(R.styleable.MyView_text_color, Color.BLACK)
//MyView_text_size文本大小
textSize = typedArray.getDimension(R.styleable.MyView_text_size, 18f.dp)
//MyView_area_color覆盖区域颜色
areaColor = typedArray.getColor(R.styleable.MyView_area_color, ContextCompat.getColor(context, R.color.trared))
//MyView_default_text_padding默认文本与图形间距
defaultTextPadding = typedArray.getDimension(R.styleable.MyView_default_text_padding, 20f)
}
}
//API21之后才使用
// 不会自动调用
// 一般是在第二个构造函数里主动调用
// 如View有style属性时
constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attributeSet, defStyleAttr, defStyleRes)
}
构造方法写好后就可以进行view的绘制了,不过一般的view绘制必须要满足一些view的属性,比如warp_content的实现,在onMeasure方法中判断warp_content并默认控件宽高
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
//设置warp_content的默认宽高
val width = 400
val height = 400
//AT_MOST对应wrap_content;EXACTLY对应match_parent
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(width, height)
} else if (widthMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(width, heightSize)
} else if (heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSize, height)
}
}
在这里我们的view还必须支持padding属性,否则在xml中的padding属性将失效,getWidth和getHeight都是包含了padding的,所以我们定义两个新的参数来存储去掉了padding的宽高
var mWidth = width - paddingLeft - paddingRight
var mHeight = height - paddingBottom - paddingTop
//比较并返回较小的值,由于都是正多边形,所以设定宽高相等是最合适的
mWidth = mWidth.coerceAtMost(mHeight)
mHeight = mWidth
自定义属性,在res的values目录下创建自定义资源文件attrs_my_view.xml,名字随意,按照下面的格式写后便可以在布局文件中使用这些属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--name是view类名--!>
<declare-styleable name="MyView">
<!--自定义属性名和值的类型--!>
<attr name="line_color" format="color" />
<attr name="line_size" format="dimension" />
<attr name="floor_count" format="integer" />
<attr name="angle_count" format="integer" />
<attr name="text_color" format="color" />
<attr name="area_color" format="color" />
<attr name="text_size" format="dimension" />
<attr name="default_text_padding" format="dimension" />
</declare-styleable>
</resources>
绘制多边形
由于各多边形的绘制坐标有出入,所以我这里依次从三角形到正八边形都绘制了一遍,在onDraw中根据代码或xml中设置的属性个数判断绘制哪一个,这里我演示一下三角形的绘制
//首先是画笔的初始化
private var paint = Paint()
paint.color = lineColor
paint.strokeWidth = paintSize
paint.style = Paint.Style.STROKE
//defaultTextPadding 是文本与边框的距离
//textSize是文字大小(单位是px,相当于每个字的宽高)
//textLength是文字最大长度,考虑美观建议标题文字长度相同
//三角形宽度,边长,去掉了padding属性的参数表示整个图包括文本的宽高
//这里的ww就是不包含文本的图形宽度
val ww = mWidth - defaultTextPadding * 2 - textLength * textSize
//中心点各对角线的夹角 一定是平分360度的
val angle = (Math.PI * 2 / angleCount).toFloat()
//本View有大量的三角函数使用,一些角度计算关系就不介绍了,我写的时候脑袋都疼!
//三角形高度,顶点到底边的垂直线 通过三角函数计算
var hh = ww * sin(angle / 2)
val pathLine = Path()
//中心点到各角的距离
val r = ww / 2 / sin(angle / 2)
找到图形中心点!!!!这里是重要的,所有图都需要根据中心点向外绘制
/**
* 图形中心点
* 各正多边形的中心点横坐标X都可以是整个控件的横向中心位置
* 但是由于三角形,正五边形,正七边形比较特殊
* 所以它们的中心点纵坐标Y并不在整个控件的中心位置
* 这个时候就需要根据实际情况做一下小运算
* 比如说三角形,首先算得三角形的宽度作为底边长
* 因为中心点到各角的连线夹角都是相等的
* 所以根据角度和边长就可算得中心点到各夹角的连线长度
* 然后根据这个长度就可已算出纵坐标Y的位置
* 之后的图形绘制都需要根据中心点和中心点到各夹角的长度来计算
*/
val centerX = width / 2
//图形中心点y
val centerY = r + paddingTop + textSize + defaultTextPadding
绘制边框
//绘制边框
//这里有个判断主要是满足有的需求(不想绘制边框的,应该没人会这么干吧)
if (isShowLine) {
for (i in 1..floorCount) {
val ur = r * i / floorCount
//moveTo是移动到某个点,用来作为绘制的初始点
pathLine.moveTo(centerX + 0f, centerY - ur)
//lineTo是从上一个点画线到当前点
pathLine.lineTo(centerX + ur * sin(angle / 2), centerY + ur * cos(angle / 2))
pathLine.lineTo(centerX - ur * sin(angle / 2), centerY + ur * cos(angle / 2))
//close是闭合整个路径,第一个点到最后一个点连线
pathLine.close()
}
canvas.drawPath(pathLine, paint)
}
绘制连线,为了更加美观
//绘制连接线
if (isShowConnect) {
pathLine.reset()
pathLine.moveTo(centerX.toFloat(), centerY)
pathLine.lineTo(centerX.toFloat(), centerY - r)
pathLine.moveTo(centerX.toFloat(), centerY)
pathLine.lineTo(centerX + r * sin(angle / 2), centerY + r * cos(angle / 2))
pathLine.moveTo(centerX.toFloat(), centerY)
pathLine.lineTo(centerX - r * sin(angle / 2), centerY + r * cos(angle / 2))
pathLine.close()
canvas.drawPath(pathLine, paint)
}
绘制属性文本
//绘制文本
paint.color = textColor
paint.textSize = textSize
paint.style = Paint.Style.FILL
canvas.drawText(title[0], centerX - textSize, centerY - r - defaultTextPadding, paint)
canvas.drawText(title[1], centerX + ww / 2 - textSize, centerY + r * cos(angle / 2) + textSize + defaultTextPadding, paint)
canvas.drawText(title[2], centerX - ww / 2 - textSize, centerY + r * cos(angle / 2) + textSize + defaultTextPadding, paint)
绘制比例覆盖
//绘制覆盖分布区域图
pathLine.reset()
pathLine.moveTo(centerX.toFloat(), centerY - r * scaleList[0] / floorCount)
pathLine.lineTo(centerX + r * scaleList[1] / floorCount * sin(angle / 2), centerY + r * scaleList[1] / floorCount * cos(angle / 2)) pathLine.lineTo(centerX - r * scaleList[2] / floorCount * sin(angle / 2), centerY + r * scaleList[2] / floorCount * cos(angle / 2))
pathLine.close()
paint.color = areaColor
canvas.drawPath(pathLine, paint)
到这呢整个三角分布图就完成了,下面贴一下各个正多边形的效果图
正四边形
正五边形
正六边形
正七边形
正八边形