前情提要
今天在開發遊戲引導架構時,遇到這樣的需求:人物對話文本支援打字機效果,且需要個别文字高亮。如果僅僅是前者的需求,是挺好實作的,建立一個
Label
,通過
getLetter(index)
擷取每個字,調用
setVisible(isVisible)
即可;但是個别文字高亮是RichText才有的功能,于是難點變成了如何擷取RichText裡的每個字?
代碼分析
話不多說,看源代碼,從
UIRichText.cpp
檔案的
formatText
方法中,我們發現
RichText
的本質就是多個
Label
的拼接:
void RichText::formatText(bool isForce)
{
...
_elementRenders.clear();
...
for (ssize_t i=0; i<_richElements.size(); i++)
{
RichElement* element = _richElements.at(i);
Node* elementRenderer = nullptr;
switch (element->_type)
{
case RichElement::Type::TEXT:
{
RichElementText* elmtText = static_cast<RichElementText*>(element);
Label* label;
if (FileUtils::getInstance()->isFileExist(elmtText->_fontName))
{
label = Label::createWithTTF(elmtText->_text, elmtText->_fontName, elmtText->_fontSize);
}
else
{
label = Label::createWithSystemFont(elmtText->_text, elmtText->_fontName, elmtText->_fontSize);
}
...
elementRenderer = label;
break;
}
case RichElement::Type::IMAGE:
{
...
}
case RichElement::Type::CUSTOM:
{
...
}
case RichElement::Type::NEWLINE:
{
...
}
default:
break;
}
if (elementRenderer)
{
Label * pLabel = dynamic_cast<Label *>(elementRenderer);
if (pLabel)
{
pLabel->setTextColor(Color4B(element->_color, element->_opacity));
}
else
{
elementRenderer->setColor(element->_color);
elementRenderer->setOpacity(element->_opacity);
}
pushToContainer(elementRenderer);
}
}
}
else
{
// 與前一段if結構基本一緻
...
}
formarRenderers();
_formatTextDirty = false;
}
}
複制
這段代碼的邏輯是通過讀取
insertElement()
方法傳入的
RichElement
,根據RichElement類别的不同,建立
Label
、
Sprite
、
Node
等,放入RichText這個容器中,因為在目前情境下,隻有
Label
被建立,是以其他不在考慮範圍。
有了
Label
就可以拿到每個文字了,那麼
Label
從哪裡擷取呢?我們把上面代碼再精簡下:
void RichText::formatText(bool isForce)
{
_elementRenders.clear();
Node* elementRender = Label::create...
pushToContainer(elementRender);
formarRenderers();
}
複制
發現
Label
被傳進了
pushToContainer(render)
方法中,這個方法的代碼很簡單:
void RichText::pushToContainer(cocos2d::Node *renderer)
{
if (_elementRenders.size() <= 0)
{
return;
}
_elementRenders[_elementRenders.size()-1]->pushBack(renderer);
}
複制
是以思路就變成了如何從
_elementRenders
中擷取所有的
Label
?
邏輯編寫
基于上述梳理,編寫擷取
RichText
中所有文字的邏輯如下:
void RichText::getAllLetters()
{
Vector<Sprite*> letters;
for (auto& element : _elementRenders)
{
Vector<Node*>* row = element;
for (ssize_t i = 0; i<row->size(); i++)
{
Node* pNode = row->at(i);
Label * pLabel = dynamic_cast<Label*>(pNode);
if (pLabel)
{
int len = pLabel->getStringLength();
for (int j = 0; j < len; j++)
{
Sprite* letter = pLabel->getLetter(j);
if (letter)
letters.pushBack(letter);
}
}
}
}
return letters;
}
複制
編寫完成,将
CPP
轉為
Lua
,看一下效果!
代碼:
local ret = richText:getAllLetters()
輸出:
ret: {}
複制
為什麼會出現這樣的情況呢?
打斷點,進入
getAllLetters()
,傻眼了,
_elementRenders
是個空數組。原因是
formatText()
方法的最後,RichText調用了
formarRenderers()
方法,我們來簡單地看一下
formarRenderers
的邏輯:
void RichText::formarRenderers()
{
// 此處省略一大坨代碼...
size_t length = _elementRenders.size();
for (size_t i = 0; i<length; i++)
{
Vector<Node*>* l = _elementRenders[i];
l->clear();
delete l;
}
_elementRenders.clear();
updateContentSizeWithTextureSize(_contentSize);
}
複制
前面做了什麼邏輯我們不關心,我們關心的是方法的最後調用了
_elementRenders.clear()
,也就是說:每次執行
formatText()
後,
_elementRenders
都會被清空,它隻是一個臨時變量,是以接下來要做的就是在
_elementRenders
被清掉前,周遊擷取每一個Letter并存下來。
于是代碼變成了這樣:
UIRichText.h
class CC_GUI_DLL RichText : public Widget
{
public:
Vector<Sprite*>& getAllLetters();
protected:
Vector<Sprite*> _letters;
};
--------------------------------------------------
UIRichText.CPP
void RichText::updateLetters()
{
_letters.clear();
for (auto& element : _elementRenders)
{
Vector<Node*>* row = element;
for (ssize_t i = 0; i<row->size(); i++)
{
Node* pNode = row->at(i);
Label * pLabel = dynamic_cast<Label*>(pNode);
if (pLabel)
{
int len = pLabel->getStringLength();
for (int j = 0; j < len; j++)
{
Sprite* letter = pLabel->getLetter(j);
if (letter)
_letters.pushBack(letter);
}
}
}
}
}
Vector<Sprite*>& RichText::getAllLetters()
{
return _letters;
}
void RichText::formarRenderers()
{
// 此處省略一大坨代碼...
// ------------------
// 添加的代碼
updateLetters();
// ------------------
size_t length = _elementRenders.size();
for (size_t i = 0; i<length; i++)
{
Vector<Node*>* l = _elementRenders[i];
l->clear();
delete l;
}
_elementRenders.clear();
updateContentSizeWithTextureSize(_contentSize);
}
複制
終于,CPP層間的邏輯算是實作了。但這時候,又有新的問題出現了。
解決LabelLetter無法轉Lua問題
按照新的邏輯,從Lua層面調用
getAllLetters()
方法,發現擷取的結果依然是空table;但是在CPP層面,卻是可以擷取到資料的。那麼問題就出在
toLua
的過程。
說明:tolua是cocos2d-x提供的lua-binding工具,位于項目tools/tolua目錄下。
繼續斷點調試,将問題定位在了下面這個方法:
/**
* Push a table converted from a cocos2d::Vector object into the Lua stack.
* The format of table as follows: {userdata1, userdata2, ..., userdataVectorSize}
* The object in the cocos2d::Vector which would be pushed into the table should be Ref type.
*
* @param L the current lua_State.
* @param inValue a cocos2d::Vector object.
*/
template <class T>
void ccvector_to_luaval(lua_State* L,const cocos2d::Vector<T>& inValue)
{
lua_newtable(L);
if (nullptr == L)
return;
int indexTable = 1;
for (const auto& obj : inValue)
{
if (nullptr == obj)
continue;
if (nullptr != dynamic_cast<cocos2d::Ref *>(obj))
{
std::string typeName = typeid(*obj).name();
auto iter = g_luaType.find(typeName);
if (g_luaType.end() != iter)
{
lua_pushnumber(L, (lua_Number)indexTable);
int ID = (obj) ? (int)obj->_ID : -1;
int* luaID = (obj) ? &obj->_luaID : NULL;
toluafix_pushusertype_ccobject(L, ID, luaID, (void*)obj,iter->second.c_str());
lua_rawset(L, -3);
++indexTable;
}
}
}
}
複制
問題的症結出在了
g_luaType
這裡,在這個數組裡,找不到
LabelLetter
類,雖然
getAllLetters()
傳回的是
Sprite
的數組,但本質上,
Label::getLetter()
中傳回的Sprite是通過
LabelLetter
建立的,而
LabelLetter
是在
CCLabel.cpp
裡面定義的,
g_luaType
根本不知道
LabelLetter
的存在。
既然如此,就一改到底吧!
template <class T>
void ccvector_to_luaval(lua_State* L,const cocos2d::Vector<T>& inValue)
{
lua_newtable(L);
if (nullptr == L)
return;
int indexTable = 1;
for (const auto& obj : inValue)
{
if (nullptr == obj)
continue;
if (nullptr != dynamic_cast<cocos2d::Ref *>(obj))
{
std::string typeName = typeid(*obj).name();
// --------------------------------------------
// 添加的代碼
if (typeName == "class cocos2d::LabelLetter")
{
typeName = "class cocos2d::Sprite";
}
// --------------------------------------------
auto iter = g_luaType.find(typeName);
if (g_luaType.end() != iter)
{
lua_pushnumber(L, (lua_Number)indexTable);
int ID = (obj) ? (int)obj->_ID : -1;
int* luaID = (obj) ? &obj->_luaID : NULL;
toluafix_pushusertype_ccobject(L, ID, luaID, (void*)obj,iter->second.c_str());
lua_rawset(L, -3);
++indexTable;
}
}
}
}
複制
在擷取typeName以後,判斷typeName的類别,若是
class cocos2d::LabelLetter
,則将其強行改為
class cocos2d::Sprite
。
至此,RichText可算可以調用
getAllLetters()
拿到所有的文字了,本教程也算告一段落。