项目需要用到该功能, 搜索和参考了很多文章,要么不支持富文本,要不没有下划线,要么是错误的,修修改改后满足我的需求,代码如下
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
namespace MyTool.Tools
{
/// <summary>
/// 文本控件支持超链接、下划线
/// </summary>
public class HyperlinkText : Text, IPointerClickHandler
{
public Action<string> onHyperlinkClick;
/// 超链接信息类
private class HyperlinkInfo
{
public int startIndex;
public int endIndex;
public string name;
public readonly List<Rect> boxes = new List<Rect>();
public List<int> linefeedIndexList = new List<int>();
}
/// 解析完最终的文本
private string m_OutputText;
/// 超链接信息列表
private readonly List<HyperlinkInfo> m_HrefInfos = new List<HyperlinkInfo>();
/// 文本构造器
protected StringBuilder s_TextBuilder = new StringBuilder();
[Tooltip("超链接文本颜色")]
[SerializeField] private Color32 innerTextColor = new Color32(36, 64, 180, 255);
/// 超链接正则
private static readonly Regex s_HrefRegex = new Regex(@"<href=([^>\n\s]+)>(.*?)(</href>)", RegexOptions.Singleline);
// ugui富文本标签
// 格式1:<b></b> <i></i>
private static readonly string[] _uguiSymbols1 = { "b", "i" };
// 格式2:<size=26></size> <color=#ffffff></color>
private static readonly string[] _uguiSymbols2 = { "size", "color" };
public string GetHyperlinkInfo { get { return text; } }
public override void SetVerticesDirty()
{
base.SetVerticesDirty();
text = GetHyperlinkInfo;
m_OutputText = GetOutputText(text);
}
protected override void OnPopulateMesh(VertexHelper toFill)
{
var orignText = m_Text;
m_Text = m_OutputText;
base.OnPopulateMesh(toFill);
m_Text = orignText;
UIVertex vert = new UIVertex();
// 处理超链接包围框
foreach (var hrefInfo in m_HrefInfos)
{
hrefInfo.boxes.Clear();
hrefInfo.linefeedIndexList.Clear();
if (hrefInfo.startIndex >= toFill.currentVertCount)
continue;
// 将超链接里面的文本顶点索引坐标加入到包围框
toFill.PopulateUIVertex(ref vert, hrefInfo.startIndex);
var pos = vert.position;
var bounds = new Bounds(pos, Vector3.zero);
hrefInfo.linefeedIndexList.Add(hrefInfo.startIndex);
for (int i = hrefInfo.startIndex, m = hrefInfo.endIndex; 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 == 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));
hrefInfo.linefeedIndexList.Add(i);
bounds = new Bounds(pos, Vector3.zero);
needEncapsulate = false;
}
}
if (needEncapsulate)
{
bounds.Encapsulate(pos); // 扩展包围框
}
}
hrefInfo.boxes.Add(new Rect(bounds.min, bounds.size));
}
//一个字一个字的划 效率差 而且字与字之间容易有接缝
DrawUnderLine(toFill);
}
private void DrawUnderLine(VertexHelper vh)
{
UIVertex vert = new UIVertex();
List<Vector3> startPosList = new List<Vector3>();
List<Vector3> endPosList = new List<Vector3>();
foreach (var hrefInfo in m_HrefInfos)
{
if (hrefInfo.startIndex >= vh.currentVertCount) continue;
float minY = float.MaxValue;
for (int i = hrefInfo.startIndex, m = hrefInfo.endIndex; i < m; i += 4)
{
if (i >= vh.currentVertCount)
break;
if (hrefInfo.linefeedIndexList.Contains(i))
{
for (int j = 0; j < startPosList.Count; j++)
{
MeshUnderLine(vh, new Vector2(startPosList[j].x, minY), new Vector2(endPosList[j].x, minY));
}
startPosList.Clear();
endPosList.Clear();
}
vh.PopulateUIVertex(ref vert, i + 3);
startPosList.Add(vert.position);
vh.PopulateUIVertex(ref vert, i + 2);
endPosList.Add(vert.position);
if (vert.position.y < minY)
{
minY = vert.position.y;
}
}
for (int j = 0; j < startPosList.Count; j++)
{
MeshUnderLine(vh, new Vector2(startPosList[j].x, minY), new Vector2(endPosList[j].x, minY));
}
startPosList.Clear();
endPosList.Clear();
}
}
private void MeshUnderLine(VertexHelper vh, Vector2 startPos, Vector2 endPos)
{
Vector2 extents = rectTransform.rect.size;
var setting = GetGenerationSettings(extents);
TextGenerator underlineText = new TextGenerator();
underlineText.Populate("—", setting);
IList<UIVertex> lineVer = underlineText.verts;/*new UIVertex[4];*///"_"的的顶点数组
Vector3[] pos = new Vector3[4];
pos[0] = startPos + new Vector2(-1f, 0);
pos[3] = startPos + new Vector2(-1f, 4f);
pos[2] = endPos + new Vector2(1f, 4f);
pos[1] = endPos + new Vector2(1f, 0);
UIVertex[] tempVerts = new UIVertex[4];
for (int i = 0; i < 4; i++)
{
tempVerts[i] = lineVer[i];
tempVerts[i].color = innerTextColor;
tempVerts[i].position = pos[i];
}
vh.AddUIVertexQuad(tempVerts);
}
/// <summary>
/// 获取超链接解析后的最后输出文本
/// </summary>
/// <returns></returns>
protected virtual string GetOutputText(string outputText)
{
s_TextBuilder.Length = 0;
m_HrefInfos.Clear();
var indexText = 0;
int count = 0;
foreach (Match match in s_HrefRegex.Matches(outputText))
{
string appendStr = outputText.Substring(indexText, match.Index - indexText);
s_TextBuilder.Append(appendStr);
//空格和回车没有顶点渲染,所以要去掉
count += appendStr.Length - appendStr.Replace(" ", "").Replace("\n", "").Length;
//去掉富文本标签的长度
for (int i = 0; i < _uguiSymbols1.Length; i++)
{
count += appendStr.Length - appendStr.Replace($"<{_uguiSymbols1[i]}>", "").Replace($"</{_uguiSymbols1[i]}>", "").Length;
}
for (int i = 0; i < _uguiSymbols2.Length; i++)
{
string pattern = $"<{_uguiSymbols2[i]}=(.*?)>";
count += appendStr.Length - Regex.Replace(appendStr, pattern, "").Length;
count += appendStr.Length - appendStr.Replace($"</{_uguiSymbols2[i]}>", "").Length;
}
int startIndex = (s_TextBuilder.Length - count) * 4;
var group = match.Groups[1];
var hrefInfo = new HyperlinkInfo
{
startIndex = startIndex, // 超链接里的文本起始顶点索引
endIndex = startIndex + (match.Groups[2].Length * 4),
name = group.Value
};
m_HrefInfos.Add(hrefInfo);
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>
/// <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))
{
if (onHyperlinkClick != null)
onHyperlinkClick.Invoke(hrefInfo.name);
return;
}
}
}
}
#if UNITY_EDITOR
private void AddVisibleBound()
{
int index = 0;
foreach (var hrefInfo in m_HrefInfos)
{
Color color = new Color(UnityEngine.Random.Range(0f, 1f), UnityEngine.Random.Range(0f, 1f), UnityEngine.Random.Range(0f, 1f), 0.2f);
index++;
foreach (Rect rect in hrefInfo.boxes)
{
GameObject gameObject = new GameObject();
gameObject.name = string.Format("GOBoundBox[{0}]", hrefInfo.name);
gameObject.transform.SetParent(this.gameObject.transform, false);
RectTransform rectTransform = gameObject.AddComponent<RectTransform>();
rectTransform.sizeDelta = rect.size;
rectTransform.localPosition = new Vector3(rect.position.x + rect.size.x / 2, rect.position.y + rect.size.y / 2, 0);
Image image = gameObject.AddComponent<Image>();
image.color = color;
image.raycastTarget = false;
}
}
}
#endif
}
}
编辑器扩展
代码
using UnityEditor;
using UnityEngine;
namespace MyTool
{
[CanEditMultipleObjects]
[CustomEditor(typeof(Tools.HyperlinkText), true)]
public class HyperlinkTextEditor : UnityEditor.UI.TextEditor
{
SerializedProperty _innerTextColor;
protected override void OnEnable()
{
base.OnEnable();
_innerTextColor = serializedObject.FindProperty("innerTextColor");
}
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
serializedObject.Update();
EditorGUILayout.PropertyField(_innerTextColor, new GUIContent("Inner Text Color"));
serializedObject.ApplyModifiedProperties();
if (GUI.changed)
{
EditorUtility.SetDirty(target);
}
}
}
}
Demo代码
using MyTool.Tools;
using UnityEngine;
public class Demo2 : MonoBehaviour
{
public HyperlinkText text;
// Start is called before the first frame update
void Start()
{
Debug.Log("啊啊啊啊");
text.onHyperlinkClick = OnClickText;
}
void OnClickText(string s)
{
if (s == "第一段第一句")
{
Debug.Log($"111---{s}");
}
else if (s == "第二段第一句")
{
Debug.Log($"222---{s}");
}
else
{
Debug.Log($"333---{s}");
}
}
}
demo测试文本
<color=#ffffff>背着手踱着。</color><href=第一段第一句>路上只我一个人</href>,这一片天地好像是我的;我也像超出了平常旳自己,到了另一世界里。我爱热闹,也爱冷静;爱群居,也爱独处。像今晚上,一个人在这苍茫旳月下,什么都可以想,什么都可以不想,便觉是个自由的人。白天里一定要做的事,一定要说的话,现在都可不理。这是独处的妙处,我且受用这无边的荷香月色好了。
<href=第二段第一句>曲曲折折的荷塘上面</href>,弥望旳是田田的叶子。叶子出水很高,像亭亭旳舞女旳裙。层层的叶子中间,零星地点缀着些白花,有袅娜(niǎo,nuó)地开着旳,有羞涩地打着朵儿旳;正如一粒粒的明珠,又如碧天里的星星,又如刚出浴的美人。微风过处,送来缕缕清香,仿佛远处高楼上渺茫的歌声似的。这时候叶子与花也有一丝的颤动,像闪电般,霎时传过荷塘的那边去了。叶子本是肩并肩密密地挨着,这便宛然有了一道凝碧的波痕。叶子底下是脉脉(mò)的流水,遮住了,不能见一些颜色;而叶子却更见风致了。
月光如流水一般,静静地泻在这一片叶子和花上。薄薄的青雾浮起在荷塘里。叶子和花仿佛在牛乳中洗过一样;又像笼着轻纱的梦。虽然是满月,天上却有一层淡淡的云,所以不能朗照;但我以为这恰是到了好处——酣眠固不可少,小睡也别有风味的。月光是隔了树照过来的,高处丛生的灌木,落下参差的斑驳的黑影,峭楞楞如鬼一般;弯弯的杨柳的稀疏的倩影,却又像是画在荷叶上。塘中的月色并不均匀;但光与影有着和谐的旋律,<href=第三段>如梵婀(ē)玲(英语violin小提琴的译音)上奏着的名曲</href>。
荷塘的四面,远远近近,高高低低都是树,而杨柳最多。<href=第四段>这些树将一片荷塘重重围住</href>;只在小路一旁,漏着几段空隙,像是特为月光留下的。树色一例是阴阴的,乍看像一团烟雾;但杨柳的丰姿,便在烟雾里也辨得出。树梢上隐隐约约的是一带远山,只有些大意罢了。树缝里也漏着一两点路灯光,没精打采的,是渴睡人的眼。这时候最热闹的,要数树上的蝉声与水里的蛙声;但热闹是它们的,我什么也没有。
ui
效果