Unity扩展 Text支持超链接文本

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

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

方案一:换用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 PopulateMeshprivate 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);}}#endregionprivate 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_Testprotected 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 Lineprivate 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 PopulateMeshprivate 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 Lineprivate 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#endregionprivate 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_Testprotected 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.mzph.cn/pingmian/42338.shtml

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

相关文章

日本服务器托管需要注意哪些问题

日本服务器托管是一项涉及多方面因素的重要决策&#xff0c;为了确保托管服务的稳定、高效与安全&#xff0c;企业或个人在托管过程中需要注意以下几个关键问题&#xff1a; 首先&#xff0c;数据中心的基础设施建设标准是决定托管稳定性的关键。这包括数据中心的建筑抗震、抗洪…

拍桌子、甩脸子、抡棒子没用,带出一流战斗力团队用好3招就够了

拍桌子、甩脸子、抡棒子没用&#xff0c;带出一流战斗力团队用好3招就够了 第一招&#xff1a;及时激励 在现实中&#xff0c;绝大部分管理者管理手段缺乏&#xff0c;只知道用钱进行激励。 而真正的高手不仅会满足员工物质上的需求&#xff0c;更注重员工心理上的满足。 他…

水箱高低水位浮球液位开关工作原理

工作原理 水箱高低水位浮球液位开关是一种利用浮球随液位升降来实现液位控制的设备。其基本原理是浮球在液体的浮力作用下上下浮动&#xff0c;通过磁性作用驱动与之相连的磁簧开关的开合&#xff0c;从而实现液位的高低控制和报警。当液位升高时&#xff0c;浮球上浮&#xf…

04-ArcGIS For JavaScript的可视域分析功能

文章目录 综述代码实现代码解析结果 综述 在数字孪生或者实景三维的项目中&#xff0c;视频融合和可视域分析&#xff0c;一直都是热点问题。Cesium中&#xff0c;支持对阴影的后处理操作&#xff0c;通过重新编写GLSL代码就能实现视域和视频融合的功能。ArcGIS之前支持的可视…

Kubernetes分享

幂等性(Idempotency) 介绍 简单来说&#xff0c;幂等性幂等性(Idempotency)是计算机科学中的一个重要概念&#xff0c;特别是在分布式系统和网络应用中。指的是某个操作可以重复执行多次&#xff0c;但其结果是相同的&#xff0c;不会因为多次执行而改变系统的状态。 https://…

IT之家最新科技热点 | 小米 AI 研究院开创多模态通用模型

人不走空 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌赋&#xff1a;斯是陋室&#xff0c;惟吾德馨 目录 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌…

【计算机视觉】基于OpenCV的直线检测

直线检测原理 霍夫变换是图像处理必然接触到的一个算法&#xff0c;它通过一种投票算法检测具有特定形状的物体,该过程在一个参数空间中通过计算累计结果的局部最大值得到一个符合该特定形状的集合作为霍夫变换结果&#xff0c;该方法可以进行圆&#xff0c;直线&#xff0c;椭…

Java入门-异常机制

java异常机制 异常概念 在Java中&#xff0c;异常处理(exception handling) : java语言或者程序员开发提供的一种机制&#xff0c;当有不正常的情况发生时&#xff0c;可以发出信号。这种发出信号的过程被称为抛出异常(throwing an exception)。 java异常体系 Error Error类对…

Android OpenGL ES 离屏幕渲染1——EGL环境的创建,以及基础概念的理解

创建EGL上下文、配置EGL环境、创建EGL DISPLAY 什么是EGL&#xff1a; 由于OpenGL ES并不负责窗口管理以及上下文管理&#xff0c;该职责由各个平台自行完成&#xff1b;在Android平台下OpenGL ES的上下文环境是依赖EGL的API进行搭建的。 对于EGL这个框架&#xff0c;谷歌已经提…

测试环境:使用OpenSSL生成证书并配置Https

文章目录 需求1、安装OpenSSL1.1、安装包下载1.2、安装&#xff08;以window 64位为例&#xff09;1.3、配置环境变量&#xff08;非必须&#xff09; 2、生成证书2.1、新建文件夹2.2、生成根证书2.2.1、生成私钥2.2.2、生成根证书&#xff0c;并且自签名 2.3、服务端证书生成2…

【双一流高校主办,Springer-LNICST出版,EI稳定检索】2024年应用计算智能、信息学与大数据国际会议(ACIIBD 2024,7月26-28)

2024年应用计算智能、信息学与大数据国际学术会议&#xff08;ACIIBD 2024&#xff09;将于2024年7月26-28日在中国广州举办。会议将聚焦于计算智能及其应用、信息、大数据等相关的研究领域&#xff0c; 广泛邀请国内外知名专家学者&#xff0c;共同探讨相关学科领域的最新发展…

rsyslog日志转发

前言 Rsyslog可用于接受来自各种来源(本地和网络)的输入&#xff0c;转换它们&#xff0c;并将结果输出到不同&#xff08;通过模板和filter过滤&#xff09;的目的地&#xff08;目录文件中&#xff09; rsyslog是一个开源工具&#xff0c;被广泛用于Linux系统以通过TCP/UDP…

[spring] Spring MVC - security(上)

[spring] Spring MVC - security&#xff08;上&#xff09; 这部分的内容基本上和 [spring] rest api security 是重合的&#xff0c;主要就是添加 验证&#xff08;authentication&#xff09;和授权&#xff08;authorization&#xff09;这两个功能 即&#xff1a; 用户…

python自动化办公之cryptography加密解密

目录 用到的库 实现效果 代码部分 1、加密2024.txt文件 2、解密2024.txt文件 用到的库 cryptography 实现效果 加密文件和解密文件 代码部分 1、加密2024.txt文件 # 加密 from cryptography.fernet import Fernet # 生成加密密钥 keyFernet.generate_key() cipher_s…

Raw Socket(一)实现TCP三次握手

实验环境&#xff1a; Windows物理机&#xff1a;192.168.1.4 WSL Ubuntu 20.04.6 LTS&#xff1a;172.19.32.196 Windows下的一个http服务器&#xff1a;HFS&#xff0c;大概长这个样子&#xff1a; 客户端就是Ubuntu&#xff0c;服务端就是这个…

收银系统源码-线上商城预售功能

1.功能描述 预售&#xff1a;智慧新零售收银系统&#xff0c;线上商城营销插件之一&#xff0c;商品出售时可设置以支付定金或全款的方式提前预售&#xff0c;门店按订单量备货&#xff0c;降低压货成本&#xff1b; 2.适用场景 易损商品提前下单备货&#xff0c;如水果生鲜…

【算法笔记自学】第 5 章 入门篇(3)——数学问题

5.1简单数学 #include <cstdio> #include <algorithm> using namespace std; bool cmp(int a,int b){return a>b; } void to_array(int n,int num[]){for(int i0;i<4;i){num[i]n%10;n /10;} } int to_number(int num[]){int sum0;for(int i0;i<4;i){sumsu…

AI微电影制作教程:轻松打造高清小人国画面

AI微电影作为一种新兴的视频内容形式&#xff0c;以其独特的视觉效果和制作技术在各大视频平台上取得了显著的流量表现。 2. AI微电影的特点 2.1 高清画质与流畅动作&#xff1a;AI微电影以其高分辨率和流畅的动作给观众带来优质的视觉体验。 2.2 微缩画面效果&#xff1a;独…

使用Docker、Docker-compose部署单机版达梦数据库(DM8)

安装前准备 Linux Centos7安装&#xff1a;https://blog.csdn.net/andyLyysh/article/details/127248551?spm1001.2014.3001.5502 Docker、Docker-compose安装&#xff1a;https://blog.csdn.net/andyLyysh/article/details/126738190?spm1001.2014.3001.5502 下载DM8镜像 …

自动控制:前馈控制

自动控制&#xff1a;前馈控制 前馈控制是一种在控制系统中通过预先计算和调整输入来应对已知扰动或变化的方法。相比于反馈控制&#xff0c;前馈控制能够更快速地响应系统的变化&#xff0c;因为它不依赖于系统输出的反馈信号。前馈控制的应用在工业过程中尤为广泛&#xff0…