UGUI表情系统&超链接解决方案

最近帮一个同事解决图文混排的问题,发现了一种犀利的UGUI表情系统的解决方案
https://blog.uwa4d.com/archives/Sparkle_UGUI.html
使用重新生成UGUI文字Mesh的方式来支持表情图片。在Shader中判断是否有第二个套UV传入来渲染表情,动态表情也在GPU端计算~ 可以合并DrawCall,支持UGUI的遮罩、自适应等等

我在原作者的基础上扩展了一些改进,使其能支持超链接


超链接的思路是先计算出超链接的顶点包围盒,监听到点击事件的时候,跟超链接包围和进行碰撞检测来判断是否点击到了某个连接。如果一个超链接跨行的时候,就需要创建多个包围盒来处理。

如何解析超链接标签?

<a href='xx'>点击加入队伍</a>

在Text发生变化的时候,UGUI会调用SetVerticesDirty函数把组件注册到ReBuilder队列里面,等待下一帧重绘。所以我们在SetVerticesDirty函数中写上解析a标签的代码

    /// <summary>
    /// 获取超链接解析后的最后输出文本
    /// </summary>
    /// <returns></returns>
    protected virtual string GetOutputText(string outputText)
    {
        s_TextBuilder.Length = 0;
        m_HrefInfos.Clear();
        var indexText = 0;

        foreach (Match match in s_HrefRegex.Matches(outputText))
        {
            s_TextBuilder.Append(outputText.Substring(indexText, match.Index - indexText));
            s_TextBuilder.Append("<color='#9ed7ff'>");  // 超链接颜色ff6600

            var group = match.Groups[1];
            var hrefInfo = new HrefInfo
            {
                startIndex = s_TextBuilder.Length * 4, // 超链接里的文本起始顶点索引
                endIndex = (s_TextBuilder.Length + match.Groups[2].Length - 1) * 4 + 3,
                name = group.Value
            };
            m_HrefInfos.Add(hrefInfo);

            s_TextBuilder.Append(match.Groups[2].Value);
            s_TextBuilder.Append("</color>");
            indexText = match.Index + match.Length;
        }

        s_TextBuilder.Append(outputText.Substring(indexText, outputText.Length - indexText));
        return s_TextBuilder.ToString();
    }

通过正则表达式匹配后,计算出超链接的起始顶点索引、结束顶点索引、name保存到一个列表里。

链接跟图片索引冲突问题

text中的表情标签如"[0]"由3个字符组成,每个字符4个顶点,所以占用12个顶点
但是在填充顶点的时候,我们只会用4个顶点渲染图片,来替换掉原来的12个顶点
所以前面计算的超链接的startIndex,endIndex也要随之改变

    private void HrefInfosIndexAdjust(int imgIndex)
    {
        foreach (var hrefInfo in m_HrefInfos)//如果后面有超链接,需要把位置往前挪
        {
            if (imgIndex < hrefInfo.startIndex)
            {
                hrefInfo.startIndex -= 8;
                hrefInfo.endIndex -= 8;
            }
        }
    }

计算超链接的包围盒

        UIVertex vert = new UIVertex();
        // 处理超链接包围框
        foreach (var hrefInfo in m_HrefInfos)
        {
            hrefInfo.boxes.Clear();
            if (hrefInfo.startIndex >= toFill.currentVertCount)
            {
                continue;
            }
            // 将超链接里面的文本顶点索引坐标加入到包围框
            toFill.PopulateUIVertex(ref vert, hrefInfo.startIndex);
            var pos = vert.position;
            var bounds = new Bounds(pos, Vector3.zero);
            for (int i = hrefInfo.startIndex, m = hrefInfo.endIndex; i < m; i++)
            {
                if (i >= toFill.currentVertCount)
                {
                    break;
                }

                toFill.PopulateUIVertex(ref vert, i);
                pos = vert.position;
                if (pos.x < bounds.min.x) // 换行重新添加包围框
                {
                    hrefInfo.boxes.Add(new Rect(bounds.min, bounds.size));
                    bounds = new Bounds(pos, Vector3.zero);
                }
                else
                {
                    bounds.Encapsulate(pos); // 扩展包围框
                }
            }
            hrefInfo.boxes.Add(new Rect(bounds.min, bounds.size));
        }

UGUI填充顶点的函数

UGUI的显示对象都是继承于Graphic的,Graphic中OnPopulateMesh函数用于填充顶点数据,然后传递到GPU渲染,这里也是通过重写该函数计算出图片的顶点坐标以及超链接的包围盒

        if (EmojiIndex == null) {
            EmojiIndex = new Dictionary<string, EmojiInfo>();

            //load emoji data, and you can overwrite this segment code base on your project.
            TextAsset emojiContent = Resources.Load<TextAsset> ("emoji");
            string[] lines = emojiContent.text.Split ('\n');
            for(int i = 1 ; i < lines.Length; i ++)
            {
                if (! string.IsNullOrEmpty (lines [i])) {
                    string[] strs = lines [i].Split ('\t');
                    EmojiInfo info;
                    info.x = float.Parse (strs [3]);
                    info.y = float.Parse (strs [4]);
                    info.size = float.Parse (strs [5]);
                    info.len = 0;
                    EmojiIndex.Add (strs [1], info);
                }
            }
        }

        //key是标签在字符串中的索引

        Dictionary<int,EmojiInfo> emojiDic = new Dictionary<int, EmojiInfo> ();
        if (supportRichText) {
            MatchCollection matches = Regex.Matches (m_OutputText, "\\[[a-z0-9A-Z]+\\]");//把表情标签全部匹配出来
            for (int i = 0; i < matches.Count; i++) {
                EmojiInfo info;
                if (EmojiIndex.TryGetValue (matches [i].Value, out info)) {
                    info.len = matches [i].Length;
                    emojiDic.Add (matches [i].Index, info);
                }
            }
        }

        // We don't care if we the font Texture changes while we are doing our Update.
        // The end result of cachedTextGenerator will be valid for this instance.
        // Otherwise we can get issues like Case 619238.
        m_DisableFontTextureRebuiltCallback = true;

        Vector2 extents = rectTransform.rect.size;

        var settings = GetGenerationSettings(extents);
        var orignText = m_Text;
        m_Text = m_OutputText;
        cachedTextGenerator.Populate(m_Text, settings);//重置网格
        m_Text = orignText;

        Rect inputRect = rectTransform.rect;

        // get the text alignment anchor point for the text in local space
        Vector2 textAnchorPivot = GetTextAnchorPivot(alignment);
        Vector2 refPoint = Vector2.zero;
        refPoint.x = Mathf.Lerp(inputRect.xMin, inputRect.xMax, textAnchorPivot.x);
        refPoint.y = Mathf.Lerp(inputRect.yMin, inputRect.yMax, textAnchorPivot.y);

        // Determine fraction of pixel to offset text mesh.
        Vector2 roundingOffset = PixelAdjustPoint(refPoint) - refPoint;

        // Apply the offset to the vertices
        IList<UIVertex> verts = cachedTextGenerator.verts;
        float unitsPerPixel = 1 / pixelsPerUnit;
        //Last 4 verts are always a new line...
        int vertCount = verts.Count - 4;

        toFill.Clear();
        if (roundingOffset != Vector2.zero)
        {
            for (int i = 0; i < vertCount; ++i)
            {
                int tempVertsIndex = i & 3;
                m_TempVerts[tempVertsIndex] = verts[i];
                m_TempVerts[tempVertsIndex].position *= unitsPerPixel;
                m_TempVerts[tempVertsIndex].position.x += roundingOffset.x;
                m_TempVerts[tempVertsIndex].position.y += roundingOffset.y;
                if (tempVertsIndex == 3)
                    toFill.AddUIVertexQuad(m_TempVerts);
            }
        }
        else
        {
            float repairDistance = 0;
            float repairDistanceHalf = 0;
            float repairY = 0;
            if (vertCount > 0) {
                repairY = verts [3].position.y;
            }
            for (int i = 0; i < vertCount; ++i) {
                EmojiInfo info;
                int index = i / 4;//每个字符4个顶点
                if (emojiDic.TryGetValue (index, out info)) {//这个顶点位置是否为表情开始的index

                    HrefInfosIndexAdjust(i);//矫正一下超链接的Index

                    //compute the distance of '[' and get the distance of emoji 
                    //计算表情标签2个顶点之间的距离, * 3 得出宽度(表情有3位)
                    float charDis = (verts [i + 1].position.x - verts [i].position.x) * 3;
                    m_TempVerts [3] = verts [i];//1
                    m_TempVerts [2] = verts [i + 1];//2
                    m_TempVerts [1] = verts [i + 2];//3
                    m_TempVerts [0] = verts [i + 3];//4

                    //the real distance of an emoji
                    m_TempVerts [2].position += new Vector3 (charDis, 0, 0);
                    m_TempVerts [1].position += new Vector3 (charDis, 0, 0);

                    float fixWidth = m_TempVerts[2].position.x - m_TempVerts[3].position.x;
                    float fixHeight = (m_TempVerts[2].position.y - m_TempVerts[1].position.y);
                    //make emoji has equal width and height
                    float fixValue = (fixWidth - fixHeight);//把宽度变得跟高度一样
                    m_TempVerts [2].position -= new Vector3 (fixValue, 0, 0);
                    m_TempVerts [1].position -= new Vector3 (fixValue, 0, 0);

                    float curRepairDis = 0;
                    if (verts [i].position.y < repairY) {// to judge current char in the same line or not
                        repairDistance = repairDistanceHalf;
                        repairDistanceHalf = 0;
                        repairY = verts [i + 3].position.y;
                    } 
                    curRepairDis = repairDistance;
                    int dot = 0;//repair next line distance
                    for (int j = info.len - 1; j > 0; j--) {
                        int infoIndex = i + j * 4 + 3;
                        if (verts.Count > infoIndex && verts[infoIndex].position.y >= verts [i + 3].position.y) {
                            repairDistance += verts [i + j * 4 + 1].position.x - m_TempVerts [2].position.x;
                            break;
                        } else {
                            dot = i + 4 * j;

                        }
                    }
                    if (dot > 0) {
                        int nextChar = i + info.len * 4;
                        if (nextChar < verts.Count) {
                            repairDistanceHalf = verts [nextChar].position.x - verts [dot].position.x;
                        }
                    }

                    //repair its distance
                    for (int j = 0; j < 4; j++) {
                        m_TempVerts [j].position -= new Vector3 (curRepairDis, 0, 0);
                    }

                    m_TempVerts [0].position *= unitsPerPixel;
                    m_TempVerts [1].position *= unitsPerPixel;
                    m_TempVerts [2].position *= unitsPerPixel;
                    m_TempVerts [3].position *= unitsPerPixel;

                    float pixelOffset = emojiDic [index].size / 32 / 2;
                    m_TempVerts [0].uv1 = new Vector2 (emojiDic [index].x + pixelOffset, emojiDic [index].y + pixelOffset);
                    m_TempVerts [1].uv1 = new Vector2 (emojiDic [index].x - pixelOffset + emojiDic [index].size, emojiDic [index].y + pixelOffset);
                    m_TempVerts [2].uv1 = new Vector2 (emojiDic [index].x - pixelOffset + emojiDic [index].size, emojiDic [index].y - pixelOffset + emojiDic [index].size);
                    m_TempVerts [3].uv1 = new Vector2 (emojiDic [index].x + pixelOffset, emojiDic [index].y - pixelOffset + emojiDic [index].size);

                    toFill.AddUIVertexQuad (m_TempVerts);

                    i += 4 * info.len - 1;
                } else {
                    int tempVertsIndex = i & 3;
                    if (tempVertsIndex == 0 && verts [i].position.y < repairY) {
                        repairY = verts [i + 3].position.y;
                        repairDistance = repairDistanceHalf;
                        repairDistanceHalf = 0;
                    }
                    m_TempVerts [tempVertsIndex] = verts [i];
                    m_TempVerts [tempVertsIndex].position -= new Vector3 (repairDistance, 0, 0);
                    m_TempVerts [tempVertsIndex].position *= unitsPerPixel;
                    if (tempVertsIndex == 3)
                        toFill.AddUIVertexQuad (m_TempVerts);
                }
            }
        }

判断是否点中了链接

把屏幕坐标转换到Text中的坐标后,再进行检测

    /// <summary>
    /// 点击事件检测是否点击到超链接文本
    /// </summary>
    public void OnPointerClick(PointerEventData eventData)
    {
        Vector2 lp;
        RectTransformUtility.ScreenPointToLocalPointInRectangle(
            rectTransform, eventData.position, eventData.pressEventCamera, out lp);

        foreach (var hrefInfo in m_HrefInfos)
        {
            var boxes = hrefInfo.boxes;
            for (var i = 0; i < boxes.Count; ++i)
            {
                if (boxes[i].Contains(lp))
                {

                    if (onHrefClick != null)
                    {
                        onHrefClick(hrefInfo.name);
                    }
                    Debug.Log("点击了:" + hrefInfo.name);
                    return;
                }
            }
        }
    }

可以在这里获取全部代码
https://github.com/lijia4423/EmojiText.git

参考:
https://blog.uwa4d.com/archives/Sparkle_UGUI.html
http://www.pudn.com/Download/item/id/3121697.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,575评论 25 707
  • 这个是我刚刚整理出的Unity面试题,为了帮助大家面试,同时帮助大家更好地复习Unity知识点,如果大家发现有什么...
    编程小火鸡阅读 3,830评论 2 35
  • 全文解析圆形Image组件的实现原理,取关键代码介绍算法细节,源码已经上传Github下载地址,欢迎下载试用。 一...
    立航阅读 3,973评论 3 33
  • 一千多年前,唐朝初年,也许正是一个月圆之夜,春风暖暖的吹拂,平静的江面上一轮明月倒映水中。江畔的诗人面对江月,生发...
    繁花落尽深眸阅读 853评论 6 14
  • 对于南京最印象深刻的莫过于南京国民 政府那段历史,民国给我的感觉就像刚开始谈恋爱的感觉是一样的,半开放的状...
    躺着贼舒服lxy阅读 657评论 2 4