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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM