一:效果演示
二:实现思路
——描边思路:一开始觉得这很简单,就直接绘制一个比当前大一圈的雷达图就可以了,试了试发现并没有那么简单 当每个顶点的权重都相同时,也就是雷达图为正多边形时绘制出来的描边是等宽的,但是因为雷达图每个顶点的权重是不均匀的,所以会导致描边的宽度也不均匀,效果如下
解决办法是对于每一个顶点,都找出上一个顶点和下一个顶点,并且得到它相邻的两条边的向外的垂线,沿着垂线向外延伸相同宽度后得到两个点,再把两个点沿着边的方向延伸相交于一点,对每个顶点重复上述步骤,就能得到描边外框的所有点了
例如对于顶点B,找到上一个顶点A和下一个顶点C,沿着垂线向外延伸相同宽度后得到两个点A1和C1,再将A1和C1沿着边的方向延伸相交与点B1,再对顶点C进行同样的操作找到交点C1.....
——求每条边向外的垂线方向向量:因为是求某条边的垂直向量,所以可以利用两个向量的点乘结果为0的性质来求,但是每条边的垂直向量都有两个,我们需要知道哪个是向外的垂直方向,经过如下推导可求得
对于顶点A,向量DA的符号为(+,+),向外垂直向量DD1的符号为(+,-),向量BA的符号为(+,-),向外的垂直向量BB1的符号为(+,+) 对于顶点B,向量AB的符号为(-,+),向外垂直向量AA1的符号为(+,+),向量CB的符号为(+,+),向外的垂直向量CC1的符号为(-,+) 结论:对于向量为(x,y),上一个顶点的边的向外垂直向量为(y,-x)。下一个顶点的边的向外垂直向量为(-y,x)
三:使用
——面板参数设置
Sprite:雷达图内部图片 Color:颜色 Raycast Target:是否接收射线 Radius:半径 Side Count:几边形(有几个顶点) Show Inner:是否显示雷达图内部 Show Outline:是否显示雷达图描边 OutlineData-Width:描边宽度 OutlineData-Color:描边颜色
——常规使用 雷达图的背景一般都是美术提供素材,调整RadarChart的Radius大小使雷达图与美术素材一致并设置RadarChart组件的相关参数
SetRadarChart:设置雷达图 SetRatioList:设置雷达图每个顶点的比例值
using System.Collections.Generic;
using UnityEngine;
public class Test : MonoBehaviour
{
public RadarChart radarChart;
private void Awake()
{
List<float> ratioList = new List<float>() { 0.5f, 0.4f, 0.3f, 0.5f, 0.7f, 1f };
radarChart.SetRadarChart();
radarChart.SetRatioList(ratioList);
}
}
四:代码实现
using UnityEngine;
using UnityEngine.Sprites;
using UnityEngine.UI;
using System.Collections.Generic;
using System;
/// <summary>
/// 雷达图组件
/// </summary>
[AddComponentMenu("LFramework/UI/RadarChart", 51)]
public class RadarChart : MaskableGraphic
{
protected RadarChart()
{
}
/// <summary>
/// 描边数据
/// </summary>
[Serializable]
public class OutlineData
{
[SerializeField]
public float width = 5;
[SerializeField]
public Color color = Color.red;
}
//Sprite图片
[SerializeField]
Sprite m_Sprite;
public Sprite Sprite
{
get { return m_Sprite; }
}
//贴图
public override Texture mainTexture
{
get
{
if (m_Sprite == null)
{
if (material != null && material.mainTexture != null)
{
return material.mainTexture;
}
return s_WhiteTexture;
}
return m_Sprite.texture;
}
}
//半径
[SerializeField]
float m_Radius = 100;
//边数(几边形)
[SerializeField]
int m_SideCount;
public int SideCount
{
get
{
m_SideCount = Mathf.Clamp(m_SideCount, 3, 65000);
return m_SideCount;
}
}
//是否显示雷达图内部
[SerializeField]
bool m_ShowInner = true;
//是否显示雷达图描边
[SerializeField]
bool m_ShowOutline;
//雷达图描边数据
[SerializeField]
OutlineData m_OutlineData;
//比例值列表
List<float> m_RatioList = new List<float>();
//顶点位置列表
List<Vector3> m_TempVertexList = new List<Vector3>();
List<Vector3> m_VertexList = new List<Vector3>();
//比例值变化后
public Action OnRatioValueChanged;
/// <summary>
/// 初始化比例值列表
/// </summary>
void InitRatioList()
{
int ratioCount = m_RatioList.Count;
if (ratioCount < SideCount)
{
for (int i = 0; i < SideCount - ratioCount; i++)
{
m_RatioList.Add(1);
}
}
}
/// <summary>
/// 设置比例值列表
/// </summary>
public void SetRatioList(List<float> ratioList)
{
for (int i = 0; i < m_RatioList.Count; i++)
{
if (ratioList.Count - 1 >= i)
{
m_RatioList[i] = ratioList[i];
}
}
SetVerticesDirty();
CalcVertexPos();
OnRatioValueChanged?.Invoke();
}
/// <summary>
/// 设置雷达图
/// </summary>
public void SetRadarChart()
{
rectTransform.sizeDelta = new Vector2(m_Radius * 2, m_Radius * 2);
InitRatioList();
}
/// <summary>
/// 得到比例值列表
/// </summary>
public List<float> GetRatioList()
{
return m_RatioList;
}
protected override void OnPopulateMesh(VertexHelper vh)
{
vh.Clear();
m_TempVertexList.Clear();
GenerateInner(vh);
if (m_ShowOutline)
{
GenerateOutline(vh);
}
}
/// <summary>
/// 生成雷达图内部
/// </summary>
void GenerateInner(VertexHelper vh)
{
Vector4 uv = m_Sprite == null
? Vector4.zero
: DataUtility.GetOuterUV(m_Sprite);
float uvWidth = uv.z - uv.x;
float uvHeight = uv.w - uv.y;
float diameter = m_Radius * 2;
Vector2 uvCenter = new Vector2((uv.x + uv.z) * 0.5f, (uv.y + uv.w) * 0.5f);
Vector3 posCenter = new Vector2((0.5f - rectTransform.pivot.x) * diameter, (0.5f - rectTransform.pivot.y) * diameter);
float uvScaleX = uvWidth / diameter;
float uvScaleY = uvHeight / diameter;
float deltaRad = 2 * Mathf.PI / SideCount;
float curRad = 0;
int vertexCount = SideCount + 1;
int triangleCount = SideCount;
UIVertex vertex = new UIVertex();
vh.AddVert(posCenter, color, uvCenter);
for (int i = 0; i < vertexCount - 1; i++)
{
float r = m_RatioList.Count <= i
? m_Radius
: m_RatioList[i] == 0 ? m_Radius : m_Radius * m_RatioList[i];
Vector3 posOffset = new Vector3(r * Mathf.Cos(curRad), r * Mathf.Sin(curRad));
vertex.position = posCenter + posOffset;
vertex.color = color;
vertex.uv0 = new Vector2(uvCenter.x + posOffset.x * uvScaleX, uvCenter.y + posOffset.y * uvScaleY);
vh.AddVert(vertex);
m_TempVertexList.Add(vertex.position);
curRad += deltaRad;
}
if (m_ShowInner)
{
for (int i = 0; i < triangleCount; i++)
{
vh.AddTriangle(0, i + 1, i + 2 >= vertexCount ? 1 : i + 2);
}
}
}
/// <summary>
/// 生成雷达图描边
/// </summary>
void GenerateOutline(VertexHelper vh)
{
int vertexCount = m_TempVertexList.Count + 1;
int triangleCount = m_TempVertexList.Count * 2;
for (int i = 0; i < m_TempVertexList.Count; i++)
{
Vector2 curPos = m_TempVertexList[i];
Vector2 prePos = i - 1 < 0 ? m_TempVertexList[m_TempVertexList.Count - 1] : m_TempVertexList[i - 1];
Vector2 nextPos = m_TempVertexList[(i + 1) % m_TempVertexList.Count];
Vector2 dir1 = (curPos - prePos).normalized;
Vector2 dir2 = (curPos - nextPos).normalized;
Vector2 normal1 = GetNormal(dir1);
Vector2 normal2 = GetNormal(-dir2);
Vector2 pos1 = prePos + normal1 * m_OutlineData.width;
Vector2 pos2 = nextPos + normal2 * m_OutlineData.width;
Vector2 crossPoint = GetCrossPoint(pos1, dir1, pos2, dir2);
vh.AddVert(curPos, m_OutlineData.color, Vector2.zero);
vh.AddVert(crossPoint, m_OutlineData.color, Vector2.zero);
}
for (int i = vertexCount; i < m_TempVertexList.Count * 3 + 1; i += 2)
{
vh.AddTriangle(i, i + 1, i + 3 >= m_TempVertexList.Count * 3 + 1 ? m_TempVertexList.Count + 2 : i + 3);
vh.AddTriangle(i, i + 2 >= m_TempVertexList.Count * 3 + 1 ? m_TempVertexList.Count + 1 : i + 2, i + 3 >= m_TempVertexList.Count * 3 + 1 ? m_TempVertexList.Count + 2 : i + 3);
}
}
/// <summary>
/// 得到法线
/// </summary>
Vector2 GetNormal(Vector2 dir)
{
return new Vector2(dir.y, -dir.x);
}
//误差范围
const float ERROR_RANGE = 0.001f;
/// <summary>
/// 得到交点
/// </summary>
Vector2 GetCrossPoint(Vector2 pos1, Vector2 dir1, Vector2 pos2, Vector2 dir2)
{
bool parallelToY1 = false;
bool parallelToY2 = false;
float k1;
float k2;
if (Mathf.Abs(dir1.x) <= ERROR_RANGE
|| Mathf.Abs(dir1.y) <= ERROR_RANGE)
{
k1 = 0;
if (Mathf.Abs(dir1.x) <= ERROR_RANGE)
{
parallelToY1 = true;
}
}
else
{
k1 = dir1.y / dir1.x;
}
if (Mathf.Abs(dir2.x) <= ERROR_RANGE
|| Mathf.Abs(dir2.y) <= ERROR_RANGE)
{
k2 = 0;
if (Mathf.Abs(dir2.x) <= ERROR_RANGE)
{
parallelToY2 = true;
}
}
else
{
k2 = dir2.y / dir2.x;
}
float b1 = pos1.y - k1 * pos1.x;
float b2 = pos2.y - k2 * pos2.x;
if (parallelToY1)
{
float x = pos1.x;
float y = k2 * x + b2;
return new Vector2(x, y);
}
else if (parallelToY2)
{
float x = pos2.x;
float y = k1 * x + b1;
return new Vector2(x, y);
}
else
{
float x = (b2 - b1) / (k1 - k2);
float y = k1 * x + b1;
return new Vector2(x, y);
}
}
/// <summary>
/// 计算顶点位置
/// </summary>
void CalcVertexPos()
{
m_VertexList.Clear();
float diameter = m_Radius * 2;
Vector3 posCenter = new Vector2((0.5f - rectTransform.pivot.x) * diameter, (0.5f - rectTransform.pivot.y) * diameter);
float deltaRad = 2 * Mathf.PI / SideCount;
float curRad = 0;
for (int i = 0; i < SideCount; i++)
{
float r = m_RatioList.Count <= i
? m_Radius
: m_RatioList[i] == 0 ? m_Radius : m_Radius * m_RatioList[i];
Vector3 pos = posCenter + new Vector3(r * Mathf.Cos(curRad), r * Mathf.Sin(curRad));
m_VertexList.Add(pos);
curRad += deltaRad;
}
}
}
|