使用Mask组件的缺点

我们知道项目中可以这样展示圆形图片,一般是Image组件,下面再加上一个圆形Mask。但是这样做有几个缺点:

  1. 使用Mask会额外消耗多一个Drawcall来创建Mask,做像素剔除。
  2. Mask不利于层级合并。原本同一图集里的ui可以合并层级,仅需一个Drawcall渲染,如果加入Mask,就会将一个ui整体分割成了Mask下的子ui与其他ui,两者只能各自进行层级合并,至少要两个Drawcall。Mask用得多了,一个ui整体会被分割得四分五裂,就会严重影响层次合并的效率了。
  3. 产生的圆形不够完美,边上有锯齿现象。

自定义圆形图片

我们使用自己定义的圆形图片,不仅只有一个DrawCall,还能和同一图集的UI进行合并,产生的圆形程度也能自己控制。上面的所有缺点都没有了。所以我们要自定义圆形图片。

继承Image类

我们可以继承Image类,然后重写OnPopulateMesh方法,重新写入圆形形状的渲染顶点,三角面片信息,从而实现我们自己的圆形Image。

圆形可以由若干个以圆形为顶点的等腰三角形片组成正多边形,三角形多了,就近似圆形。所以首先定义一个参数圆形由多少个三角形组成。

然后我们添加顶点信息,首先添加圆心为顶点的信息,大致伪代码是这样的:

public class CircleImage : Image
{
    public int segements = 50;

    protected override void OnPopulateMesh(VertexHelper vh)
    {
        //清除原来画的信息
        vh.Clear();

        UIVertex origin = new UIVertex();  //原点
        origin.color = color;
        //计算顶点vertexPos
        origin.position = vertexPos;
        //计算uv0
        origin.uv0 =uv0Pos ;
        vh.AddVert(origin);
    }
}

我们需要计算一个点的position和uv0坐标。原点的position就是(0,0),在圆上的点的坐标就是(半径Cos弧度,半径SIn弧度),半径我们知道,角度可以通过(2pi/三角形个数)求出。

而Uv的值,首先我们可以通过DataUtility.GetOuterUV,得到一个vector4的对象,这个对象的x,y,z,w表示一个矩形,左下,右下,左上,右上。我们的Position的值是可以知道的。因此我们可以通过uv和position的比值来求出对应的uv值,在原点的position是(0,0)。对应的uv值就是(原点position.x比率+uv中心点.x,原点y比率+uv中心点.y)。注意这里是添加要分的分数+1个顶点,有要分的份上个三角形。然后把这些顶点,三角形都添加到VertexHelper 的实例类中。

Vector4 uv4 = overrideSprite == null ? Vector4.zero : DataUtility.GetOuterUV(overrideSprite);

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

        float uvWidth = uv4.z - uv4.x;
        float uvHeight = uv4.w - uv4.y;

        Vector2 uvCenter  = new Vector2(uvWidth/2,uvHeight/2);

        Vector2 ratio = new Vector2(uvWidth/width,uvHeight/height);

这是计算position和uv的宽高,得出uv和position的比率。

UIVertex origin = new UIVertex();
        origin.color = color;
        origin.position = Vector3.zero;
        origin.uv0  = new Vector2(origin.position.x*ratio.x+uvCenter.x,origin.position.y*ratio.y+uvCenter.y);
        vh.AddVert(origin);

        float angle = 2 * Mathf.PI / segements;
        float curAngle = 0;
        float radius = width / 2;
        for (int i = 0; i < segements+1; i++)
        {
            float x = radius * Mathf.Cos(curAngle);
            float y = radius * Mathf.Sin(curAngle);
            curAngle += angle;

            UIVertex originTemp = new UIVertex();
            originTemp.color = color;
            originTemp.position = new Vector2(x,y);
            originTemp.uv0 = new Vector2(originTemp.position.x * ratio.x + uvCenter.x, originTemp.position.y * ratio.y + uvCenter.y);
            vh.AddVert(originTemp);
        }

        for (int i = 0; i < segements; i++)
        {
            vh.AddTriangle(i+1,0,i+2);
        }

这是把顶点和三角形都添加到vh类里面,Image会自动去绘制图形。

就这样,我们就完成了简单的圆形Image制作,但是还是有很多缺陷,例如无法实现精确点击,无法使用锚点,没有暴露参数。但是只有你一步一步修改,就能完成达到你想要的效果,我这里不仅完善了以上的缺陷,还增加了可以按照百分比显示遮罩。想要的童鞋可以参考我的源码。

实现效果:

UNITY button圆角 unity 圆形_List

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Sprites;
using UnityEngine.UI;


/// <summary>
///1.为什么不用Mask组件
///1.1 增加Drawcall
///1.2 边上有锯齿
///
///2.Position和uv的关系
/// 以rect的中心点为中心,Position左下,右下,左上,右上可以表示为(-200,-200)(200,-200)(-200,200)(200,200)
/// 以左下为中心,Uv可以表示为四个点距离左下的位置(0,0,1,1)
/// uv换算成Position就是让uv填满整个rect   uv=position*(uv/position)
/// </summary>
public class CircleImage : Image
{
    //圆形由多少块三角形组成
    [SerializeField]
    public int segements=60;

    [SerializeField]
    public float showPercent;

    //半径
    private float radius;
    private readonly Color32 GRAY_COLOR = new Color32(60, 60, 60, 255);
    private List<Vector3> _vertexList;

    protected override void OnPopulateMesh(VertexHelper vh)
    {
        vh.Clear();
        _vertexList = new List<Vector3>();
        int realsegments = (int) (segements * showPercent);

        //rect的width 和 height
        float width = rectTransform.rect.width;
        float height = rectTransform.rect.height;

        radius = width > height ? height / 2 : width / 2;

        //获取当前UV信息
        Vector4 uv = Vector4.zero;
        uv = DataUtility.GetOuterUV(overrideSprite);
        //uv为什么是4维变量  对应着离原点的距离x ,y,以及x+width,y+height;

        float uvWidth = uv.z - uv.x;
        float uvHight = uv.w - uv.y;

        
        Vector2 uvCenter = new Vector2(uvWidth / 2, uvHight / 2);

        //uv坐标和rect之间的换算比例
        Vector2 converRatio = new Vector2(uvWidth / width, uvHight / height);


        //求出一个小三角形的弧度
        float radian = (2 * Mathf.PI) / segements;

        Vector2 vertexPos = new Vector2((0.5f - rectTransform.pivot.x) * width, (0.5f - rectTransform.pivot.y) * height);

        //求出一个小三角形的三个顶点
        //通过传入点的顺序 判断是正面(顺时针)还是背面  背面不渲染
        UIVertex origin = new UIVertex();  //原点
        origin.color = GetOriginColor();
        Vector2 uvrela= Vector2.zero;
        origin.uv0 = new Vector2(uvrela.x * converRatio.x+uvCenter.x, uvrela.y * converRatio.y+uvCenter.y);
        origin.position = vertexPos;
        vh.AddVert(origin);
        //需要的顶点数等于块数+1
        int vertexCount = realsegments + 1;
        float curRadian = 0;
        for (int i = 0; i < segements+1; i++)
        {
            float x = Mathf.Cos(curRadian)* radius;
            float y = Mathf.Sin(curRadian)* radius;
            curRadian += radian;
            UIVertex vertexTemp = new UIVertex();  //原点
            vertexTemp.color = i < vertexCount ?  new Color32(60, 60, 60, 255):(Color32)color;
            if (realsegments==0)
            {
                vertexTemp.color = color;
            }
            uvrela = new Vector2(x,y);
            vertexTemp.uv0 = new Vector2(uvrela.x * converRatio.x + uvCenter.x, uvrela.y * converRatio.y + uvCenter.y);
            vertexTemp.position = new Vector2(x,y)+vertexPos;
            vh.AddVert(vertexTemp);
            _vertexList.Add(vertexTemp.position);
        }

        int id = 1;
        //生成面片生成UV
        for (int i = 0; i < segements; i++)
        {
            //id按照传入三角形的顺序生成
            vh.AddTriangle(id,0,id+1);
            id++;
        }
    }


    private Color32 GetOriginColor()
    {
        //随着showPercent从0变到1 ,color从白变成灰
        Color32 colorTemp = (Color.white - GRAY_COLOR) * showPercent;
        return new Color32((byte) (255 - colorTemp.r),
            (byte)(255- colorTemp.g),
            (byte)(255 - colorTemp.b),
            255);
    }


    public override bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)
    {
        Vector2 localPoint;
        RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, screenPoint, eventCamera,
            out localPoint);
        return IsValid(localPoint);
    }

    private bool IsValid(Vector2 localPoint)
    {
        return GetCrossPointNum(localPoint, _vertexList) %2 ==1;

    }

    private int GetCrossPointNum(Vector2 localPoint,List<Vector3> vertexList)
    {
        int count = 0;
        Vector3 vert1 = Vector3.zero;
        Vector3 vert2 = Vector3.zero;
        int vertCount = vertexList.Count;
        for (int i = 0; i < vertCount; i++)
        {
            vert1 = vertexList[i];
            vert2 = vertexList[(i + 1) % vertCount];
            if (IsYInRang(localPoint,vert1,vert2))
            {

                if (localPoint.x<GetX(vert1,vert2,localPoint.y))
                {
                    count++;
                }
            }
        }
        return count;
    }

    /// <summary>
    ///获取相交点的X坐标
    /// </summary>
    /// <param name="vert1"></param>
    /// <param name="vert2"></param>
    /// <param name="localPointY"></param>
    /// <returns></returns>
    private float GetX(Vector3 vert1, Vector3 vert2, float localPointY)
    {

        float k = (vert1.y - vert2.y) / (vert1.x - vert2.x);
        return vert1.x + (localPointY - vert1.y) / k;
    }

    private bool IsYInRang(Vector2 localPoint, Vector3 vert1, Vector3 vert2)
    {
        if (vert1.y>vert2.y)
        {
            return vert1.y > localPoint.y && localPoint.y > vert2.y;
        }
        else
        {
            return vert2.y>localPoint.y && localPoint.y>vert1.y;
        }
        
    }
}