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