为什么会有LayoutRebuilder.ForceRebuildLayoutImmediate(RectTransform)接口? 什么时候用这个接口?

我们在某一帧的Update或者EventInput中执行了一段代码, 这段代码会修改某一UGUI组件的某些属性,我们知道,当一个UGUI组件的属性发生变化时,这个UGUI组件下面的所有UGUI组件的属性也会连带着发生变化. 但是在一帧内,这种变化不是真的同时发生的,UGUI的数据更新发生在LateUpdate之后,OnPreCull之前. 这是笔者还亲自验证的. 具体验证过程在这篇博客中.也就是说Unity会先执行完所有的代码逻辑, 然后再去更新这些UGUI组件的属性, 所以在这一帧中我们执行的修改UGUI组件属性的操作,只有在下一帧时,我们才能够拿到这个组件下UGUI组件的属性的正确值.
如果我们想在当前这一帧就获得正确的UGUI组件的属性,那么LayoutRebuilder.ForceRebuildLayoutImmediate(RectTransform)接口就派上用场了. 这个接口会强制更新传入UGUI及其子UGUI的RectTransform组件的属性, 然后我们就可以在当前帧获得正确的属性值了. 在笔者的开发经验中, 该接口一般会在使用UGUI里的Layout相关组件的时候比较常用.

为什么ForceRebuildLayoutImmediate开销十分大

几个LayoutGroup组件的实现原理

一个例子就是让父UI组件的尺寸跟着子UI组件的尺寸变化
https://blog.csdn.net/yhx956058885/article/details/130729325

Coffee UIEffect这个UI扩展库 里面的UIShadow组件上的值的实现原理和不同是什么? Outline和Outline8的区别? Fit还有一个什么忘了的区别?

Screen接口中的width和height 和 currentResolution的区别?

让UI变灰色

给UI要变灰的UI组件挂载一个变灰材质

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
Shader "UI/Gray"
{
Properties
{
[PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
_Color ("Tint", Color) = (1,1,1,1)

_StencilComp ("Stencil Comparison", Float) = 8
_Stencil ("Stencil ID", Float) = 0
_StencilOp ("Stencil Operation", Float) = 0
_StencilWriteMask ("Stencil Write Mask", Float) = 255
_StencilReadMask ("Stencil Read Mask", Float) = 255

_ColorMask ("Color Mask", Float) = 15

[Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
}

SubShader
{
Tags
{
"Queue"="Transparent"
"IgnoreProjector"="True"
"RenderType"="Transparent"
"PreviewType"="Plane"
"CanUseSpriteAtlas"="True"
}

Stencil
{
Ref [_Stencil]
Comp [_StencilComp]
Pass [_StencilOp]
ReadMask [_StencilReadMask]
WriteMask [_StencilWriteMask]
}

Cull Off
Lighting Off
ZWrite Off
ZTest [unity_GUIZTestMode]
Blend SrcAlpha OneMinusSrcAlpha
ColorMask [_ColorMask]

Pass
{
Name "Default"
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 2.0

#include "UnityCG.cginc"
#include "UnityUI.cginc"

#pragma multi_compile __ UNITY_UI_CLIP_RECT
#pragma multi_compile __ UNITY_UI_ALPHACLIP

struct appdata_t
{
float4 vertex : POSITION;
float4 color : COLOR;
float2 texcoord : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct v2f
{
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
float2 texcoord : TEXCOORD0;
float4 worldPosition : TEXCOORD1;
UNITY_VERTEX_OUTPUT_STEREO
};

sampler2D _MainTex;
fixed4 _Color;
fixed4 _TextureSampleAdd;
float4 _ClipRect;
float4 _MainTex_ST;

v2f vert(appdata_t v)
{
v2f OUT;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
OUT.worldPosition = v.vertex;
OUT.vertex = UnityObjectToClipPos(OUT.worldPosition);

OUT.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);

OUT.color = v.color * _Color;
return OUT;
}

fixed4 frag(v2f IN) : SV_Target
{
half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;

#ifdef UNITY_UI_CLIP_RECT
color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
#endif

#ifdef UNITY_UI_ALPHACLIP
clip (color.a - 0.001);
#endif
// 色彩变灰 亮度提取矩阵
float gray = dot(color.xyz, float3(0.299, 0.587, 0.114));
color.xyz = float3(gray, gray, gray);
return color;
}
ENDCG
}
}
}

UGUI实现文字边缘发光效果

Unity实现Text部分区域可响应点击

源码

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

namespace UI.UIItems
{
public class HyperlinkText : Text, IPointerClickHandler
{
protected override void Awake()
{
Set_TextLinkFuncCB(Application.OpenURL);
}
/// <summary>
/// 解析完最终的文本
/// </summary>
private string m_OutputText;

/// <summary>
/// 超链接信息列表
/// </summary>
private readonly List<HrefInfo_> m_HrefInfos = new List<HrefInfo_>();

/// <summary>
/// 文本构造器
/// </summary>
protected static readonly StringBuilder s_TextBuilder = new StringBuilder();

/// <summary>
/// 超链接正则
/// </summary>
private static readonly Regex s_HrefRegex =
new Regex(@"<a href=([^>\n\s]+)>(.*?)(</a>)", RegexOptions.Singleline);

//
private static readonly Regex s_VertexFilter = new Regex(@"(|[ \n\r\t]+)", RegexOptions.Singleline);

VertexHelper _toFill = null;
/// <summary>
/// 是否使用超链接 默认未False
/// </summary>
bool bool_IsLink = true;

private Action<string> linkFunc_Cb = null;

private RectTransform rect_Parent;
private RectTransform Rect_Parent
{
get
{
if (rect_Parent == null)
{
Transform trans = this.transform.parent != null ? this.transform.parent.transform : this.transform;
rect_Parent = trans.GetComponent<RectTransform>();
}

return rect_Parent;
}
}

//设置 文本 超链接的点击回调事件
public void Set_TextLinkFuncCB(Action<string> linkFunc_Cb)
{
bool_IsLink = true;
if (this.linkFunc_Cb != null)
{
this.linkFunc_Cb = null;
}
this.linkFunc_Cb = linkFunc_Cb;
OnPopulateMesh(_toFill);
}

//字符顶点数
const int perCharVerCount = 4;

/// <summary>
/// 文本构造器
/// </summary>
protected static readonly StringBuilder textRebuild = new StringBuilder();

protected override void OnPopulateMesh(VertexHelper toFill)
{
if (toFill == null)
{
return;
}
_toFill = toFill;
//TODO 编辑器状态下这里不执行, 方便调试看到效果用
if (!bool_IsLink)
{
m_Text = GetOutputText_Nomal(text);
base.OnPopulateMesh(toFill);
return;
}

var orignText = m_Text;
m_OutputText = GetOutputText_Init(text);
m_Text = m_OutputText;
text = m_OutputText;
base.OnPopulateMesh(toFill);
m_Text = orignText;
GetOutputText(text, toFill.currentVertCount);

UIVertex vert = new UIVertex();

// 处理超链接包围框
foreach (var hrefInfo in m_HrefInfos)
{
hrefInfo.boxes.Clear();
if (hrefInfo.startIndex >= toFill.currentVertCount)
{
continue;
}

// 将超链接里面的文本顶点索引坐标加入到包围框
toFill.PopulateUIVertex(ref vert, hrefInfo.startIndex);
var pos = vert.position;
var bounds = new Bounds(pos, Vector3.zero);
Vector3 previousPos = Vector3.zero;
for (int i = hrefInfo.startIndex, m = hrefInfo.endIndex; i < m; i++)
{
if (i >= toFill.currentVertCount)
{
break;
}

toFill.PopulateUIVertex(ref vert, i);
pos = vert.position;
if ((i - hrefInfo.startIndex) % 4 == 1)
{
previousPos = pos;
}
if (previousPos != Vector3.zero && (i - hrefInfo.startIndex) % 4 == 0 && pos.x < previousPos.x) // 换行重新添加包围框
{
hrefInfo.boxes.Add(new Rect(bounds.min, bounds.size));
bounds = new Bounds(pos, Vector3.zero);
}
else
{
bounds.Encapsulate(pos); // 扩展包围框
}
}
hrefInfo.boxes.Add(new Rect(bounds.min, bounds.size));
}


if (this.gameObject.activeInHierarchy)
{
StartCoroutine(RefrehLayout());
}
}

IEnumerator RefrehLayout()
{
yield return new WaitForEndOfFrame();
LayoutRebuilder.ForceRebuildLayoutImmediate(Rect_Parent);
}

//初始化超链接文本 获取最终结果的定点数用
string GetOutputText_Init(string outputText)
{
s_TextBuilder.Length = 0;
m_HrefInfos.Clear();
var indexText = 0;
foreach (Match match in s_HrefRegex.Matches(outputText))
{
s_TextBuilder.Append(outputText.Substring(indexText, match.Index - indexText));
s_TextBuilder.Append(match.Groups[2].Value);
indexText = match.Index + match.Length;
}
s_TextBuilder.Append(outputText.Substring(indexText, outputText.Length - indexText));
return s_TextBuilder.ToString();
}

/// <summary>
/// 获取超链接解析后的最后输出文本
/// </summary>
/// <returns></returns>
string GetOutputText(string outputText, int currentVertCount)
{
s_TextBuilder.Length = 0;
m_HrefInfos.Clear();
var indexText = 0;
int vertCount = Regex.Replace(Regex.Replace(outputText.ToString(), @"\s", ""), @"<(.*?)>", "").Length * 4;
int vercCount_Offset_Start = 0;
int vercCount_Offset_End = 0;
bool isLineCup = false;
if (currentVertCount > vertCount)
{
isLineCup = true;
vercCount_Offset_Start = 80;
vercCount_Offset_End = 88;
}
foreach (Match match in s_HrefRegex.Matches(outputText))
{
s_TextBuilder.Append(outputText.Substring(indexText, match.Index - indexText));
int offset_Len = 0;
if (isLineCup)
{
offset_Len = (s_TextBuilder.Length - Regex.Replace(s_TextBuilder.ToString(), @"<(.*?)>", "").Length) * 4;
}

s_TextBuilder.Append("<i><b><color=#f49037>"); // 超链接颜色

var str = Regex.Replace(s_TextBuilder.ToString(), @"\s", "");
var group = match.Groups[1];
var hrefInfo = new HrefInfo_
{
startIndex = Regex.Replace(str, @"<(.*?)>", "").Length * 4 + vercCount_Offset_Start + offset_Len, // 超链接里的文本起始顶点索引
endIndex = (Regex.Replace(str, @"<(.*?)>", "").Length +
Regex.Replace(Regex.Replace(match.Groups[2].ToString(), @"\s", "")
, @"<(.*?)>", "").Length - 1) * 4 + 3 + vercCount_Offset_End + offset_Len,
name = group.Value
};
m_HrefInfos.Add(hrefInfo);
//Debug.Log($"顶点信息,开始的:{hrefInfo.startIndex},结束的:{hrefInfo.endIndex}");

s_TextBuilder.Append(match.Groups[2].Value);
s_TextBuilder.Append("</color></b></i>");
indexText = match.Index + match.Length;
}

s_TextBuilder.Append(outputText.Substring(indexText, outputText.Length - indexText));
return s_TextBuilder.ToString();
}

//获取祛除掉超链接 保留普通文本 保证配置里超链接标签的文本 在其他地方也可以正常使用,只有调用了超链接初始化的 才会给超链接形式的文本
string GetOutputText_Nomal(string outputText)
{
s_TextBuilder.Length = 0;
m_HrefInfos.Clear();
var indexText = 0;
MatchCollection matchs = s_HrefRegex.Matches(outputText);
if (matchs.Count <= 0)
{
return outputText;
}
foreach (Match match in matchs)
{
s_TextBuilder.Append(outputText.Substring(indexText, match.Index - indexText));
s_TextBuilder.Append(match.Groups[2].Value);
indexText = match.Index + match.Length;
}
s_TextBuilder.Append(outputText.Substring(indexText, outputText.Length - indexText));
return s_TextBuilder.ToString();
}

public void OnPointerClick(PointerEventData eventData)
{
Vector2 lp;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
rectTransform, eventData.position, eventData.pressEventCamera, out lp);

foreach (var hrefInfo in m_HrefInfos)
{
var boxes = hrefInfo.boxes;
for (var i = 0; i < boxes.Count; ++i)
{
if (boxes[i].Contains(lp))
{
//Debug.Log("技能 超链接 点击了:" + hrefInfo.name);
linkFunc_Cb?.Invoke(hrefInfo.name);
return;
}
}
}
}
}
/// <summary>
/// 超链接信息类
/// </summary>
class HrefInfo_
{
public int startIndex;

public int endIndex;

public string name;

public readonly List<Rect> boxes = new List<Rect>();
}
}

使用

1
2
3
4
5
var terms2 = $"<color=#aa3900><a herf={PrivacyPolicyDefine.UrlService}>{ScriptLocalization.terms_of_use}</a></color>";

var policy2 = $"<color=#aa3900><a herf={PrivacyPolicyDefine.UrlPrivacy}>{ScriptLocalization.privacy_policy}</a></color>";

_ui.contentText.text = string.Format(ScriptLocalization.terms_policy_dlg_content, terms2, policy2);