优化一下之前的代码,封装成几个API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
using UnityEngine;
using UnityEngine.Sprites;
using UnityEngine.UI;

public class CircleImage : Image
{
/// <summary>
/// 三角面个数
/// </summary>
[SerializeField] private int segments = 100;
[SerializeField] private float showPercent = 1;
private readonly Color32 GRAY_COLOR = new Color32(60, 60, 60, 255);
protected override void OnPopulateMesh(VertexHelper vh)
{
vh.Clear();
AddVertex(vh, segments);
AddTriangle(vh, segments);
}
private void AddVertex(VertexHelper vh,int segments)
{
float width = rectTransform.rect.width;
float height = rectTransform.rect.height;
int realSegment = (int)(segments * showPercent);

Vector4 uv = overrideSprite != null ? DataUtility.GetOuterUV(overrideSprite) : Vector4.zero;
float uvWidth = uv.z - uv.x;
float uvHeight = uv.w - uv.y;
Vector2 uvCenter = new Vector2(uvWidth * 0.5f, uvHeight * 0.5f);
Vector2 convertRatio = new Vector2(uvWidth / width, uvHeight / height);

float radian = (2 * Mathf.PI) / segments;
float radius = width * 0.5f;

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

Color32 colorTemp = GetOriginColor();
UIVertex origin = GetUIVertex(colorTemp, originPos, vertPos, uvCenter, convertRatio);
vh.AddVert(origin);

int vertexCount = realSegment + 1;
float curRadian = 0;
Vector2 posTemp;//顶点的坐标,用于计算uv,不受pivot和rectTrans的影响
for (int i = 0; i < segments + 1; i++)//每次都要绘制全部
{
float x = Mathf.Cos(curRadian) * radius;
float y = Mathf.Sin(curRadian) * radius;
curRadian += radian;

if (i < vertexCount - 1)//在指定绘制范围内的顶点,保持原色
{
colorTemp = color;
}
else if (showPercent >= 1)//防止绘制范围为全图时,最后一个顶点没有保持原色
{
colorTemp = color;
}
else//在指定绘制范围外的
{
colorTemp = GRAY_COLOR;
}
posTemp = new Vector2(x, y);//顶点的坐标,用于计算uv,不受pivot和rectTrans的影响
UIVertex vertexTemp = GetUIVertex(colorTemp, posTemp + originPos, posTemp, uvCenter, convertRatio);
vh.AddVert(vertexTemp);
}
}
private Color32 GetOriginColor()//将补色加上,防止出现半透明颜色
{
Color32 colorTemp = (Color.white - GRAY_COLOR) * showPercent;
return new Color32(
(byte)(GRAY_COLOR.r + colorTemp.r),
(byte)(GRAY_COLOR.g + colorTemp.g),
(byte)(GRAY_COLOR.b + colorTemp.b),
255);
}
private void AddTriangle(VertexHelper vh, int realSegments)
{
int id = 1;
for (int i = 0; i < realSegments; i++)
{
vh.AddTriangle(id, 0, id + 1);
id++;
}
}
private UIVertex GetUIVertex(Color32 color, Vector3 pos, Vector2 uvPos,Vector2 uvCenter,Vector2 uvScale)
{
UIVertex vertexTemp = new UIVertex();
vertexTemp.color = color;
vertexTemp.position = pos;
vertexTemp.uv0 = new Vector2(uvPos.x * uvScale.x + uvCenter.x, uvPos.y * uvScale.y + uvCenter.y);
return vertexTemp;
}
}

注意GetOriginColor方法,这里是将补色给加上,上一节的代码其实是个半透明的颜色。

添加点击相应逻辑

目前我们只是实现了“画圆”,还没有实现点击逻辑。

控制响应区域

首先,我们需要实现一个算法,这个算法规定只有点击了“圆形”内部才能响应点击事件。即使是五边形,六边形,也是只有内部才能响应。这个算法的逻辑是我们从点击的位置发射一条(向右的)射线,如果和图形的边缘的交点是奇数个,说明点击的位置是在图形内部,如果是偶数个,就是在外部。

修改CircleImage,添加一个List,记录所有的边的顶点(圆心顶点除外)位置。注意,这里记录的是RectTransform的位置。

1
2
3
4
5
6
7
8
9
10
11
12
private List<Vector3> mVertexList;
//...
private void AddVertex(VertexHelper vh,int segments)
{
//...
for (int i = 0; i < segments + 1; i++)//每次都要绘制全部
{
//...
vh.AddVert(vertexTemp);
mVertexList.Add(posTemp + originPos);
}
}

然后添加如下方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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,mVertexList) % 2 == 1;
}
private int GetCrossPointNum(Vector2 localPoint,List<Vector3> vertexList)
{
int crossCount = 0;
Vector3 vert1 = Vector3.zero;
Vector3 vert2 = Vector3.zero;
int count = vertexList.Count;
for (int i = 0; i < count; i++)
{
vert1 = vertexList[i];
vert2 = vertexList[(i + 1) % count];//防止最后一个下标越界
if (IsYInRange(localPoint, vert1, vert2))
{
if (localPoint.x < GetX(vert1, vert2, localPoint.y))
{
crossCount++;
}
}
}
return crossCount;
}
private bool IsYInRange(Vector2 localPoint, Vector3 vert1, Vector3 vert2)
{
if(vert1.y > vert2.y)
{
return localPoint.y < vert1.y && localPoint.y > vert2.y;
}
else
{
return localPoint.y > vert1.y && localPoint.y < vert2.y;
}
}
private float GetX(Vector3 vert1, Vector3 vert2,float y)
{
float k = (vert1.y - vert2.y) / (vert1.x - vert2.x);//求得直线斜率
return vert1.x + (y - vert1.y) / k;//根据直线公式y = kx + b,已知斜率k,求y已知的情况下x的值
}

IsRaycastLocationValid是Image内部的虚方法,只有它返回true时才表示这个图片被Canvas的Graphic射线投射到了。

IsValid是判断点击位置向右与图形边缘的交点是不是奇数,是奇数就表示在图形内部了

GetCrossPointNum是计算交点数量的方法。

IsYInRange是判断点击位置的y值是否在当前计算的两个顶点y值之间的方法,如果点击位置没有在两个顶点之间,那么就没有计算交点的必要了。

GetX是点击位置向右的射线和图形边缘相交时,返回交点x坐标的方法,用到了直线公式y = kx + b。只要交点x坐标比点击位置x坐标大,就说明相交了。

此时我们点击图形,会发现图形边缘空白部分点击后,图形不会响应了(配合Button组件)。