Last active
October 27, 2024 11:21
-
-
Save yasirkula/391fa12bc173acdf5ac48c466f180708 to your computer and use it in GitHub Desktop.
Combining UI Image's Sliced+Filled features together in Unity
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using UnityEngine; | |
using UnityEngine.UI; | |
#if UNITY_2017_4 || UNITY_2018_2_OR_NEWER | |
using UnityEngine.U2D; | |
#endif | |
using Sprites = UnityEngine.Sprites; | |
#if UNITY_EDITOR | |
using UnityEditor; | |
// Custom Editor to order the variables in the Inspector similar to Image component | |
[CustomEditor( typeof( SlicedFilledImage ) ), CanEditMultipleObjects] | |
public class SlicedFilledImageEditor : Editor | |
{ | |
private SerializedProperty spriteProp, colorProp; | |
private GUIContent spriteLabel; | |
private void OnEnable() | |
{ | |
spriteProp = serializedObject.FindProperty( "m_Sprite" ); | |
colorProp = serializedObject.FindProperty( "m_Color" ); | |
spriteLabel = new GUIContent( "Source Image" ); | |
} | |
public override void OnInspectorGUI() | |
{ | |
serializedObject.Update(); | |
EditorGUILayout.PropertyField( spriteProp, spriteLabel ); | |
EditorGUILayout.PropertyField( colorProp ); | |
DrawPropertiesExcluding( serializedObject, "m_Script", "m_Sprite", "m_Color", "m_OnCullStateChanged" ); | |
serializedObject.ApplyModifiedProperties(); | |
} | |
} | |
#endif | |
// Credit: https://bitbucket.org/Unity-Technologies/ui/src/2018.4/UnityEngine.UI/UI/Core/Image.cs | |
[RequireComponent( typeof( CanvasRenderer ) )] | |
[AddComponentMenu( "UI/Sliced Filled Image", 11 )] | |
public class SlicedFilledImage : MaskableGraphic, ISerializationCallbackReceiver, ILayoutElement, ICanvasRaycastFilter | |
{ | |
private static class SetPropertyUtility | |
{ | |
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; | |
} | |
} | |
public enum FillDirection { Right = 0, Left = 1, Up = 2, Down = 3 } | |
private static readonly Vector3[] s_Vertices = new Vector3[4]; | |
private static readonly Vector2[] s_UVs = new Vector2[4]; | |
private static readonly Vector2[] s_SlicedVertices = new Vector2[4]; | |
private static readonly Vector2[] s_SlicedUVs = new Vector2[4]; | |
#pragma warning disable 1692 | |
#pragma warning disable IDE1006 // Suppress 'Naming rule violation' warnings | |
#pragma warning disable 0649 | |
[SerializeField] | |
private Sprite m_Sprite; | |
public Sprite sprite | |
{ | |
get { return m_Sprite; } | |
set | |
{ | |
if( SetPropertyUtility.SetClass( ref m_Sprite, value ) ) | |
{ | |
SetAllDirty(); | |
TrackImage(); | |
} | |
} | |
} | |
[SerializeField] | |
private FillDirection m_FillDirection; | |
public FillDirection fillDirection | |
{ | |
get { return m_FillDirection; } | |
set | |
{ | |
if( SetPropertyUtility.SetStruct( ref m_FillDirection, value ) ) | |
SetVerticesDirty(); | |
} | |
} | |
[Range( 0, 1 )] | |
[SerializeField] | |
private float m_FillAmount = 1f; | |
public float fillAmount | |
{ | |
get { return m_FillAmount; } | |
set | |
{ | |
if( SetPropertyUtility.SetStruct( ref m_FillAmount, Mathf.Clamp01( value ) ) ) | |
SetVerticesDirty(); | |
} | |
} | |
[SerializeField] | |
private bool m_FillCenter = true; | |
public bool fillCenter | |
{ | |
get { return m_FillCenter; } | |
set | |
{ | |
if( SetPropertyUtility.SetStruct( ref m_FillCenter, value ) ) | |
SetVerticesDirty(); | |
} | |
} | |
[SerializeField] | |
private float m_PixelsPerUnitMultiplier = 1f; | |
public float pixelsPerUnitMultiplier | |
{ | |
get { return m_PixelsPerUnitMultiplier; } | |
set { m_PixelsPerUnitMultiplier = Mathf.Max( 0.01f, value ); } | |
} | |
public float pixelsPerUnit | |
{ | |
get | |
{ | |
float spritePixelsPerUnit = 100; | |
if( activeSprite ) | |
spritePixelsPerUnit = activeSprite.pixelsPerUnit; | |
float referencePixelsPerUnit = 100; | |
if( canvas ) | |
referencePixelsPerUnit = canvas.referencePixelsPerUnit; | |
return m_PixelsPerUnitMultiplier * spritePixelsPerUnit / referencePixelsPerUnit; | |
} | |
} | |
#pragma warning restore 0649 | |
[NonSerialized] | |
private Sprite m_OverrideSprite; | |
public Sprite overrideSprite | |
{ | |
get { return activeSprite; } | |
set | |
{ | |
if( SetPropertyUtility.SetClass( ref m_OverrideSprite, value ) ) | |
{ | |
SetAllDirty(); | |
TrackImage(); | |
} | |
} | |
} | |
private Sprite activeSprite { get { return m_OverrideSprite != null ? m_OverrideSprite : m_Sprite; } } | |
public override Texture mainTexture | |
{ | |
get | |
{ | |
if( activeSprite != null ) | |
return activeSprite.texture; | |
return material != null && material.mainTexture != null ? material.mainTexture : s_WhiteTexture; | |
} | |
} | |
public bool hasBorder | |
{ | |
get | |
{ | |
if( activeSprite != null ) | |
{ | |
Vector4 v = activeSprite.border; | |
return v.sqrMagnitude > 0f; | |
} | |
return false; | |
} | |
} | |
public override Material material | |
{ | |
get | |
{ | |
if( m_Material != null ) | |
return m_Material; | |
if( activeSprite && activeSprite.associatedAlphaSplitTexture != null ) | |
{ | |
#if UNITY_EDITOR | |
if( Application.isPlaying ) | |
#endif | |
return Image.defaultETC1GraphicMaterial; | |
} | |
return defaultMaterial; | |
} | |
set { base.material = value; } | |
} | |
public float alphaHitTestMinimumThreshold { get; set; } | |
#pragma warning restore IDE1006 | |
#pragma warning restore 1692 | |
protected SlicedFilledImage() | |
{ | |
useLegacyMeshGeneration = false; | |
} | |
protected override void OnEnable() | |
{ | |
base.OnEnable(); | |
TrackImage(); | |
} | |
protected override void OnDisable() | |
{ | |
base.OnDisable(); | |
if( m_Tracked ) | |
UnTrackImage(); | |
} | |
#if UNITY_EDITOR | |
protected override void OnValidate() | |
{ | |
base.OnValidate(); | |
m_PixelsPerUnitMultiplier = Mathf.Max( 0.01f, m_PixelsPerUnitMultiplier ); | |
} | |
#endif | |
protected override void OnPopulateMesh( VertexHelper vh ) | |
{ | |
if( activeSprite == null ) | |
{ | |
base.OnPopulateMesh( vh ); | |
return; | |
} | |
GenerateSlicedFilledSprite( vh ); | |
} | |
/// <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 ); | |
} | |
private void GenerateSlicedFilledSprite( VertexHelper vh ) | |
{ | |
vh.Clear(); | |
if( m_FillAmount < 0.001f ) | |
return; | |
Rect rect = GetPixelAdjustedRect(); | |
Vector4 outer = Sprites.DataUtility.GetOuterUV( activeSprite ); | |
Vector4 padding = Sprites.DataUtility.GetPadding( activeSprite ); | |
if( !hasBorder ) | |
{ | |
Vector2 size = activeSprite.rect.size; | |
int spriteW = Mathf.RoundToInt( size.x ); | |
int spriteH = Mathf.RoundToInt( size.y ); | |
// Image's dimensions used for drawing. X = left, Y = bottom, Z = right, W = top. | |
Vector4 vertices = new Vector4( | |
rect.x + rect.width * ( padding.x / spriteW ), | |
rect.y + rect.height * ( padding.y / spriteH ), | |
rect.x + rect.width * ( ( spriteW - padding.z ) / spriteW ), | |
rect.y + rect.height * ( ( spriteH - padding.w ) / spriteH ) ); | |
GenerateFilledSprite( vh, vertices, outer, m_FillAmount ); | |
return; | |
} | |
Vector4 inner = Sprites.DataUtility.GetInnerUV( activeSprite ); | |
Vector4 border = GetAdjustedBorders( activeSprite.border / pixelsPerUnit, rect ); | |
padding = padding / pixelsPerUnit; | |
s_SlicedVertices[0] = new Vector2( padding.x, padding.y ); | |
s_SlicedVertices[3] = new Vector2( rect.width - padding.z, rect.height - padding.w ); | |
s_SlicedVertices[1].x = border.x; | |
s_SlicedVertices[1].y = border.y; | |
s_SlicedVertices[2].x = rect.width - border.z; | |
s_SlicedVertices[2].y = rect.height - border.w; | |
for( int i = 0; i < 4; ++i ) | |
{ | |
s_SlicedVertices[i].x += rect.x; | |
s_SlicedVertices[i].y += rect.y; | |
} | |
s_SlicedUVs[0] = new Vector2( outer.x, outer.y ); | |
s_SlicedUVs[1] = new Vector2( inner.x, inner.y ); | |
s_SlicedUVs[2] = new Vector2( inner.z, inner.w ); | |
s_SlicedUVs[3] = new Vector2( outer.z, outer.w ); | |
float rectStartPos; | |
float _1OverTotalSize; | |
if( m_FillDirection == FillDirection.Left || m_FillDirection == FillDirection.Right ) | |
{ | |
rectStartPos = s_SlicedVertices[0].x; | |
float totalSize = ( s_SlicedVertices[3].x - s_SlicedVertices[0].x ); | |
_1OverTotalSize = totalSize > 0f ? 1f / totalSize : 1f; | |
} | |
else | |
{ | |
rectStartPos = s_SlicedVertices[0].y; | |
float totalSize = ( s_SlicedVertices[3].y - s_SlicedVertices[0].y ); | |
_1OverTotalSize = totalSize > 0f ? 1f / totalSize : 1f; | |
} | |
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; | |
float sliceStart, sliceEnd; | |
switch( m_FillDirection ) | |
{ | |
case FillDirection.Right: | |
sliceStart = ( s_SlicedVertices[x].x - rectStartPos ) * _1OverTotalSize; | |
sliceEnd = ( s_SlicedVertices[x2].x - rectStartPos ) * _1OverTotalSize; | |
break; | |
case FillDirection.Up: | |
sliceStart = ( s_SlicedVertices[y].y - rectStartPos ) * _1OverTotalSize; | |
sliceEnd = ( s_SlicedVertices[y2].y - rectStartPos ) * _1OverTotalSize; | |
break; | |
case FillDirection.Left: | |
sliceStart = 1f - ( s_SlicedVertices[x2].x - rectStartPos ) * _1OverTotalSize; | |
sliceEnd = 1f - ( s_SlicedVertices[x].x - rectStartPos ) * _1OverTotalSize; | |
break; | |
case FillDirection.Down: | |
sliceStart = 1f - ( s_SlicedVertices[y2].y - rectStartPos ) * _1OverTotalSize; | |
sliceEnd = 1f - ( s_SlicedVertices[y].y - rectStartPos ) * _1OverTotalSize; | |
break; | |
default: // Just there to get rid of the "Use of unassigned local variable" compiler error | |
sliceStart = sliceEnd = 0f; | |
break; | |
} | |
if( sliceStart >= m_FillAmount ) | |
continue; | |
Vector4 vertices = new Vector4( s_SlicedVertices[x].x, s_SlicedVertices[y].y, s_SlicedVertices[x2].x, s_SlicedVertices[y2].y ); | |
Vector4 uvs = new Vector4( s_SlicedUVs[x].x, s_SlicedUVs[y].y, s_SlicedUVs[x2].x, s_SlicedUVs[y2].y ); | |
float fillAmount = ( m_FillAmount - sliceStart ) / ( sliceEnd - sliceStart ); | |
GenerateFilledSprite( vh, vertices, uvs, fillAmount ); | |
} | |
} | |
} | |
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; | |
} | |
private void GenerateFilledSprite( VertexHelper vh, Vector4 vertices, Vector4 uvs, float fillAmount ) | |
{ | |
if( m_FillAmount < 0.001f ) | |
return; | |
float uvLeft = uvs.x; | |
float uvBottom = uvs.y; | |
float uvRight = uvs.z; | |
float uvTop = uvs.w; | |
if( fillAmount < 1f ) | |
{ | |
if( m_FillDirection == FillDirection.Left || m_FillDirection == FillDirection.Right ) | |
{ | |
if( m_FillDirection == FillDirection.Left ) | |
{ | |
vertices.x = vertices.z - ( vertices.z - vertices.x ) * fillAmount; | |
uvLeft = uvRight - ( uvRight - uvLeft ) * fillAmount; | |
} | |
else | |
{ | |
vertices.z = vertices.x + ( vertices.z - vertices.x ) * fillAmount; | |
uvRight = uvLeft + ( uvRight - uvLeft ) * fillAmount; | |
} | |
} | |
else | |
{ | |
if( m_FillDirection == FillDirection.Down ) | |
{ | |
vertices.y = vertices.w - ( vertices.w - vertices.y ) * fillAmount; | |
uvBottom = uvTop - ( uvTop - uvBottom ) * fillAmount; | |
} | |
else | |
{ | |
vertices.w = vertices.y + ( vertices.w - vertices.y ) * fillAmount; | |
uvTop = uvBottom + ( uvTop - uvBottom ) * fillAmount; | |
} | |
} | |
} | |
s_Vertices[0] = new Vector3( vertices.x, vertices.y ); | |
s_Vertices[1] = new Vector3( vertices.x, vertices.w ); | |
s_Vertices[2] = new Vector3( vertices.z, vertices.w ); | |
s_Vertices[3] = new Vector3( vertices.z, vertices.y ); | |
s_UVs[0] = new Vector2( uvLeft, uvBottom ); | |
s_UVs[1] = new Vector2( uvLeft, uvTop ); | |
s_UVs[2] = new Vector2( uvRight, uvTop ); | |
s_UVs[3] = new Vector2( uvRight, uvBottom ); | |
int startIndex = vh.currentVertCount; | |
for( int i = 0; i < 4; i++ ) | |
vh.AddVert( s_Vertices[i], color, s_UVs[i] ); | |
vh.AddTriangle( startIndex, startIndex + 1, startIndex + 2 ); | |
vh.AddTriangle( startIndex + 2, startIndex + 3, startIndex ); | |
} | |
int ILayoutElement.layoutPriority { get { return 0; } } | |
float ILayoutElement.minWidth { get { return 0; } } | |
float ILayoutElement.minHeight { get { return 0; } } | |
float ILayoutElement.flexibleWidth { get { return -1; } } | |
float ILayoutElement.flexibleHeight { get { return -1; } } | |
float ILayoutElement.preferredWidth | |
{ | |
get | |
{ | |
if( activeSprite == null ) | |
return 0; | |
return Sprites.DataUtility.GetMinSize( activeSprite ).x / pixelsPerUnit; | |
} | |
} | |
float ILayoutElement.preferredHeight | |
{ | |
get | |
{ | |
if( activeSprite == null ) | |
return 0; | |
return Sprites.DataUtility.GetMinSize( activeSprite ).y / pixelsPerUnit; | |
} | |
} | |
void ILayoutElement.CalculateLayoutInputHorizontal() { } | |
void ILayoutElement.CalculateLayoutInputVertical() { } | |
bool ICanvasRaycastFilter.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(); | |
// Convert to have lower left corner as reference point. | |
local.x += rectTransform.pivot.x * rect.width; | |
local.y += rectTransform.pivot.y * rect.height; | |
Rect spriteRect = activeSprite.rect; | |
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; | |
} | |
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 ); | |
} | |
// Normalize local coordinates. | |
Rect textureRect = activeSprite.textureRect; | |
Vector2 normalized = new Vector2( local.x / textureRect.width, local.y / textureRect.height ); | |
// Convert to texture space. | |
float x = Mathf.Lerp( textureRect.x, textureRect.xMax, normalized.x ) / activeSprite.texture.width; | |
float y = Mathf.Lerp( textureRect.y, textureRect.yMax, normalized.y ) / activeSprite.texture.height; | |
switch( m_FillDirection ) | |
{ | |
case FillDirection.Right: | |
if( x > m_FillAmount ) | |
return false; | |
break; | |
case FillDirection.Left: | |
if( 1f - x > m_FillAmount ) | |
return false; | |
break; | |
case FillDirection.Up: | |
if( y > m_FillAmount ) | |
return false; | |
break; | |
case FillDirection.Down: | |
if( 1f - y > m_FillAmount ) | |
return false; | |
break; | |
} | |
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; | |
} | |
} | |
void ISerializationCallbackReceiver.OnBeforeSerialize() { } | |
void ISerializationCallbackReceiver.OnAfterDeserialize() | |
{ | |
m_FillAmount = Mathf.Clamp01( m_FillAmount ); | |
} | |
// Whether this is being tracked for Atlas Binding | |
private bool m_Tracked = false; | |
#if UNITY_2017_4 || UNITY_2018_2_OR_NEWER | |
private static List<SlicedFilledImage> m_TrackedTexturelessImages = new List<SlicedFilledImage>(); | |
private static bool s_Initialized; | |
#endif | |
private void TrackImage() | |
{ | |
if( activeSprite != null && activeSprite.texture == null ) | |
{ | |
#if UNITY_2017_4 || UNITY_2018_2_OR_NEWER | |
if( !s_Initialized ) | |
{ | |
SpriteAtlasManager.atlasRegistered += RebuildImage; | |
s_Initialized = true; | |
} | |
m_TrackedTexturelessImages.Add( this ); | |
#endif | |
m_Tracked = true; | |
} | |
} | |
private void UnTrackImage() | |
{ | |
#if UNITY_2017_4 || UNITY_2018_2_OR_NEWER | |
m_TrackedTexturelessImages.Remove( this ); | |
#endif | |
m_Tracked = false; | |
} | |
#if UNITY_2017_4 || UNITY_2018_2_OR_NEWER | |
private static void RebuildImage( SpriteAtlas spriteAtlas ) | |
{ | |
for( int i = m_TrackedTexturelessImages.Count - 1; i >= 0; i-- ) | |
{ | |
SlicedFilledImage image = m_TrackedTexturelessImages[i]; | |
if( spriteAtlas.CanBindTo( image.activeSprite ) ) | |
{ | |
image.SetAllDirty(); | |
m_TrackedTexturelessImages.RemoveAt( i ); | |
} | |
} | |
} | |
#endif | |
} |
Thank you 👍
Works like a charm, thank you.
Thank you so much, you saved my day!
Just <3 Thanks ton
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thank you 🙂