热门IT资讯网

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

发表于:2024-11-24 作者:热门IT资讯网编辑
编辑最后更新 2024年11月24日,本身ngui是自带图文混排的,这个可以在ngui的Example里找到。但是为什么不能用网上已经说得很清楚,比如雨松momo的http://www.xuanyusong.com/archives/29

本身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。

接下来看代码:(最后会贴出所有代码)

///   /// label中有表情在显示前调用进行转换  ///   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 tempVerts = new BetterList();         BetterList tempIndices = new BetterList();         //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 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 verts,out BetterList indices)  {      //把换好的文字放回label再计算sprite应该放的坐标,      //计算当前所有字符的位置      MyLabel.text = str;      MyLabel.UpdateNGUIText();      BetterList tempVerts = new BetterList();      BetterList tempIndices = new BetterList();      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 vectList, BetterList 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 m_spriteList = new List();  ///   /// label中有表情在显示前调用进行转换  ///   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 vectList, BetterList 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 tempVerts = new BetterList();      BetterList tempIndices = new BetterList();      //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();      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 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 vecList, BetterList 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 vecList, BetterList 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 vecList, BetterList 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 vecList, BetterList 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 vecList, BetterList 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 verts,out BetterList indices)  {      //把换好的文字放回label再计算sprite应该放的坐标,      //计算当前所有字符的位置      MyLabel.text = str;      MyLabel.UpdateNGUIText();      BetterList tempVerts = new BetterList();      BetterList tempIndices = new BetterList();      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;  }  
0