至于怎么挂载脚本怎么能够运行请参考折线图制作(二)的文章。
之前写的折线图一直被锯齿烦恼着,一直无法使用。一开始一直以为要自己写边际模糊算法来模糊编辑的锯齿部分,这可太难了。后来我的美术搭档告诉我可以用渐变图来模糊边界。这里要感谢他的想法帮我把困扰两三年的问题解决了。
如果需要使用图片,我们就不能继承自MaskableGraphic,因为他没有对图片的操作。我们要继承自image,这样有sprite的引用。代码没什么变化,主要是在设置uv时需要注意。将模糊边界的UV贴到矩形的长斜边上。
//折线图
public class LineChart : Image, ICanvasRaycastFilter
{
[SerializeField]//曲线个数
private int lineCount = 1;
[SerializeField]//颜色
private List<Color> lineColors = new List<Color>() { };
[SerializeField]
private int xwidth = 20;//数据间隔
[SerializeField]
private float pointWidth = 4;//线的宽度
[SerializeField]
private float linewidth = 6;//线的宽度
private List<float> pointList = new List<float>();
private Vector3 pos;//数据点的坐标
private RectTransform rectTransform;
private Text numText;
//两个数据之间的间隔
private float xLength;
private float initPosx = 3;
//最多显示的数据数量
private const int RemainCount = 50;
public Vector3 Pos
{
get
{
return pos;
}
}
protected override void Awake()
{
base.Awake();
rectTransform = GetComponent<RectTransform>();
xLength = transform.parent.parent.GetComponent<RectTransform>().sizeDelta.x;
numText = transform.Find("NumText").GetComponent<Text>();
}
private bool IsReachLength = false;
public void AddPoint(float point)
{
pointList.Add(point);
int count = pointList.Count;
if (count > lineCount)//如果只有一条曲线,则至少有两个点才可以开始绘制曲线
{
Vector2 size = rectTransform.sizeDelta;
rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, xwidth * (count)); //当数据量达到我们设定的显示上限 数据个数保持不变 这个时候设置他的大小是不发生变化的
rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, xwidth * (count + 1));//所以我们就先设置小一单位 在设置加一单位 保证大小变化
//此函数改变RectTransform的大小 可以触发OnPopulateMesh调用
SetVerticesDirty();
if (size.x > xLength)//显示区域的大小
{
if (count > RemainCount)//当数据个数大于我们规定的显示个数 就需要移除前面的数据
{
pointList.RemoveAt(0);
Vector3 pos = transform.localPosition;
transform.localPosition = pos + new Vector3(xwidth, 0, 0);//把显示往前移动一个单位 然后做移动动画
}
transform.DOLocalMoveX(transform.localPosition.x - xwidth, 0.3f);
}
}
}
protected override void OnPopulateMesh(VertexHelper vh)
{
int _count = pointList.Count;
//画线
if (_count > lineCount)
{
vh.Clear();
for (int i = 0; i < _count - lineCount; i++)
{
//让曲线宽度在各种斜率下宽度一致
float k = (pointList[i + lineCount] - pointList[i]) / xwidth;
float _y = Mathf.Sqrt(Mathf.Pow(Mathf.Abs(k) + 1, 2)) * linewidth / 2;
_y = Mathf.Abs(_y);
UIVertex[] verts = new UIVertex[4];
verts[0].position = new Vector3(xwidth * (i / lineCount) + initPosx, pointList[i] - _y / 2);
verts[1].position = new Vector3(xwidth * (i / lineCount) + initPosx, _y / 2 + pointList[i]);
verts[2].position = new Vector3(xwidth * ((i + lineCount) / lineCount) + initPosx, pointList[i + lineCount] + _y / 2);
verts[3].position = new Vector3(xwidth * ((i + lineCount) / lineCount) + initPosx, pointList[i + lineCount] - _y / 2);
//注意下面设置的UV是解决抗锯齿的关键
verts[0].uv0 = Vector2.zero;
verts[1].uv0 = new Vector2(0.1f, 1f);
verts[2].uv0 = Vector2.one;
verts[3].uv0 = new Vector2(0.9f, 0f);
for (int j = 0; j < 4; j++)
{
verts[j].color = lineColors[(i % lineCount)];
}
vh.AddUIVertexQuad(verts);
}
}
//draw quad 显示数据大小的方块点
for (int i = 0; i < _count; i++)
{
UIVertex[] quadverts = new UIVertex[4];
quadverts[0].position = new Vector3((i / lineCount) * xwidth - pointWidth + initPosx, pointList[i] - pointWidth);
quadverts[0].color = Color.white;
quadverts[0].uv0 = new Vector2(0.5f,0.5f);
quadverts[1].position = new Vector3((i / lineCount) * xwidth - pointWidth + initPosx, pointList[i] + pointWidth);
quadverts[1].color = Color.white;
quadverts[1].uv0 = new Vector2(0.5f, 0.5f);
quadverts[2].position = new Vector3((i / lineCount) * xwidth + pointWidth + initPosx, pointList[i] + pointWidth);
quadverts[2].color = Color.white;
quadverts[2].uv0 = new Vector2(0.5f, 0.5f);
quadverts[3].position = new Vector3((i / lineCount) * xwidth + pointWidth + initPosx, pointList[i] - pointWidth);
quadverts[3].color = Color.white;
quadverts[3].uv0 = new Vector2(0.5f, 0.5f);
vh.AddUIVertexQuad(quadverts);
}
}
//如果鼠标在数据点上 就会返回true
public bool IsRaycastLocationValid(UnityEngine.Vector2 sp, UnityEngine.Camera eventCamera)
{
Vector2 local;
RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, sp, eventCamera, out local);
Rect rect = GetPixelAdjustedRect();
local.x += rectTransform.pivot.x * rect.width;
local.y += rectTransform.pivot.y * rect.height;
int _count = pointList.Count;
for (int i = 0; i < _count; i++)
{
if (local.x > (i / lineCount) * xwidth - 3f && local.x < ((i / lineCount) * xwidth + 3f) && local.y > (pointList[i] - 3f)
&& local.y < (pointList[i] + 3f))
{
pos = new Vector3((i / lineCount) * xwidth, pointList[i], 0);
return true;
}
}
return false;
}
void Update()
{
//鼠标是否放在白点处
if (IsRaycastLocationValid(Input.mousePosition, null))
{
numText.gameObject.SetActive(true);
numText.text = (pos.y).ToString();
numText.transform.localPosition = pos;
}
else
{
numText.gameObject.SetActive(false);
}
}
}
找美术做一张上下边是渐变的图片放到Image的sprite中。运行效果如下:
我们在面板上发现看不到参数值了,那是因为我们image重新写了他的编辑器设置脚本,而我们想要显示我们的变量就需要重写我们的参数值。参考代码如下:
[CustomEditor(typeof(LineChart)), CanEditMultipleObjects]
public class LineChatEditor : ImageEditor
{
private static SerializedProperty lineCount, lineColors, xwidth, pointWidth, linewidth;
protected override void OnEnable()
{
base.OnEnable();
lineCount = serializedObject.FindProperty("lineCount");
lineColors = serializedObject.FindProperty("lineColors");
xwidth = serializedObject.FindProperty("xwidth");
pointWidth = serializedObject.FindProperty("pointWidth");
linewidth = serializedObject.FindProperty("linewidth");
}
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
serializedObject.Update();
EditorGUILayout.PropertyField(lineCount);
EditorGUILayout.PropertyField(lineColors);
EditorGUILayout.PropertyField(xwidth);
EditorGUILayout.PropertyField(pointWidth);
EditorGUILayout.PropertyField(linewidth);
serializedObject.ApplyModifiedProperties();
}
}
2022.5.11更
还是使用XCharts插件吧,作者干这个非常专业。或许有些小问题,但是大部分需求还是满足的。本文其实还是让我们了解实现的底层原理。XCharts也是用的OnPopulateMesh顶点绘制。性能确实是very good。