需求是,我们在一个text组件中像写网页那样写入链接,然后点击这个链接,就能访问配置的网页啥的。比如:
<a href="hello">链接文本</a></summary>
最终的效果如下:
图中,image区域就是各个链接的点击范围。原理是获取text中,每个字符的位置,然后算出每个链接对应的点击区域,最后返回鼠标点到的那个区域的链接。代码比较简单,就直接写点注释看吧。实现是继承了text组件,当然写成静态方法传入text来计算也可以。
比较一下网上搜到的其他方案,这个方法不用重载mesh,效率应该是比较高的。
#define TEST_CheckClickURL
using System.Collections.Generic;
using System.Text.RegularExpressions;
using UnityEngine;
using UnityEngine.Profiling;
using UnityEngine.UI;
public class TestClickURL : Text
{
public Button button;
public void OnButtonClick()
{
Debug.Log(CheckClickURL()?.url);
}
// 定义返回的结果
public class CheckClickURLResult
{
public string url;
public string text;
public Rect rect;
public CheckClickURLResult(string url, string text, Rect rect)
{
this.url = url;
this.text = text;
this.rect = rect;
}
}
private static Regex hrefRegex =
new Regex(@"<a href=([^>\n\s]+)>(.*?)(</a>)",
RegexOptions.Singleline);
/// <summary> 计算点击到的URL文本内容,返回网址
/// 格式如下:<a href="hello">链接文本</a></summary>
public CheckClickURLResult CheckClickURL()
{
Profiler.BeginSample("CheckClickURL");
if (hrefRegex == null)
hrefRegex = new Regex(
@"<a href=([^>\n\s]+)>(.*?)(</a>)", RegexOptions.Singleline);
InitDebugGOList();
// 将点击位置从屏幕坐标转为本地坐标
RectTransformUtility.ScreenPointToLocalPointInRectangle(
this.rectTransform, Input.mousePosition,
null, out var mouseLocalPosition);
//注意使用UI相机
// 获取生成的文本数据。
// characters 保存了每个字符左上角的位置。
// lines 保存了每行开始字符ID,和行高。
var generator = cachedTextGenerator;
var charList = generator.characters;
var lineList = generator.lines;
var textStr = text;
// 正则表达式查找链接文本
var matchs = hrefRegex.Matches(textStr);
foreach (Match match in matchs)
{
var urlGroup = match.Groups[1];
var textGroup = match.Groups[0];
var textStartIndex = textGroup.Index;
var textEndIndex = textGroup.Index + textGroup.Length;
// 我们的字符可能是换行的,所以要按行分割。
// 倒着遍历就很容易获取每行开始和结束位置。
var lineEndIndex = charList.Count - 1;
for (int i = lineList.Count - 1; i >= 0; i--)
{
var lineStartIndex = lineList[i].startCharIdx;
// 处理换行后的截取
var realStart = Mathf.Max(lineStartIndex, textStartIndex);
var realEnd = Mathf.Min(lineEndIndex, textEndIndex);
// 本行没有链接内容的情况
if (realStart > realEnd) continue;
// 问题简化成单行的点击检查,提个函数继续处理。
var result = CheckLine(realStart, realEnd, lineList[i].height, mouseLocalPosition, out var rect);
if (result) return new CheckClickURLResult(urlGroup.Value, textGroup.Value, rect);
//Debug.Log($"{start}/{end}");
lineEndIndex = lineStartIndex - 1;
}
}
Profiler.EndSample();
return null;
}
public bool CheckLine(int start, int end, float lineHeight, Vector2 mouseLocalPosition, out Rect rect)
{
// 获取生成的文本数据。
var charList = cachedTextGenerator.characters;
var startPoint = charList[start].cursorPos;
var endPoint = charList[end].cursorPos;
// 直接计算出本行中链接可点击区域。
var x = startPoint.x;
var y = startPoint.y - lineHeight;
var width = endPoint.x - startPoint.x;
var height = lineHeight;
rect = new Rect(x, y, width, height);
var result = rect.Contains(mouseLocalPosition);
CreateDebugImage(rect, result);
return result;
}
#if TEST_CheckClickURL
// 测试用。生成空image展示出点击判定范围。
public static List<GameObject> debugGOList;
public void CreateDebugImage(Rect rect, bool contains)
{
Debug.Log($"rect={rect}");
var go = new GameObject("DebugImage",
typeof(RectTransform), typeof(Image));
debugGOList.Add(go);
var rtf = go.GetComponent<RectTransform>();
rtf.SetParent(transform);
rtf.pivot = Vector2.zero;
rtf.anchorMin = Vector2.one / 2;
rtf.anchorMax = Vector2.one / 2;
rtf.sizeDelta = rect.size;
rtf.localScale = Vector3.one;
rtf.rotation = Quaternion.identity;
rtf.anchoredPosition = rect.position - rectTransform.rect.center;
// 点击到的那个范围展示为红色。
if (contains)
go.GetComponent<Image>().color = Color.red;
}
public void InitDebugGOList()
{
if (debugGOList == null)
debugGOList = new List<GameObject>();
debugGOList.ForEach(p => Destroy(p));
}
#else
public void CreateDebugImage(Rect rect, bool contains) { }
public void InitDebugGOList() { }
#endif
}