1. 程式人生 > >【小松教你手遊開發】【系統模組開發】圖文混排 (在label中插入表情)

【小松教你手遊開發】【系統模組開發】圖文混排 (在label中插入表情)

本身ngui是自帶圖文混排的,這個可以在ngui的Example裡找到。但是為什麼不能用網上已經說得很清楚,比如雨鬆momo的http://www.xuanyusong.com/archives/2908

最重要的一點就是我們肯定不會選擇一個完整的中文字型檔,動態字型無辦法使用ngui的圖文混排

所以還是需要自己寫一個圖文混排。

首先圖文混排的基本邏輯是:

1.定義固定字串格式作為圖片資訊。

2.找到文字中的圖片資訊的字串提取並換成空格

3.根據圖片資訊生成uisprite,並放在適當的position

4.輸出文字和圖片

圖文混排有幾個重點是必須解決的:

1.找到圖片應該放的position

2.如果圖片在文字末尾判斷是否放得下是否會被遮擋,是的話要把圖片放到下一行的開頭

3.按照圖片的高度判斷這一行的開頭需要多少個換行符

4.如果一排有多個圖片且尺寸不一,這一排的圖片需要統一高度,不然會出現下面的情況


(如果圖片格式統一的話3,4倒是可以用湊合的辦法省略,但是我們想做一個適用各種大小圖片,每行可能有幾張圖片,適合各種情況的圖文混排)

接下來就是實現。

我的思路是:

有一大段文字且裡面有許多圖片資訊的前提下

1.首先把所有文字輸入都某個函式,識別出第一個圖片資訊的字串,把這個包含圖片資訊的字串以及前面的文字裁剪下來,和裁剪以後的文字形成兩部分。

2.把裁剪的前面部分(包含圖片資訊)分析出圖片資訊,各種計算,最後得到圖片的position,生成gameObject並擺放好。儲存各種資訊。圖片部分用空格留出位置,形成新的字串,和裁剪的第二部分的文字組合成新文字。

3.輸入到1裡的那個函式。遞迴。

4.最終一次過輸出所有文字。

程式碼直接寫到UILabel.cs裡,也可以寫一個UIEmotionLabel.cs繼承UILabel.cs。

接下來看程式碼:(最後會貼出所有程式碼)

    /// <summary>
    /// label中有表情在顯示前呼叫進行轉換
    /// </summary>
    public void ShowEmotionLabel()
    {
        m_newEmotionText = "";
        string originalText = MyLabel.text;

        //遞迴找表情並生成文字
        CutAndShowEmotionLabel(originalText);

        //輸出文字
        MyLabel.text = m_newEmotionText;
        MyLabel.UpdateNGUIText();

        //每一行的表情重新排序對其
        SortAllSprite();
    }

這個是唯一外部呼叫介面,當要顯示圖片的時候呼叫這個函式。

通過註釋就可以看懂裡面的邏輯,最後的SortAllSprite()最後會再解釋一下。

所以先看CutAndShowEmotionLabel(string str)這個函式。

 void CutAndShowEmotionLabel(string str)
    {
        EmotionData emoData = GetEmotionData(str);//解析str中的第一個表情字串

        if (emoData != null)
        {
            m_spriteList.Add(emoData);

            //把str按第一個表情字串的最後一個字母分成兩部分
            string trimString = str.Substring(0, emoData.end_index);
            string trimLeftString = str.Substring(emoData.end_index);

            //生成表情和表情前面的文字部分
            GenEmotionLabel(emoData, trimString);
            m_newEmotionText = m_newEmotionText + trimLeftString;

            //遞迴繼續找表情
            CutAndShowEmotionLabel(m_newEmotionText);
        }
        else
        {
            //找不到表情返回,最後確定文字輸出
            m_newEmotionText =str;
            return;
        }

    }
第一行就是用自己的方法解析。

上面的邏輯就是按思路寫的

唯一有點不一樣的就是多了一個m_spriteList.Add(emoData);

因為最後需要把所有圖片按每行輸出時可能要對其高度,所以都要先儲存下來。

這裡面最重要的是GenEmotionLabel(emoData, trimString);這個函式

    void GenEmotionLabel(EmotionData emoData, string tramString)
    {
        //生成gameobject
        GameObject go = CreateEmotionSprite(emoData);
        float spriteWidth = NGUIMath.CalculateRelativeWidgetBounds(gameobject.transform, go.transform, true).size.x / go.transform.localScale.x;
        float spriteHeight = NGUIMath.CalculateRelativeWidgetBounds(gameobject.transform, go.transform, true).size.y / go.transform.localScale.y;

        //計算出圖片的位置,判斷文字的轉換和空格
        Vector3 position = CalcuEmotionSpritePosition(tramString, emoData.start_index, spriteWidth, spriteHeight);

        //擺放圖片位置
        PlaceEmotionSprite(go, position);

        m_spriteList[m_spriteList.Count - 1].go = go;
    }
CreateEmotionSprite()就是根據分析出來的圖片資訊例項化一個GameObject,但是這時候position位置還是不能確定。

在算出圖片的寬高後。把這些資料都輸入到CalcuEmotionSpritePosition();這個函式裡算出最後的position。

獲得position資料在PlaceEmotionSprite()函式正確的擺放
所以這裡最關鍵的還是CalcuEmotionSpritePosition()。

    Vector3 CalcuEmotionSpritePosition(string str, int startIndex, float spriteWidth, float spriteHeight)
    {
        Vector3 position = GenBlankString(str, startIndex, spriteWidth, spriteHeight);
        return position;
    }

這裡看GenBlankString()函式。

 Vector3 GenBlankString(string str, int startIndex, float spriteWidth, float spriteHeight)
    {
        int finalIndex = startIndex;

        BetterList<Vector3> tempVerts = new BetterList<Vector3>();
        BetterList<int> tempIndices = new BetterList<int>();


        //1.把圖片資訊換成空格
        string emontionText = str.Substring(startIndex);
        int blankNeedCount = CaculateBlankNeed(spriteWidth);
        str = str.Replace(emontionText, GenBlank(blankNeedCount));
        //把換好的文字放回label再計算sprite應該放的座標,
        UpdateCharacterPosition(str,out tempVerts,out tempIndices);

        
        //2.如果在label末尾且圖片放不下,判斷是否換行
        bool needWrap = NeedWrap(tempVerts, tempIndices, startIndex, startIndex + blankNeedCount);
        if (needWrap)
        {
            str = str.Insert(startIndex, "\n");
            finalIndex +=1;

            //重新計算當前所有字元的位置
            UpdateCharacterPosition(str, out tempVerts, out tempIndices);
        }

        //3.按圖片的高,生成回車(換行)
        int returnCount = GenCarriageReturn(tempVerts, tempIndices, ref str, finalIndex, spriteHeight, needWrap);
        finalIndex += returnCount;


        //4.重新賦值要輸出的str
        m_newEmotionText = str;

        //重新計算當前所有字元的位置
        UpdateCharacterPosition(str, out tempVerts, out tempIndices);


        //儲存行數,最後重新排放每行的圖片使用
        m_spriteList[m_spriteList.Count - 1].line_index = CalcuLineIndex(tempVerts, tempIndices, startIndex) - lastScale;


        //最終計算圖片該放的位置
        Vector3 position = new Vector3();
        if (needWrap)
        {
            position = new Vector3(tempVerts[0].x, tempVerts[GetIndexFormIndices(finalIndex, tempIndices)].y, tempVerts[0].z);
        }
        else
        {
            position = tempVerts[GetIndexFormIndices(finalIndex, tempIndices)];
        }

        return position;
    }

先介紹一下NGUI提供的計算每個字元在字串中位置的函式。

NGUIText.PrintCharacterPositions(str, tempVerts, tempIndices);

輸入str,輸出tempVerts,tempIndices。通過這兩個變數獲取每個字元的position資訊

這裡我封裝了個函式通過字元在字串中的index來獲取在tempVerts中index_v,繼而通過tempVerts[index_v]獲取vecter3

    int GetIndexFormIndices(int index, BetterList<int> list)
    {
        for (int i = 0; i < list.size; i++)
            if (list[i] == index)
                return i;
        return 0;
    }

我把NGUIText.PrintCharacterPositions(str, tempVerts, tempIndices)的用法寫成一個介面。

    void UpdateCharacterPosition(string str,out BetterList<Vector3> verts,out BetterList<int> indices)
    {
        //把換好的文字放回label再計算sprite應該放的座標,
        //計算當前所有字元的位置
        MyLabel.text = str;
        MyLabel.UpdateNGUIText();
        BetterList<Vector3> tempVerts = new BetterList<Vector3>();
        BetterList<int> tempIndices = new BetterList<int>();
        NGUIText.PrintCharacterPositions(str, tempVerts, tempIndices);

        verts = tempVerts;
        indices = tempIndices;
    }

這個介面的意思就是把str放到label裡,讓NGUI重新擺放一下文字,之後呼叫PrintCharacterPositions,返回這兩個變數,就更新了位置資訊。這時候就可以取得每個字元的位置資訊,也就是圖片將要擺放的位置。(在每次改變文字後都要重新呼叫才能確定位置準確)

回到上面的GenBlankString().

1.首先根據圖片寬度計算需要多少個空格來預留出位置。呼叫UpdateCharacterPosition()更新,重新獲得位置資訊(這部分我暫時是估算哈,比如5畫素1空格)

2.判斷是否需要換行。呼叫UpdateCharacterPosition()更新,重新獲得位置資訊(判斷圖片資訊字串(已換成空格)的第一個字元和最後一個字元是否在同一行,如果不同行證明要換行)
3.按圖片的高,生成換行符。呼叫UpdateCharacterPosition()更新,重新獲得位置資訊
4.這時文字已經確定不會再新增任何符號,所以重新複製最終要輸出的文字m_newEmotionText = str;

步驟3需要特別講一下:

 int lastScale = 1;
    int lastIndex = 0;
    int GenCarriageReturn(BetterList<Vector3> vectList, BetterList<int> indexList, ref string str, int startIndex, float spriteHeight, bool isWrap)
    {
        float fontSize = MyLabel.fontSize * gameobject.transform.localScale.x;

        int scale = Mathf.CeilToInt(spriteHeight / fontSize) - 1;


        if (CheckIfSameLine(vectList, indexList, startIndex, lastIndex))
        {
            if (lastScale < scale)
            {
                scale = scale - lastScale;
                lastScale = scale + lastScale;
            }
            else
            {
                scale = 0;
            }
        }
        else
        {
            lastScale = scale;
        }
        lastIndex = startIndex;


        string CarriageReturn = "";
        for (int i = 0; i < scale; i++)
        {
            CarriageReturn = CarriageReturn + '\n';
            lastIndex += 1;
        }

        //if(CheckIfIsLineFirstCharacter(vectList, indexList, startIndex))
        //{
        //    CarriageReturn = CarriageReturn + '\n';
        //    scale += 1;
        //}

        if (!isWrap && scale > 0)
        {
            CarriageReturn = CarriageReturn + '\n';
            scale += 1;
            lastIndex += 1;
            lastScale += 1;
        }

        str = str.Insert(FindLineFirstIndex(vectList, indexList, startIndex) - 1, CarriageReturn);

        return scale;
    }
可以看到在scale就是我需要多少個換行符。

接著下面的邏輯是如果這次判斷的startIndex(這個圖片的第一個字元)和上次lastIndex(上一個圖片的第一個字元)如果是同一行的話,需要判斷後面的圖片有沒有比前面的更大,如果更大需要判斷大多少,還需要多少個回車。

因為如果同一行內多個圖片的大小不一,只取最大的圖片的大小生成換行符。


再後面是判斷,有種情況是本身文字放到label剛好處於文字末尾(就是本身就需要一個換行符),所以如果是這種情況需要再插入一個換行符。

接著就把換行符插入到這一行的第一個字元前(還是通過位置資訊去判斷這行的第一個字元)

這個就是判斷圖片位置的邏輯,然後就一遍遍的遞迴把所有圖片找出來放置好。

最後還需要把每一行的圖片檢索一下,同一行有多個圖片時,所有圖片的y軸都跟最後一個對齊(因為最後一個的y軸肯定是最低的,要跟最低的對齊)

    void SortAllSprite()
    {
        for (int i = m_spriteList.Count - 1; i > 0; i--)
        {
            if (m_spriteList[i].line_index == m_spriteList[i - 1].line_index)
            {
                m_spriteList[i - 1].pos.y = m_spriteList[i].pos.y;
                m_spriteList[i - 1].go.transform.localPosition = m_spriteList[i - 1].pos;
            }

        }
    }

這樣就完成了圖文混排。

下面是所有程式碼(掛在UILabel.cs上, UILabel的程式碼不顯示)

    string m_newEmotionText = "";
    List<EmotionData> m_spriteList = new List<EmotionData>();

    /// <summary>
    /// label中有表情在顯示前呼叫進行轉換
    /// </summary>
    public void ShowEmotionLabel()
    {
        m_newEmotionText = "";
        string originalText = MyLabel.text;

        //遞迴找表情並生成文字
        CutAndShowEmotionLabel(originalText);

        //輸出文字
        MyLabel.text = m_newEmotionText;
        MyLabel.UpdateNGUIText();

        //每一行的表情重新排序對其
        SortAllSprite();
    }

    #region 圖文混排輔助函式
    void CutAndShowEmotionLabel(string str)
    {
        EmotionData emoData = GetEmotionData(str);//解析str中的第一個表情字串

        if (emoData != null)
        {
            m_spriteList.Add(emoData);

            //把str按第一個表情字串的最後一個字母分成兩部分
            string trimString = str.Substring(0, emoData.end_index);
            string trimLeftString = str.Substring(emoData.end_index);

            //生成表情和表情前面的文字部分
            GenEmotionLabel(emoData, trimString);
            m_newEmotionText = m_newEmotionText + trimLeftString;

            //遞迴繼續找表情
            CutAndShowEmotionLabel(m_newEmotionText);
        }
        else
        {
            //找不到表情返回,最後確定文字輸出
            m_newEmotionText =str;
            return;
        }

    }

    void GenEmotionLabel(EmotionData emoData, string tramString)
    {
        //生成gameobject
        GameObject go = CreateEmotionSprite(emoData);
        float spriteWidth = NGUIMath.CalculateRelativeWidgetBounds(gameobject.transform, go.transform, true).size.x / go.transform.localScale.x;
        float spriteHeight = NGUIMath.CalculateRelativeWidgetBounds(gameobject.transform, go.transform, true).size.y / go.transform.localScale.y;

        //計算出圖片的位置,判斷文字的轉換和空格
        Vector3 position = CalcuEmotionSpritePosition(tramString, emoData.start_index, spriteWidth, spriteHeight);

        //擺放圖片位置
        PlaceEmotionSprite(go, position);

        m_spriteList[m_spriteList.Count - 1].go = go;
    }

    int lastScale = 1;
    int lastIndex = 0;
    int GenCarriageReturn(BetterList<Vector3> vectList, BetterList<int> indexList, ref string str, int startIndex, float spriteHeight, bool isWrap)
    {
        float fontSize = MyLabel.fontSize * gameobject.transform.localScale.x;

        int scale = Mathf.CeilToInt(spriteHeight / fontSize) - 1;


        if (CheckIfSameLine(vectList, indexList, startIndex, lastIndex))
        {
            if (lastScale < scale)
            {
                scale = scale - lastScale;
                lastScale = scale + lastScale;
            }
            else
            {
                scale = 0;
            }
        }
        else
        {
            lastScale = scale;
        }
        lastIndex = startIndex;


        string CarriageReturn = "";
        for (int i = 0; i < scale; i++)
        {
            CarriageReturn = CarriageReturn + '\n';
            lastIndex += 1;
        }

        //if(CheckIfIsLineFirstCharacter(vectList, indexList, startIndex))
        //{
        //    CarriageReturn = CarriageReturn + '\n';
        //    scale += 1;
        //}

        if (!isWrap && scale > 0)
        {
            CarriageReturn = CarriageReturn + '\n';
            scale += 1;
            lastIndex += 1;
            lastScale += 1;
        }

        str = str.Insert(FindLineFirstIndex(vectList, indexList, startIndex) - 1, CarriageReturn);

        return scale;
    }

    Vector3 CalcuEmotionSpritePosition(string str, int startIndex, float spriteWidth, float spriteHeight)
    {
        Vector3 position = GenBlankString(str, startIndex, spriteWidth, spriteHeight);
        return position;
    }

    Vector3 GenBlankString(string str, int startIndex, float spriteWidth, float spriteHeight)
    {
        int finalIndex = startIndex;

        BetterList<Vector3> tempVerts = new BetterList<Vector3>();
        BetterList<int> tempIndices = new BetterList<int>();


        //1.把圖片資訊換成空格
        string emontionText = str.Substring(startIndex);
        int blankNeedCount = CaculateBlankNeed(spriteWidth);
        str = str.Replace(emontionText, GenBlank(blankNeedCount));
        //把換好的文字放回label再計算sprite應該放的座標,
        UpdateCharacterPosition(str,out tempVerts,out tempIndices);

        
        //2.如果在label末尾且圖片放不下,判斷是否換行
        bool needWrap = NeedWrap(tempVerts, tempIndices, startIndex, startIndex + blankNeedCount);
        if (needWrap)
        {
            str = str.Insert(startIndex, "\n");
            finalIndex +=1;

            //重新計算當前所有字元的位置
            UpdateCharacterPosition(str, out tempVerts, out tempIndices);
        }

        //3.按圖片的高,生成回車(換行)
        int returnCount = GenCarriageReturn(tempVerts, tempIndices, ref str, finalIndex, spriteHeight, needWrap);
        finalIndex += returnCount;


        //4.重新賦值要輸出的str
        m_newEmotionText = str;

        //重新計算當前所有字元的位置
        UpdateCharacterPosition(str, out tempVerts, out tempIndices);


        //儲存行數,最後重新排放每行的圖片使用
        m_spriteList[m_spriteList.Count - 1].line_index = CalcuLineIndex(tempVerts, tempIndices, startIndex) - lastScale;


        //最終計算圖片該放的位置
        Vector3 position = new Vector3();
        if (needWrap)
        {
            position = new Vector3(tempVerts[0].x, tempVerts[GetIndexFormIndices(finalIndex, tempIndices)].y, tempVerts[0].z);
        }
        else
        {
            position = tempVerts[GetIndexFormIndices(finalIndex, tempIndices)];
        }

        return position;
    }

    GameObject CreateEmotionSprite(EmotionData data)
    {
        GameObject go = new GameObject("(clone)emotion_sprite");
        go.transform.parent = gameobject.transform;

        UISprite sprite = go.AddComponent<UISprite>();
        sprite.atlas = CResourceManager.Instance.GetAtlas(data.atlas_name);
        sprite.spriteName = data.sprite_name;
        sprite.MakePixelPerfect();
        sprite.pivot = UIWidget.Pivot.BottomLeft;

        float scaleFactor = 1 / gameobject.transform.localScale.x;
        go.transform.localScale = new Vector3(scaleFactor, scaleFactor, scaleFactor);//字型可能縮小了0.5,所以掛在字型下要放大2倍

        go.transform.localPosition = new Vector3(5000, 5000, 0);//先把它放到看不見的地方

        return go;
    }

    void PlaceEmotionSprite(GameObject go, Vector3 position)
    {
        float fontSize = MyLabel.fontSize * gameobject.transform.localScale.x;

        float div = fontSize * go.transform.localScale.x / 2;

        Vector3 newPosition = new Vector3(position.x, position.y - div, position.z);
        //Vector3 newPosition = position;
        go.transform.localPosition = newPosition;

        m_spriteList[m_spriteList.Count - 1].pos = newPosition;
    }

    EmotionData GetEmotionData(string text)
    {
        EmotionData tempData = null;
        int index = text.IndexOf("%p");
        if (index != -1)
        {
            tempData = new EmotionData();
            tempData.start_index = index;

            int altasEndIndex = text.IndexOf("$", index);
            tempData.atlas_name = text.Substring(index + 2, altasEndIndex - (index + 2));

            int spriteEndIndex = text.IndexOf("$", altasEndIndex + 1);
            tempData.sprite_name = text.Substring(altasEndIndex + 1, spriteEndIndex - (altasEndIndex + 1));

            tempData.end_index = spriteEndIndex + 1;
        }
        return tempData;
    }

    int GetIndexFormIndices(int index, BetterList<int> list)
    {
        for (int i = 0; i < list.size; i++)
            if (list[i] == index)
                return i;
        return 0;
    }

    int CaculateBlankNeed(float spriteWidth)
    {
        int count = Mathf.CeilToInt(spriteWidth / (float)6);
        return count;
    }

    string GenBlank(int count)
    {
        string blank = "";
        for (int i = 0; i < count; i++)
        {
            blank = blank + " ";
        }
        return blank;
    }

    bool NeedWrap(BetterList<Vector3> vecList, BetterList<int> indicList, int startIndex, int endIndex)
    {
        int startIndic = GetIndexFormIndices(startIndex, indicList);
        int endIndic = GetIndexFormIndices(endIndex, indicList);

        if (vecList[startIndic].y == vecList[endIndic].y)
            return false;
        else
            return true;
    }

    bool CheckIfSameLine(BetterList<Vector3> vecList, BetterList<int> indicList, int firstIndex, int SecondIndex)
    {
        int firstIndic = GetIndexFormIndices(firstIndex, indicList);
        int secondIndic = GetIndexFormIndices(SecondIndex, indicList);

        if (vecList[firstIndic].y == vecList[secondIndic].y)
            return true;
        else
            return false;
    }

    int FindLineFirstIndex(BetterList<Vector3> vecList, BetterList<int> indicList, int index)
    {
        int startIndic = GetIndexFormIndices(index, indicList);
        if (startIndic > 1)
        {
            if (vecList[startIndic].y == vecList[startIndic - 1].y)
                index = FindLineFirstIndex(vecList, indicList, index - 1);
            else
                return index;
        }
        else
        {
            return 1;
        }
        return index;
    }


    int CalcuLineIndex(BetterList<Vector3> vecList, BetterList<int> indicList, int index)
    {
        int startIndic = GetIndexFormIndices(index, indicList);
        int count = 0;
        float lastVecY = 0;
        for (int i = 0; i < vecList.size; i++)
        //for (int i =0;i< startIndic; i++)
        {
            if (lastVecY != vecList[i].y)
            {
                count++;
                lastVecY = vecList[i].y;
            }
        }
        return count;
    }

    bool CheckIfIsLineFirstCharacter(BetterList<Vector3> vecList, BetterList<int> indicList, int index)
    {
        int startIndic = GetIndexFormIndices(index, indicList);
        if (startIndic > 1)
        {
            if (vecList[startIndic].y == vecList[startIndic - 1].y)
                return false;
            else
                return true;
        }
        else
        {
            return false;
        }
    }

    void SortAllSprite()
    {
        for (int i = m_spriteList.Count - 1; i > 0; i--)
        {
            if (m_spriteList[i].line_index == m_spriteList[i - 1].line_index)
            {
                m_spriteList[i - 1].pos.y = m_spriteList[i].pos.y;
                m_spriteList[i - 1].go.transform.localPosition = m_spriteList[i - 1].pos;
            }

        }
    }

    void UpdateCharacterPosition(string str,out BetterList<Vector3> verts,out BetterList<int> indices)
    {
        //把換好的文字放回label再計算sprite應該放的座標,
        //計算當前所有字元的位置
        MyLabel.text = str;
        MyLabel.UpdateNGUIText();
        BetterList<Vector3> tempVerts = new BetterList<Vector3>();
        BetterList<int> tempIndices = new BetterList<int>();
        NGUIText.PrintCharacterPositions(str, tempVerts, tempIndices);

        verts = tempVerts;
        indices = tempIndices;
    }
    #endregion


補上EmotionData類

public class EmotionData
{
    public int start_index;
    public int end_index;
    public string atlas_name;
    public string sprite_name;
    public float sprite_width;

    public int line_index;
    public Vector3 pos;
    public GameObject go;
}

鑑於很多人都要求要Demo,就抽空做了一個,發現原來這裡還有問題,有空再解決吧哈哈哈

http://pan.baidu.com/s/1hs1LzYs