【小松教你手遊開發】【系統模組開發】圖文混排 (在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。
接下來看程式碼:(最後會貼出所有程式碼)
/// <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