unity ugui text 超链接和下划线,支持部分富文本格式

unity版本:2021.3.6f1
局限性:
1.测试发现不能使用 size 富文本标签,
2.同一文本不能设置不同颜色的超链接文本
其它:代码中注释掉使用innerTextColor的地方,可以使用富文本设置超链接颜色, 但是下划线是文本本身颜色

项目需要用到该功能, 搜索和参考了很多文章,要么不支持富文本,要不没有下划线,要么是错误的,修修改改后满足我的需求,代码如下

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=([^>ns]+)>(.*?)(</href>)", RegexOptions.Singleline);

        // ugui富文本标签
        // 格式1:<b></b>  <i></i>
        private static readonly string[] _uguiSymbols1 = { "b", "i" };
        // 格式2:<color=#ffffff></color> <color=red></color>
        private static readonly string[] _uguiSymbols2 = { "color", "size" };

        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 System.Collections;
using UnityEngine;

public class Demo2 : MonoBehaviour
{
    public HyperlinkText text;
    // Start is called before the first frame update
    void Start()
    {
        //设置点击回调
        text.onHyperlinkClick = OnClickText;

        StartCoroutine(AddVisibleBound());
    }

    IEnumerator AddVisibleBound()
    {
        yield return null;
        text.AddVisibleBound();
    }

    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><b><size=36>背着手踱着。</size></b></color><color=ffffff><href=第一段第一句>路上只我一个人</href></color><size=45>这一片天地好像是我的;我也像超出了平常旳自己,到了另一世界里。我爱热闹,也爱冷静;爱群居,也爱独处。像今晚上,一个人在这苍茫旳月下,什么都可以想,什么都可以不想,便觉是个自由的人。</size>白天里一定要做的事,一定要说的话,现在都可不理。<b><color=green>这是独处的妙处,我且受用这无边的荷香月色好了。</color></b>

<href=第二段第一句>曲曲折折的荷塘上面</href>,弥望旳是田田的叶子。叶子出水很高,像亭亭旳舞女旳裙。层层的叶子中间,零星地点缀着些白花,有袅娜(niǎo,nuó)地开着旳,有羞涩地打着朵儿旳;正如一粒粒的明珠,又如碧天里的星星,又如刚出浴的美人。微风过处,送来缕缕清香,仿佛远处高楼上渺茫的歌声似的。这时候叶子与花也有一丝的颤动,像闪电般,霎时传过荷塘的那边去了。叶子本是肩并肩密密地挨着,这便宛然有了一道凝碧的波痕。叶子底下是脉脉()的流水,遮住了,不能见一些颜色;而叶子却更见风致了。

<size=60>月光如流水一般,静静地泻在这一片叶子和花上。薄薄的青雾浮起在荷塘里。叶子和花仿佛在牛乳中洗过一样;又像笼着轻纱的梦。虽然是满月,天上却有一层淡淡的云,所以不能朗照;但我以为</size>这恰是到了好处——酣眠固不可少,小睡也别有风味的。月光是隔了树照过来的,高处丛生的灌木,落下参差的斑驳的黑影,峭楞楞如鬼一般;弯弯的杨柳的稀疏的倩影,却又像是画在荷叶上。塘中的月色并不均匀;但光与影有着和谐的旋律,<href=第三段>如梵婀(ē)(英语violin小提琴的译音)上奏着的名曲</href><color=red><i>荷塘的四面,远远近近,高高低低都是树,而杨柳最多。</i></color><href=第四段>这些树将一片荷塘重重围住</href>;只在小路一旁,漏着几段空隙,像是特为月光留下的。树色一例是阴阴的,乍看像一团烟雾;但杨柳的丰姿,便在烟雾里也辨得出。树梢上隐隐约约的是一带远山,只有些大意罢了。树缝里也漏着一两点路灯光,没精打采的,是渴睡人的眼。这时候最热闹的,要数树上的蝉声与水里的蛙声;但热闹是它们的,我什么也没有。

ui
在这里插入图片描述
效果
在这里插入图片描述

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>