【小松教你手游开发】【系统模块开发】图文混排 (在label中插入表情)
更新:HHH   时间:2023-1-7


本身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;  
}  
返回游戏开发教程...