【小松教你手游开发】【系统模块开发】图文混排 (在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。
接下来看代码:(最后会贴出所有代码)
/// /// 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; }