UGUI表情系统&超链接解决方案

时间:2022-05-29 00:36:43

最近帮一个同事解决图文混排的问题,发现了一种犀利的UGUI表情系统的解决方案

https://blog.uwa4d.com/archives/Sparkle_UGUI.html

使用重新生成UGUI文字Mesh的方式来支持表情图片。在Shader中判断是否有第二个套UV传入来渲染表情,动态表情也在GPU端计算~ 可以合并DrawCall,支持UGUI的遮罩、自适应等等

我在原作者的基础上扩展了一些改进,使其能支持超链接

UGUI表情系统&超链接解决方案

超链接的思路是先计算出超链接的顶点包围盒,监听到点击事件的时候,跟超链接包围和进行碰撞检测来判断是否点击到了某个连接。如果一个超链接跨行的时候,就需要创建多个包围盒来处理。

如何解析超链接标签?

<a href='xx'>点击加入队伍</a>

在Text发生变化的时候,UGUI会调用SetVerticesDirty函数把组件注册到ReBuilder队列里面,等待下一帧重绘。所以我们在SetVerticesDirty函数中写上解析a标签的代码

    /// <summary>
/// 获取超链接解析后的最后输出文本
/// </summary>
/// <returns></returns>
protected virtual string GetOutputText(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("<color='#9ed7ff'>"); // 超链接颜色ff6600 var group = match.Groups[1];
var hrefInfo = new HrefInfo
{
startIndex = s_TextBuilder.Length * 4, // 超链接里的文本起始顶点索引
endIndex = (s_TextBuilder.Length + match.Groups[2].Length - 1) * 4 + 3,
name = group.Value
};
m_HrefInfos.Add(hrefInfo); s_TextBuilder.Append(match.Groups[2].Value);
s_TextBuilder.Append("</color>");
indexText = match.Index + match.Length;
} s_TextBuilder.Append(outputText.Substring(indexText, outputText.Length - indexText));
return s_TextBuilder.ToString();
}

通过正则表达式匹配后,计算出超链接的起始顶点索引、结束顶点索引、name保存到一个列表里。

链接跟图片索引冲突问题

text中的表情标签如"[0]"由3个字符组成,每个字符4个顶点,所以占用12个顶点

但是在填充顶点的时候,我们只会用4个顶点渲染图片,来替换掉原来的12个顶点

所以前面计算的超链接的startIndex,endIndex也要随之改变

    private void HrefInfosIndexAdjust(int imgIndex)
{
foreach (var hrefInfo in m_HrefInfos)//如果后面有超链接,需要把位置往前挪
{
if (imgIndex < hrefInfo.startIndex)
{
hrefInfo.startIndex -= 8;
hrefInfo.endIndex -= 8;
}
}
}

计算超链接的包围盒

        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);
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 (pos.x < bounds.min.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));
}

UGUI填充顶点的函数

UGUI的显示对象都是继承于Graphic的,Graphic中OnPopulateMesh函数用于填充顶点数据,然后传递到GPU渲染,这里也是通过重写该函数计算出图片的顶点坐标以及超链接的包围盒

		if (EmojiIndex == null) {
EmojiIndex = new Dictionary<string, EmojiInfo>(); //load emoji data, and you can overwrite this segment code base on your project.
TextAsset emojiContent = Resources.Load<TextAsset> ("emoji");
string[] lines = emojiContent.text.Split ('\n');
for(int i = 1 ; i < lines.Length; i ++)
{
if (! string.IsNullOrEmpty (lines [i])) {
string[] strs = lines [i].Split ('\t');
EmojiInfo info;
info.x = float.Parse (strs [3]);
info.y = float.Parse (strs [4]);
info.size = float.Parse (strs [5]);
info.len = 0;
EmojiIndex.Add (strs [1], info);
}
}
} //key是标签在字符串中的索引 Dictionary<int,EmojiInfo> emojiDic = new Dictionary<int, EmojiInfo> ();
if (supportRichText) {
MatchCollection matches = Regex.Matches (m_OutputText, "\\[[a-z0-9A-Z]+\\]");//把表情标签全部匹配出来
for (int i = 0; i < matches.Count; i++) {
EmojiInfo info;
if (EmojiIndex.TryGetValue (matches [i].Value, out info)) {
info.len = matches [i].Length;
emojiDic.Add (matches [i].Index, info);
}
}
} // We don't care if we the font Texture changes while we are doing our Update.
// The end result of cachedTextGenerator will be valid for this instance.
// Otherwise we can get issues like Case 619238.
m_DisableFontTextureRebuiltCallback = true; Vector2 extents = rectTransform.rect.size; var settings = GetGenerationSettings(extents);
var orignText = m_Text;
m_Text = m_OutputText;
cachedTextGenerator.Populate(m_Text, settings);//重置网格
m_Text = orignText; Rect inputRect = rectTransform.rect; // get the text alignment anchor point for the text in local space
Vector2 textAnchorPivot = GetTextAnchorPivot(alignment);
Vector2 refPoint = Vector2.zero;
refPoint.x = Mathf.Lerp(inputRect.xMin, inputRect.xMax, textAnchorPivot.x);
refPoint.y = Mathf.Lerp(inputRect.yMin, inputRect.yMax, textAnchorPivot.y); // Determine fraction of pixel to offset text mesh.
Vector2 roundingOffset = PixelAdjustPoint(refPoint) - refPoint; // Apply the offset to the vertices
IList<UIVertex> verts = cachedTextGenerator.verts;
float unitsPerPixel = 1 / pixelsPerUnit;
//Last 4 verts are always a new line...
int vertCount = verts.Count - 4; toFill.Clear();
if (roundingOffset != Vector2.zero)
{
for (int i = 0; i < vertCount; ++i)
{
int tempVertsIndex = i & 3;
m_TempVerts[tempVertsIndex] = verts[i];
m_TempVerts[tempVertsIndex].position *= unitsPerPixel;
m_TempVerts[tempVertsIndex].position.x += roundingOffset.x;
m_TempVerts[tempVertsIndex].position.y += roundingOffset.y;
if (tempVertsIndex == 3)
toFill.AddUIVertexQuad(m_TempVerts);
}
}
else
{
float repairDistance = 0;
float repairDistanceHalf = 0;
float repairY = 0;
if (vertCount > 0) {
repairY = verts [3].position.y;
}
for (int i = 0; i < vertCount; ++i) {
EmojiInfo info;
int index = i / 4;//每个字符4个顶点
if (emojiDic.TryGetValue (index, out info)) {//这个顶点位置是否为表情开始的index HrefInfosIndexAdjust(i);//矫正一下超链接的Index //compute the distance of '[' and get the distance of emoji
//计算表情标签2个顶点之间的距离, * 3 得出宽度(表情有3位)
float charDis = (verts [i + 1].position.x - verts [i].position.x) * 3;
m_TempVerts [3] = verts [i];//1
m_TempVerts [2] = verts [i + 1];//2
m_TempVerts [1] = verts [i + 2];//3
m_TempVerts [0] = verts [i + 3];//4 //the real distance of an emoji
m_TempVerts [2].position += new Vector3 (charDis, 0, 0);
m_TempVerts [1].position += new Vector3 (charDis, 0, 0); float fixWidth = m_TempVerts[2].position.x - m_TempVerts[3].position.x;
float fixHeight = (m_TempVerts[2].position.y - m_TempVerts[1].position.y);
//make emoji has equal width and height
float fixValue = (fixWidth - fixHeight);//把宽度变得跟高度一样
m_TempVerts [2].position -= new Vector3 (fixValue, 0, 0);
m_TempVerts [1].position -= new Vector3 (fixValue, 0, 0); float curRepairDis = 0;
if (verts [i].position.y < repairY) {// to judge current char in the same line or not
repairDistance = repairDistanceHalf;
repairDistanceHalf = 0;
repairY = verts [i + 3].position.y;
}
curRepairDis = repairDistance;
int dot = 0;//repair next line distance
for (int j = info.len - 1; j > 0; j--) {
int infoIndex = i + j * 4 + 3;
if (verts.Count > infoIndex && verts[infoIndex].position.y >= verts [i + 3].position.y) {
repairDistance += verts [i + j * 4 + 1].position.x - m_TempVerts [2].position.x;
break;
} else {
dot = i + 4 * j; }
}
if (dot > 0) {
int nextChar = i + info.len * 4;
if (nextChar < verts.Count) {
repairDistanceHalf = verts [nextChar].position.x - verts [dot].position.x;
}
} //repair its distance
for (int j = 0; j < 4; j++) {
m_TempVerts [j].position -= new Vector3 (curRepairDis, 0, 0);
} m_TempVerts [0].position *= unitsPerPixel;
m_TempVerts [1].position *= unitsPerPixel;
m_TempVerts [2].position *= unitsPerPixel;
m_TempVerts [3].position *= unitsPerPixel; float pixelOffset = emojiDic [index].size / 32 / 2;
m_TempVerts [0].uv1 = new Vector2 (emojiDic [index].x + pixelOffset, emojiDic [index].y + pixelOffset);
m_TempVerts [1].uv1 = new Vector2 (emojiDic [index].x - pixelOffset + emojiDic [index].size, emojiDic [index].y + pixelOffset);
m_TempVerts [2].uv1 = new Vector2 (emojiDic [index].x - pixelOffset + emojiDic [index].size, emojiDic [index].y - pixelOffset + emojiDic [index].size);
m_TempVerts [3].uv1 = new Vector2 (emojiDic [index].x + pixelOffset, emojiDic [index].y - pixelOffset + emojiDic [index].size); toFill.AddUIVertexQuad (m_TempVerts); i += 4 * info.len - 1;
} else {
int tempVertsIndex = i & 3;
if (tempVertsIndex == 0 && verts [i].position.y < repairY) {
repairY = verts [i + 3].position.y;
repairDistance = repairDistanceHalf;
repairDistanceHalf = 0;
}
m_TempVerts [tempVertsIndex] = verts [i];
m_TempVerts [tempVertsIndex].position -= new Vector3 (repairDistance, 0, 0);
m_TempVerts [tempVertsIndex].position *= unitsPerPixel;
if (tempVertsIndex == 3)
toFill.AddUIVertexQuad (m_TempVerts);
}
}
}

判断是否点中了链接

把屏幕坐标转换到Text中的坐标后,再进行检测

    /// <summary>
/// 点击事件检测是否点击到超链接文本
/// </summary>
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))
{ if (onHrefClick != null)
{
onHrefClick(hrefInfo.name);
}
Debug.Log("点击了:" + hrefInfo.name);
return;
}
}
}
}

可以在这里获取全部代码

https://github.com/lijia4423/EmojiText.git

参考:

https://blog.uwa4d.com/archives/Sparkle_UGUI.html

http://www.pudn.com/Download/item/id/3121697.html