目录
一、前言:
在上一篇我写过创建一个新的扩展Button:Unity自定义按钮-CSDN博客,此文是接在之后的,是创建一个TextImage,它的作用是当鼠标悬浮在Image上一段时间,显示Text文本信息。创建的逻辑与编辑器扩展其实看看源码并修改就可以写出来了,这里作为参考,我会把具体的修改单独拿出来。
二、代码
核心代码:
namespace UnityEngine.UI
{
internal static class SetPropertyUtility
{
public static bool SetColor(ref Color currentValue, Color newValue)
{
if (currentValue.r == newValue.r && currentValue.g == newValue.g && currentValue.b == newValue.b && currentValue.a == newValue.a)
{
return false;
}
currentValue = newValue;
return true;
}
public static bool SetStruct<T>(ref T currentValue, T newValue) where T : struct
{
if (EqualityComparer<T>.Default.Equals(currentValue, newValue))
{
return false;
}
currentValue = newValue;
return true;
}
public static bool SetClass<T>(ref T currentValue, T newValue) where T : class
{
if ((currentValue == null && newValue == null) || (currentValue != null && currentValue.Equals(newValue)))
{
return false;
}
currentValue = newValue;
return true;
}
}
/// <summary>
/// Image is a textured element in the UI hierarchy.
/// </summary>
[RequireComponent(typeof(CanvasRenderer))]
[AddComponentMenu("UI/TextImage", 11)]
/// <summary>
/// Displays a Sprite inside the UI System.
/// </summary>
public class TextImage : MaskableGraphic, ISerializationCallbackReceiver, ILayoutElement, ICanvasRaycastFilter,IPointerEnterHandler,IPointerExitHandler
{
[SerializeField]private Text m_text;
public Text text
{
get{return m_text;}
set{m_text=value;}
}
[SerializeField]private float m_delayShowTime=0.5f;
public float delayShowTime
{
get{return delayShowTime;}
set{m_delayShowTime=value;}
}
private Coroutine coroutine;
private IEnumerator ShowTooltipDelayed()
{
yield return new WaitForSeconds(m_delayShowTime);
if(m_text!=null&&m_text.gameObject!=null)
m_text.gameObject.SetActive(true);
}
public void OnPointerEnter(PointerEventData eventData)
{
if(coroutine!=null)
StopCoroutine(coroutine);
coroutine=StartCoroutine(ShowTooltipDelayed());
}
public void OnPointerExit(PointerEventData eventData)
{
if(coroutine!=null)
StopCoroutine(coroutine);
if(m_text!=null&&m_text.gameObject!=null)
m_text.gameObject.SetActive(false);
}
/// <summary>
/// Image fill type controls how to display the image.
/// </summary>
public enum Type
{
Simple,
Sliced,
Tiled,
Filled
}
/// <summary>
/// The possible fill method types for a Filled Image.
/// </summary>
public enum FillMethod
{
Horizontal,
Vertical,
Radial90,
Radial180,
Radial360,
}
/// <summary>
/// Origin for the Image.FillMethod.Horizontal.
/// </summary>
public enum OriginHorizontal
{
/// <summary>
/// >Origin at the Left side.
/// </summary>
Left,
/// <summary>
/// >Origin at the Right side.
/// </summary>
Right,
}
/// <summary>
/// Origin for the Image.FillMethod.Vertical.
/// </summary>
public enum OriginVertical
{
/// <summary>
/// >Origin at the Bottom Edge.
/// </summary>
Bottom,
/// <summary>
/// >Origin at the Top Edge.
/// </summary>
Top,
}
/// <summary>
/// Origin for the Image.FillMethod.Radial90.
/// </summary>
public enum Origin90
{
/// <summary>
/// Radial starting at the Bottom Left corner.
/// </summary>
BottomLeft,
/// <summary>
/// Radial starting at the Top Left corner.
/// </summary>
TopLeft,
/// <summary>
/// Radial starting at the Top Right corner.
/// </summary>
TopRight,
/// <summary>
/// Radial starting at the Bottom Right corner.
/// </summary>
BottomRight,
}
/// <summary>
/// Origin for the Image.FillMethod.Radial180.
/// </summary>
public enum Origin180
{
/// <summary>
/// Center of the radial at the center of the Bottom edge.
/// </summary>
Bottom,
/// <summary>
/// Center of the radial at the center of the Left edge.
/// </summary>
Left,
/// <summary>
/// Center of the radial at the center of the Top edge.
/// </summary>
Top,
/// <summary>
/// Center of the radial at the center of the Right edge.
/// </summary>
Right,
}
/// <summary>
/// One of the points of the Arc for the Image.FillMethod.Radial360.
/// </summary>
public enum Origin360
{
/// <summary>
/// Arc starting at the center of the Bottom edge.
/// </summary>
Bottom,
/// <summary>
/// Arc starting at the center of the Right edge.
/// </summary>
Right,
/// <summary>
/// Arc starting at the center of the Top edge.
/// </summary>
Top,
/// <summary>
/// Arc starting at the center of the Left edge.
/// </summary>
Left,
}
static protected Material s_ETC1DefaultUI = null;
[FormerlySerializedAs("m_Frame")]
[SerializeField]
private Sprite m_Sprite;
public Sprite sprite
{
get { return m_Sprite; }
set
{
if (m_Sprite != null)
{
if (m_Sprite != value)
{
m_SkipLayoutUpdate = m_Sprite.rect.size.Equals(value ? value.rect.size : Vector2.zero);
m_SkipMaterialUpdate = m_Sprite.texture == (value ? value.texture : null);
m_Sprite = value;
ResetAlphaHitThresholdIfNeeded();
SetAllDirty();
TrackSprite();
}
}
else if (value != null)
{
m_SkipLayoutUpdate = value.rect.size == Vector2.zero;
m_SkipMaterialUpdate = value.texture == null;
m_Sprite = value;
ResetAlphaHitThresholdIfNeeded();
SetAllDirty();
TrackSprite();
}
void ResetAlphaHitThresholdIfNeeded()
{
if (!SpriteSupportsAlphaHitTest() && m_AlphaHitTestMinimumThreshold > 0)
{
Debug.LogWarning("Sprite was changed for one not readable or with Crunch Compression. Resetting the AlphaHitThreshold to 0.", this);
m_AlphaHitTestMinimumThreshold = 0;
}
}
bool SpriteSupportsAlphaHitTest()
{
return m_Sprite != null && m_Sprite.texture != null && !GraphicsFormatUtility.IsCrunchFormat(m_Sprite.texture.format) && m_Sprite.texture.isReadable;
}
}
}
/// <summary>
/// Disable all automatic sprite optimizations.
/// </summary>
/// <remarks>
/// When a new Sprite is assigned update optimizations are automatically applied.
/// </remarks>
public void DisableSpriteOptimizations()
{
m_SkipLayoutUpdate = false;
m_SkipMaterialUpdate = false;
}
[NonSerialized]
private Sprite m_OverrideSprite;
public Sprite overrideSprite
{
get { return activeSprite; }
set
{
if (SetPropertyUtility.SetClass(ref m_OverrideSprite, value))
{
SetAllDirty();
TrackSprite();
}
}
}
private Sprite activeSprite { get { return m_OverrideSprite != null ? m_OverrideSprite : sprite; } }
/// How the Image is drawn.
[SerializeField] private Type m_Type = Type.Simple;
public Type type { get { return m_Type; } set { if (SetPropertyUtility.SetStruct(ref m_Type, value)) SetVerticesDirty(); } }
[SerializeField] private bool m_PreserveAspect = false;
/// <summary>
/// Whether this image should preserve its Sprite aspect ratio.
/// </summary>
public bool preserveAspect { get { return m_PreserveAspect; } set { if (SetPropertyUtility.SetStruct(ref m_PreserveAspect, value)) SetVerticesDirty(); } }
[SerializeField] private bool m_FillCenter = true;
public bool fillCenter { get { return m_FillCenter; } set { if (SetPropertyUtility.SetStruct(ref m_FillCenter, value)) SetVerticesDirty(); } }
/// Filling method for filled sprites.
[SerializeField] private FillMethod m_FillMethod = FillMethod.Radial360;
public FillMethod fillMethod { get { return m_FillMethod; } set { if (SetPropertyUtility.SetStruct(ref m_FillMethod, value)) { SetVerticesDirty(); m_FillOrigin = 0; } } }
/// Amount of the Image shown. 0-1 range with 0 being nothing shown, and 1 being the full Image.
[Range(0, 1)]
[SerializeField]
private float m_FillAmount = 1.0f;
public float fillAmount { get { return m_FillAmount; } set { if (SetPropertyUtility.SetStruct(ref m_FillAmount, Mathf.Clamp01(value))) SetVerticesDirty(); } }
/// Whether the Image should be filled clockwise (true) or counter-clockwise (false).
[SerializeField] private bool m_FillClockwise = true;
public bool fillClockwise { get { return m_FillClockwise; } set { if (SetPropertyUtility.SetStruct(ref m_FillClockwise, value)) SetVerticesDirty(); } }
/// Controls the origin point of the Fill process. Value means different things with each fill method.
[SerializeField] private int m_FillOrigin;
public int fillOrigin { get { return m_FillOrigin; } set { if (SetPropertyUtility.SetStruct(ref m_FillOrigin, value)) SetVerticesDirty(); } }
// Not serialized until we support read-enabled sprites better.
private float m_AlphaHitTestMinimumThreshold = 0;
// Whether this is being tracked for Atlas Binding.
private bool m_Tracked = false;
[Obsolete("eventAlphaThreshold has been deprecated. Use eventMinimumAlphaThreshold instead (UnityUpgradable) -> alphaHitTestMinimumThreshold")]
/// <summary>
/// Obsolete. You should use UI.Image.alphaHitTestMinimumThreshold instead.
/// The alpha threshold specifies the minimum alpha a pixel must have for the event to considered a "hit" on the Image.
/// </summary>
public float eventAlphaThreshold { get { return 1 - alphaHitTestMinimumThreshold; } set { alphaHitTestMinimumThreshold = 1 - value; } }
public float alphaHitTestMinimumThreshold { get { return m_AlphaHitTestMinimumThreshold; }
set
{
if (sprite != null && (GraphicsFormatUtility.IsCrunchFormat(sprite.texture.format) || !sprite.texture.isReadable))
throw new InvalidOperationException("alphaHitTestMinimumThreshold should not be modified on a texture not readeable or not using Crunch Compression.");
m_AlphaHitTestMinimumThreshold = value;
}
}
/// Controls whether or not to use the generated mesh from the sprite importer.
[SerializeField] private bool m_UseSpriteMesh;
/// <summary>
/// Allows you to specify whether the UI Image should be displayed using the mesh generated by the TextureImporter, or by a simple quad mesh.
/// </summary>
/// <remarks>
/// When this property is set to false, the UI Image uses a simple quad. When set to true, the UI Image uses the sprite mesh generated by the [[TextureImporter]]. You should set this to true if you want to use a tightly fitted sprite mesh based on the alpha values in your image.
/// Note: If the texture importer's SpriteMeshType property is set to SpriteMeshType.FullRect, it will only generate a quad, and not a tightly fitted sprite mesh, which means this UI image will be drawn using a quad regardless of the value of this property. Therefore, when enabling this property to use a tightly fitted sprite mesh, you must also ensure the texture importer's SpriteMeshType property is set to Tight.
/// </remarks>
public bool useSpriteMesh { get { return m_UseSpriteMesh; } set { if (SetPropertyUtility.SetStruct(ref m_UseSpriteMesh, value)) SetVerticesDirty(); } }
protected TextImage()
{
useLegacyMeshGeneration = false;
}
/// <summary>
/// Cache of the default Canvas Ericsson Texture Compression 1 (ETC1) and alpha Material.
/// </summary>
/// <remarks>
/// Stores the ETC1 supported Canvas Material that is returned from GetETC1SupportedCanvasMaterial().
/// Note: Always specify the UI/DefaultETC1 Shader in the Always Included Shader list, to use the ETC1 and alpha Material.
/// </remarks>
static public Material defaultETC1GraphicMaterial
{
get
{
if (s_ETC1DefaultUI == null)
s_ETC1DefaultUI = Canvas.GetETC1SupportedCanvasMaterial();
return s_ETC1DefaultUI;
}
}
/// <summary>
/// Image's texture comes from the UnityEngine.Image.
/// </summary>
public override Texture mainTexture
{
get
{
if (activeSprite == null)
{
if (material != null && material.mainTexture != null)
{
return material.mainTexture;
}
return s_WhiteTexture;
}
return activeSprite.texture;
}
}
/// <summary>
/// Whether the Sprite of the image has a border to work with.
/// </summary>
public bool hasBorder
{
get
{
if (activeSprite != null)
{
Vector4 v = activeSprite.border;
return v.sqrMagnitude > 0f;
}
return false;
}
}
[SerializeField]
private float m_PixelsPerUnitMultiplier = 1.0f;
/// <summary>
/// Pixel per unit modifier to change how sliced sprites are generated.
/// </summary>
public float pixelsPerUnitMultiplier
{
get { return m_PixelsPerUnitMultiplier; }
set
{
m_PixelsPerUnitMultiplier = Mathf.Max(0.01f, value);
SetVerticesDirty();
}
}
// case 1066689 cache referencePixelsPerUnit when canvas parent is disabled;
private float m_CachedReferencePixelsPerUnit = 100;
public float pixelsPerUnit
{
get
{
float spritePixelsPerUnit = 100;
if (activeSprite)
spritePixelsPerUnit = activeSprite.pixelsPerUnit;
if (canvas)
m_CachedReferencePixelsPerUnit = canvas.referencePixelsPerUnit;
return spritePixelsPerUnit / m_CachedReferencePixelsPerUnit;
}
}
protected float multipliedPixelsPerUnit
{
get { return pixelsPerUnit * m_PixelsPerUnitMultiplier; }
}
/// <summary>
/// The specified Material used by this Image. The default Material is used instead if one wasn't specified.
/// </summary>
public override Material material
{
get
{
if (m_Material != null)
return m_Material;
//Edit and Runtime should use Split Alpha Shader if EditorSettings.spritePackerMode = Sprite Atlas V2
#if UNITY_EDITOR
if ((Application.isPlaying || EditorSettings.spritePackerMode == SpritePackerMode.SpriteAtlasV2) &&
activeSprite && activeSprite.associatedAlphaSplitTexture != null)
{
return defaultETC1GraphicMaterial;
}
#else
if (activeSprite && activeSprite.associatedAlphaSplitTexture != null)
return defaultETC1GraphicMaterial;
#endif
return defaultMaterial;
}
set
{
base.material = value;
}
}
/// <summary>
/// See ISerializationCallbackReceiver.
/// </summary>
public virtual void OnBeforeSerialize() {}
/// <summary>
/// See ISerializationCallbackReceiver.
/// </summary>
public virtual void OnAfterDeserialize()
{
if (m_FillOrigin < 0)
m_FillOrigin = 0;
else if (m_FillMethod == FillMethod.Horizontal && m_FillOrigin > 1)
m_FillOrigin = 0;
else if (m_FillMethod == FillMethod.Vertical && m_FillOrigin > 1)
m_FillOrigin = 0;
else if (m_FillOrigin > 3)
m_FillOrigin = 0;
m_FillAmount = Mathf.Clamp(m_FillAmount, 0f, 1f);
}
private void PreserveSpriteAspectRatio(ref Rect rect, Vector2 spriteSize)
{
var spriteRatio = spriteSize.x / spriteSize.y;
var rectRatio = rect.width / rect.height;
if (spriteRatio > rectRatio)
{
var oldHeight = rect.height;
rect.height = rect.width * (1.0f / spriteRatio);
rect.y += (oldHeight - rect.height) * rectTransform.pivot.y;
}
else
{
var oldWidth = rect.width;
rect.width = rect.height * spriteRatio;
rect.x += (oldWidth - rect.width) * rectTransform.pivot.x;
}
}
/// Image's dimensions used for drawing. X = left, Y = bottom, Z = right, W = top.
private Vector4 GetDrawingDimensions(bool shouldPreserveAspect)
{
var padding = activeSprite == null ? Vector4.zero : Sprites.DataUtility.GetPadding(activeSprite);
var size = activeSprite == null ? Vector2.zero : new Vector2(activeSprite.rect.width, activeSprite.rect.height);
Rect r = GetPixelAdjustedRect();
// Debug.Log(string.Format("r:{2}, size:{0}, padding:{1}", size, padding, r));
int spriteW = Mathf.RoundToInt(size.x);
int spriteH = Mathf.RoundToInt(size.y);
var v = new Vector4(
padding.x / spriteW,
padding.y / spriteH,
(spriteW - padding.z) / spriteW,
(spriteH - padding.w) / spriteH);
if (shouldPreserveAspect && size.sqrMagnitude > 0.0f)
{
PreserveSpriteAspectRatio(ref r, size);
}
v = new Vector4(
r.x + r.width * v.x,
r.y + r.height * v.y,
r.x + r.width * v.z,
r.y + r.height * v.w
);
return v;
}
/// <summary>
/// Adjusts the image size to make it pixel-perfect.
/// </summary>
/// <remarks>
/// This means setting the Images RectTransform.sizeDelta to be equal to the Sprite dimensions.
/// </remarks>
public override void SetNativeSize()
{
if (activeSprite != null)
{
float w = activeSprite.rect.width / pixelsPerUnit;
float h = activeSprite.rect.height / pixelsPerUnit;
rectTransform.anchorMax = rectTransform.anchorMin;
rectTransform.sizeDelta = new Vector2(w, h);
SetAllDirty();
}
}
/// <summary>
/// Update the UI renderer mesh.
/// </summary>
protected override void OnPopulateMesh(VertexHelper toFill)
{
if (activeSprite == null)
{
base.OnPopulateMesh(toFill);
return;
}
switch (type)
{
case Type.Simple:
if (!useSpriteMesh)
GenerateSimpleSprite(toFill, m_PreserveAspect);
else
GenerateSprite(toFill, m_PreserveAspect);
break;
case Type.Sliced:
GenerateSlicedSprite(toFill);
break;
case Type.Tiled:
GenerateTiledSprite(toFill);
break;
case Type.Filled:
GenerateFilledSprite(toFill, m_PreserveAspect);
break;
}
}
private void TrackSprite()
{
if (activeSprite != null && activeSprite.texture == null)
{
TrackImage(this);
m_Tracked = true;
}
}
protected override void OnEnable()
{
base.OnEnable();
TrackSprite();
if(m_text==null)
if(GetComponentInChildren<Text>()!=null)
m_text=GetComponentInChildren<Text>();
coroutine=null;
if(m_text!=null)
{
m_text.gameObject.SetActive(false);
m_text.raycastTarget=false;
}
}
protected override void OnDisable()
{
base.OnDisable();
if (m_Tracked)
UnTrackImage(this);
if(m_text!=null)
{
m_text.gameObject.SetActive(false);
m_text.raycastTarget=false;
}
}
/// <summary>
/// Update the renderer's material.
/// </summary>
protected override void UpdateMaterial()
{
base.UpdateMaterial();
// check if this sprite has an associated alpha texture (generated when splitting RGBA = RGB + A as two textures without alpha)
if (activeSprite == null)
{
canvasRenderer.SetAlphaTexture(null);
return;
}
Texture2D alphaTex = activeSprite.associatedAlphaSplitTexture;
if (alphaTex != null)
{
canvasRenderer.SetAlphaTexture(alphaTex);
}
}
protected override void OnCanvasHierarchyChanged()
{
base.OnCanvasHierarchyChanged();
if (canvas == null)
{
m_CachedReferencePixelsPerUnit = 100;
}
else if (canvas.referencePixelsPerUnit != m_CachedReferencePixelsPerUnit)
{
m_CachedReferencePixelsPerUnit = canvas.referencePixelsPerUnit;
if (type == Type.Sliced || type == Type.Tiled)
{
SetVerticesDirty();
SetLayoutDirty();
}
}
}
/// <summary>
/// Generate vertices for a simple Image.
/// </summary>
void GenerateSimpleSprite(VertexHelper vh, bool lPreserveAspect)
{
Vector4 v = GetDrawingDimensions(lPreserveAspect);
var uv = (activeSprite != null) ? Sprites.DataUtility.GetOuterUV(activeSprite) : Vector4.zero;
var color32 = color;
vh.Clear();
vh.AddVert(new Vector3(v.x, v.y), color32, new Vector2(uv.x, uv.y));
vh.AddVert(new Vector3(v.x, v.w), color32, new Vector2(uv.x, uv.w));
vh.AddVert(new Vector3(v.z, v.w), color32, new Vector2(uv.z, uv.w));
vh.AddVert(new Vector3(v.z, v.y), color32, new Vector2(uv.z, uv.y));
vh.AddTriangle(0, 1, 2);
vh.AddTriangle(2, 3, 0);
}
private void GenerateSprite(VertexHelper vh, bool lPreserveAspect)
{
var spriteSize = new Vector2(activeSprite.rect.width, activeSprite.rect.height);
// Covert sprite pivot into normalized space.
var spritePivot = activeSprite.pivot / spriteSize;
var rectPivot = rectTransform.pivot;
Rect r = GetPixelAdjustedRect();
if (lPreserveAspect & spriteSize.sqrMagnitude > 0.0f)
{
PreserveSpriteAspectRatio(ref r, spriteSize);
}
var drawingSize = new Vector2(r.width, r.height);
var spriteBoundSize = activeSprite.bounds.size;
// Calculate the drawing offset based on the difference between the two pivots.
var drawOffset = (rectPivot - spritePivot) * drawingSize;
var color32 = color;
vh.Clear();
Vector2[] vertices = activeSprite.vertices;
Vector2[] uvs = activeSprite.uv;
for (int i = 0; i < vertices.Length; ++i)
{
vh.AddVert(new Vector3((vertices[i].x / spriteBoundSize.x) * drawingSize.x - drawOffset.x, (vertices[i].y / spriteBoundSize.y) * drawingSize.y - drawOffset.y), color32, new Vector2(uvs[i].x, uvs[i].y));
}
UInt16[] triangles = activeSprite.triangles;
for (int i = 0; i < triangles.Length; i += 3)
{
vh.AddTriangle(triangles[i + 0], triangles[i + 1], triangles[i + 2]);
}
}
static readonly Vector2[] s_VertScratch = new Vector2[4];
static readonly Vector2[] s_UVScratch = new Vector2[4];
/// <summary>
/// Generate vertices for a 9-sliced Image.
/// </summary>
private void GenerateSlicedSprite(VertexHelper toFill)
{
if (!hasBorder)
{
GenerateSimpleSprite(toFill, false);
return;
}
Vector4 outer, inner, padding, border;
if (activeSprite != null)
{
outer = Sprites.DataUtility.GetOuterUV(activeSprite);
inner = Sprites.DataUtility.GetInnerUV(activeSprite);
padding = Sprites.DataUtility.GetPadding(activeSprite);
border = activeSprite.border;
}
else
{
outer = Vector4.zero;
inner = Vector4.zero;
padding = Vector4.zero;
border = Vector4.zero;
}
Rect rect = GetPixelAdjustedRect();
Vector4 adjustedBorders = GetAdjustedBorders(border / multipliedPixelsPerUnit, rect);
padding = padding / multipliedPixelsPerUnit;
s_VertScratch[0] = new Vector2(padding.x, padding.y);
s_VertScratch[3] = new Vector2(rect.width - padding.z, rect.height - padding.w);
s_VertScratch[1].x = adjustedBorders.x;
s_VertScratch[1].y = adjustedBorders.y;
s_VertScratch[2].x = rect.width - adjustedBorders.z;
s_VertScratch[2].y = rect.height - adjustedBorders.w;
for (int i = 0; i < 4; ++i)
{
s_VertScratch[i].x += rect.x;
s_VertScratch[i].y += rect.y;
}
s_UVScratch[0] = new Vector2(outer.x, outer.y);
s_UVScratch[1] = new Vector2(inner.x, inner.y);
s_UVScratch[2] = new Vector2(inner.z, inner.w);
s_UVScratch[3] = new Vector2(outer.z, outer.w);
toFill.Clear();
for (int x = 0; x < 3; ++x)
{
int x2 = x + 1;
for (int y = 0; y < 3; ++y)
{
if (!m_FillCenter && x == 1 && y == 1)
continue;
int y2 = y + 1;
// Check for zero or negative dimensions to prevent invalid quads (UUM-71372)
if ((s_VertScratch[x2].x - s_VertScratch[x].x <= 0) || (s_VertScratch[y2].y - s_VertScratch[y].y <= 0))
continue;
AddQuad(toFill,
new Vector2(s_VertScratch[x].x, s_VertScratch[y].y),
new Vector2(s_VertScratch[x2].x, s_VertScratch[y2].y),
color,
new Vector2(s_UVScratch[x].x, s_UVScratch[y].y),
new Vector2(s_UVScratch[x2].x, s_UVScratch[y2].y));
}
}
}
/// <summary>
/// Generate vertices for a tiled Image.
/// </summary>
void GenerateTiledSprite(VertexHelper toFill)
{
Vector4 outer, inner, border;
Vector2 spriteSize;
if (activeSprite != null)
{
outer = Sprites.DataUtility.GetOuterUV(activeSprite);
inner = Sprites.DataUtility.GetInnerUV(activeSprite);
border = activeSprite.border;
spriteSize = activeSprite.rect.size;
}
else
{
outer = Vector4.zero;
inner = Vector4.zero;
border = Vector4.zero;
spriteSize = Vector2.one * 100;
}
Rect rect = GetPixelAdjustedRect();
float tileWidth = (spriteSize.x - border.x - border.z) / multipliedPixelsPerUnit;
float tileHeight = (spriteSize.y - border.y - border.w) / multipliedPixelsPerUnit;
border = GetAdjustedBorders(border / multipliedPixelsPerUnit, rect);
var uvMin = new Vector2(inner.x, inner.y);
var uvMax = new Vector2(inner.z, inner.w);
// Min to max max range for tiled region in coordinates relative to lower left corner.
float xMin = border.x;
float xMax = rect.width - border.z;
float yMin = border.y;
float yMax = rect.height - border.w;
toFill.Clear();
var clipped = uvMax;
// if either width is zero we cant tile so just assume it was the full width.
if (tileWidth <= 0)
tileWidth = xMax - xMin;
if (tileHeight <= 0)
tileHeight = yMax - yMin;
if (activeSprite != null && (hasBorder || activeSprite.packed || activeSprite.texture != null && activeSprite.texture.wrapMode != TextureWrapMode.Repeat))
{
// Sprite has border, or is not in repeat mode, or cannot be repeated because of packing.
// We cannot use texture tiling so we will generate a mesh of quads to tile the texture.
// Evaluate how many vertices we will generate. Limit this number to something sane,
// especially since meshes can not have more than 65000 vertices.
long nTilesW = 0;
long nTilesH = 0;
if (m_FillCenter)
{
nTilesW = (long)Math.Ceiling((xMax - xMin) / tileWidth);
nTilesH = (long)Math.Ceiling((yMax - yMin) / tileHeight);
double nVertices = 0;
if (hasBorder)
{
nVertices = (nTilesW + 2.0) * (nTilesH + 2.0) * 4.0; // 4 vertices per tile
}
else
{
nVertices = nTilesW * nTilesH * 4.0; // 4 vertices per tile
}
if (nVertices > 65000.0)
{
Debug.LogError("Too many sprite tiles on Image \"" + name + "\". The tile size will be increased. To remove the limit on the number of tiles, set the Wrap mode to Repeat in the Image Import Settings", this);
double maxTiles = 65000.0 / 4.0; // Max number of vertices is 65000; 4 vertices per tile.
double imageRatio;
if (hasBorder)
{
imageRatio = (nTilesW + 2.0) / (nTilesH + 2.0);
}
else
{
imageRatio = (double)nTilesW / nTilesH;
}
double targetTilesW = Math.Sqrt(maxTiles / imageRatio);
double targetTilesH = targetTilesW * imageRatio;
if (hasBorder)
{
targetTilesW -= 2;
targetTilesH -= 2;
}
nTilesW = (long)Math.Floor(targetTilesW);
nTilesH = (long)Math.Floor(targetTilesH);
tileWidth = (xMax - xMin) / nTilesW;
tileHeight = (yMax - yMin) / nTilesH;
}
}
else
{
if (hasBorder)
{
// Texture on the border is repeated only in one direction.
nTilesW = (long)Math.Ceiling((xMax - xMin) / tileWidth);
nTilesH = (long)Math.Ceiling((yMax - yMin) / tileHeight);
double nVertices = (nTilesH + nTilesW + 2.0 /*corners*/) * 2.0 /*sides*/ * 4.0 /*vertices per tile*/;
if (nVertices > 65000.0)
{
Debug.LogError("Too many sprite tiles on Image \"" + name + "\". The tile size will be increased. To remove the limit on the number of tiles, set the Wrap mode to Repeat in the Image Import Settings", this);
double maxTiles = 65000.0 / 4.0; // Max number of vertices is 65000; 4 vertices per tile.
double imageRatio = (double)nTilesW / nTilesH;
double targetTilesW = (maxTiles - 4 /*corners*/) / (2 * (1.0 + imageRatio));
double targetTilesH = targetTilesW * imageRatio;
nTilesW = (long)Math.Floor(targetTilesW);
nTilesH = (long)Math.Floor(targetTilesH);
tileWidth = (xMax - xMin) / nTilesW;
tileHeight = (yMax - yMin) / nTilesH;
}
}
else
{
nTilesH = nTilesW = 0;
}
}
if (m_FillCenter)
{
// TODO: we could share vertices between quads. If vertex sharing is implemented. update the computation for the number of vertices accordingly.
for (long j = 0; j < nTilesH; j++)
{
float y1 = yMin + j * tileHeight;
float y2 = yMin + (j + 1) * tileHeight;
if (y2 > yMax)
{
clipped.y = uvMin.y + (uvMax.y - uvMin.y) * (yMax - y1) / (y2 - y1);
y2 = yMax;
}
clipped.x = uvMax.x;
for (long i = 0; i < nTilesW; i++)
{
float x1 = xMin + i * tileWidth;
float x2 = xMin + (i + 1) * tileWidth;
if (x2 > xMax)
{
clipped.x = uvMin.x + (uvMax.x - uvMin.x) * (xMax - x1) / (x2 - x1);
x2 = xMax;
}
AddQuad(toFill, new Vector2(x1, y1) + rect.position, new Vector2(x2, y2) + rect.position, color, uvMin, clipped);
}
}
}
if (hasBorder)
{
clipped = uvMax;
for (long j = 0; j < nTilesH; j++)
{
float y1 = yMin + j * tileHeight;
float y2 = yMin + (j + 1) * tileHeight;
if (y2 > yMax)
{
clipped.y = uvMin.y + (uvMax.y - uvMin.y) * (yMax - y1) / (y2 - y1);
y2 = yMax;
}
AddQuad(toFill,
new Vector2(0, y1) + rect.position,
new Vector2(xMin, y2) + rect.position,
color,
new Vector2(outer.x, uvMin.y),
new Vector2(uvMin.x, clipped.y));
AddQuad(toFill,
new Vector2(xMax, y1) + rect.position,
new Vector2(rect.width, y2) + rect.position,
color,
new Vector2(uvMax.x, uvMin.y),
new Vector2(outer.z, clipped.y));
}
// Bottom and top tiled border
clipped = uvMax;
for (long i = 0; i < nTilesW; i++)
{
float x1 = xMin + i * tileWidth;
float x2 = xMin + (i + 1) * tileWidth;
if (x2 > xMax)
{
clipped.x = uvMin.x + (uvMax.x - uvMin.x) * (xMax - x1) / (x2 - x1);
x2 = xMax;
}
AddQuad(toFill,
new Vector2(x1, 0) + rect.position,
new Vector2(x2, yMin) + rect.position,
color,
new Vector2(uvMin.x, outer.y),
new Vector2(clipped.x, uvMin.y));
AddQuad(toFill,
new Vector2(x1, yMax) + rect.position,
new Vector2(x2, rect.height) + rect.position,
color,
new Vector2(uvMin.x, uvMax.y),
new Vector2(clipped.x, outer.w));
}
// Corners
AddQuad(toFill,
new Vector2(0, 0) + rect.position,
new Vector2(xMin, yMin) + rect.position,
color,
new Vector2(outer.x, outer.y),
new Vector2(uvMin.x, uvMin.y));
AddQuad(toFill,
new Vector2(xMax, 0) + rect.position,
new Vector2(rect.width, yMin) + rect.position,
color,
new Vector2(uvMax.x, outer.y),
new Vector2(outer.z, uvMin.y));
AddQuad(toFill,
new Vector2(0, yMax) + rect.position,
new Vector2(xMin, rect.height) + rect.position,
color,
new Vector2(outer.x, uvMax.y),
new Vector2(uvMin.x, outer.w));
AddQuad(toFill,
new Vector2(xMax, yMax) + rect.position,
new Vector2(rect.width, rect.height) + rect.position,
color,
new Vector2(uvMax.x, uvMax.y),
new Vector2(outer.z, outer.w));
}
}
else
{
// Texture has no border, is in repeat mode and not packed. Use texture tiling.
Vector2 uvScale = new Vector2((xMax - xMin) / tileWidth, (yMax - yMin) / tileHeight);
if (m_FillCenter)
{
AddQuad(toFill, new Vector2(xMin, yMin) + rect.position, new Vector2(xMax, yMax) + rect.position, color, Vector2.Scale(uvMin, uvScale), Vector2.Scale(uvMax, uvScale));
}
}
}
static void AddQuad(VertexHelper vertexHelper, Vector3[] quadPositions, Color32 color, Vector3[] quadUVs)
{
int startIndex = vertexHelper.currentVertCount;
for (int i = 0; i < 4; ++i)
vertexHelper.AddVert(quadPositions[i], color, quadUVs[i]);
vertexHelper.AddTriangle(startIndex, startIndex + 1, startIndex + 2);
vertexHelper.AddTriangle(startIndex + 2, startIndex + 3, startIndex);
}
static void AddQuad(VertexHelper vertexHelper, Vector2 posMin, Vector2 posMax, Color32 color, Vector2 uvMin, Vector2 uvMax)
{
int startIndex = vertexHelper.currentVertCount;
vertexHelper.AddVert(new Vector3(posMin.x, posMin.y, 0), color, new Vector2(uvMin.x, uvMin.y));
vertexHelper.AddVert(new Vector3(posMin.x, posMax.y, 0), color, new Vector2(uvMin.x, uvMax.y));
vertexHelper.AddVert(new Vector3(posMax.x, posMax.y, 0), color, new Vector2(uvMax.x, uvMax.y));
vertexHelper.AddVert(new Vector3(posMax.x, posMin.y, 0), color, new Vector2(uvMax.x, uvMin.y));
vertexHelper.AddTriangle(startIndex, startIndex + 1, startIndex + 2);
vertexHelper.AddTriangle(startIndex + 2, startIndex + 3, startIndex);
}
private Vector4 GetAdjustedBorders(Vector4 border, Rect adjustedRect)
{
Rect originalRect = rectTransform.rect;
for (int axis = 0; axis <= 1; axis++)
{
float borderScaleRatio;
// The adjusted rect (adjusted for pixel correctness)
// may be slightly larger than the original rect.
// Adjust the border to match the adjustedRect to avoid
// small gaps between borders (case 833201).
if (originalRect.size[axis] != 0)
{
borderScaleRatio = adjustedRect.size[axis] / originalRect.size[axis];
border[axis] *= borderScaleRatio;
border[axis + 2] *= borderScaleRatio;
}
// If the rect is smaller than the combined borders, then there's not room for the borders at their normal size.
// In order to avoid artefacts with overlapping borders, we scale the borders down to fit.
float combinedBorders = border[axis] + border[axis + 2];
if (adjustedRect.size[axis] < combinedBorders && combinedBorders != 0)
{
borderScaleRatio = adjustedRect.size[axis] / combinedBorders;
border[axis] *= borderScaleRatio;
border[axis + 2] *= borderScaleRatio;
}
}
return border;
}
static readonly Vector3[] s_Xy = new Vector3[4];
static readonly Vector3[] s_Uv = new Vector3[4];
/// <summary>
/// Generate vertices for a filled Image.
/// </summary>
void GenerateFilledSprite(VertexHelper toFill, bool preserveAspect)
{
toFill.Clear();
if (m_FillAmount < 0.001f)
return;
Vector4 v = GetDrawingDimensions(preserveAspect);
Vector4 outer = activeSprite != null ? Sprites.DataUtility.GetOuterUV(activeSprite) : Vector4.zero;
UIVertex uiv = UIVertex.simpleVert;
uiv.color = color;
float tx0 = outer.x;
float ty0 = outer.y;
float tx1 = outer.z;
float ty1 = outer.w;
// Horizontal and vertical filled sprites are simple -- just end the Image prematurely
if (m_FillMethod == FillMethod.Horizontal || m_FillMethod == FillMethod.Vertical)
{
if (fillMethod == FillMethod.Horizontal)
{
float fill = (tx1 - tx0) * m_FillAmount;
if (m_FillOrigin == 1)
{
v.x = v.z - (v.z - v.x) * m_FillAmount;
tx0 = tx1 - fill;
}
else
{
v.z = v.x + (v.z - v.x) * m_FillAmount;
tx1 = tx0 + fill;
}
}
else if (fillMethod == FillMethod.Vertical)
{
float fill = (ty1 - ty0) * m_FillAmount;
if (m_FillOrigin == 1)
{
v.y = v.w - (v.w - v.y) * m_FillAmount;
ty0 = ty1 - fill;
}
else
{
v.w = v.y + (v.w - v.y) * m_FillAmount;
ty1 = ty0 + fill;
}
}
}
s_Xy[0] = new Vector2(v.x, v.y);
s_Xy[1] = new Vector2(v.x, v.w);
s_Xy[2] = new Vector2(v.z, v.w);
s_Xy[3] = new Vector2(v.z, v.y);
s_Uv[0] = new Vector2(tx0, ty0);
s_Uv[1] = new Vector2(tx0, ty1);
s_Uv[2] = new Vector2(tx1, ty1);
s_Uv[3] = new Vector2(tx1, ty0);
{
if (m_FillAmount < 1f && m_FillMethod != FillMethod.Horizontal && m_FillMethod != FillMethod.Vertical)
{
if (fillMethod == FillMethod.Radial90)
{
if (RadialCut(s_Xy, s_Uv, m_FillAmount, m_FillClockwise, m_FillOrigin))
AddQuad(toFill, s_Xy, color, s_Uv);
}
else if (fillMethod == FillMethod.Radial180)
{
for (int side = 0; side < 2; ++side)
{
float fx0, fx1, fy0, fy1;
int even = m_FillOrigin > 1 ? 1 : 0;
if (m_FillOrigin == 0 || m_FillOrigin == 2)
{
fy0 = 0f;
fy1 = 1f;
if (side == even)
{
fx0 = 0f;
fx1 = 0.5f;
}
else
{
fx0 = 0.5f;
fx1 = 1f;
}
}
else
{
fx0 = 0f;
fx1 = 1f;
if (side == even)
{
fy0 = 0.5f;
fy1 = 1f;
}
else
{
fy0 = 0f;
fy1 = 0.5f;
}
}
s_Xy[0].x = Mathf.Lerp(v.x, v.z, fx0);
s_Xy[1].x = s_Xy[0].x;
s_Xy[2].x = Mathf.Lerp(v.x, v.z, fx1);
s_Xy[3].x = s_Xy[2].x;
s_Xy[0].y = Mathf.Lerp(v.y, v.w, fy0);
s_Xy[1].y = Mathf.Lerp(v.y, v.w, fy1);
s_Xy[2].y = s_Xy[1].y;
s_Xy[3].y = s_Xy[0].y;
s_Uv[0].x = Mathf.Lerp(tx0, tx1, fx0);
s_Uv[1].x = s_Uv[0].x;
s_Uv[2].x = Mathf.Lerp(tx0, tx1, fx1);
s_Uv[3].x = s_Uv[2].x;
s_Uv[0].y = Mathf.Lerp(ty0, ty1, fy0);
s_Uv[1].y = Mathf.Lerp(ty0, ty1, fy1);
s_Uv[2].y = s_Uv[1].y;
s_Uv[3].y = s_Uv[0].y;
float val = m_FillClockwise ? fillAmount * 2f - side : m_FillAmount * 2f - (1 - side);
if (RadialCut(s_Xy, s_Uv, Mathf.Clamp01(val), m_FillClockwise, ((side + m_FillOrigin + 3) % 4)))
{
AddQuad(toFill, s_Xy, color, s_Uv);
}
}
}
else if (fillMethod == FillMethod.Radial360)
{
for (int corner = 0; corner < 4; ++corner)
{
float fx0, fx1, fy0, fy1;
if (corner < 2)
{
fx0 = 0f;
fx1 = 0.5f;
}
else
{
fx0 = 0.5f;
fx1 = 1f;
}
if (corner == 0 || corner == 3)
{
fy0 = 0f;
fy1 = 0.5f;
}
else
{
fy0 = 0.5f;
fy1 = 1f;
}
s_Xy[0].x = Mathf.Lerp(v.x, v.z, fx0);
s_Xy[1].x = s_Xy[0].x;
s_Xy[2].x = Mathf.Lerp(v.x, v.z, fx1);
s_Xy[3].x = s_Xy[2].x;
s_Xy[0].y = Mathf.Lerp(v.y, v.w, fy0);
s_Xy[1].y = Mathf.Lerp(v.y, v.w, fy1);
s_Xy[2].y = s_Xy[1].y;
s_Xy[3].y = s_Xy[0].y;
s_Uv[0].x = Mathf.Lerp(tx0, tx1, fx0);
s_Uv[1].x = s_Uv[0].x;
s_Uv[2].x = Mathf.Lerp(tx0, tx1, fx1);
s_Uv[3].x = s_Uv[2].x;
s_Uv[0].y = Mathf.Lerp(ty0, ty1, fy0);
s_Uv[1].y = Mathf.Lerp(ty0, ty1, fy1);
s_Uv[2].y = s_Uv[1].y;
s_Uv[3].y = s_Uv[0].y;
float val = m_FillClockwise ?
m_FillAmount * 4f - ((corner + m_FillOrigin) % 4) :
m_FillAmount * 4f - (3 - ((corner + m_FillOrigin) % 4));
if (RadialCut(s_Xy, s_Uv, Mathf.Clamp01(val), m_FillClockwise, ((corner + 2) % 4)))
AddQuad(toFill, s_Xy, color, s_Uv);
}
}
}
else
{
AddQuad(toFill, s_Xy, color, s_Uv);
}
}
}
/// <summary>
/// Adjust the specified quad, making it be radially filled instead.
/// </summary>
static bool RadialCut(Vector3[] xy, Vector3[] uv, float fill, bool invert, int corner)
{
// Nothing to fill
if (fill < 0.001f) return false;
// Even corners invert the fill direction
if ((corner & 1) == 1) invert = !invert;
// Nothing to adjust
if (!invert && fill > 0.999f) return true;
// Convert 0-1 value into 0 to 90 degrees angle in radians
float angle = Mathf.Clamp01(fill);
if (invert) angle = 1f - angle;
angle *= 90f * Mathf.Deg2Rad;
// Calculate the effective X and Y factors
float cos = Mathf.Cos(angle);
float sin = Mathf.Sin(angle);
RadialCut(xy, cos, sin, invert, corner);
RadialCut(uv, cos, sin, invert, corner);
return true;
}
/// <summary>
/// Adjust the specified quad, making it be radially filled instead.
/// </summary>
static void RadialCut(Vector3[] xy, float cos, float sin, bool invert, int corner)
{
int i0 = corner;
int i1 = ((corner + 1) % 4);
int i2 = ((corner + 2) % 4);
int i3 = ((corner + 3) % 4);
if ((corner & 1) == 1)
{
if (sin > cos)
{
cos /= sin;
sin = 1f;
if (invert)
{
xy[i1].x = Mathf.Lerp(xy[i0].x, xy[i2].x, cos);
xy[i2].x = xy[i1].x;
}
}
else if (cos > sin)
{
sin /= cos;
cos = 1f;
if (!invert)
{
xy[i2].y = Mathf.Lerp(xy[i0].y, xy[i2].y, sin);
xy[i3].y = xy[i2].y;
}
}
else
{
cos = 1f;
sin = 1f;
}
if (!invert) xy[i3].x = Mathf.Lerp(xy[i0].x, xy[i2].x, cos);
else xy[i1].y = Mathf.Lerp(xy[i0].y, xy[i2].y, sin);
}
else
{
if (cos > sin)
{
sin /= cos;
cos = 1f;
if (!invert)
{
xy[i1].y = Mathf.Lerp(xy[i0].y, xy[i2].y, sin);
xy[i2].y = xy[i1].y;
}
}
else if (sin > cos)
{
cos /= sin;
sin = 1f;
if (invert)
{
xy[i2].x = Mathf.Lerp(xy[i0].x, xy[i2].x, cos);
xy[i3].x = xy[i2].x;
}
}
else
{
cos = 1f;
sin = 1f;
}
if (invert) xy[i3].y = Mathf.Lerp(xy[i0].y, xy[i2].y, sin);
else xy[i1].x = Mathf.Lerp(xy[i0].x, xy[i2].x, cos);
}
}
/// <summary>
/// See ILayoutElement.CalculateLayoutInputHorizontal.
/// </summary>
public virtual void CalculateLayoutInputHorizontal() {}
/// <summary>
/// See ILayoutElement.CalculateLayoutInputVertical.
/// </summary>
public virtual void CalculateLayoutInputVertical() {}
/// <summary>
/// See ILayoutElement.minWidth.
/// </summary>
public virtual float minWidth { get { return 0; } }
/// <summary>
/// If there is a sprite being rendered returns the size of that sprite.
/// In the case of a slided or tiled sprite will return the calculated minimum size possible
/// </summary>
public virtual float preferredWidth
{
get
{
if (activeSprite == null)
return 0;
if (type == Type.Sliced || type == Type.Tiled)
return Sprites.DataUtility.GetMinSize(activeSprite).x / pixelsPerUnit;
return activeSprite.rect.size.x / pixelsPerUnit;
}
}
/// <summary>
/// See ILayoutElement.flexibleWidth.
/// </summary>
public virtual float flexibleWidth { get { return -1; } }
/// <summary>
/// See ILayoutElement.minHeight.
/// </summary>
public virtual float minHeight { get { return 0; } }
/// <summary>
/// If there is a sprite being rendered returns the size of that sprite.
/// In the case of a slided or tiled sprite will return the calculated minimum size possible
/// </summary>
public virtual float preferredHeight
{
get
{
if (activeSprite == null)
return 0;
if (type == Type.Sliced || type == Type.Tiled)
return Sprites.DataUtility.GetMinSize(activeSprite).y / pixelsPerUnit;
return activeSprite.rect.size.y / pixelsPerUnit;
}
}
/// <summary>
/// See ILayoutElement.flexibleHeight.
/// </summary>
public virtual float flexibleHeight { get { return -1; } }
/// <summary>
/// See ILayoutElement.layoutPriority.
/// </summary>
public virtual int layoutPriority { get { return 0; } }
/// <summary>
/// Calculate if the ray location for this image is a valid hit location. Takes into account a Alpha test threshold.
/// </summary>
/// <param name="screenPoint">The screen point to check against</param>
/// <param name="eventCamera">The camera in which to use to calculate the coordinating position</param>
/// <returns>If the location is a valid hit or not.</returns>
/// <remarks> Also see See:ICanvasRaycastFilter.</remarks>
public virtual bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)
{
if (alphaHitTestMinimumThreshold <= 0)
return true;
if (alphaHitTestMinimumThreshold > 1)
return false;
if (activeSprite == null)
return true;
Vector2 local;
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, screenPoint, eventCamera, out local))
return false;
Rect rect = GetPixelAdjustedRect();
if (m_PreserveAspect)
PreserveSpriteAspectRatio(ref rect, new Vector2(activeSprite.texture.width, activeSprite.texture.height));
// Convert to have lower left corner as reference point.
local.x += rectTransform.pivot.x * rect.width;
local.y += rectTransform.pivot.y * rect.height;
local = MapCoordinate(local, rect);
// Convert local coordinates to texture space.
float x = local.x / activeSprite.texture.width;
float y = local.y / activeSprite.texture.height;
try
{
return activeSprite.texture.GetPixelBilinear(x, y).a >= alphaHitTestMinimumThreshold;
}
catch (UnityException e)
{
Debug.LogError("Using alphaHitTestMinimumThreshold greater than 0 on Image whose sprite texture cannot be read. " + e.Message + " Also make sure to disable sprite packing for this sprite.", this);
return true;
}
}
private Vector2 MapCoordinate(Vector2 local, Rect rect)
{
Rect spriteRect = activeSprite.rect;
if (type == Type.Simple || type == Type.Filled)
return new Vector2(spriteRect.position.x + local.x * spriteRect.width / rect.width, spriteRect.position.y + local.y * spriteRect.height / rect.height);
Vector4 border = activeSprite.border;
Vector4 adjustedBorder = GetAdjustedBorders(border / pixelsPerUnit, rect);
for (int i = 0; i < 2; i++)
{
if (local[i] <= adjustedBorder[i])
continue;
if (rect.size[i] - local[i] <= adjustedBorder[i + 2])
{
local[i] -= (rect.size[i] - spriteRect.size[i]);
continue;
}
if (type == Type.Sliced)
{
float lerp = Mathf.InverseLerp(adjustedBorder[i], rect.size[i] - adjustedBorder[i + 2], local[i]);
local[i] = Mathf.Lerp(border[i], spriteRect.size[i] - border[i + 2], lerp);
}
else
{
local[i] -= adjustedBorder[i];
local[i] = Mathf.Repeat(local[i], spriteRect.size[i] - border[i] - border[i + 2]);
local[i] += border[i];
}
}
return local + spriteRect.position;
}
// To track textureless images, which will be rebuild if sprite atlas manager registered a Sprite Atlas that will give this image new texture
static List<TextImage> m_TrackedTexturelessImages = new List<TextImage>();
static bool s_Initialized;
static void RebuildImage(SpriteAtlas spriteAtlas)
{
for (var i = m_TrackedTexturelessImages.Count - 1; i >= 0; i--)
{
var g = m_TrackedTexturelessImages[i];
if (null != g.activeSprite && spriteAtlas.CanBindTo(g.activeSprite))
{
g.SetAllDirty();
m_TrackedTexturelessImages.RemoveAt(i);
}
}
}
private static void TrackImage(TextImage g)
{
if (!s_Initialized)
{
SpriteAtlasManager.atlasRegistered += RebuildImage;
s_Initialized = true;
}
m_TrackedTexturelessImages.Add(g);
}
private static void UnTrackImage(TextImage g)
{
m_TrackedTexturelessImages.Remove(g);
}
protected override void OnDidApplyAnimationProperties()
{
SetMaterialDirty();
SetVerticesDirty();
SetRaycastDirty();
}
#if UNITY_EDITOR
protected override void OnValidate()
{
base.OnValidate();
m_PixelsPerUnitMultiplier = Mathf.Max(0.01f, m_PixelsPerUnitMultiplier);
}
#endif
}
}

这一块便是核心的实现逻辑
同时在OnEnable和Disable也做了相应处理

编辑器代码:
namespace UnityEditor.UI
{
#if UNITY_EDITOR
/// <summary>
/// Editor class used to edit UI Sprites.
/// </summary>
[CustomEditor(typeof(TextImage), true)]
[CanEditMultipleObjects]
/// <summary>
/// Custom Editor for the Image Component.
/// Extend this class to write a custom editor for a component derived from Image.
/// </summary>
public class TextImageEditor : GraphicEditor
{
SerializedProperty m_text;
SerializedProperty m_delayShowTime;
SerializedProperty m_FillMethod;
SerializedProperty m_FillOrigin;
SerializedProperty m_FillAmount;
SerializedProperty m_FillClockwise;
SerializedProperty m_Type;
SerializedProperty m_FillCenter;
SerializedProperty m_Sprite;
SerializedProperty m_PreserveAspect;
SerializedProperty m_UseSpriteMesh;
SerializedProperty m_PixelsPerUnitMultiplier;
GUIContent m_SpriteContent;
GUIContent m_SpriteTypeContent;
GUIContent m_ClockwiseContent;
AnimBool m_ShowSlicedOrTiled;
AnimBool m_ShowSliced;
AnimBool m_ShowTiled;
AnimBool m_ShowFilled;
AnimBool m_ShowType;
bool m_bIsDriven;
private class Styles
{
public static GUIContent text = EditorGUIUtility.TrTextContent("Fill Origin");
public static GUIContent[] OriginHorizontalStyle =
{
EditorGUIUtility.TrTextContent("Left"),
EditorGUIUtility.TrTextContent("Right")
};
public static GUIContent[] OriginVerticalStyle =
{
EditorGUIUtility.TrTextContent("Bottom"),
EditorGUIUtility.TrTextContent("Top")
};
public static GUIContent[] Origin90Style =
{
EditorGUIUtility.TrTextContent("BottomLeft"),
EditorGUIUtility.TrTextContent("TopLeft"),
EditorGUIUtility.TrTextContent("TopRight"),
EditorGUIUtility.TrTextContent("BottomRight")
};
public static GUIContent[] Origin180Style =
{
EditorGUIUtility.TrTextContent("Bottom"),
EditorGUIUtility.TrTextContent("Left"),
EditorGUIUtility.TrTextContent("Top"),
EditorGUIUtility.TrTextContent("Right")
};
public static GUIContent[] Origin360Style =
{
EditorGUIUtility.TrTextContent("Bottom"),
EditorGUIUtility.TrTextContent("Right"),
EditorGUIUtility.TrTextContent("Top"),
EditorGUIUtility.TrTextContent("Left")
};
}
protected override void OnEnable()
{
base.OnEnable();
m_SpriteContent = EditorGUIUtility.TrTextContent("Source Image");
m_SpriteTypeContent = EditorGUIUtility.TrTextContent("Image Type");
m_ClockwiseContent = EditorGUIUtility.TrTextContent("Clockwise");
m_text = serializedObject.FindProperty("m_text");
m_delayShowTime = serializedObject.FindProperty("m_delayShowTime");
m_Sprite = serializedObject.FindProperty("m_Sprite");
m_Type = serializedObject.FindProperty("m_Type");
m_FillCenter = serializedObject.FindProperty("m_FillCenter");
m_FillMethod = serializedObject.FindProperty("m_FillMethod");
m_FillOrigin = serializedObject.FindProperty("m_FillOrigin");
m_FillClockwise = serializedObject.FindProperty("m_FillClockwise");
m_FillAmount = serializedObject.FindProperty("m_FillAmount");
m_PreserveAspect = serializedObject.FindProperty("m_PreserveAspect");
m_UseSpriteMesh = serializedObject.FindProperty("m_UseSpriteMesh");
m_PixelsPerUnitMultiplier = serializedObject.FindProperty("m_PixelsPerUnitMultiplier");
m_ShowType = new AnimBool(m_Sprite.objectReferenceValue != null);
m_ShowType.valueChanged.AddListener(Repaint);
var typeEnum = (TextImage.Type)m_Type.enumValueIndex;
m_ShowSlicedOrTiled = new AnimBool(!m_Type.hasMultipleDifferentValues && typeEnum == TextImage.Type.Sliced);
m_ShowSliced = new AnimBool(!m_Type.hasMultipleDifferentValues && typeEnum == TextImage.Type.Sliced);
m_ShowTiled = new AnimBool(!m_Type.hasMultipleDifferentValues && typeEnum == TextImage.Type.Tiled);
m_ShowFilled = new AnimBool(!m_Type.hasMultipleDifferentValues && typeEnum == TextImage.Type.Filled);
m_ShowSlicedOrTiled.valueChanged.AddListener(Repaint);
m_ShowSliced.valueChanged.AddListener(Repaint);
m_ShowTiled.valueChanged.AddListener(Repaint);
m_ShowFilled.valueChanged.AddListener(Repaint);
SetShowNativeSize(true);
m_bIsDriven = false;
}
protected override void OnDisable()
{
base.OnDisable();
m_ShowType.valueChanged.RemoveListener(Repaint);
m_ShowSlicedOrTiled.valueChanged.RemoveListener(Repaint);
m_ShowSliced.valueChanged.RemoveListener(Repaint);
m_ShowTiled.valueChanged.RemoveListener(Repaint);
m_ShowFilled.valueChanged.RemoveListener(Repaint);
}
public override void OnInspectorGUI()
{
serializedObject.Update();
TextImage image = target as TextImage;
RectTransform rect = image.GetComponent<RectTransform>();
m_bIsDriven = (rect.drivenByObject as Slider)?.fillRect == rect;
EditorGUILayout.PropertyField(m_text, new GUIContent("Text"));
EditorGUILayout.PropertyField(m_delayShowTime, new GUIContent("DelayShowTime"));
SpriteGUI();
AppearanceControlsGUI();
RaycastControlsGUI();
MaskableControlsGUI();
m_ShowType.target = m_Sprite.objectReferenceValue != null;
if (EditorGUILayout.BeginFadeGroup(m_ShowType.faded))
TypeGUI();
EditorGUILayout.EndFadeGroup();
SetShowNativeSize(false);
if (EditorGUILayout.BeginFadeGroup(m_ShowNativeSize.faded))
{
EditorGUI.indentLevel++;
if ((TextImage.Type)m_Type.enumValueIndex == TextImage.Type.Simple)
EditorGUILayout.PropertyField(m_UseSpriteMesh);
EditorGUILayout.PropertyField(m_PreserveAspect);
EditorGUI.indentLevel--;
}
EditorGUILayout.EndFadeGroup();
NativeSizeButtonGUI();
serializedObject.ApplyModifiedProperties();
}
void SetShowNativeSize(bool instant)
{
TextImage.Type type = (TextImage.Type)m_Type.enumValueIndex;
bool showNativeSize = (type == TextImage.Type.Simple || type == TextImage.Type.Filled) && m_Sprite.objectReferenceValue != null;
base.SetShowNativeSize(showNativeSize, instant);
}
/// <summary>
/// Draw the atlas and Image selection fields.
/// </summary>
protected void SpriteGUI()
{
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(m_Sprite, m_SpriteContent);
if (EditorGUI.EndChangeCheck())
{
var newSprite = m_Sprite.objectReferenceValue as Sprite;
if (newSprite)
{
TextImage.Type oldType = (TextImage.Type)m_Type.enumValueIndex;
if (newSprite.border.SqrMagnitude() > 0)
{
m_Type.enumValueIndex = (int)TextImage.Type.Sliced;
}
else if (oldType == TextImage.Type.Sliced)
{
m_Type.enumValueIndex = (int)TextImage.Type.Simple;
}
}
(serializedObject.targetObject as TextImage).DisableSpriteOptimizations();
}
}
/// <summary>
/// Sprites's custom properties based on the type.
/// </summary>
protected void TypeGUI()
{
EditorGUILayout.PropertyField(m_Type, m_SpriteTypeContent);
++EditorGUI.indentLevel;
{
TextImage.Type typeEnum = (TextImage.Type)m_Type.enumValueIndex;
bool showSlicedOrTiled = (!m_Type.hasMultipleDifferentValues && (typeEnum == TextImage.Type.Sliced || typeEnum == TextImage.Type.Tiled));
if (showSlicedOrTiled && targets.Length > 1)
showSlicedOrTiled = targets.Select(obj => obj as TextImage).All(img => img.hasBorder);
m_ShowSlicedOrTiled.target = showSlicedOrTiled;
m_ShowSliced.target = (showSlicedOrTiled && !m_Type.hasMultipleDifferentValues && typeEnum == TextImage.Type.Sliced);
m_ShowTiled.target = (showSlicedOrTiled && !m_Type.hasMultipleDifferentValues && typeEnum == TextImage.Type.Tiled);
m_ShowFilled.target = (!m_Type.hasMultipleDifferentValues && typeEnum == TextImage.Type.Filled);
TextImage image = target as TextImage;
if (EditorGUILayout.BeginFadeGroup(m_ShowSlicedOrTiled.faded))
{
if (image.hasBorder)
EditorGUILayout.PropertyField(m_FillCenter);
EditorGUILayout.PropertyField(m_PixelsPerUnitMultiplier);
}
EditorGUILayout.EndFadeGroup();
if (EditorGUILayout.BeginFadeGroup(m_ShowSliced.faded))
{
if (image.sprite != null && !image.hasBorder)
EditorGUILayout.HelpBox("This Image doesn't have a border.", MessageType.Warning);
}
EditorGUILayout.EndFadeGroup();
if (EditorGUILayout.BeginFadeGroup(m_ShowTiled.faded))
{
if (image.sprite != null && !image.hasBorder && (image.sprite.texture != null && image.sprite.texture.wrapMode != TextureWrapMode.Repeat || image.sprite.packed))
EditorGUILayout.HelpBox("It looks like you want to tile a sprite with no border. It would be more efficient to modify the Sprite properties, clear the Packing tag and set the Wrap mode to Repeat.", MessageType.Warning);
}
EditorGUILayout.EndFadeGroup();
if (EditorGUILayout.BeginFadeGroup(m_ShowFilled.faded))
{
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(m_FillMethod);
if (EditorGUI.EndChangeCheck())
{
m_FillOrigin.intValue = 0;
}
var shapeRect = EditorGUILayout.GetControlRect(true);
switch ((TextImage.FillMethod)m_FillMethod.enumValueIndex)
{
case TextImage.FillMethod.Horizontal:
EditorGUI.Popup(shapeRect, Styles.text,m_FillOrigin.intValue, Styles.OriginHorizontalStyle);
break;
case TextImage.FillMethod.Vertical:
EditorGUI.Popup(shapeRect,Styles.text, m_FillOrigin.intValue, Styles.OriginVerticalStyle);
break;
case TextImage.FillMethod.Radial90:
EditorGUI.Popup(shapeRect,Styles.text, m_FillOrigin.intValue, Styles.Origin90Style);
break;
case TextImage.FillMethod.Radial180:
EditorGUI.Popup(shapeRect,Styles.text, m_FillOrigin.intValue, Styles.Origin180Style);
break;
case TextImage.FillMethod.Radial360:
EditorGUI.Popup(shapeRect,Styles.text, m_FillOrigin.intValue, Styles.Origin360Style);
break;
}
if (m_bIsDriven)
EditorGUILayout.HelpBox("The Fill amount property is driven by Slider.", MessageType.None);
using (new EditorGUI.DisabledScope(m_bIsDriven))
{
EditorGUILayout.PropertyField(m_FillAmount);
}
if ((TextImage.FillMethod)m_FillMethod.enumValueIndex > TextImage.FillMethod.Vertical)
{
EditorGUILayout.PropertyField(m_FillClockwise, m_ClockwiseContent);
}
}
EditorGUILayout.EndFadeGroup();
}
--EditorGUI.indentLevel;
}
/// <summary>
/// All graphics have a preview.
/// </summary>
public override bool HasPreviewGUI() { return true; }
/// <summary>
/// Draw the Image preview.
/// </summary>
public override void OnPreviewGUI(Rect rect, GUIStyle background)
{
TextImage image = target as TextImage;
if (image == null) return;
Sprite sf = image.sprite;
if (sf == null) return;
SpriteDrawUtility.DrawSprite(sf, rect, image.canvasRenderer.GetColor());
}
/// <summary>
/// A string containing the Image details to be used as a overlay on the component Preview.
/// </summary>
/// <returns>
/// The Image details.
/// </returns>
public override string GetInfoString()
{
TextImage image = target as TextImage;
Sprite sprite = image.sprite;
int x = (sprite != null) ? Mathf.RoundToInt(sprite.rect.width) : 0;
int y = (sprite != null) ? Mathf.RoundToInt(sprite.rect.height) : 0;
return string.Format("Image Size: {0}x{1}", x, y);
}
}
internal class SpriteDrawUtility
{
private static Texture2D s_ContrastTex;
private static Texture2D contrastTexture
{
get
{
if (s_ContrastTex == null)
{
s_ContrastTex = CreateCheckerTex(new Color(0f, 0f, 0f, 0.5f), new Color(1f, 1f, 1f, 0.5f));
}
return s_ContrastTex;
}
}
private static Texture2D CreateCheckerTex(Color c0, Color c1)
{
Texture2D texture2D = new Texture2D(16, 16);
texture2D.name = "[Generated] Checker Texture";
texture2D.hideFlags = HideFlags.DontSave;
for (int i = 0; i < 8; i++)
{
for (int j = 0; j < 8; j++)
{
texture2D.SetPixel(j, i, c1);
}
}
for (int k = 8; k < 16; k++)
{
for (int l = 0; l < 8; l++)
{
texture2D.SetPixel(l, k, c0);
}
}
for (int m = 0; m < 8; m++)
{
for (int n = 8; n < 16; n++)
{
texture2D.SetPixel(n, m, c0);
}
}
for (int num = 8; num < 16; num++)
{
for (int num2 = 8; num2 < 16; num2++)
{
texture2D.SetPixel(num2, num, c1);
}
}
texture2D.Apply();
texture2D.filterMode = FilterMode.Point;
return texture2D;
}
private static Texture2D CreateGradientTex()
{
Texture2D texture2D = new Texture2D(1, 16);
texture2D.name = "[Generated] Gradient Texture";
texture2D.hideFlags = HideFlags.DontSave;
Color a = new Color(1f, 1f, 1f, 0f);
Color b = new Color(1f, 1f, 1f, 0.4f);
for (int i = 0; i < 16; i++)
{
float num = Mathf.Abs((float)i / 15f * 2f - 1f);
num *= num;
texture2D.SetPixel(0, i, Color.Lerp(a, b, num));
}
texture2D.Apply();
texture2D.filterMode = FilterMode.Bilinear;
return texture2D;
}
private static void DrawTiledTexture(Rect rect, Texture tex)
{
float width = rect.width / (float)tex.width;
float height = rect.height / (float)tex.height;
Rect texCoords = new Rect(0f, 0f, width, height);
TextureWrapMode wrapMode = tex.wrapMode;
tex.wrapMode = TextureWrapMode.Repeat;
GUI.DrawTextureWithTexCoords(rect, tex, texCoords);
tex.wrapMode = wrapMode;
}
public static void DrawSprite(Sprite sprite, Rect drawArea, Color color)
{
if (!(sprite == null))
{
Texture2D texture = sprite.texture;
if (!(texture == null))
{
Rect rect = sprite.rect;
Rect inner = rect;
inner.xMin += sprite.border.x;
inner.yMin += sprite.border.y;
inner.xMax -= sprite.border.z;
inner.yMax -= sprite.border.w;
Vector4 outerUV = DataUtility.GetOuterUV(sprite);
Rect uv = new Rect(outerUV.x, outerUV.y, outerUV.z - outerUV.x, outerUV.w - outerUV.y);
Vector4 padding = DataUtility.GetPadding(sprite);
padding.x /= rect.width;
padding.y /= rect.height;
padding.z /= rect.width;
padding.w /= rect.height;
DrawSprite(texture, drawArea, padding, rect, inner, uv, color, null);
}
}
}
public static void DrawSprite(Texture tex, Rect drawArea, Rect outer, Rect uv, Color color)
{
DrawSprite(tex, drawArea, Vector4.zero, outer, outer, uv, color, null);
}
private static void DrawSprite(Texture tex, Rect drawArea, Vector4 padding, Rect outer, Rect inner, Rect uv, Color color, Material mat)
{
Rect position = drawArea;
position.width = Mathf.Abs(outer.width);
position.height = Mathf.Abs(outer.height);
if (position.width > 0f)
{
float num = drawArea.width / position.width;
position.width *= num;
position.height *= num;
}
if (drawArea.height > position.height)
{
position.y += (drawArea.height - position.height) * 0.5f;
}
else if (position.height > drawArea.height)
{
float num2 = drawArea.height / position.height;
position.width *= num2;
position.height *= num2;
}
if (drawArea.width > position.width)
{
position.x += (drawArea.width - position.width) * 0.5f;
}
EditorGUI.DrawTextureTransparent(position, null, ScaleMode.ScaleToFit, outer.width / outer.height);
GUI.color = color;
Rect position2 = new Rect(position.x + position.width * padding.x, position.y + position.height * padding.w, position.width - position.width * (padding.z + padding.x), position.height - position.height * (padding.w + padding.y));
if (mat == null)
{
GUI.DrawTextureWithTexCoords(position2, tex, uv, alphaBlend: true);
}
else
{
EditorGUI.DrawPreviewTexture(position2, tex, mat);
}
GUI.BeginGroup(position);
tex = contrastTexture;
GUI.color = Color.white;
if (inner.xMin != outer.xMin)
{
float x = (inner.xMin - outer.xMin) / outer.width * position.width - 1f;
DrawTiledTexture(new Rect(x, 0f, 1f, position.height), tex);
}
if (inner.xMax != outer.xMax)
{
float x2 = (inner.xMax - outer.xMin) / outer.width * position.width - 1f;
DrawTiledTexture(new Rect(x2, 0f, 1f, position.height), tex);
}
if (inner.yMin != outer.yMin)
{
float num3 = (inner.yMin - outer.yMin) / outer.height * position.height - 1f;
DrawTiledTexture(new Rect(0f, position.height - num3, position.width, 1f), tex);
}
if (inner.yMax != outer.yMax)
{
float num4 = (inner.yMax - outer.yMin) / outer.height * position.height - 1f;
DrawTiledTexture(new Rect(0f, position.height - num4, position.width, 1f), tex);
}
GUI.EndGroup();
}
}
#endif
}
编辑器就比较简单,只是扩展了相应的参数和处理


创建TextImage
这一步和之前创建按钮是一致的,需要写一下创建代码,里面的函数就在上一篇:Unity自定义按钮-CSDN博客,我就不单独拿出来了文章太长了。
[MenuItem("GameObject/UI/TextImage")]
static void CreateTextImage(MenuCommand menuCmd)
{
// 创建游戏对象
float w = 100f;
float h = 100;
GameObject textImage = CreateUIElementRoot("TextImage", w, h);
// 创建Text对象
GameObject childText = CreateUIObject("Text", textImage);
Text v = childText.AddComponent<Text>();
v.text = "TextImage";
v.alignment = TextAnchor.MiddleCenter;
SetDefaultTextValues(v);
RectTransform r = childText.GetComponent<RectTransform>();
r.anchorMin = Vector2.zero;
r.anchorMax = Vector2.one;
r.sizeDelta = Vector2.zero;
// 添加脚本
textImage.AddComponent<CanvasRenderer>();
TextImage img = textImage.AddComponent<TextImage>();
img.color = Color.white;
img.fillCenter = true;
img.raycastTarget = true;
img.sprite = findRes<Sprite>("UISprite");
if (img.sprite != null)
img.type = TextImage.Type.Sliced;
img.text=v;
// 放入到UI Canvas中
PlaceUIElementRoot(textImage, menuCmd);
}
三、效果:
