BaseImage.cs
using System;
using UnityEngine;
using UnityEngine.UI;
namespace MYTOOL.UI
{
public class BaseImage : MaskableGraphic, ISerializationCallbackReceiver, ILayoutElement, ICanvasRaycastFilter
{
[SerializeField] private Sprite m_Sprite;
public Sprite sprite { get { return m_Sprite; } set { if (Tools.UniUtils.SetClass(ref m_Sprite, value)) SetAllDirty(); } }
[NonSerialized] private Sprite m_OverrideSprite;
public Sprite overrideSprite { get { return m_OverrideSprite == null ? sprite : m_OverrideSprite; } set { if (Tools.UniUtils.SetClass(ref m_OverrideSprite, value)) SetAllDirty(); } }
///
/// Image's texture comes from the UnityEngine.Image.
///
public override Texture mainTexture
{
get
{
return overrideSprite == null ? s_WhiteTexture : overrideSprite.texture;
}
}
public float pixelsPerUnit
{
get
{
float spritePixelsPerUnit = 100;
if (sprite)
spritePixelsPerUnit = sprite.pixelsPerUnit;
float referencePixelsPerUnit = 100;
if (canvas)
referencePixelsPerUnit = canvas.referencePixelsPerUnit;
return spritePixelsPerUnit / referencePixelsPerUnit;
}
}
public override void SetNativeSize()
{
if (overrideSprite != null)
{
float w = overrideSprite.rect.width / pixelsPerUnit;
float h = overrideSprite.rect.height / pixelsPerUnit;
rectTransform.anchorMax = rectTransform.anchorMin;
rectTransform.sizeDelta = new Vector2(w, h);
SetAllDirty();
}
}
///
/// 子类需要重写该方法来自定义Image形状
///
///
protected override void OnPopulateMesh(VertexHelper vh)
{
base.OnPopulateMesh(vh);
}
#region >> ISerializationCallbackReceiver
public void OnAfterDeserialize() { }
public void OnBeforeSerialize() { }
#endregion
#region >> ILayoutElement
public virtual void CalculateLayoutInputHorizontal() { }
public virtual void CalculateLayoutInputVertical() { }
public virtual float minWidth { get { return 0; } }
public virtual float preferredWidth
{
get
{
if (overrideSprite == null)
return 0;
return overrideSprite.rect.size.x / pixelsPerUnit;
}
}
public virtual float flexibleWidth { get { return -1; } }
public virtual float minHeight { get { return 0; } }
public virtual float preferredHeight
{
get
{
if (overrideSprite == null)
return 0;
return overrideSprite.rect.size.y / pixelsPerUnit;
}
}
public virtual float flexibleHeight { get { return -1; } }
public virtual int layoutPriority { get { return 0; } }
#endregion
#region >> ICanvasRaycastFilter
public virtual bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)
{
return true;
}
#endregion
}
}
CircleImage.cs
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Sprites;
using System.Collections.Generic;
namespace MYTOOL.UI
{
[RequireComponent(typeof(CanvasRenderer))]
public class CircleImage : BaseImage
{
protected override void Awake()
{
dirty = false;
innerVertices = new List<Vector3>();
outterVertices = new List<Vector3>();
}
void Update()
{
if (!fill && dirty)
{
dirty = false;
thickness = Mathf.Clamp(thickness, 0, rectTransform.rect.width / 2);
}
}
[Tooltip("是否填充圆形")]
[SerializeField] private bool fill = true;
[Tooltip("圆形或扇形填充比例")]
[SerializeField, Range(0, 1)] private float fillPercent = 1f;
[Tooltip("圆形")]
[SerializeField, Range(3, 100)] private int segements = 30;
[Tooltip("圆环宽度")]
[SerializeField] private float thickness = 10;
private bool dirty;
private List<Vector3> innerVertices;
private List<Vector3> outterVertices;
protected override void OnPopulateMesh(VertexHelper vh)
{
vh.Clear();
innerVertices.Clear();
outterVertices.Clear();
float degreeDelta = (float)(2 * Mathf.PI / segements);
int curSegements = (int)(segements * fillPercent);
float tw = rectTransform.rect.width;
float th = rectTransform.rect.height;
float outerRadius = rectTransform.pivot.x * tw;
float innerRadius = rectTransform.pivot.x * tw - thickness;
Vector4 uv = overrideSprite != null ? DataUtility.GetOuterUV(overrideSprite) : Vector4.zero;
float uvCenterX = (uv.x + uv.z) * 0.5f;
float uvCenterY = (uv.y + uv.w) * 0.5f;
float uvScaleX = (uv.z - uv.x) / tw;
float uvScaleY = (uv.w - uv.y) / th;
float curDegree = 0;
UIVertex uiVertex;
int verticeCount;
int triangleCount;
Vector2 curVertice;
if (fill) //圆形
{
curVertice = Vector2.zero;
verticeCount = curSegements + 1;
uiVertex = new UIVertex();
uiVertex.color = color;
uiVertex.position = curVertice;
uiVertex.uv0 = new Vector2(curVertice.x * uvScaleX + uvCenterX, curVertice.y * uvScaleY + uvCenterY);
vh.AddVert(uiVertex);
for (int i = 1; i < verticeCount; i++)
{
float cosA = Mathf.Cos(curDegree);
float sinA = Mathf.Sin(curDegree);
curVertice = new Vector2(cosA * outerRadius, sinA * outerRadius);
curDegree += degreeDelta;
uiVertex = new UIVertex();
uiVertex.color = color;
uiVertex.position = curVertice;
uiVertex.uv0 = new Vector2(curVertice.x * uvScaleX + uvCenterX, curVertice.y * uvScaleY + uvCenterY);
vh.AddVert(uiVertex);
outterVertices.Add(curVertice);
}
triangleCount = curSegements * 3;
for (int i = 0, vIdx = 1; i < triangleCount - 3; i += 3, vIdx++)
{
vh.AddTriangle(vIdx, 0, vIdx + 1);
}
if (fillPercent == 1)
{
//首尾顶点相连
vh.AddTriangle(verticeCount - 1, 0, 1);
}
}
else //圆环
{
verticeCount = curSegements * 2;
for (int i = 0; i < verticeCount; i += 2)
{
float cosA = Mathf.Cos(curDegree);
float sinA = Mathf.Sin(curDegree);
curDegree += degreeDelta;
curVertice = new Vector3(cosA * innerRadius, sinA * innerRadius);
uiVertex = new UIVertex();
uiVertex.color = color;
uiVertex.position = curVertice;
uiVertex.uv0 = new Vector2(curVertice.x * uvScaleX + uvCenterX, curVertice.y * uvScaleY + uvCenterY);
vh.AddVert(uiVertex);
innerVertices.Add(curVertice);
curVertice = new Vector3(cosA * outerRadius, sinA * outerRadius);
uiVertex = new UIVertex();
uiVertex.color = color;
uiVertex.position = curVertice;
uiVertex.uv0 = new Vector2(curVertice.x * uvScaleX + uvCenterX, curVertice.y * uvScaleY + uvCenterY);
vh.AddVert(uiVertex);
outterVertices.Add(curVertice);
}
triangleCount = curSegements * 3 * 2;
for (int i = 0, vIdx = 0; i < triangleCount - 6; i += 6, vIdx += 2)
{
vh.AddTriangle(vIdx + 1, vIdx, vIdx + 3);
vh.AddTriangle(vIdx, vIdx + 2, vIdx + 3);
}
if (fillPercent == 1)
{
//首尾顶点相连
vh.AddTriangle(verticeCount - 1, verticeCount - 2, 1);
vh.AddTriangle(verticeCount - 2, 0, 1);
}
dirty = true;
}
}
public override bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)
{
Sprite sprite = overrideSprite;
if (sprite == null)
return true;
Vector2 local;
RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, screenPoint, eventCamera, out local);
return Contains(local, outterVertices, innerVertices);
}
private bool Contains(Vector2 p, List<Vector3> outterVertices, List<Vector3> innerVertices)
{
var crossNumber = 0;
RayCrossing(p, innerVertices, ref crossNumber);//检测内环
RayCrossing(p, outterVertices, ref crossNumber);//检测外环
return (crossNumber & 1) == 1;
}
///
/// 使用RayCrossing算法判断点击点是否在封闭多边形里
///
///
///
///
private void RayCrossing(Vector2 p, List<Vector3> vertices, ref int crossNumber)
{
for (int i = 0, count = vertices.Count; i < count; i++)
{
var v1 = vertices[i];
var v2 = vertices[(i + 1) % count];
//点击点水平线必须与两顶点线段相交
if (((v1.y <= p.y) && (v2.y > p.y))
|| ((v1.y > p.y) && (v2.y <= p.y)))
{
//只考虑点击点右侧方向,点击点水平线与线段相交,且交点x > 点击点x,则crossNumber+1
if (p.x < v1.x + (p.y - v1.y) / (v2.y - v1.y) * (v2.x - v1.x))
{
crossNumber += 1;
}
}
}
}
}
}
CircleImageEditor.cs
using System;
using System.Reflection;
using UnityEditor;
using UnityEngine;
namespace MYTOOL.UI
{
[CustomEditor(typeof(CircleImage))]
public class CircleImageEditor : Editor
{
private SerializedProperty m_Script;
private SerializedProperty m_Sprite;
private SerializedProperty m_Color;
private SerializedProperty m_RaycastTarget;
private SerializedProperty fill;
private SerializedProperty fillPercent;
private SerializedProperty segements;
private SerializedProperty thickness;
public void OnEnable()
{
m_Script = serializedObject.FindProperty("m_Script");
m_Sprite = serializedObject.FindProperty("m_Sprite");
m_Color = serializedObject.FindProperty("m_Color");
m_RaycastTarget = serializedObject.FindProperty("m_RaycastTarget");
fill = serializedObject.FindProperty("fill");
fillPercent = serializedObject.FindProperty("fillPercent");
segements = serializedObject.FindProperty("segements");
thickness = serializedObject.FindProperty("thickness");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUI.BeginDisabledGroup(true);
EditorGUILayout.PropertyField(m_Script);
EditorGUI.EndDisabledGroup();
EditorGUILayout.Space();
EditorGUILayout.PropertyField(m_Sprite);
EditorGUILayout.PropertyField(m_Color);
EditorGUILayout.PropertyField(m_RaycastTarget);
EditorGUILayout.Space();
EditorGUILayout.PropertyField(fill);
EditorGUILayout.PropertyField(fillPercent);
EditorGUILayout.PropertyField(segements);
if (!fill.boolValue)
{
EditorGUILayout.PropertyField(thickness);
}
serializedObject.ApplyModifiedProperties(); // 应用修改
}
[MenuItem("GameObject/UI/CircleImage", false, priority = 90)]
static void CreateCircleImage(MenuCommand menuCommand)
{
GameObject go = new GameObject("CircleImage", typeof(RectTransform), typeof(CircleImage));
Undo.RegisterCreatedObjectUndo(go, $"CreatCircleImage{Time.frameCount}");
// 以下代码通过反射获取 UGUI 中新增 UI 组件的体验:会自动构建 UI 运行环境
try
{
Type type = Type.GetType("UnityEditor.UI.MenuOptions,UnityEditor.UI.dll", true);
var method = type.GetMethod("PlaceUIElementRoot", BindingFlags.Static | BindingFlags.NonPublic);
method.Invoke(null, new object[] { go, menuCommand });
}
catch (Exception e)
{
Debug.LogWarning($"{nameof(CircleImageEditor)}: 挂载组件失败,可能是 API 变更!");
throw e;
}
Selection.activeGameObject = go;
}
}
}