一、效果展示
二、实现过程
2.1 准备工作
首先导入一张雷达图的背景图,将其挂载到「Image」上添加到场景中,命名为「RadarBg」。
在「RadarBg」下添加一个空的子物体,命名为「RadarChart」并挂载同名脚本。它用来展示雷达图上层的数据信息。「RadarChart」需要继承「Image」类。
public class RadarChart : Image
{
}
2.2 生成顶点
这里的顶点用来标记雷达图背景的范围。我们需要在编辑器中规定顶点的个数,因此创建一个成员变量_pointCount
。为了能在编辑器模式下保存数据信息,所以给成员变量添加[SerializeField]
特性。然后就是根据顶点个数创建顶点,并将顶点缓存起来
[SerializeField]
private int _pointCount;
[SerializeField]
private List<RectTransform> _points;
/// <summary>
/// 创建顶点
/// </summary>
private void SpawnPoint()
{
for (int i = 0; i < _pointCount; i++)
{
GameObject point = new GameObject("Point" + i);
point.transform.SetParent(transform);
_points.Add(point.AddComponent<RectTransform>());
}
}
生成顶点后,需要设置顶点的默认位置。我们可以以(0,0)点为圆心,让生成的点位于雷达图中心与顶点的连线上
/// <summary>
/// 设置顶点初始位置
/// </summary>
private void SetPointPos()
{
// 顶点间间隔的弧长
float radian = 2 * Mathf.PI / _pointCount;
// 生成点与中心的距离
float radius = 100f;
// 起始点从1/2π开始
float curRadian = Mathf.PI / 2;
for (int i = 0; i < _pointCount; i++)
{
float x = Mathf.Cos(curRadian) * radius;
float y = Mathf.Sin(curRadian) * radius;
curRadian += radian;
_points[i].anchoredPosition = new Vector2(x, y);
}
}
另外,在生成顶点前,需要清除_points
中已存在的顶点。这些方法统一在初始化顶点时执行
/// <summary>
/// 初始化顶点
/// </summary>
public void InitPoint()
{
ClearPoint();
_points = new List<RectTransform>();
SpawnPoint();
SetPointPos();
}
/// <summary>
/// 清除点
/// </summary>
private void ClearPoint()
{
if(_points == null)
return;
foreach (var point in _points)
{
if(point != null)
DestroyImmediate(point);
}
}
2.3 生成操作点
操作点是指雷达图中表示数据部分的图形的顶点。它可以根据不同的比例,在中心与顶点的连线上移动。
操作点的生成与顶点生成的过程差不多。首先我们新建一个操作点类「RadarChartHandler」,在类中定义好必须的API
public class RadarChartHandler : MonoBehaviour
{
private RectTransform _rect;
private RectTransform Rect
{
get
{
if (_rect == null)
_rect = GetComponent<RectTransform>();
return _rect;
}
}
private Image _img;
private Image Img
{
get
{
if (_img == null)
_img = GetComponent<Image>();
return _img;
}
}
/// <summary>
/// 设置父物体
/// </summary>
/// <param name="parentTrans"></param>
public void SetParent(Transform parentTrans)
{
transform.SetParent(parentTrans);
}
/// <summary>
/// 设置大小
/// </summary>
/// <param name="size"></param>
public void SetSize(Vector2 size)
{
Rect.sizeDelta = size;
}
/// <summary>
/// 设置图片
/// </summary>
/// <param name="sprite"></param>
public void SetSprite(Sprite sprite)
{
Img.sprite = sprite;
}
/// <summary>
/// 设置颜色
/// </summary>
/// <param name="color"></param>
public void SetColor(Color color)
{
Img.color = color;
}
/// <summary>
/// 设置位置
/// </summary>
/// <param name="pos"></param>
public void SetPos(Vector2 pos)
{
Rect.anchoredPosition = pos;
}
}
在「RadarChart」类中,同样是「清空->创建->设置位置」的过程,直接上代码
[SerializeField]
private List<RadarChartHandler> _handlers;
[SerializeField]
private Sprite _pointSprite;
[SerializeField]
private Color _pointColor = Color.white;
[SerializeField]
private Vector2 _pointSize = new Vector2(10,10);
[SerializeField]
private float[] _handlerRadio;
/// <summary>
/// 初始化操作点
/// </summary>
public void InitHandler()
{
ClearHandler();
_handlers = new List<RadarChartHandler>();
SpawnHandler();
SetHandlerPos();
}
/// <summary>
/// 清除操作点
/// </summary>
private void ClearHandler()
{
if(_handlers == null)
return;
foreach (var point in _handlers)
{
if(point != null)
DestroyImmediate(point);
}
}
/// <summary>
/// 创建操作点
/// </summary>
private void SpawnHandler()
{
for (int i = 0; i < _pointCount; i++)
{
GameObject point = new GameObject("Handler" + i);
point.AddComponent<RectTransform>();
point.AddComponent<Image>();
RadarChartHandler handler = point.AddComponent<RadarChartHandler>();
handler.SetParent(transform);
handler.SetColor(_pointColor);
handler.SetSprite(_pointSprite);
handler.SetSize(_pointSize);
_handlers.Add(handler);
}
}
/// <summary>
/// 设置操作点位置
/// </summary>
private void SetHandlerPos()
{
if (_handlerRadio == null || _handlerRadio.Length == 0)
{
for (int i = 0; i < _pointCount; i++)
{
_handlers[i].SetPos(_points[i].anchoredPosition);
}
}
else
{
for (int i = 0; i < _pointCount; i++)
{
_handlers[i].SetPos(_points[i].anchoredPosition * _handlerRadio[i]);
}
}
}
2.4 将成员变量添加到编辑器
我们只是给成员变量添加了[SerializeField]
特性,要想在编辑器上显示出来还需要进行编辑器扩展。代码都是固定的,这里不再详述
[CustomEditor(typeof(RadarChart), true)]
[CanEditMultipleObjects]
public class RadarChartEditor : UnityEditor.UI.ImageEditor
{
SerializedProperty _pointCount;
SerializedProperty _pointSprite;
SerializedProperty _pointColor;
SerializedProperty _pointSize;
SerializedProperty _handlerRadio;
protected override void OnEnable()
{
base.OnEnable();
_pointCount = serializedObject.FindProperty("_pointCount");
_pointSprite = serializedObject.FindProperty("_pointSprite");
_pointColor = serializedObject.FindProperty("_pointColor");
_pointSize = serializedObject.FindProperty("_pointSize");
_handlerRadio = serializedObject.FindProperty("_handlerRadio");
}
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
serializedObject.Update();
EditorGUILayout.PropertyField(_pointCount);
EditorGUILayout.PropertyField(_pointSprite);
EditorGUILayout.PropertyField(_pointColor);
EditorGUILayout.PropertyField(_pointSize);
EditorGUILayout.PropertyField(_handlerRadio,true);
RadarChart radar = target as RadarChart;
if (radar != null)
{
if (GUILayout.Button("生成雷达图顶点"))
{
radar.InitPoint();
}
if (GUILayout.Button("生成内部可操作顶点"))
{
radar.InitHandler();
}
}
serializedObject.ApplyModifiedProperties();
if (GUI.changed)
{
EditorUtility.SetDirty(target);
}
}
}
接下来设定好操作点的sprite、大小、颜色等属性,就可以进行生成操作了。生成后的效果如下
2.5 填充雷达图
观察上面的截图可以发现,雷达图中黄色的填充部分还没有处理。那么如何让它填充成五边形呢?这就又要用到父类中的OnPopulateMesh()
方法了。我们在这个方法中重新添加顶点,就可以让它渲染成多边形
protected override void OnPopulateMesh(VertexHelper toFill)
{
toFill.Clear();
AddVert(toFill);
AddTriangle(toFill);
}
/// <summary>
/// 添加顶点
/// </summary>
/// <param name="toFill"></param>
private void AddVert(VertexHelper toFill)
{
// 添加中心点
toFill.AddVert(Vector3.zero, color,Vector4.zero);
foreach (var handler in _handlers)
{
toFill.AddVert(handler.transform.localPosition,color,Vector4.zero);
}
}
/// <summary>
/// 添加三角形
/// </summary>
/// <param name="toFill"></param>
private void AddTriangle(VertexHelper toFill)
{
for (int i = 1; i < _pointCount; i++)
{
// 始终以中心点为起点
toFill.AddTriangle(0,i+1,i);
}
toFill.AddTriangle(0,_pointCount,1);
}
返回Unity,可以看到效果如下
但目前拖动操作点,填充图并不会发生变化,需要在Update()
方法中实时刷新。Graphic
类中自带的SetVerticesDirty()
方法可以将顶点标记为脏数据,并重建。
private void Update()
{
SetVerticesDirty();
}
效果如下
为了能在运行时也可以进行拖动操作,我们给「RadarChartHandler」类加上OnDrag()
方法。在拖动时,给操作点的位置加上鼠标的位移即可实现。但是这个位移值会受到父物体缩放的影响,可以通过Rect.lossyScale
获取到总的缩放系数,然后用位移除以缩放系数,即可抵消影响。
public void OnDrag(PointerEventData eventData)
{
Rect.anchoredPosition += new Vector2(GetScaleX(eventData),GetScaleY(eventData));
}
private float GetScaleX(PointerEventData eventData)
{
return eventData.delta.x/Rect.lossyScale.x;
}
private float GetScaleY(PointerEventData eventData)
{
return eventData.delta.y/Rect.lossyScale.y;
}
效果如下