工程截图
UIEmojiTextEditor.cs (将这个脚本放在Editor目录下)
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using UnityEditor; namespace UnityEditor.UI { [CustomEditor(typeof(UIEmojiText), true)] [CanEditMultipleObjects] public class UIEmojiTextEditor : GraphicEditor { SerializedProperty m_Text; SerializedProperty m_FontData; SerializedProperty m_EmojiAtlas; protected override void OnEnable() { base.OnEnable(); m_Text = serializedObject.FindProperty("m_Text"); m_FontData = serializedObject.FindProperty("m_FontData"); m_EmojiAtlas = serializedObject.FindProperty("m_EmojiAtlas"); } public override void OnInspectorGUI() { serializedObject.Update(); EditorGUILayout.PropertyField(m_Text); EditorGUILayout.PropertyField(m_EmojiAtlas); EditorGUILayout.PropertyField(m_FontData); AppearanceControlsGUI(); RaycastControlsGUI(); serializedObject.ApplyModifiedProperties(); } } }
UIEmojiText.cs
using System; using System.Collections; using System.Collections.Generic; using System.Text.RegularExpressions; using UnityEngine; using UnityEngine.UI; using UnityEngine.Sprites; using UnityEngine.U2D; using UnityEngine.Events; using UnityEngine.EventSystems; using UnityEngine.Serialization; /// <summary> /// 支持 聊天表情&超链接 Text /// </summary> public class UIEmojiText : Text, IPointerClickHandler { [SerializeField] private SpriteAtlas m_EmojiAtlas; private bool m_EmojiDirty = false; private bool m_InsertLine = true; private bool m_OnPopulateMesh = false; private List<EmojiInfo> m_EmojiList = new List<EmojiInfo>(); private List<Image> m_ImageList = new List<Image>(); //超链接列表 private List<HyperLink> m_HyperLinkList = new List<HyperLink>(); //超链接点击事件 public Action<HyperLink> OnClickHyperLinkEvent; private bool m_ParseHyperLink = true; private const string HYPER_LINK_PATTERN = "<a href=\"(?<url>[\\s\\S]*?)\">(?<text>[\\s\\S]*?)</a>"; public struct HyperLink { //区域 public Rect rect; //超链接参数 public string href; //超链接文本 public string text; } private class EmojiInfo { public string name; public Vector3 position; } protected override void Start() { base.Start(); DestroyEmoji(); } public override string text { get { return m_Text; } set { if (String.IsNullOrEmpty(value)) { if (String.IsNullOrEmpty(m_Text)) return; m_Text = ""; SetVerticesDirty(); } else if (m_Text != value) { m_Text = value; SetVerticesDirty(); SetLayoutDirty(); m_InsertLine = true; m_ParseHyperLink = true; } } } protected override void OnPopulateMesh(VertexHelper toFill) { base.OnPopulateMesh(toFill); //不能在OnPopulateMesh方法中创建资源,否则会报以下错: //Trying to add xxx (UnityEngine.UI.Image) for graphic rebuild while we are already inside a graphic rebuild loop. This is not supported. m_EmojiList.Clear(); //解决Text组件在占位符位置显示乱码问题 string s = text; string pattern = "<quad name=(?<name>\\d*?) size=\\d+ width=\\d+/>"; int preIndex = 0; int verIndex = 0; foreach (Match match in Regex.Matches(s, pattern, RegexOptions.Multiline)) { int index = match.Index; string name = match.Groups["name"].Value; //表情名,例如 001 string str = s.Substring(preIndex, index - preIndex); preIndex = index + match.Value.Length; int length = CalculateLength(str); verIndex += length * 4;//占位符起始顶点索引 SetUIVertex(toFill, verIndex); //在占位符处创建表情 EmojiInfo ei = new EmojiInfo(); ei.name = name; ei.position = CalculateEmojiPosition(verIndex); m_EmojiList.Add(ei); m_EmojiDirty = true; //Bug: //自动换行时toFill.currentVertCount值会突然变大很多,导致表情坐标计算错误. //解决方法:在原本自动换行处手动插入\n //Debug.LogFormat("{0} {1} length={2} verIndex={3} total_length={4} toFill.currentVertCount={5}", //name, ei.position.ToString(), length, verIndex, s.Length, toFill.currentVertCount); verIndex += 4; //跳过占位符顶点索引 } m_OnPopulateMesh = true; } private void Update() { if (!m_OnPopulateMesh) return; if (m_InsertLine) { m_Text = InsertLine(m_Text); //Debug.Log(m_Text); m_InsertLine = false; SetVerticesDirty(); } else if (m_EmojiDirty) { ReleaseEmoji(); CreateEmoji(); } else if (m_ParseHyperLink) { ParseHyperLink(); m_ParseHyperLink = false; } } #region 聊天表情处理 private void SetUIVertex(VertexHelper toFill, int index) { if (index + 4 > toFill.currentVertCount) return; //1个字符包含4个顶点 for (int i = index; i < index + 4; i++) { UIVertex vert = new UIVertex(); //通过修改占位符uv坐标来隐藏乱码显示 vert.uv0 = Vector2.zero; vert.uv1 = Vector2.zero; vert.uv2 = Vector2.zero; vert.uv3 = Vector2.zero; toFill.SetUIVertex(vert, i); } } private int CalculateLength(string str) { int length = 0; bool findSymbol = false; for (int i = 0; i < str.Length; i++) { if (str[i] == '\r' || str[i] == '\n' || str[i] == ' ') { continue; } if (str[i] == '<') { findSymbol = true; continue; } if (str[i] == '>') { findSymbol = false; continue; } if (findSymbol) continue; length++; } return length; } // 插入换行符 private string InsertLine(string str) { if (!Application.isPlaying) return str; string s = str; float width = 0, nextWidth; int offset = 0; float rectWidth = rectTransform.sizeDelta.x; TextGenerator textGen = cachedTextGenerator; IList<UICharInfo> infos = textGen.characters; for (int i=0; i<infos.Count; i++) { width += infos[i].charWidth; nextWidth = i + 1 < infos.Count ? infos[i + 1].charWidth : 0; if (width + nextWidth > rectWidth) { //Debug.LogFormat("Insert Line: {0}FL{1}", str[i-1], str[i]); s = s.Insert(i + offset, "\n"); width = 0; offset++; } } //超链接文本中不允许换行,将超链接中的换行符提到文本之前 foreach (Match match in Regex.Matches(s, HYPER_LINK_PATTERN)) { int index = match.Index; string url = match.Groups["url"].Value; string text = match.Groups["text"].Value; int lineIndex = text.IndexOf('\n'); if (lineIndex != -1) { lineIndex = s.IndexOf('\n', index); s = s.Remove(lineIndex, 1); s = s.Insert(index, "\n"); } } return s; } // 计算Emoji坐标 private Vector2 CalculateEmojiPosition(int startIndex) { IList<UIVertex> verts = cachedTextGenerator.verts; Vector3 pos = Vector3.zero; for (int i=startIndex; i<startIndex+4; i++) { if (i >= verts.Count) break; pos += verts[i].position; } pos /= 4; pos /= canvas.scaleFactor; //适应不同分辨率 return pos; } // 创建表情 private void CreateEmoji() { if (m_EmojiDirty) { m_EmojiDirty = false; for (int i = 0; i < m_EmojiList.Count; i++) { EmojiInfo ei = m_EmojiList[i]; Image image = CreateEmoji(ei.name, ei.position); m_ImageList.Add(image); } m_EmojiList.Clear(); Debug.LogFormat("Pool created element count: {0}", EmojiPool.Instance.elementCount); } } // 创建表情 private Image CreateEmoji(string name, Vector2 position) { if (EmojiPool.Instance.emojiTemplet == null) { Transform emoji_templet = this.transform.Find("emoji_templet"); EmojiPool.Instance.emojiTemplet = emoji_templet.GetComponent<Image>(); } Image emoji = EmojiPool.Instance.Get(name); if (emoji == null) return null; CanvasUpdateRegistry.UnRegisterCanvasElementForRebuild(emoji); emoji.rectTransform.SetParent(this.transform); RectTransform rt = emoji.rectTransform; rt.anchoredPosition = position; emoji.gameObject.SetActive(true); emoji.enabled = true; return emoji; } // 释放表情 private void ReleaseEmoji() { for (int i=0; i<m_ImageList.Count; i++) { EmojiPool.Instance.Release(m_ImageList[i]); } m_ImageList.Clear(); } // 销毁Emoji对象 private void DestroyEmoji() { Image[] arr = transform.GetComponentsInChildren<Image>(); if (arr == null) return; for (int i=0; i<arr.Length; i++) { if (Application.isPlaying) Destroy(arr[i].gameObject); else DestroyImmediate(arr[i].gameObject); } } // 设置Unity默认字体 private void AssignDefaultFont() { font = Resources.GetBuiltinResource<Font>("Arial.ttf"); } #endregion #region 超链接处理 // 点击事件 public void OnPointerClick(PointerEventData eventData) { Vector2 position = eventData.position; Vector2 rect_position = UGUITool.ScreenPointToLocal(rectTransform, position); HyperLink hyper = GetHyperLinkByPosition(rect_position); if (!string.IsNullOrEmpty(hyper.text)) { Debug.LogFormat("Click: href={0}, text={1}", hyper.href, hyper.text); if (OnClickHyperLinkEvent != null) OnClickHyperLinkEvent(hyper); } } // 通过点击坐标获取超链接 private HyperLink GetHyperLinkByPosition(Vector2 rect_position) { HyperLink hyperLink = default(HyperLink); float x = rect_position.x; float y = rect_position.y; for (int i = 0; i < m_HyperLinkList.Count; i++) { HyperLink hyper = m_HyperLinkList[i]; if (x > hyper.rect.xMin && x < hyper.rect.xMax && y > hyper.rect.yMin && y < hyper.rect.yMax) { hyperLink = hyper; break; } } return hyperLink; } // 解析文本中的超链接 private void ParseHyperLink() { if (!Application.isPlaying) return; m_HyperLinkList.Clear(); string input = m_Text; string nosymbol_text = StripSymbol(input); foreach (Match match in Regex.Matches(input, HYPER_LINK_PATTERN)) { string url = match.Groups["url"].Value; string text = match.Groups["text"].Value; Capture capture = match.Groups["text"]; Debug.LogFormat("HyperLink: url={0}, text={1}", url, text); TextGenerator tg = cachedTextGeneratorForLayout; //UGUI中标记符的字符不占用文本宽度(即,UICharInfo.charWidth=0) UICharInfo[] charInfos = tg.GetCharactersArray();//数组中包含标记符 int start_index = capture.Index; int end_index = capture.Index + capture.Length; float charHeight = 0; float linkWidth = 0; //找字符高度 for (int i = start_index; i < end_index; i++) { UICharInfo info = charInfos[i]; if (info.charWidth > 0) { if (info.charWidth > charHeight) charHeight = info.charWidth; linkWidth += info.charWidth; } } //计算超链接在文本中的x坐标 float linkStartX = 0;//超链接起始X坐标 for (int i = start_index; i >= 0; i--) { char c = input[i]; if (c == '\n') break; UICharInfo info = charInfos[i]; linkStartX += info.charWidth; } float linkEndX = linkStartX + linkWidth;//超链接终止X坐标 UICharInfo start_info = charInfos[start_index]; UICharInfo end_info = charInfos[end_index]; Rect rect = new Rect(); rect.xMin = linkStartX; rect.xMax = linkEndX; //y坐标是负值 rect.yMin = end_info.cursorPos.y - charHeight; rect.yMax = start_info.cursorPos.y; HyperLink hyper = new HyperLink(); hyper.href = url; hyper.text = StripSymbol(text); hyper.rect = rect; m_HyperLinkList.Add(hyper); } } // 返回不含标记符的文本 private string StripSymbol(string text) { if (string.IsNullOrEmpty(text)) return string.Empty; string str = string.Empty; List<char> list = new List<char>(); bool findSymbol = false; for (int i = 0; i < text.Length; i++) { if (text[i] == '<') { findSymbol = true; continue; } if (text[i] == '>') { findSymbol = false; continue; } if (findSymbol) continue; list.Add(text[i]); } str = new string(list.ToArray()); return str; } #endregion } #region Emoji对象池 /// <summary> /// Emoji对象池 /// </summary> [ExecuteInEditMode] public class EmojiPool { private static EmojiPool _instance = null; public static EmojiPool Instance { get { if (_instance == null) _instance = new EmojiPool(); return _instance; } private set { } } public Image emojiTemplet; public int elementCount; private SpriteAtlas atlas; private readonly Dictionary<string, Stack<Image>> dic = new Dictionary<string, Stack<Image>>(); private EmojiPool() { atlas = Resources.Load<SpriteAtlas>("UI/EmojiAtlas"); } public Image Get(string name) { if (atlas == null || emojiTemplet == null) return null; Image image; Sprite sprite; if (dic.ContainsKey(name)) { Stack<Image> stack = dic[name]; if (stack.Count <= 0) { sprite = atlas.GetSprite(name); image = GameObject.Instantiate<Image>(emojiTemplet); image.sprite = sprite; image.name = name; elementCount++; } else image = stack.Pop(); } else { sprite = atlas.GetSprite(name); image = GameObject.Instantiate<Image>(emojiTemplet); image.sprite = sprite; image.name = name; elementCount++; } return image; } public void Release(Image image) { if (image == null) return; string name = image.name; if (!dic.ContainsKey(name)) dic.Add(name, new Stack<Image>()); Stack<Image> stack = dic[name]; stack.Push(image); image.gameObject.SetActive(false); } } #endregion
效果
测试用例
emojiText.text = "<color=red>【系统】</color><quad name=001 size=32 width=1/>恭喜<a href=\"263665629\"><color=green>埃布尔☆亚当</color></a>获得<quad name=084 size=32 width=1/><a href=\"this is mofaxianglian\"><color=yellow>魔法项链</color></a>最高级别防御武器<quad name=004 size=32 width=1/>真是让人羡慕嫉妒恨<quad name=041 size=32 width=1/>~~~~~~~~~~~~~~~~~<a href=\"fangtianhuaji\"><color=yellow>方天化戟</color></a>";