Unity扩展 Text支持超链接文本

news2024/9/20 14:41:41

重点提示:当前的文本扩展支持多个超链接,支持修改超链接规则和支持修改超链接颜色。

近期在邮件文本中用到了超链接。最初是在邮件窗口中新加一个按钮用来超链接跳转,之后发现效果表现不如直接在文本中添加,后经过几个小时的资料查询将遇到的解决方法和问题贴出来。

方案一:换用TMP组件

问题:需要制作字体库等额外操作,改动较大不太适合。

方案二:网上找相关Text组件扩展

问题:在大多数扩展中,仅支持一个超链接文本。当文本中出现多个超链接文本时,只会响应第一个匹配的超链接点击事件。

最后在GitHub中找到了一个比较适合的Text扩展,支持多个正则超链接规则:GitHub - setchi/uGUI-Hypertext: Hypertext for uGUI

这里的解决方案,就是在这个脚本上进行修改而来的。原脚本中,每个超链接对应独立的点击事件,以及超链接颜色修改。更详细可直接看支持库中的例子。

根据需要,绑定超链接唯一点击事件,添加颜色开关等等,具体可直接查看代码:

///
/// 《超链接文本》支持多个链接 支持正则表达式
/// 当前版本修改于 uGUI-Hypertext GitHub:https://github.com/setchi/uGUI-Hypertext/tree/master
/// 新增超链接颜色修改控制。
/// 统一事件点击回调
/// 默认支持href匹配
/// 版本:0.10.0
/// 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEngine.UI;

namespace Hyperlink
{
    /// <summary>
    /// 顶点池子
    /// </summary>
    /// <typeparam name="T"></typeparam>
    internal class ObjectPool<T> where T : new()
    {
        private readonly Stack<T> _stack = new Stack<T>();
        private readonly Action<T> _getAction;
        private readonly Action<T> _releaseAction;
        
        /// <summary>
        /// 总数
        /// </summary>
        public int Count { get; set; }
        /// <summary>
        /// 没有被使用的数量
        /// </summary>
        public int UnusedCount => _stack.Count;
        /// <summary>
        /// 已经使用的数量
        /// </summary>
        public int UsedCount => Count - UnusedCount;

        public ObjectPool(Action<T> onGetAction, Action<T> onRelease)
        {
            _getAction = onGetAction;
            _releaseAction = onRelease;
        }
        
        public T Get()
        {
            T element;
            if (_stack.Count == 0)
            {
                element = new T();
                Count++;
            }
            else
            {
                element = _stack.Pop();
            }

            _getAction?.Invoke(element);

            return element;
        }

        public void Release(T element)
        {
            if (_stack.Count > 0 && ReferenceEquals(_stack.Peek(), element))
            {
                UnityEngine.Debug.LogError("试图归还已经归还的对象。");
            }

            _releaseAction?.Invoke(element);

            _stack.Push(element);
        }
    }

    /// <summary>
    /// 超链接信息块
    /// </summary>
    internal class LinkInfo
    {
        public readonly int StartIndex;
        public readonly int Length;
        public readonly string Link = null;
        public readonly string Text;
        public readonly Color Color;
        public  readonly bool OverwriteColor = false;
        public readonly ClickLinkEvent Callback;
        public List<Rect> Boxes;

        public LinkInfo(int startIndex, int length, Color? color, ClickLinkEvent callback)
        {
            StartIndex = startIndex;
            Length = length;
            Link = null;
            Text = null;
            OverwriteColor = color.HasValue;
            if (color.HasValue)
            {
                Color = color.Value;
            }
            Callback = callback;
            Boxes = new List<Rect>();
        }

        public LinkInfo(int startIndex, int length, string link, string text, Color? color, ClickLinkEvent callback)
        {
            StartIndex = startIndex;
            Length = length;
            Link = link;
            Text = text;
            OverwriteColor = color.HasValue;
            if (color.HasValue)
            {
                Color = color.Value;
            }
            Callback = callback;
            Boxes = new List<Rect>();
        }

        public LinkInfo(int startIndex, string link, string text, Color? color, ClickLinkEvent callback) : this(startIndex, text.Length, link, text, color,
            callback)
        {
        }

        public LinkInfo(int startIndex, string link, string text, ClickLinkEvent callback) : this(startIndex, link, text, Color.blue, 
            callback)
        {
        }
    }
    
    /// <summary>
    /// 超链接点击事件
    /// </summary>
    [Serializable]
    public class ClickLinkEvent : UnityEvent<string,string>
    {
    }

    /// <summary>
    /// 超链接正则表达式
    /// </summary>
    [Serializable]
    public class RegexPattern
    {
        public string pattern;
        public Color color;
        public bool overwriteColor = false;
        
        public RegexPattern(string regexPattern, Color color,bool overwriteColor = true)
        {
            this.pattern = regexPattern;
            this.overwriteColor = overwriteColor;
            this.color = color;
        }
        
        public RegexPattern(string regexPattern,bool overwriteColor = true):this(regexPattern,Color.blue,overwriteColor)
        {
        }
    }
    
    public class TextHyperlink : Text, IPointerClickHandler
    {
        private const int CharVertex = 6;
        private const char Tab = '\t', LineFeed = '\n', Space = ' ', LesserThan = '<', GreaterThan = '>';
        /// <summary>
        /// 看不见顶点的字符
        /// </summary>
        private readonly char[] _invisibleChars =
        {
            Space,
            Tab,
            LineFeed
        };
        /// <summary>
        /// 超链接信息块
        /// </summary>
        private readonly List<LinkInfo> _links = new List<LinkInfo>();
        /// <summary>
        /// 字符顶点池
        /// </summary>
        private static readonly ObjectPool<List<UIVertex>> UIVerticesPool = new ObjectPool<List<UIVertex>>(null, l => l.Clear());
        /// <summary>
        /// 字符索引映射
        /// </summary>
        private int[] _charIndexMap;
        
        private Canvas _root;
        private Canvas RootCanvas => _root ? _root : (_root = GetComponentInParent<Canvas>());

        /// <summary>
        /// 超链接匹配规则
        /// </summary>
        public List<RegexPattern> linkRegexPattern = new List<RegexPattern>()
        {
            new(@"<a href=([^>\n\s]+)>(.*?)(</a>)"),
        };

        [SerializeField]
        private ClickLinkEvent _onClickLink = new ClickLinkEvent();

        /// <summary>
        /// 超链接点击事件
        /// </summary>
        public ClickLinkEvent onClickLink
        {
            get => _onClickLink;
            set => _onClickLink = value;
        }

        #region PopulateMesh

        private readonly UIVertex[] _tempVerts = new UIVertex[4];
        
        protected override void OnPopulateMesh(VertexHelper toFill)
        {
            if (font == null)
            {
                return;
            }

            m_DisableFontTextureRebuiltCallback = true;

            var extents = rectTransform.rect.size;

            var settings = GetGenerationSettings(extents);
            settings.generateOutOfBounds = true;
            cachedTextGenerator.PopulateWithErrors(text, settings, gameObject);

            var verts = cachedTextGenerator.verts;
            var unitsPerPixel = 1 / pixelsPerUnit;
            var vertCount = verts.Count;

            if (vertCount <= 0)
            {
                toFill.Clear();
                return;
            }

            var roundingOffset = new Vector2(verts[0].position.x, verts[0].position.y) * unitsPerPixel;
            roundingOffset = PixelAdjustPoint(roundingOffset) - roundingOffset;
            toFill.Clear();

            if (roundingOffset != Vector2.zero)
            {
                for (var i = 0; i < vertCount; ++i)
                {
                    var tempVertsIndex = i & 3;
                    _tempVerts[tempVertsIndex] = verts[i];
                    _tempVerts[tempVertsIndex].position *= unitsPerPixel;
                    _tempVerts[tempVertsIndex].position.x += roundingOffset.x;
                    _tempVerts[tempVertsIndex].position.y += roundingOffset.y;

                    if (tempVertsIndex == 3)
                    {
                        toFill.AddUIVertexQuad(_tempVerts);
                    }
                }
            }
            else
            {
                for (var i = 0; i < vertCount; ++i)
                {
                    var tempVertsIndex = i & 3;
                    _tempVerts[tempVertsIndex] = verts[i];
                    _tempVerts[tempVertsIndex].position *= unitsPerPixel;

                    if (tempVertsIndex == 3)
                    {
                        toFill.AddUIVertexQuad(_tempVerts);
                    }
                }
            }

            var vertices = UIVerticesPool.Get();
            toFill.GetUIVertexStream(vertices);

            GenerateCharIndexMap(vertices.Count < text.Length * CharVertex);

            _links.Clear();
            TryAddMatchLink();
            GenerateHrefBoxes(ref vertices);

            toFill.Clear();
            toFill.AddUIVertexTriangleStream(vertices);
            UIVerticesPool.Release(vertices);

            m_DisableFontTextureRebuiltCallback = false;
        }

        /// <summary>
        /// 生成超链接包围框
        /// </summary>
        /// <param name="vertices"></param>
        private void GenerateHrefBoxes(ref List<UIVertex> vertices)
        {
            var verticesCount = vertices.Count;

            for (var i = 0; i < _links.Count; i++)
            {
                var linkInfo = _links[i];

                var startIndex = _charIndexMap[linkInfo.StartIndex];
                var endIndex = _charIndexMap[linkInfo.StartIndex + linkInfo.Length - 1];

                for (var textIndex = startIndex; textIndex <= endIndex; textIndex++)
                {
                    var vertexStartIndex = textIndex * CharVertex;
                    if (vertexStartIndex + CharVertex > verticesCount)
                    {
                        break;
                    }

                    var min = Vector2.one * float.MaxValue;
                    var max = Vector2.one * float.MinValue;

                    for (var vertexIndex = 0; vertexIndex < CharVertex; vertexIndex++)
                    {
                        var vertex = vertices[vertexStartIndex + vertexIndex];
                        if (linkInfo.OverwriteColor)
                        {
                            vertex.color = linkInfo.Color;
                        }
                        vertices[vertexStartIndex + vertexIndex] = vertex;

                        var pos = vertices[vertexStartIndex + vertexIndex].position;

                        if (pos.y < min.y)
                        {
                            min.y = pos.y;
                        }

                        if (pos.x < min.x)
                        {
                            min.x = pos.x;
                        }

                        if (pos.y > max.y)
                        {
                            max.y = pos.y;
                        }

                        if (pos.x > max.x)
                        {
                            max.x = pos.x;
                        }
                    }

                    linkInfo.Boxes.Add(new Rect {min = min, max = max});
                }

                linkInfo.Boxes = CalculateLineBoxes(linkInfo.Boxes);
            }
        }

        /// <summary>
        /// 计算行包围框
        /// </summary>
        /// <param name="boxes"></param>
        /// <returns></returns>
        private static List<Rect> CalculateLineBoxes(List<Rect> boxes)
        {
            var lineBoxes = new List<Rect>();
            var lineStartIndex = 0;

            for (var i = 1; i < boxes.Count; i++)
            {
                if (boxes[i].xMin >= boxes[i - 1].xMin)
                {
                    continue;
                }

                lineBoxes.Add(CalculateAABB(boxes.GetRange(lineStartIndex, i - lineStartIndex)));
                lineStartIndex = i;
            }

            if (lineStartIndex < boxes.Count)
            {
                lineBoxes.Add(CalculateAABB(boxes.GetRange(lineStartIndex, boxes.Count - lineStartIndex)));
            }

            return lineBoxes;
        }

        private static Rect CalculateAABB(IReadOnlyList<Rect> rects)
        {
            var min = Vector2.one * float.MaxValue;
            var max = Vector2.one * float.MinValue;

            for (var i = 0; i < rects.Count; i++)
            {
                if (rects[i].xMin < min.x)
                {
                    min.x = rects[i].xMin;
                }

                if (rects[i].yMin < min.y)
                {
                    min.y = rects[i].yMin;
                }

                if (rects[i].xMax > max.x)
                {
                    max.x = rects[i].xMax;
                }

                if (rects[i].yMax > max.y)
                {
                    max.y = rects[i].yMax;
                }
            }

            return new Rect {min = min, max = max};
        }

        /// <summary>
        /// 生成字节索引映射
        /// </summary>
        /// <param name="verticesReduced"></param>
        private void GenerateCharIndexMap(bool verticesReduced)
        {
            if (_charIndexMap == null || _charIndexMap.Length < text.Length)
            {
                Array.Resize(ref _charIndexMap, text.Length);
            }

            if (!verticesReduced)
            {
                for (var i = 0; i < _charIndexMap.Length; i++)
                {
                    _charIndexMap[i] = i;
                }
                return;
            }

            var offset = 0;
            var inTag = false;

            for (var i = 0; i < text.Length; i++)
            {
                var character = text[i];

                if (inTag)
                {
                    offset--;

                    if (character == GreaterThan)
                    {
                        inTag = false;
                    }
                }
                else if (supportRichText && character == LesserThan)
                {
                    offset--;
                    inTag = true;
                }
                else if (_invisibleChars.Contains(character))
                {
                    offset--;
                }

                _charIndexMap[i] = Mathf.Max(0, i + offset);
            }
        }

        #endregion

        private Vector3 CalculateLocalPosition(Vector3 position, Camera pressEventCamera)
        {
            if (!RootCanvas)
            {
                return Vector3.zero;
            }

            if (RootCanvas.renderMode == RenderMode.ScreenSpaceOverlay)
            {
                return transform.InverseTransformPoint(position);
            }

            RectTransformUtility.ScreenPointToLocalPointInRectangle(
                rectTransform,
                position,
                pressEventCamera,
                out var localPosition
            );

            return localPosition;
        }
        
        void IPointerClickHandler.OnPointerClick(PointerEventData eventData)
        {
            var localPosition = CalculateLocalPosition(eventData.position, eventData.pressEventCamera);

            foreach (var linkInfo in _links)
            {
                if (!linkInfo.Boxes.Any(t => t.Contains(localPosition))) continue;
                var subText = text.Substring(linkInfo.StartIndex, linkInfo.Length);
                var link = linkInfo.Link ?? subText;
                var content = linkInfo.Text ?? subText;
                linkInfo.Callback?.Invoke(link,content);
            }
        }

        #region Add Text Link
        
        /// <summary>
        /// 尝试添加超链接
        /// </summary>
        private void TryAddMatchLink()
        {
            foreach (var entry in linkRegexPattern)
            {
                var matches = Regex.Matches(text, entry.pattern, RegexOptions.Singleline);
                foreach (Match match in matches)
                {
                    var regex = new Regex(entry.pattern, RegexOptions.Singleline);
                    var regexMatch = regex.Match(match.Value);
                    var overwriteColor = entry.overwriteColor == true ? entry.color : (Color?)null;
                    if (regexMatch.Success)
                    {
                        var group = match.Groups[1];
                        AddLink(match.Index, group.Value,match.Value, overwriteColor, _onClickLink);
                    }
                    else
                    {
                        AddLink(match.Index, match.Value.Length, overwriteColor, _onClickLink);
                    }
                }
            }
        }

        private void CheckLinkException(int startIndex, int length, ClickLinkEvent onClick)
        {
            if (onClick == null)
            {
                throw new ArgumentNullException(nameof(onClick));
            }

            if (startIndex < 0 || startIndex > text.Length - 1)
            {
                throw new ArgumentOutOfRangeException(nameof(startIndex));
            }

            if (length < 1 || startIndex + length > text.Length)
            {
                throw new ArgumentOutOfRangeException(nameof(length));
            }
        }
        
        private void AddLink(int startIndex, int length, Color? linkColor, ClickLinkEvent onClick)
        {
            CheckLinkException(startIndex, length, onClick);

            _links.Add(new LinkInfo(startIndex, length, linkColor, onClick));
        }
        
        private void AddLink(int startIndex, string link, string content, Color? linkColor, ClickLinkEvent onClick)
        {
            CheckLinkException(startIndex, content.Length, onClick);

            _links.Add(new LinkInfo(startIndex, link, content, linkColor, onClick));
        }

        protected void AddLink(int startIndex, string link, string content, ClickLinkEvent onClick)
        {
            CheckLinkException(startIndex, content.Length, onClick);

            _links.Add(new LinkInfo(startIndex, link, content, onClick));
        }
        
        protected void CleanLink()
        {
            _links.Clear();
            linkRegexPattern.Clear();
        }

        #endregion
        
        #region Hyperlink_Test

        #if Hyperlink_Test
        protected override void OnEnable()
        {
            base.OnEnable();
            onClickLink.AddListener(OnClickLinkText);
        }

        protected override void OnDisable()
        {
            base.OnDisable();
            onClickLink.RemoveListener(OnClickLinkText);
        }

        /// <summary>
        /// 当前点击超链接回调
        /// </summary>
        private void OnClickLinkText(string link,string content)
        {
            Debug.Log($"超链接信息:{link}\n{content}");
            Application.OpenURL(link);
        }
        #endif
        #endregion
    }
}

编辑器面板扩展:

继承自UGUI-Text面板,在原有数据显示上,添加可编辑的扩展属性。

using UnityEditor;
using UnityEditor.UI;

namespace Hyperlink.Editor
{
    [CustomEditor(typeof(TextHyperlink), true)]
    [CanEditMultipleObjects]
    public class TextHyperlinkEditor : TextEditor
    {
        private SerializedProperty _linkRegexPattern;
        private SerializedProperty _onClickLink;
        protected override void OnEnable()
        {
            base.OnEnable();
            _linkRegexPattern = serializedObject.FindProperty("linkRegexPattern");
            _onClickLink = serializedObject.FindProperty("_onClickLink");
        }

        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();
            
            serializedObject.Update();
            EditorGUILayout.PropertyField(_linkRegexPattern);
            EditorGUILayout.Space();
            EditorGUILayout.PropertyField(_onClickLink);
            serializedObject.ApplyModifiedProperties();
        }
    }
}

效果图:

在使用过程中,应策划需求加超链接下划线,参考这篇文档:Unity超链接:支持点击事件,下划线以及自定义颜色-CSDN博客 

添加了下划线颜色和下划线符号。


        /// <summary>
        /// 超链接默认颜色
        /// </summary>
        private static readonly Color LinkColor = new Color(75 / 255f, 122 / 255f, 247 / 255f, 1f); 
        /// <summary>
        /// 下划线
        /// </summary>
        public string underline = " ̄";

        #region Under Line

        private void DrawUnderLine(VertexHelper vh)
        {
            foreach (var link in _links)
            {
                foreach (var rect in link.Boxes)
                {
                    var height = rect.height;
                    // 左下
                    var pos1 = new Vector3(rect.min.x, rect.min.y, 0);
                    // 右下
                    var pos2 = new Vector3(rect.max.x, rect.max.y, 0) - new Vector3(0, height, 0);

                    MeshUnderLine(vh, pos1, pos2, link.Color);
                }
            }
        }

        private void MeshUnderLine(VertexHelper vh, Vector2 startPos, Vector2 endPos, Color lineColor)
        {            
        	var extents = rectTransform.rect.size;
            var setting = GetGenerationSettings(extents);

            var underlineText = new TextGenerator();
            underlineText.Populate(underline, setting);

            var lineVer = underlineText.verts; //" ̄"的的顶点数组

            var pos = new Vector3[4];
            pos[0] = startPos + new Vector2(-8, 0);
            pos[3] = startPos + new Vector2(-8, -4f);
            pos[2] = endPos + new Vector2(8, -4f);
            pos[1] = endPos + new Vector2(8, 0);

            if (lineVer.Count != 4) return;
            
            var tempVerts = new UIVertex[4];
            for (var i = 0; i < 4; i++)
            {
                tempVerts[i] = lineVer[i];
                tempVerts[i].color = lineColor;
                tempVerts[i].position = pos[i];
                tempVerts[i].uv0 = lineVer[i].uv0;
                tempVerts[i].uv1 = lineVer[i].uv1;
                tempVerts[i].uv2 = lineVer[i].uv2;
                tempVerts[i].uv3 = lineVer[i].uv3;
            }

            vh.AddUIVertexQuad(tempVerts);
        }

        #endregion

优化后的完整代码:

///
/// 《超链接文本》支持多个链接 支持正则表达式
/// 当前版本修改于 uGUI-Hypertext GitHub:https://github.com/setchi/uGUI-Hypertext/tree/master
/// 新增超链接颜色修改控制。
/// 统一事件点击回调
/// 默认支持href匹配
/// 添加超链接下划线
/// 添加默认超链接颜色
/// 版本:0.10.1
/// 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEngine.UI;

namespace Hyperlink
{
    /// <summary>
    /// 顶点池子
    /// </summary>
    /// <typeparam name="T"></typeparam>
    internal class ObjectPool<T> where T : new()
    {
        private readonly Stack<T> _stack = new Stack<T>();
        private readonly Action<T> _getAction;
        private readonly Action<T> _releaseAction;
        
        /// <summary>
        /// 总数
        /// </summary>
        public int Count { get; set; }
        /// <summary>
        /// 没有被使用的数量
        /// </summary>
        public int UnusedCount => _stack.Count;
        /// <summary>
        /// 已经使用的数量
        /// </summary>
        public int UsedCount => Count - UnusedCount;

        public ObjectPool(Action<T> onGetAction, Action<T> onRelease)
        {
            _getAction = onGetAction;
            _releaseAction = onRelease;
        }
        
        public T Get()
        {
            T element;
            if (_stack.Count == 0)
            {
                element = new T();
                Count++;
            }
            else
            {
                element = _stack.Pop();
            }

            _getAction?.Invoke(element);

            return element;
        }

        public void Release(T element)
        {
            if (_stack.Count > 0 && ReferenceEquals(_stack.Peek(), element))
            {
                UnityEngine.Debug.LogError("试图归还已经归还的对象。");
            }

            _releaseAction?.Invoke(element);

            _stack.Push(element);
        }
    }

    /// <summary>
    /// 超链接信息块
    /// </summary>
    internal class LinkInfo
    {
        public readonly int StartIndex;
        public readonly int Length;
        public readonly string Link = null;
        public readonly string Text;
        public readonly Color Color = new Color(75 / 255f, 122 / 255f, 247 / 255f, 1f);
        public  readonly bool OverwriteColor = false;
        public readonly ClickLinkEvent Callback;
        public List<Rect> Boxes;

        public LinkInfo(int startIndex, int length, Color? color, ClickLinkEvent callback)
        {
            StartIndex = startIndex;
            Length = length;
            Link = null;
            Text = null;
            OverwriteColor = color.HasValue;
            if (color.HasValue)
            {
                Color = color.Value;
            }
            Callback = callback;
            Boxes = new List<Rect>();
        }

        public LinkInfo(int startIndex, int length, string link, string text, Color? color, ClickLinkEvent callback)
        {
            StartIndex = startIndex;
            Length = length;
            Link = link;
            Text = text;
            OverwriteColor = color.HasValue;
            if (color.HasValue)
            {
                Color = color.Value;
            }
            Callback = callback;
            Boxes = new List<Rect>();
        }

        public LinkInfo(int startIndex, string link, string text, Color? color, ClickLinkEvent callback) : this(startIndex, text.Length, link, text, color,
            callback)
        {
        }

        public LinkInfo(int startIndex, string link, string text, ClickLinkEvent callback) : this(startIndex, link, text, Color.blue, 
            callback)
        {
        }
    }
    
    /// <summary>
    /// 超链接点击事件
    /// </summary>
    [Serializable]
    public class ClickLinkEvent : UnityEvent<string,string>
    {
    }

    /// <summary>
    /// 超链接正则表达式
    /// </summary>
    [Serializable]
    public class RegexPattern
    {
        public string pattern;
        public Color color;
        public bool overwriteColor = false;
        
        public RegexPattern(string regexPattern, Color color,bool overwriteColor = true)
        {
            this.pattern = regexPattern;
            this.overwriteColor = overwriteColor;
            this.color = color;
        }
    }
    
    public class TextHyperlink : Text, IPointerClickHandler
    {
        private const int CharVertex = 6;
        private const char Tab = '\t', LineFeed = '\n', Space = ' ', LesserThan = '<', GreaterThan = '>';
        /// <summary>
        /// 看不见顶点的字符
        /// </summary>
        private readonly char[] _invisibleChars =
        {
            Space,
            Tab,
            LineFeed
        };
        /// <summary>
        /// 超链接信息块
        /// </summary>
        private readonly List<LinkInfo> _links = new List<LinkInfo>();
        /// <summary>
        /// 字符顶点池
        /// </summary>
        private static readonly ObjectPool<List<UIVertex>> UIVerticesPool = new ObjectPool<List<UIVertex>>(null, l => l.Clear());
        /// <summary>
        /// 字符索引映射
        /// </summary>
        private int[] _charIndexMap;

        /// <summary>
        /// 超链接默认颜色
        /// </summary>
        private static readonly Color LinkColor = new Color(75 / 255f, 122 / 255f, 247 / 255f, 1f); 
        
        private Canvas _root;
        private Canvas RootCanvas => _root ? _root : (_root = GetComponentInParent<Canvas>());

        [SerializeField]
        private ClickLinkEvent _onClickLink = new ClickLinkEvent();

        /// <summary>
        /// 超链接匹配规则
        /// </summary>
        public List<RegexPattern> linkRegexPattern = new List<RegexPattern>()
        {
            new(@"<a href=([^>\n\s]+)>(.*?)(</a>)", LinkColor),
        };

        /// <summary>
        /// 下划线
        /// </summary>
        public string underline = " ̄";

        /// <summary>
        /// 超链接点击事件
        /// </summary>
        public ClickLinkEvent onClickLink
        {
            get => _onClickLink;
            set => _onClickLink = value;
        }

        #region PopulateMesh

        private readonly UIVertex[] _tempVerts = new UIVertex[4];
        
        protected override void OnPopulateMesh(VertexHelper toFill)
        {
            if (font == null)
            {
                return;
            }

            m_DisableFontTextureRebuiltCallback = true;

            var extents = rectTransform.rect.size;

            var settings = GetGenerationSettings(extents);
            settings.generateOutOfBounds = true;
            cachedTextGenerator.PopulateWithErrors(text, settings, gameObject);

            var verts = cachedTextGenerator.verts;
            var unitsPerPixel = 1 / pixelsPerUnit;
            var vertCount = verts.Count;

            if (vertCount <= 0)
            {
                toFill.Clear();
                return;
            }

            var roundingOffset = new Vector2(verts[0].position.x, verts[0].position.y) * unitsPerPixel;
            roundingOffset = PixelAdjustPoint(roundingOffset) - roundingOffset;
            toFill.Clear();

            if (roundingOffset != Vector2.zero)
            {
                for (var i = 0; i < vertCount; ++i)
                {
                    var tempVertsIndex = i & 3;
                    _tempVerts[tempVertsIndex] = verts[i];
                    _tempVerts[tempVertsIndex].position *= unitsPerPixel;
                    _tempVerts[tempVertsIndex].position.x += roundingOffset.x;
                    _tempVerts[tempVertsIndex].position.y += roundingOffset.y;

                    if (tempVertsIndex == 3)
                    {
                        toFill.AddUIVertexQuad(_tempVerts);
                    }
                }
            }
            else
            {
                for (var i = 0; i < vertCount; ++i)
                {
                    var tempVertsIndex = i & 3;
                    _tempVerts[tempVertsIndex] = verts[i];
                    _tempVerts[tempVertsIndex].position *= unitsPerPixel;

                    if (tempVertsIndex == 3)
                    {
                        toFill.AddUIVertexQuad(_tempVerts);
                    }
                }
            }

            var vertices = UIVerticesPool.Get();
            toFill.GetUIVertexStream(vertices);

            GenerateCharIndexMap(vertices.Count < text.Length * CharVertex);

            _links.Clear();
            TryAddMatchLink();
            GenerateHrefBoxes(ref vertices);

            toFill.Clear();
            toFill.AddUIVertexTriangleStream(vertices);
            
            DrawUnderLine(toFill);
            
            UIVerticesPool.Release(vertices);

            m_DisableFontTextureRebuiltCallback = false;
        }

        /// <summary>
        /// 生成超链接包围框
        /// </summary>
        /// <param name="vertices"></param>
        private void GenerateHrefBoxes(ref List<UIVertex> vertices)
        {
            var verticesCount = vertices.Count;

            for (var i = 0; i < _links.Count; i++)
            {
                var linkInfo = _links[i];

                var startIndex = _charIndexMap[linkInfo.StartIndex];
                var endIndex = _charIndexMap[linkInfo.StartIndex + linkInfo.Length - 1];

                for (var textIndex = startIndex; textIndex <= endIndex; textIndex++)
                {
                    var vertexStartIndex = textIndex * CharVertex;
                    if (vertexStartIndex + CharVertex > verticesCount)
                    {
                        break;
                    }

                    var min = Vector2.one * float.MaxValue;
                    var max = Vector2.one * float.MinValue;

                    for (var vertexIndex = 0; vertexIndex < CharVertex; vertexIndex++)
                    {
                        var vertex = vertices[vertexStartIndex + vertexIndex];
                        if (linkInfo.OverwriteColor)
                        {
                            vertex.color = linkInfo.Color;
                        }
                        vertices[vertexStartIndex + vertexIndex] = vertex;

                        var pos = vertices[vertexStartIndex + vertexIndex].position;

                        if (pos.y < min.y)
                        {
                            min.y = pos.y;
                        }

                        if (pos.x < min.x)
                        {
                            min.x = pos.x;
                        }

                        if (pos.y > max.y)
                        {
                            max.y = pos.y;
                        }

                        if (pos.x > max.x)
                        {
                            max.x = pos.x;
                        }
                    }

                    linkInfo.Boxes.Add(new Rect {min = min, max = max});
                }

                linkInfo.Boxes = CalculateLineBoxes(linkInfo.Boxes);
            }
        }

        /// <summary>
        /// 计算行包围框
        /// </summary>
        /// <param name="boxes"></param>
        /// <returns></returns>
        private static List<Rect> CalculateLineBoxes(List<Rect> boxes)
        {
            var lineBoxes = new List<Rect>();
            var lineStartIndex = 0;

            for (var i = 1; i < boxes.Count; i++)
            {
                if (boxes[i].xMin >= boxes[i - 1].xMin)
                {
                    continue;
                }

                lineBoxes.Add(CalculateAABB(boxes.GetRange(lineStartIndex, i - lineStartIndex)));
                lineStartIndex = i;
            }

            if (lineStartIndex < boxes.Count)
            {
                lineBoxes.Add(CalculateAABB(boxes.GetRange(lineStartIndex, boxes.Count - lineStartIndex)));
            }

            return lineBoxes;
        }

        private static Rect CalculateAABB(IReadOnlyList<Rect> rects)
        {
            var min = Vector2.one * float.MaxValue;
            var max = Vector2.one * float.MinValue;

            for (var i = 0; i < rects.Count; i++)
            {
                if (rects[i].xMin < min.x)
                {
                    min.x = rects[i].xMin;
                }

                if (rects[i].yMin < min.y)
                {
                    min.y = rects[i].yMin;
                }

                if (rects[i].xMax > max.x)
                {
                    max.x = rects[i].xMax;
                }

                if (rects[i].yMax > max.y)
                {
                    max.y = rects[i].yMax;
                }
            }

            return new Rect {min = min, max = max};
        }

        /// <summary>
        /// 生成字节索引映射
        /// </summary>
        /// <param name="verticesReduced"></param>
        private void GenerateCharIndexMap(bool verticesReduced)
        {
            if (_charIndexMap == null || _charIndexMap.Length < text.Length)
            {
                Array.Resize(ref _charIndexMap, text.Length);
            }

            if (!verticesReduced)
            {
                for (var i = 0; i < _charIndexMap.Length; i++)
                {
                    _charIndexMap[i] = i;
                }
                return;
            }

            var offset = 0;
            var inTag = false;

            for (var i = 0; i < text.Length; i++)
            {
                var character = text[i];

                if (inTag)
                {
                    offset--;

                    if (character == GreaterThan)
                    {
                        inTag = false;
                    }
                }
                else if (supportRichText && character == LesserThan)
                {
                    offset--;
                    inTag = true;
                }
                else if (_invisibleChars.Contains(character))
                {
                    offset--;
                }

                _charIndexMap[i] = Mathf.Max(0, i + offset);
            }
        }

        #region Under Line

        private void DrawUnderLine(VertexHelper vh)
        {
            foreach (var link in _links)
            {
                foreach (var rect in link.Boxes)
                {
                    var height = rect.height;
                    // 左下
                    var pos1 = new Vector3(rect.min.x, rect.min.y, 0);
                    // 右下
                    var pos2 = new Vector3(rect.max.x, rect.max.y, 0) - new Vector3(0, height, 0);

                    MeshUnderLine(vh, pos1, pos2, link.Color);
                }
            }
        }

        private void MeshUnderLine(VertexHelper vh, Vector2 startPos, Vector2 endPos, Color lineColor)
        {            
        	var extents = rectTransform.rect.size;
            var setting = GetGenerationSettings(extents);

            var underlineText = new TextGenerator();
            underlineText.Populate(underline, setting);

            var lineVer = underlineText.verts; //" ̄"的的顶点数组

            var pos = new Vector3[4];
            pos[0] = startPos + new Vector2(-8, 0);
            pos[3] = startPos + new Vector2(-8, -4f);
            pos[2] = endPos + new Vector2(8, -4f);
            pos[1] = endPos + new Vector2(8, 0);

            if (lineVer.Count != 4) return;
            
            var tempVerts = new UIVertex[4];
            for (var i = 0; i < 4; i++)
            {
                tempVerts[i] = lineVer[i];
                tempVerts[i].color = lineColor;
                tempVerts[i].position = pos[i];
                tempVerts[i].uv0 = lineVer[i].uv0;
                tempVerts[i].uv1 = lineVer[i].uv1;
                tempVerts[i].uv2 = lineVer[i].uv2;
                tempVerts[i].uv3 = lineVer[i].uv3;
            }

            vh.AddUIVertexQuad(tempVerts);
        }

        #endregion
        #endregion

        private Vector3 CalculateLocalPosition(Vector3 position, Camera pressEventCamera)
        {
            if (!RootCanvas)
            {
                return Vector3.zero;
            }

            if (RootCanvas.renderMode == RenderMode.ScreenSpaceOverlay)
            {
                return transform.InverseTransformPoint(position);
            }

            RectTransformUtility.ScreenPointToLocalPointInRectangle(
                rectTransform,
                position,
                pressEventCamera,
                out var localPosition
            );

            return localPosition;
        }
        
        void IPointerClickHandler.OnPointerClick(PointerEventData eventData)
        {
            var localPosition = CalculateLocalPosition(eventData.position, eventData.pressEventCamera);

            foreach (var linkInfo in _links)
            {
                if (!linkInfo.Boxes.Any(t => t.Contains(localPosition))) continue;
                var subText = text.Substring(linkInfo.StartIndex, linkInfo.Length);
                var link = linkInfo.Link ?? subText;
                var content = linkInfo.Text ?? subText;
                linkInfo.Callback?.Invoke(link,content);
            }
        }

        #region Add Text Link
        
        /// <summary>
        /// 尝试添加超链接
        /// </summary>
        private void TryAddMatchLink()
        {
            foreach (var entry in linkRegexPattern)
            {
                var matches = Regex.Matches(text, entry.pattern, RegexOptions.Singleline);
                foreach (Match match in matches)
                {
                    var regex = new Regex(entry.pattern, RegexOptions.Singleline);
                    var regexMatch = regex.Match(match.Value);
                    var overwriteColor = entry.overwriteColor == true ? entry.color : (Color?)null;
                    if (regexMatch.Success)
                    {
                        var group = match.Groups[1];
                        AddLink(match.Index, group.Value,match.Value, overwriteColor, _onClickLink);
                    }
                    else
                    {
                        AddLink(match.Index, match.Value.Length, overwriteColor, _onClickLink);
                    }
                }
            }
        }

        private void CheckLinkException(int startIndex, int length, ClickLinkEvent onClick)
        {
            if (onClick == null)
            {
                throw new ArgumentNullException(nameof(onClick));
            }

            if (startIndex < 0 || startIndex > text.Length - 1)
            {
                throw new ArgumentOutOfRangeException(nameof(startIndex));
            }

            if (length < 1 || startIndex + length > text.Length)
            {
                throw new ArgumentOutOfRangeException(nameof(length));
            }
        }
        
        private void AddLink(int startIndex, int length, Color? linkColor, ClickLinkEvent onClick)
        {
            CheckLinkException(startIndex, length, onClick);

            _links.Add(new LinkInfo(startIndex, length, linkColor, onClick));
        }
        
        private void AddLink(int startIndex, string link, string content, Color? linkColor, ClickLinkEvent onClick)
        {
            CheckLinkException(startIndex, content.Length, onClick);

            _links.Add(new LinkInfo(startIndex, link, content, linkColor, onClick));
        }

        protected void AddLink(int startIndex, string link, string content, ClickLinkEvent onClick)
        {
            CheckLinkException(startIndex, content.Length, onClick);

            _links.Add(new LinkInfo(startIndex, link, content, onClick));
        }
        
        protected void CleanLink()
        {
            _links.Clear();
            linkRegexPattern.Clear();
        }

        #endregion
        
        #region Hyperlink_Test

        //#if Hyperlink_Test
        protected override void OnEnable()
        {
            base.OnEnable();
            onClickLink.AddListener(OnClickLinkText);
        }

        protected override void OnDisable()
        {
            base.OnDisable();
            onClickLink.RemoveListener(OnClickLinkText);
        }

        /// <summary>
        /// 当前点击超链接回调
        /// </summary>
        private void OnClickLinkText(string link,string content)
        {
            Debug.Log($"超链接信息:{link}\n{content}");
            Application.OpenURL(link);
        }
        //#endif
        #endregion
    }
}

编辑器面板:

using UnityEditor;
using UnityEditor.UI;

namespace Hyperlink.Editor
{
    [CustomEditor(typeof(TextHyperlink), true)]
    [CanEditMultipleObjects]
    public class TextHyperlinkEditor : TextEditor
    {
        private SerializedProperty _linkRegexPattern;
        private SerializedProperty _underline;
        private SerializedProperty _onClickLink;
        protected override void OnEnable()
        {
            base.OnEnable();
            _underline = serializedObject.FindProperty("underline");
            _linkRegexPattern = serializedObject.FindProperty("linkRegexPattern");
            _onClickLink = serializedObject.FindProperty("_onClickLink");
        }

        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();
            
            serializedObject.Update();
            EditorGUILayout.LabelField("Hyperlink", EditorStyles.boldLabel);
            EditorGUI.indentLevel++;
            EditorGUILayout.PropertyField(_underline);
            EditorGUILayout.PropertyField(_linkRegexPattern);
            EditorGUI.indentLevel--;
            EditorGUILayout.Space();
            EditorGUILayout.PropertyField(_onClickLink);
            serializedObject.ApplyModifiedProperties();
        }
    }
}

效果图:

 

最后,目前是将超链接下划线的颜色和超链接颜色绑定起来,如果有需要可以在超链接信息块中定义下划线相关的配置。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1907586.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

STM32中的DMA:解锁高效数据传输的秘密武器(内附实例)

目录 引言 理解DMA&#xff1a;数据的高效搬运工 DMA的主要特性 多优先级请求 事件标志 数据对齐 多样化的数据传输路径 广泛的数据源与目标 最大数据长度 DMA寄存器详解 增量与循环模式 DMA中断机制 ​编辑 小实验&#xff1a;DMA-ADC串口发送 引言 在现代嵌入…

SpringCloudAlibaba基础五 Nacos配置中心

一 Nacos配置中心介绍 官方文档&#xff1a;https://github.com/alibaba/spring-cloud-alibaba/wiki/Nacos-config Nacos 提供用于存储配置和其他元数据的 key/value 存储&#xff0c;为分布式系统中的外部化配置提供服务器端和客户端支持。使用 Spring Cloud Alibaba Nacos C…

集成sa-token前后端分离部署配置corsFliter解决跨域失效的真正原因

文章目录 1.前言2.问题复现3.解决方法3.1 方式一&#xff1a;后端修改CorsFilter源码3.2 方式二&#xff1a;前端禁用或移除浏览器referrer-policy引用者策略 4.总结 1.前言 缘由请参看下面这篇文章&#xff1a;sa-token前后端分离解决跨域的正确姿势 https://mp.weixin.qq.co…

气象观测站应该怎么选?

在气候变化日益严峻的今天&#xff0c;气象观测站的重要性不言而喻。它们不仅为气象部门提供宝贵的数据支持&#xff0c;还直接关系到农业生产、交通运输、城市规划等多个领域的决策。 在选择气象观测站时&#xff0c;首先要明确自己的功能需求。例如&#xff0c;是用于学术研究…

每日一练全新考试模式解锁|考试升级

&#x1f64b;频繁有小伙伴咨询&#xff1a;我想举办一场历时一个月的答题活动&#xff0c;学生可以每天打开答题&#xff0c;活动完结后可以导出每天的答题成绩 此前我们都会让小伙伴创建30场考试&#xff0c;然后使用批量分享功能组合起来&#xff0c;对外分享一个链接就可以…

类与对象2 3 十十一 杂烩

目录 组合类 作用域 static friend 常对象常成员函数 常引用 对象指针&#xff08;指向对象&#xff09; string 组合类 类的组合/聚合&#xff1a;将已有的类的对象作为新的类的成员。 组合类初始化&#xff1a;内嵌对象成员初始化 普通数据成员初始化。 类必须先…

24下软考《系统规划与管理师》,一个超好背的核心知识点几页纸!

距离下半年软考考试的时间越来越近了&#xff0c;想要备考《系统规划与管理师》的小伙伴们趁着这两周赶紧准备起来&#xff0c;虽说系规相对较好考&#xff0c;但作为高级科目&#xff0c;它要记得东西还是不少的。 今天给大家整理了——系统规划与管理师考前几页纸&#xff0c…

C++语言相关的常见面试题目(二)

1.vector底层实现原理 以下是 std::vector 的一般底层实现原理&#xff1a; 内存分配&#xff1a;当创建一个 std::vector 对象时&#xff0c;会分配一块初始大小的连续内存空间来存储元素。这个大小通常会随着 push_back() 操作而动态增加。 容量和大小&#xff1a;std::vec…

多卡(3090)部署通义千问Qwen2-72B大模型并加速至38tps:vLLM库的使用和错误排查

前一篇文章做了Qwen1的加速&#xff0c;其中关于Auto-GPTQ的安装问题在Qwen2中依然适用。但是Qwen2比Qwen1加载模型快了很多&#xff0c;笔者也不知道为什么。 下面是Hugging Face transformer版的千问2&#xff0c;token生成速度在15个每秒左右&#xff0c;但还不够快&#x…

Spring——IOC创建对象方式

可参考官网&#xff1a;https://docs.spring.io/spring-framework/reference/core/beans/dependencies/factory-collaborators.htmlhttps://docs.spring.io/spring-framework/reference/core/beans/dependencies/factory-collaborators.html 1. 使用无参构造创建对象&#xff0…

Unity--射线检测--RayCast

Unity–射线检测–RayCast 1.射线检测的含义 射线检测,根据名称而言,使用一条射线来检测是击中了某个物体/多个物体 射线检测的包含两个部分: 射线和检测 2.射线检测可以用在哪些地方 射击游戏&#xff1a; 玩家的瞄准和射击&#xff1a;检测玩家视线是否与敌人或其他目标…

初识c++(命名空间,缺省参数,函数重载)

一、命名空间 1、namespace的意义 在C/C中&#xff0c;变量、函数和后面要学到的类都是大量存在的&#xff0c;这些变量、函数和类的名称将都存在于全 局作用域中&#xff0c;可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化&#xff0c;以避免命名 冲突…

ubuntu24.04按关键字卸载不需要的apt包

使用的时候发现一个imagemagic无法正常读取文件&#xff0c;试图卸载 man apt经过尝试后&#xff0c;发现list的一个神奇关键字&#xff0c;用来显示已安装的软件包 sudo apt list --installed | grep image按image关键字过滤&#xff1a; 之后按软件名卸载即可 sudo apt pu…

数学建模论文写作文档word

目录 1. 摘要写法1.1 确定题目与方法1.2 编写开头段落1.3 填写问题一1.4 重复步骤3填写其他问题1.5 编写结尾段落1.6 编写关键词 2. 问题重述2.1 问题背景2.2 问题提出 3. 问题分析4. 问题X模型的建立与求解5. 模型的分析5.1 灵敏度分析5.2 误差分析&#xff08;主要用于预测类…

DAY2:插件学习

文章目录 插件学习ClangGoogle TestCMakeDoxygen 收获 插件学习 Clang 是什么&#xff1a;Clang 是指 LLVM 项目的编译器的前端部分&#xff0c;支持对 C 家族语言(C、C、Objective-C)的编译。Clang 的功能包括&#xff1a;词法分析、语法分析、语义分析、生成中间中间代码 L…

浅析C++引用

浅析C引用"&" ​ C中引入了一个新的语言特性——引用(&)&#xff0c;它表示某一对象的别名&#xff0c;对象与该对象的引用都是指向统一地址。那么我们就来看看关于引用的一些知识点吧&#x1f9d0; 特性 引用在定义时必须初始化一个变量可以有多个引用引…

C-10 凸包

凸包 数学定义 平面的一个子集S被称为是凸的&#xff0c;当且仅当对于任意两点A&#xff0c;B属于S&#xff0c;线段PS都完全属于S过于基础就不详细介绍了 凸包的计算 github上找到了别人的代码&#xff0c;用4种方式实现了凸包的计算&#xff0c;把他放在这里链接地址htt…

六、数据可视化—Wordcloud词云(爬虫及数据可视化)

六、数据可视化—Wordcloud词云&#xff08;爬虫及数据可视化&#xff09; 也是一个应用程序 http://amueller.github.io/word_cloud/ Wordcloud词云&#xff0c;在一些知乎&#xff0c;论坛等有这样一些东西&#xff0c;要么做封面&#xff0c;要么做讲解&#xff0c;进行分析…

Echarts 实现数据可视化

Echarts 简介 Echarts 是一个开源的、免费的、成熟的、商业级图表可视化框架&#xff0c;是 Apache 开源社区的顶级项目之一&#xff0c;也是国内使用最多和最为广泛的可视化图表框架之一。 数据可视化图表框架并没有一个统一的行业标准&#xff0c;比较常见的有 D3、Highchart…

电子设备常用的胶水有哪些?

目录 1、502胶水 2、703胶水 3、704胶水 4、AB胶 5、红胶 6、Underfill 7、导电胶 8、UV胶 9、热熔胶 10、环氧树脂胶 11、硅酮胶 12、聚氨酯胶 13、丙烯酸胶 14、丁基胶 1、502胶水 502胶水&#xff0c;也被称为瞬间胶或快干胶&#xff0c;是一种非常常见的粘合…