Unity3D UGUI系列之自定义圆形按钮

Unity3D UGUI系列之自定义圆形按钮

2021年3月7日 0 作者 老王

1 效果展示

只在圆形区域内点击有效。
圆形按钮

2 分析

  • 参照UGUI源码中的Image,创建一个子类继承自MaskableGraphic
  • 重写OnPopulateMesh方法,创建圆形图片的网格(UGUI控件的本质是网格,这点可参考之前的博客《Unity3D UGUI系列之合批》)
  • 实现ICanvasRaycastFilter接口,用来过滤掉圆形区域外的点击

3 项目

项目链接:https://pan.baidu.com/s/1VBHjsIrlTInb8QxdE2phcA
提取码:dn3p

BaseImage.cs

using System;
using UnityEngine;
using UnityEngine.Serialization;
using UnityEngine.UI;

namespace CustomUI
{
    public class BaseImage : MaskableGraphic, ISerializationCallbackReceiver, ILayoutElement, ICanvasRaycastFilter
    {
        [FormerlySerializedAs("m_Frame")]
        [SerializeField]
        private Sprite m_Sprite;
        public Sprite sprite { get { return m_Sprite; } set { if (SetPropertyUtility.SetClass(ref m_Sprite, value)) SetAllDirty(); } }

        [NonSerialized]
        private Sprite m_OverrideSprite;
        public Sprite overrideSprite { get { return m_OverrideSprite == null ? sprite : m_OverrideSprite; } set { if (SetPropertyUtility.SetClass(ref m_OverrideSprite, value)) SetAllDirty(); } }

        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;
            }
        }

        protected override void OnPopulateMesh(VertexHelper vh)
        {
            base.OnPopulateMesh(vh);
        }

        #region ISerializationCallbackReceiver
        public void OnAfterDeserialize()
        {
        }

        //     Implement this method to receive a callback after unity serialized your object.
        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

    }
}

SetPropertyUtility.cs

using UnityEngine;

namespace CustomUI
{
    public 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(ref T currentValue, T newValue) where T : struct
        {
            if (currentValue.Equals(newValue))
                return false;

            currentValue = newValue;
            return true;
        }

        public static bool SetClass(ref T currentValue, T newValue) where T : class
        {
            if ((currentValue == null && newValue == null) || (currentValue != null && currentValue.Equals(newValue)))
                return false;

            currentValue = newValue;
            return true;
        }
    }
}

CircularImage.cs

using UnityEngine;
using UnityEngine.Sprites;
using UnityEngine.UI;

namespace CustomUI
{
    // 圆形按钮
    public class CircularImage : BaseImage
    {
        [SerializeField] public float radius = 50;                    // 半径
        [Range(3, 100)]
        [SerializeField] public int segment = 20;                     // 分段数

        public override bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera)
        {
            RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, sp, eventCamera,
                out Vector2 localPos);
            float dis = Vector3.Distance(localPos, Vector3.zero);
            return dis <= radius;
        }

        protected override void OnPopulateMesh(VertexHelper vh)
        {
            Color32 color32 = color;
            vh.Clear();

            float deltaRad = 2 * Mathf.PI / segment;
            Vector3 center = Vector3.zero;

            float tw = rectTransform.rect.width;
            float th = rectTransform.rect.height;

            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;

            // 顶点是以pivot为参考的
            for (int i = 0; i < segment; i++)
            {
                float rad = deltaRad * i;
                float sin = Mathf.Sin(rad);
                float cos = Mathf.Cos(rad);
                float x = radius * sin;
                float y = radius * cos;
                vh.AddVert(new Vector3(x, y), color32, new Vector2(x * uvScaleX + uvCenterX, y * uvScaleY + uvCenterY));
            }
            vh.AddVert(center, color32, new Vector2(uvCenterX, uvCenterY));

            for (int i = 0; i < segment; i++)
            {
                if (i == segment - 1)
                {
                    vh.AddTriangle(i, 0, segment);
                }
                else
                {
                    vh.AddTriangle(i, i+1, segment);
                }
            }
        }
    }
}

4 参考文章