天天看点

【小松教你手游开发】【系统模块开发】图文混排 (在label中插入表情)

本身ngui是自带图文混排的,这个可以在ngui的Example里找到。但是为什么不能用网上已经说得很清楚,比如雨松momo的http://www.xuanyusong.com/archives/2908

最重要的一点就是我们肯定不会选择一个完整的中文字库,动态字体无办法使用ngui的图文混排

所以还是需要自己写一个图文混排。

首先图文混排的基本逻辑是:

1.定义固定字符串格式作为图片信息。

2.找到文字中的图片信息的字符串提取并换成空格

3.根据图片信息生成uisprite,并放在适当的position

4.输出文字和图片

图文混排有几个重点是必须解决的:

1.找到图片应该放的position

2.如果图片在文字末尾判断是否放得下是否会被遮挡,是的话要把图片放到下一行的开头

3.按照图片的高度判断这一行的开头需要多少个换行符

4.如果一排有多个图片且尺寸不一,这一排的图片需要统一高度,不然会出现下面的情况

【小松教你手游开发】【系统模块开发】图文混排 (在label中插入表情)

(如果图片格式统一的话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