【UNITY】原生Text组件实现超链接并且添加超链接下划线的一种实现方式
·
1.使用举例
参考链接
参考链接中的代码好像无法处理多个超链接以及标签嵌套、点击对象包含空格的问题,在本文中已经优化。


更加复杂的超链接文本


2.实现代码
using Framework;
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEngine.UI;
public class HyperlinkText: Text, IPointerClickHandler
{
/// <summary>
/// 超链接信息类,[startIndex,endIndex)
/// </summary>
private class HyperlinkInfo
{
/// <summary>
/// 渲染开始的下标
/// </summary>
public int startIndex;
/// <summary>
/// 结束的下标
/// </summary>
public int endIndex;
public string url;
/// <summary>
/// 所有的包围盒,换行的话会有多个
/// </summary>
public readonly List<Rect> boxes = new List<Rect>();
}
/// <summary>
/// 文本构造器
/// </summary>
protected readonly StringBuilder s_TextBuilder = new StringBuilder();
/// <summary>
/// 超链接信息列表
/// </summary>
private readonly List<HyperlinkInfo> m_HrefInfos = new List<HyperlinkInfo>();
//计算定点信息的缓存数组
private readonly UIVertex[] m_TempVerts = new UIVertex[4];
/// <summary>
/// 超链接正则
/// </summary>
private static readonly Regex s_HrefRegex = new Regex(@"<url='([^']*)'>(.*?)</url>", RegexOptions.Singleline);
/// <summary>
/// 点击超链接的事件
/// </summary>
public Action<string> onHyperlinkClick;
/// <summary>
/// 超链接文本颜色
/// </summary>
public static Color32 innerTextColor = new Color32(0, 0, 255, 255);
/// <summary>
/// 超链接下划线颜色
/// </summary>
public static Color32 innerLineColor = new Color32(0,0,255,255);
/// <summary>
/// 下划线的厚度
/// </summary>
public static float width = 8;
/// <summary>
/// 下划线偏移位置
/// </summary>
public static float innerLineOffset = 0.15f;
/// <summary>
/// 解析完最终的文本
/// </summary>
private string m_OutputText;
public override void SetVerticesDirty()
{
base.SetVerticesDirty();
m_OutputText = GetOutputText(text);
}
/// <summary>
/// 构建mesh
/// </summary>
protected override void OnPopulateMesh(VertexHelper toFill)
{
//为了使Inspector面板显示正确文本
var orignText = m_Text;//记录原始文本
m_Text = m_OutputText;//更换要展示的文本
base.OnPopulateMesh(toFill);//生成需要展示的文本的网格
m_Text = orignText;//恢复原始文本,使得Inspector面板正确显示
UIVertex vert = new UIVertex();//获取顶点信息
// 处理超链接包围框
foreach (var hrefInfo in m_HrefInfos)
{
hrefInfo.boxes.Clear();
if (hrefInfo.startIndex * 4 >= toFill.currentVertCount)//每个字符四个顶点组成,toFill.currentVertCount是去除标签后实际上渲染的字数*4
{
continue;
}
toFill.PopulateUIVertex(ref vert, hrefInfo.startIndex * 4); //获取超链接文本第一个字符的第一个顶点的位置
var pos = vert.position;
var bounds = new Bounds(pos, Vector3.zero);//并以此位置初始化一个包围盒
//遍历超链接文本的所有顶点
for (int i = hrefInfo.startIndex * 4, m = hrefInfo.endIndex * 4; i < m; i++)
{
if (i >= toFill.currentVertCount)
{
break;
}
toFill.PopulateUIVertex(ref vert, i);//获取这个顶点的信息
vert.color = innerTextColor;//修改颜色
toFill.SetUIVertex(vert, i);//重新写会修改的信息
pos = vert.position;//更新当前顶点的位置
bool needEncapsulate = true;//是否需要加入包围盒
if (i > 4 && (i - hrefInfo.startIndex * 4) % 4 == 0)//检查是否换行(只在字符的第一个顶点检测)
{
UIVertex lastV = new UIVertex();//前一个字符的顶点数据
toFill.PopulateUIVertex(ref lastV, i - 4);
var lastPos = lastV.position;//上一个顶点位置
if (pos.x < lastPos.x && pos.y < lastPos.y) //换行判断
{
hrefInfo.boxes.Add(new Rect(bounds.min, bounds.size));//保存当前行的包围盒
bounds = new Bounds(pos, Vector3.zero);//在新一行创建一个包围盒
needEncapsulate = false;
}
}
if (needEncapsulate)
{
bounds.Encapsulate(pos); //扩展包围盒
}
}
hrefInfo.boxes.Add(new Rect(bounds.min, bounds.size));//保存最终的包围盒
}
DrwaUnderLine(toFill);
}
/// <summary>
/// 画超链接的下划线
/// </summary>
protected virtual void DrwaUnderLine(VertexHelper toFill)
{
Vector2 extents = rectTransform.rect.size;//获取文本组件的矩形尺寸
var settings = GetGenerationSettings(extents);//获取文本配置信息
TextGenerator underlineText = new TextGenerator();
underlineText.Populate("——", settings);//作为下划线模板
IList<UIVertex> tut = underlineText.verts;//四个顶点数据
foreach (var hrefInfo in m_HrefInfos)
{
if (hrefInfo.startIndex * 4 >= toFill.currentVertCount)
{
continue;
}
for (int i = 0; i < hrefInfo.boxes.Count; i++)
{
//计算下划线的位置
Vector3[] ulPos = new Vector3[4];
float underlineY = hrefInfo.boxes[i].yMin - fontSize *innerLineOffset;
ulPos[0] = new Vector3(hrefInfo.boxes[i].xMin, underlineY);
ulPos[1] = new Vector3(hrefInfo.boxes[i].xMax, underlineY);
ulPos[2] = new Vector3(hrefInfo.boxes[i].xMax, underlineY + width);
ulPos[3] = new Vector3(hrefInfo.boxes[i].xMin, underlineY + width);
//构建下划线
for (int j = 0; j < 4; j++)
{
m_TempVerts[j] = tut[j];
m_TempVerts[j].color = innerLineColor;
m_TempVerts[j].position = ulPos[j];
}
toFill.AddUIVertexQuad(m_TempVerts);//添加到网格
}
}
}
/// <summary>
/// 获取超链接解析后的最后输出文本
/// </summary>
/// <returns></returns>
protected virtual string GetOutputText(string outputText)
{
s_TextBuilder.Length = 0;
m_HrefInfos.Clear();
//处理到的原始字符串的下标位置
int indexText = 0;
//渲染开始的下标位置
int StartIndex = 0;
foreach (Match match in s_HrefRegex.Matches(outputText))
{
//这个url到前面匹配的url之间的原始字符串,如何前面没有匹配项就是该url前面的原始字符串
string appendStr = outputText.Substring(indexText, match.Index - indexText);
//这个字符串不包含标签的字符串,也就是能够真正展示的字符串(此时包含空格和回车)
var notagStr = RemoveRichTextTags(appendStr);
//添加到输出字符串
s_TextBuilder.Append(appendStr);
//因为空格会占一个字符串位置,但是不会渲染顶点,所以在顶点处理时要去掉
//获取空格数量
int spacecount = GetSpaceCount(notagStr);
StartIndex =StartIndex+notagStr.Length-spacecount;
//去除匹配项内的标签
string trueStr = RemoveRichTextTags(match.Groups[2].Value);
//超链接标签内的字符串的空格数量
int urlTagSpaceCount = GetSpaceCount(trueStr);
var hrefInfo = new HyperlinkInfo
{
startIndex = StartIndex, // 超链接里的文本起始顶点索引
endIndex = StartIndex + (trueStr.Length) - urlTagSpaceCount,
url = match.Groups[1].Value
};
m_HrefInfos.Add(hrefInfo);
s_TextBuilder.Append(match.Groups[2].Value);
StartIndex += trueStr.Length- urlTagSpaceCount;
indexText = match.Index + match.Length;
}
//添加剩下的字符串
s_TextBuilder.Append(outputText.Substring(indexText, outputText.Length - indexText));
return s_TextBuilder.ToString();
}
/// <summary>
/// 处理一个原始字符串,去掉标签,得到会被展示的字符
/// </summary>
public static string RemoveRichTextTags(string input)
{
if (string.IsNullOrEmpty(input))
return input;
StringBuilder result = new StringBuilder();
int index = 0;
int len = input.Length;
while (index < len)
{
if (input[index] == '<')
{
// 尝试查找下一个 '>' 的位置
int endIndex = input.IndexOf('>', index + 1);
if (endIndex == -1) // 没有闭合的 '>'
{
result.Append(input[index]); // 将 '<' 视为普通字符
index++;
continue;
}
// 提取标签内容(包括 '<' 和 '>')
string tag = input.Substring(index, endIndex - index + 1);
if (IsValidTag(tag))
{
// 跳过合法标签
index = endIndex + 1;
continue;
}
else
{
// 非法标签,将 '<' 视为普通字符
result.Append(input[index]);
index++;
continue;
}
}
else
{
// 普通字符直接添加
result.Append(input[index]);
index++;
}
}
return result.ToString();
}
/// <summary>
/// 是否是合法的标签
/// </summary>
/// <param name="tag"></param>
/// <returns></returns>
private static bool IsValidTag(string tag)
{
// 最小标签长度检查(如 <b> 长度为3)
if (tag.Length < 3)
return false;
// 检查结束标签(如 </i>)
if (tag.StartsWith("</"))
{
// 必须形如 </X>,长度至少4
if (tag[tag.Length - 1] != '>')
return false;
// 提取标签名(去除 </ 和 >)
string tagName = tag.Substring(2, tag.Length - 3);
// 标签名必须由纯字母组成
foreach (char c in tagName)
{
if (!char.IsLetter(c))
return false;
}
return tagName.Length > 0; // 标签名不能为空
}
// 检查开始标签(如 <color=red>)
else
{
// 标签名首字符必须是字母
if (!char.IsLetter(tag[1]))
return false;
// 检查标签名部分(直到遇到空格/属性或结束符)
int pos = 1;
while (pos < tag.Length - 1 && char.IsLetter(tag[pos]))
{
pos++;
}
// 标签名长度至少为1,且必须以 '>' 结尾
return pos > 1 && tag[tag.Length - 1] == '>';
}
}
/// <summary>
/// 获取一个字符串中空格和回车的数量
/// </summary>
private static int GetSpaceCount(string str)
{
int Count = 0;
foreach (char c in str)
{
if (c == ' '|| c == '\n' || c == '\r')
Count++;
}
return Count;
}
/// <summary>
/// 点击事件检测是否点击到超链接文本
/// </summary>
/// <param name="eventData"></param>
public void OnPointerClick(PointerEventData eventData)
{
Vector2 lp = Vector2.zero;
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.url);
URLHandler.OpenURL(hrefInfo.url);
return;
}
}
}
}
#if UNITY_EDITOR
private void OnDrawGizmos()
{
Gizmos.color = Color.cyan;
foreach (var item in m_HrefInfos)
{
var boxs = item.boxes;
foreach (var box in boxs)
{
Vector3 ld = new Vector3(box.xMin, box.yMin) + transform.position;
Vector3 lt = new Vector3(box.xMin, box.yMax) + transform.position;
Vector3 rd = new Vector3(box.xMax, box.yMin) + transform.position;
Vector3 rt = new Vector3(box.xMax, box.yMax) + transform.position;
Gizmos.DrawLine(ld, lt);
Gizmos.DrawLine(ld, rd);
Gizmos.DrawLine(rd, rt);
Gizmos.DrawLine(lt, rt);
}
}
}
#endif
}
更多推荐



所有评论(0)