鸟语天空
UGUI—显示聊天表情
post by:追风剑情 2020-4-14 14:34

工程截图

3333.png

444.png

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>";
1111.png 222.png

评论:
发表评论:
昵称

邮件地址 (选填)

个人主页 (选填)

内容