UGUI表情系統(tǒng)&超鏈接解決方案

最近幫一個同事解決圖文混排的問題角雷,發(fā)現(xiàn)了一種犀利的UGUI表情系統(tǒng)的解決方案
https://blog.uwa4d.com/archives/Sparkle_UGUI.html
使用重新生成UGUI文字Mesh的方式來支持表情圖片。在Shader中判斷是否有第二個套UV傳入來渲染表情乎串,動態(tài)表情也在GPU端計算~ 可以合并DrawCall,支持UGUI的遮罩橡伞、自適應(yīng)等等

我在原作者的基礎(chǔ)上擴展了一些改進逗物,使其能支持超鏈接


超鏈接的思路是先計算出超鏈接的頂點包圍盒跌前,監(jiān)聽到點擊事件的時候钮糖,跟超鏈接包圍和進行碰撞檢測來判斷是否點擊到了某個連接。如果一個超鏈接跨行的時候枢析,就需要創(chuàng)建多個包圍盒來處理玉掸。

如何解析超鏈接標簽?

<a href='xx'>點擊加入隊伍</a>

在Text發(fā)生變化的時候醒叁,UGUI會調(diào)用SetVerticesDirty函數(shù)把組件注冊到ReBuilder隊列里面司浪,等待下一幀重繪。所以我們在SetVerticesDirty函數(shù)中寫上解析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();
    }

通過正則表達式匹配后把沼,計算出超鏈接的起始頂點索引啊易、結(jié)束頂點索引、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填充頂點的函數(shù)

UGUI的顯示對象都是繼承于Graphic的牙瓢,Graphic中OnPopulateMesh函數(shù)用于填充頂點數(shù)據(jù),然后傳遞到GPU渲染间校,這里也是通過重寫該函數(shù)計算出圖片的頂點坐標以及超鏈接的包圍盒

        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);//重置網(wǎng)格
        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);
                }
            }
        }

判斷是否點中了鏈接

把屏幕坐標轉(zhuǎn)換到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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末憔足,一起剝皮案震驚了整個濱河市胁附,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌滓彰,老刑警劉巖控妻,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異揭绑,居然都是意外死亡弓候,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門他匪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來菇存,“玉大人,你說我怎么就攤上這事邦蜜∫琅福” “怎么了?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵悼沈,是天一觀的道長贱迟。 經(jīng)常有香客問我姐扮,道長,這世上最難降的妖魔是什么衣吠? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任溶握,我火速辦了婚禮,結(jié)果婚禮上蒸播,老公的妹妹穿的比我還像新娘睡榆。我一直安慰自己,他們只是感情好袍榆,可當(dāng)我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布胀屿。 她就那樣靜靜地躺著,像睡著了一般包雀。 火紅的嫁衣襯著肌膚如雪宿崭。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天才写,我揣著相機與錄音葡兑,去河邊找鬼。 笑死赞草,一個胖子當(dāng)著我的面吹牛讹堤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播厨疙,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼洲守,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了沾凄?” 一聲冷哼從身側(cè)響起梗醇,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎撒蟀,沒想到半個月后叙谨,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡保屯,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年手负,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片配椭。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡虫溜,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出股缸,到底是詐尸還是另有隱情衡楞,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站瘾境,受9級特大地震影響歧杏,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜迷守,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一犬绒、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧兑凿,春花似錦凯力、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至圣絮,卻和暖如春祈惶,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背扮匠。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工捧请, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人棒搜。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓疹蛉,卻偏偏與公主長得像,于是被迫代替她去往敵國和親帮非。 傳聞我的和親對象是個殘疾皇子氧吐,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,071評論 25 707
  • 這個是我剛剛整理出的Unity面試題,為了幫助大家面試末盔,同時幫助大家更好地復(fù)習(xí)Unity知識點,如果大家發(fā)現(xiàn)有什么...
    編程小火雞閱讀 3,894評論 2 35
  • 全文解析圓形Image組件的實現(xiàn)原理,取關(guān)鍵代碼介紹算法細節(jié)版仔,源碼已經(jīng)上傳Github下載地址游盲,歡迎下載試用。 一...
    立航閱讀 4,179評論 3 33
  • 一千多年前蛮粮,唐朝初年益缎,也許正是一個月圓之夜,春風(fēng)暖暖的吹拂然想,平靜的江面上一輪明月倒映水中莺奔。江畔的詩人面對江月,生發(fā)...
    繁花落盡深眸閱讀 911評論 6 14
  • 對于南京最印象深刻的莫過于南京國民 政府那段歷史恼琼,民國給我的感覺就像剛開始談戀愛的感覺是一樣的,半開放的狀...
    躺著賊舒服lxy閱讀 728評論 2 4