• 一个Unity富文本插件的实现思路


    项目中原来的富文本组件不太好用,做了一些修改,记述主要思路。缺陷很多。
    仅适用于没用TextMeshPro,且不打算用的项目,否则请直接用TextMeshPro

    原组件特点:

    1. 使用占位符模式,创建新的GameObject,挂载Image组件实现图文混排
    2. 主要通过正则匹配分析语法,扩展不便
    3. 固定RectTransform的anchor、pivot,Text的alignment,修改排版后需要手动计算相关位置,不能实现自动布局

    新组件目标

    1. 通过逐步读取的方式分析语法
    2. 实现混排内容位置的自动计算

    主要实现思路

    需要实现的混排功能

    1. 静态图片(sprite)
    2. 动态表情
    3. 链接点击响应
    4. 颜色(简略代号和#FFFFFF)
    5. 下划线
    6. UGUI原生Text标记(斜体、粗体、大小)

    混排位置计算的实现原理

    通过Text组件中的字符顶点信息,计算对应位置

    主要代码结构

    RichText.cs - 接口和生命周期处理
    RichText.MarkItem.cs - 定义结构类型和对象池处理
    RichText.MarkType.cs - 定义类型枚举
    RichText.Utils.cs - 辅助函数
    RichText.Analyzor.cs - 语法分析
    RichText.Generator.cs - 生成数据结构
    RichText.Drawer.cs - 绘制额外内容
    RichText.LinkListener.cs - 链接点击响应处理

    语法分析

    标记结构

    private class MarkItem
    {
        public int markId;
        public int markType;
        public string value;
        public int startIndex;    // 起始字符位置
        public int endIndex;      // 结束字符位置
    
        // 对象池略
    }
    

    语法主要模式

    #f001#n - 动态表情
    #c#FF0000#n红色文字#n - 文字颜色
    

    (即把Text的<>修改为#)

    语法分析步骤

    1. 预处理输入的字符串,清除UGUI Text的<>标记内容,进行一些其他需要的前期处理
    2. 清理分析栈、根节点、已存储的数据,根节点入栈
    3. 按顺序读取预处理后的字符串
    4. 如果下一个字符不是'#',读到下一个'#n',存为一个普通字符类型标记
    5. 如果下一个字符是'#',如果是'#n',结束上一个标记,否则读到下一个'#n'
    6. 读取到字符串结束
    7. 检验分析结果,若中间有标记不匹配,或最后分析栈不是只包含根节点,分析结果错误,直接输出原字符串;否则正确,开始生成特殊标记

    一些实现细节

    [RequireComponent(typeof(Text))]
    public partial class RichText : MonoBehavior
    {
        private MarkItem m_TreeRoot;
        private readonly Stack<int> m_AnalyzeStack = new Stack<int>();
        private readonly Dictionary<int, int> m_ParentDict = new Dictionary<int, int>();
        private readonly Dictionary<int, List<int>> m_ChildrenDict = new Dictionary<int, List<int>>();
    
        private void AnalyzeOriginalText()
        {
            string tempText = m_OriginalText;
            tempText = s_UnityMarkRegex.Replace(tempText, ""); // <.*?>
            // 其他处理
            Clear();
            
            int pos = 0;
            int length = tempText.Length;
            bool success = true;
    
            while (pos < length)
            {
                int curLength = 0;
                if (tempText[pos] != '#')
                {
                    int nextSharpPos = tempText.IndexOf('#', pos);
                    if (nextSharpPos < 0)
                    {
                        curLength = length - pos;
                    }
                    else
                    {
                        curLength = nextSharpPos - pos;
                    }
    
                    success = CreateNewMarkItem(MARK_NORMAL, tempText.Substring(pos, curLength));
                    if (!success) break;
                }
                else
                {
                    if (endMarkPos < 0)
                    {
                        curLength = length - pos;
                        success = CreateNewMarkItem(MARK_NORMAL, tempText.Substring(pos, curLength));
                        if (!success) break;
                    }
                    else {
                        curLength = endMarkPos - pos + 2;
                        success = CreateStyleMarkItem(tempText.Substring(pos, curLength));
                        if (!success) break;
                    }
                }
    
                pos += curLength;
            }
            
            if (m_AnalyzeStack.Count != 1)
            {
                Clear();
                CreateNewMarkItem(MARK_NORAML, tempText);
            }
        }
    
        private bool CreateNewMarkItem(int markType, string value)
        {
            int markId = m_MarkItemList.Count;
            MarkItem item = MarkItem.Get();
            item.markType = markType;
            item.markId = m_MarkItemList.Count;
            item.value = value;
    
            m_MarkItemList.Add(item);
            if (m_AnalyzeStack.Count == 0)
            {
                return false; //分析栈中根节点已经弹出,语法错误
            }
    
            int parentId = m_AnalyzeStack.Peek();
            if (!m_ChildrenDict.ContainsKey(parentId))
            {
                m_ChildrenDict[parentId] = new List<int>();
            }
            m_ChildrenDict[parentId].Add(item.markId);
    
            return true;
        }
    
        private bool CreateStyleMarkItem(string text)
        {
            int markType = MARK_NORMAL;
            string value = "";
            int length = text.Length;
            switch(text[1])
            {
                //...标记类型
            }
            
            if (length > 4)
            {
                value = text.Substring(2, length - 4);
            }
    
            bool success = CreateNewMarkItem(markType, value);
            if (!success)
            {
                return false;
            }
    
            switch (markType)
            {
                //可包含子节点的标记类型,入m_AnalyzeStack
            }
    
            return true;
        }
    }
    

    为markId使用自增id存入列表,使用字典存储父子关系,还有优化的地方

    结构生成

    主要遍历上一步生成的语法树

    //RichText.Generator.cs
    
    private readonly m_StringBuilder = new StringBuilder();
    
    private void GeneratorDisplayContent()
    {
        TraversalMarkItemNode(ROOT_ID);
        m_Text.text = m_StringBuilder.ToString();
    }
    
    private void TraversalMarkItemNode(int nodeId)
    {
        MarkItem node = m_MarkItemList[nodeId];
        int startIndex = m_StringBuilder.Length;
        node.startIndex = startIndex;
    
        switch(node.markType)
        {
            //普通类型略
            case MARK_PHOTO:
            case MARK_FACE:
                m_StringBuilder.Append("<color=#ffffff00>");
                float width, height;
                // 即得出需要使用几个占位符
                int placeholderCount = GetSpriteParams(node.markType, node.value, out width, out height);
                m_StringBuilder.Insert(m_StringBuilder.Length, PLACEHOLDER, placeholderCount);
                m_StringBuilder.Append("</color>");
    
                m_ActiveImageCount++;
                if (m_ImageItemList == null)
                {
                    m_ImageItemList = new List<ImageItem>();
                }
                ImageItem iItem = ImageItem.Get();
                iItem.markId = node.markId;
                iItem.startIndex = startIndex;
                iItem.endIndex = m_StringBuilder.Length;
                iItem.width = width;
                iItem.height = height;
                m_ImageItemList.Add(iItem);
    
                if (node.markType == MARK_FACE)
                {
                    m_ActiveFaceCount++;
                }
                break;
        }
    
        if (m_ChildrenDict.ContainsKey(nodeId))
        {
            List<int> list = m_ChildrenDict[nodeId];
            int size = list.Count;
            for (int i = 0; i < size; i++)
            {
                TraversalMarkItemNode(list[i]);
            }
        }
    
        //标记闭合处理
        int endIndex = m_StringBuilder.Length;
        node.endIndex = endIndex;
        switch(node.markType)
        {
            //普通类型略
            case MARK_LINK:
                //和上面Image类似,生成LinkItem,有参数可以做一些处理
                break;
            case MARK_UNDERLINE:
                //同上
                break;
        }
    }
    

    绘制额外内容

    首先解决在合适的位置绘制额外内容的问题

    // RichText.Drawer.cs
    
    private float m_PlaceholderPixelWidth = 0;
    private IList<UICharInfo> m_CurrentUICharInfoList = null;
    private IList<UILineInfo> m_CurrentUILineInfoList = null;
    private readonly List<int> m_CurrentLineStartIndexList = new List<int>();
    private readonly List<int> m_CurrentLineEndIndexList = new List<int>();
    private int m_CurrentCharactersCount = 0;
    private int m_CUrrentLinesCount = 0;
    
    
    private void RefreshExtraContents()
    {
        RefreshGeneratorResults();
        ResetImageGameObjects();
        ResetLinkGameObjects();
        ResetUnderlineGameObjects();
    }
    
    // 修改字体大小时调用,计算占位符宽度
    private void RefreshGeneratorParams()
    {
        TextGenerator textGenerator = new TextGenerator();
        Rect rect = m_RectTransform.rect;
        Vector2 extents = new Vector2(rect.width, rect.height);
        TextGenerationSettings settings = m_Text.GetGenerationSettings(extents);
        m_PlaceholderPixelWidth = textGenerator.GetPreferredWidth(PLACEHOLDER, settings);
    }
    
    // 拷贝生成器结果
    private void RefreshGeneratorResults()
    {
        TextGenerator generator = m_Text.cachedTextGenerator;
    
        // 第一次传值时未及时生成
        if (generator.characterCount == 0)
        {
            Rect rect = m_RectTransform.rect;
            Vector2 extents = new Vector2(rect.width, rect.height);
            TextGenerationSettings settings = m_Text.GetGenerationSettings(extents);
            generator.Populate(m_Text.text, setting);
        }
        m_CurrentCharactersCount = generator.characterCount; //显示部分(生成的)的字符数量,有点坑
        m_CurrentUICharInfoList = generator.characters;
        m_CurrentUILineInfoList = generator.lines;
    
        //刷新m_CurrentLineStartIndexList, m_CurrentLineEndIndexList
    }
    
    // 对象重用略
    
    //重设图片位置
    private void ResetImageGameObjects()
    {
        if (m_ActiveImageCount == 0)
        {
            if (m_ImageGo != null)
            {
                m_ImageGo.SetActive(false);
            }
    
            return;
        }
    
        if (m_ImageGo == null)
        {
            m_ImageGo = CreateUIGameObject(transform, IMAGE_GO_NAME, true);
        }
        m_ImageGo.SetActive(true);
    
        for (int i = 0; i < m_ActiveImageCount; i++)
        {
            ImageItem item = m_ImageItemList[i];
            GameObject go = item.gameObject;
    
            if (go == null)
            {
                go = GetImageGameObject(m_ImageGo.transform, i, i.ToString());
                item.gameObject = go;
            }
    
            Image image = go.GetComponent<Image>();
            MarkItem node = m_MarkItemList[item.markId];
    
            image.sprite = GetSprite(node.markType, node.value);
            RectTransform rectTransform = go.GetComponent<RectTransform>();
            rectTransform.sizeDelta = new Vector2(item.width, item.height);
            float sl, sr, st, sb, el, er, et, eb;
            // 一些字符是不显示的,如“<color=#ffffff>”, 获取实际位置
            int realStartIndex, readEndIndex;
            bool success = true;
            success &= TryGetNextValidCharPos(item.startIndex, out sl, out sr, out st, out sb, out realStartIndex);
            success &= TryGetPrevValidCharPos(item.endIndex, out el, out er, out et, out eb, out realEndIndex);
            success &= realStartIndex <= realEndIndex;
            if (!success)
            {
                item.active = false;
                go.SetActive(false);
            }
            else
            {
                item.active = true;
                go.SetActive(true);
            }
    
            float x = (sl + er) / 2;
            float y = (st + sb) / 2;
            rectTransform.localPosition = new Vector2(x, y);
    
            if (node.markType == MARK_FACE)
            {
                // 创建动画
            }
        }
    
        Transform container = m_ImageGo.transform;
        for (int i = m_ActiveImageCount; i < container.childCount; i++)
        {
            container.GetChild(i).gameObject.SetActive(false);
        }
    }
    
    //重设链接位置,外层基本和ResetImageGameObjects相同
    private void ResetLinkGameObjects()
    {
        //略
    
        for (int i = 0; i < m_ActiveLinkCount; i++)
        {
            //略
    
            //多行处理
            int curValidLines = 0;
            for (int line = 0; line < m_CUrrentLinesCount; line++)
            {
                int lineStartIndex = m_CurrentLineStartIndexList[line];
                int lineEndIndex = m_CurrentLineEndIndexList[line];
                if (startIndex > lineEndIndex) continue;
                if (lineStartIndex > endIndex) break;
                UILineInfo info = m_CurrentUILineInfoList[line];
                int curLineStartIndex = startIndex > lineStartIndex ? startIndex : lineStartIndex;
                int curLineEndIndex = endIndex < lineEndIndex ? endIndex : lineEndIndex;
                float sl, sr, st, sb, el, er, et, eb;
                int realStartIndex, realEndIndex;
                bool success = true;
                success &= TryGetNextValidCharPos(item.startIndex, out sl, out sr, out st, out sb, out realStartIndex);
                success &= TryGetPrevValidCharPos(item.endIndex, out el, out er, out et, out eb, out realEndIndex);
                success &= realStartIndex <= realEndIndex;
                success &= realStartIndex <= curLineEndIndex;
                success &= realEndIndex >= curLineStartIndex;
                if (!success)
                {
                    continue;
                }
    
                curValidLines++;
                float x = (sl + er) / 2;
                float y = info.topY - info.height / 2;
                float width = er - sl;
                if (width <= 0) continue;
                float height = info.height;
    
                GameObejct curGo = GetLinkGameObject(curContainer, curValidLines, string.Format("{0}_{1}", i, curValidLines));
                curGo.SetActive(true);
                RectTransform rectTransform = curGo.GetComponent<RectTransform>();
                rectTransform.localPosition = new Vector2(x, y);
                rectTransform.sizeDelta = new Vector2(width, height);
            }
    
            // 略
        }
    
        // 略
    }
    
    // ResetUnderlineGameObjects() 和 ResetLinkGameObjects() 基本相同,略
    
    
    // RichText.Utils.cs
    
    private bool TryGetPrevValidCharPos(int index, out float left, out float right, out float top, out float bottom, out int realIndex)
    {
        int size = m_CurrentUICharInfoList.Count;
        while (true)
        {
            if (index >= size || index < 0)
            {
                left = 0;
                right = 0;
                top = 0;
                bottom = 0;
                realIndex = 0;
                return false;
            }
    
            UICharInfo info = m_CurrentUICharInfoList[index];
            if (info.charWidth == 0)
            {
                index--;
                continue;
            }
    
            realIndex = index;
            left = info.cursorPos.x;
            top = info.cursorPos.y;
            right = left + info.charWidth;
            UILineInfo lineInfo;
            if (TryGetUILineInfoByCharacterIndex(realIndex, out linInfo))
            {
                bottom = top - lineInfo.fontSize;
            }
            else
            {
                bottom = top - m_Text.fontSize;
            }
    
            return true;
        }
    }
    

    在不设置RectTransform的anchor的情况下,上面的代码基本满足功能,待要修改布局的话,需要针对RectTransform的参数修改做相关处理

    private Vector2 m_CachedPivot;
    private Vector2 m_CachedAnchorMin;
    private Vector2 m_CachedAnchorMax;
    
    private void RefreshGameObjectPositions()
    {
        Vector2 pivot = m_RectTransform.pivot;
        Vector2 anchorMin = m_RectTransform.anchorMin;
        Vector2 anchorMax = m_RectTransform.anchorMax;
    
        if (pivot == m_CachedPivot && anchorMin == m_CachedAnchorMin && anchorMax == m_CachedAnchorMax)
        {
            return;
        }
    
        m_CachedPivot = pivot;
        m_CachedAnchorMin = anchorMin;
        m_CachedAnchorMax = anchorMax;
    
        if (m_ImageGo)
        {
            // 传递下去
        }
    
        // 后略
    }
    
    private void OnRectTransformDimensionsChange()
    {
        if (!m_Inited)
        {
            return;
        }
    
        RefreshExtraContents();
    }
    

    修改后可以随RectTransform的变化而变化,但发现很容易出现错位现象,原因是unity的自动布局等常在下一帧生效,延时一会儿即可

    public int repaintDelayFrame = 3;
    private int m_NextRepaintFrame = -1;
    
    private void Update()
    {
        if (!m_Inited)
        {
            return;
        }
    
        if (m_Text.cachedTextGenerator.characterCount != m_CurrentCharactersCount)
        {
            m_NextRepaintFrame = repaintDelayFrame;
            m_CurrentCharactersCount = m_Text.cachedTextGenerator.characterCount;
        }
    
        if (m_NextRepaintFrame > 0)
        {
            m_NextRepaintFrame--;
        }
        else if (m_NextRepaintFrame == 0)
        {
            Repaint();
            m_NextRepaintFrame--;
        }
    
        // 略
    }
    
    private void OnRectTransformDimensionsChange()
    {
        if (!m_Inited)
        {
            return;
        }
    
        m_NextRepaintFrame = repaintDelayFrame;
    }
    

    bug注意

    1. Canvas的RenderMode设置为ScreenSpace - CameraOverlay时,若因CanvasScaler设置了缩放,TextGenerator.characters得到的坐标、宽度数据会有缩放,位置计算出现偏差,需要除一次缩放比率
  • 相关阅读:
    ASP.NET Core 中的路由约束
    专治拖延症,好方法
    虚拟机hadoop集群搭建
    python爬虫+词云图,爬取网易云音乐评论
    gp数据库运维
    kafka和springboot整合应用
    kafka配置监控和消费者测试
    集群运维ansible
    SpringBoot和微服务
    python每天定时发送短信脚本
  • 原文地址:https://www.cnblogs.com/lunoctis/p/12228228.html
Copyright © 2020-2023  润新知