项目要实现圆形头像,用继承Image组件重写部分方法实现圆形头像 并且实现精准点击
实现原理:
①:Image如何绘制到屏幕上
cpu准备顶点数据–>GPU渲染显示,所以显示图片,需要顶点和三角形数据,由顶点组成三角形 三角形组成图形
②:如何实现:
重写 OnPopulateMesh(VertexHelper vh) 方法 ,因为没有真正的圆形,圆形是由n个三角形组成,重新计算要渲染的定点和三角形个数,添加到对应的顶点/三角形数组中。
具体代码:
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Sprites;
[AddComponentMenu("UI/CircleImage", 12)]//在属性面板添加组件
public class CircleImage : Image
{
private readonly Color32 GRAY_COLOR = new Color32(60, 60, 60, 255);//灰色
/// <summary>
///圆形由多少块三角形拼成
/// </summary>
public int segements = 100;
/// <summary>
/// 显示部分占圆形的百分比
/// </summary>
[SerializeField]
public float showPercent = 1;
private List<Vector3> _vertexList=new List<Vector3>();//图形上所有的顶点几何
public bool accurateRaycast = false;//准确的射线检测
//生成UI网格数据 可以重写 自定义定点信息
protected override void OnPopulateMesh(VertexHelper vh)
{
//计算图形所有定点信息 添加到VertexHelper中 由底层渲染
vh.Clear();//清理数据
_vertexList.Clear();//清理缓存数据
//自己生成定点数据
AddVertex(vh);
AddTriangle(vh);
}
/// <summary>
/// 添加顶点数据
/// </summary>
private void AddVertex(VertexHelper vh)
{
//获取图形的宽和高
float width = rectTransform.rect.width;
float height = rectTransform.rect.height;
int realSegements = (int)(segements * showPercent);//真实要显示三角形的个数
Vector4 uv = overrideSprite != null ? DataUtility.GetInnerUV(overrideSprite) : Vector4.zero;//获取sprite uv
//计算uv宽和高
float uvWidth = uv.z - uv.x;
float uvHeight = uv.w - uv.y;
Vector2 uvCenter = new Vector2((uv.x + uv.z) * 0.5f, (uv.y + uv.w) * 0.5f);//uv中心点坐标
Vector2 convertRatio = new Vector2(uvWidth / width, uvHeight / height);//uv和UI坐标的转化率
//计算每块对应的弧度和计算半径
float radian = (2 * Mathf.PI) / segements;//弧度 一个整圆弧度是2π
float radius = width * 0.5f;//半径
//计算每个三角形定点坐标 1.计算圆心点信息(所有三角形公用顶点) 2.由圆心顶点计算三角形其他顶点信息 利用sin@ cos@ 函数
Vector2 originPos = new Vector2((0.5f - rectTransform.pivot.x) * width, (0.5f - rectTransform.pivot.y) * height);//圆心顶点UI坐标 默认轴心点(0.5,0.5) 默认坐标(0,0)
Vector2 originuvPos = Vector2.zero;//圆心顶点uv坐标
Color32 colorTemp = GetOriginColor();
UIVertex origin = GetUIVertex(colorTemp, originPos, originuvPos, uvCenter, convertRatio); //圆心的定点信息
vh.AddVert(origin);
int vertexCount = realSegements + 1;//真实顶点个数
if (showPercent == 0)//显示比例0 要显示顶点个数==0
vertexCount = 0;
float curRadian = 0;//当前的弧度
Vector2 posTermp = Vector2.zero;
for (int i = 0; i <segements+1; i++)//显示所有顶点
{
float x = Mathf.Cos(curRadian) * radius;
float y = Mathf.Sin(curRadian) * radius;
curRadian += radian;//计算下个弧度
if (i <vertexCount)
{
colorTemp = color;
}
else//不显示部分 顶点颜色设置变灰
{
colorTemp = GRAY_COLOR;
}
posTermp = new Vector2(x, y);//设置顶点UI坐标
UIVertex vertexTemp = GetUIVertex(colorTemp, posTermp + originPos, posTermp, uvCenter, convertRatio);//计算下个顶点
vh.AddVert(vertexTemp);
if(accurateRaycast)
_vertexList.Add(posTermp + originPos);
}
}
private void AddTriangle(VertexHelper vh)
{
//三角形的定点按顺时针添加 因为GPU默认背面剔除 逆时针被认为 是背面
int id = 1;
for (int i = 0; i < segements; i++)
{
if (id == segements)//最后一个点 首尾顶点相连
{
vh.AddTriangle(id, 0, 1);
}
else
{
vh.AddTriangle(id, 0, id + 1);
}
id++;
}
}
/// <summary>
/// 得到圆心顶点的颜色
/// </summary>
/// <returns></returns>
private Color32 GetOriginColor()
{
Color32 colorTemp = (Color.white - GRAY_COLOR) * showPercent;
colorTemp= new Color32((byte)(GRAY_COLOR.r + colorTemp.r), (byte)(GRAY_COLOR.g + colorTemp.g), (byte)(GRAY_COLOR.b + colorTemp.b), 255);
return colorTemp;
}
/// <summary>
/// 获取UV顶点
/// </summary>
private UIVertex GetUIVertex(Color32 col, Vector3 pos, Vector2 uvPos, Vector2 uvCenter, Vector2 uvScale)
{
UIVertex vertex = new UIVertex();
vertex.color = col;//设置顶点颜色
vertex.position = pos;//设置顶点位置
//计算顶点位置对应的图片的uv坐标--顶点pos--uv==》转化公式 :uv.x=pos.x*ratio.x+uvCenter.x --》uvCenter.x 防止轴心点改变 图片偏移
vertex.uv0 = new Vector2(uvPos.x * uvScale.x + uvCenter.x, uvPos.y * uvScale.y + uvCenter.y);//设置顶点uv
return vertex;
}
/// <summary>
/// 重写射线检测方法
/// </summary>
public override bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)
{
if (accurateRaycast)//开启精准射线检测
{
//将点击屏幕的点-->局部坐标
Vector2 clickPos = Vector2.zero;
RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, screenPoint, eventCamera, out clickPos);
//判断点击的点 是否有效
//算法原理:图形上相邻的2点组成一条直线 判断点击的点向右发射的射线 与直线相交点 是不是奇数 奇数代表 点在图形内部 偶数代表 点在图形外部
bool flag = GetCrossPointNum(clickPos, _vertexList) % 2 == 1 ? true : false;
return flag;
}
else//默认射线检测
{
return base.IsRaycastLocationValid(screenPoint, eventCamera);
}
}
/// <summary>
/// 得到点与直线相交点的个数
/// </summary>
/// <param name="clickPos">点击屏幕的点的局部坐标</param>
/// <param name="_vertexList">组成的图形的所有点(除了圆形点)</param>
/// <returns></returns>
private int GetCrossPointNum(Vector2 clickPos, List<Vector3> _vertexList)
{
int crossCount = 0; //相交点个数
Vector3 vert1 = Vector3.zero;
Vector3 vert2 = Vector3.zero;
int vertexCount = _vertexList.Count;//组成图形顶点个数
//遍历所有顶点 找所有到由相邻2个点组成的直线
for (int i = 0; i < vertexCount; i++)
{
vert1 = _vertexList[i];
vert2 = _vertexList[(i+1)% vertexCount];//取余 防止越界 和 点收尾相连
if (IsYInRang(clickPos, vert1, vert2))//点击的点 在直线Y轴范围内
{
float x = GetX(vert1, vert2, clickPos.y);
if (clickPos.x < x)//满足向右的射线
{
crossCount++;
}
}
}
return crossCount;
}
/// <summary>
/// 点击的点是否在Y轴有效范围内
/// </summary>
private bool IsYInRang(Vector2 clickPos, Vector3 vert1, Vector3 vert2)
{
if (vert1.y > vert2.y)
{
return clickPos.y < vert1.y && clickPos.y > vert2.y;
}
else
{
return clickPos.y>vert1.y && clickPos.y< vert2.y;
}
}
/// <summary>
/// 得到相交的坐标的X点 Y点已知
/// </summary>
private float GetX(Vector3 vert1, Vector3 vert2, float y)
{
//根据直线公式 斜截式:y=k*x+b k=(y1-y2)/(x1-x2)-->k已知 又已知一点(x1,y1) 和y--》可求x
float k = (vert1.x - vert2.x) / (vert1.y - vert2.y);//计算斜率k
//有2点 (x,y) (vert2.x,vert2.y)
float x = (k / (y - vert2.y)) + vert2.x;
return x;
}
}