本文对CCLabel的实现进行相关介绍:
1 一般性绘制流程
绘制流程与之前介绍的Render常规性流程保持一致,其入口为visit:
void Label::visit(Renderer *renderer, const Mat4 &parentTransform, uint32_t parentFlags)
{
//...
if (_systemFontDirty || _contentDirty) // 文本内容发生更新
{
updateContent();
}
//...
if (!_children.empty())
{
sortAllChildren();
int i = 0;
for (; i < _children.size(); i++)
{
auto node = _children.at(i);
if (node && node->getLocalZOrder() < 0)
node->visit(renderer, _modelViewTransform, flags);
else
break;
}
this->drawSelf(visibleByCamera, renderer, flags); // 绘制自身
for (auto it = _children.cbegin() + i; it != _children.cend(); ++it)
{
(*it)->visit(renderer, _modelViewTransform, flags);
}
}
else
{
this->drawSelf(visibleByCamera, renderer, flags);
}
}
visit中主要包含两部分:1) 文本信息更新;2)绘制自身。
1.1 文本信息更新
visit中当发现文本信息发生任何更新时,执行updateContent操作。这里用了两个变量记录这一变化:_systemFontDirty(系统字体设置发生相关更改时置为true) 与_contentDirty(自定义字体发生任何属性变化时置为true)。
updateContent方法首先对自定义字体做了是否清除的判断:使用了自定义字体后重设系统字体,则将原有自定义字体删除,删除时清空一个相对重要的辅助自定义字体显示的数组_batchNodes。接着,基于字体类型更新文本:如果是自定义字体,则计算所有字符属性以供后续布局及绘制,如果是系统字体,则直接创建一个承载该字体的sprite。
void Label::updateContent()
{
if (_systemFontDirty) // 系统字体属性发生变更
{
if (_fontAtlas) // 说明在设置系统字体属性之前 已设置了自定义字体 此时需要做一些自定义字体相关的清空操作
{
_batchNodes.clear(); // _batchNodes变量的意义将在后面介绍
FontAtlasCache::releaseFontAtlas(_fontAtlas);
_fontAtlas = nullptr;
}
_systemFontDirty = false;
}
if (_fontAtlas) // 自定义字体
{
std::u16string utf16String;
if (StringUtils::UTF8ToUTF16(_utf8Text, utf16String)) // _utf8Text在label调用setString时赋值 记录了label的文本
{
_utf16Text = utf16String;
}
computeHorizontalKernings(_utf16Text); // 计算文字的水平字距 计算结果将用于后续的字符位置计算
updateFinished = alignText(); // 计算每个字符属性
}
else
{
auto fontDef = _getFontDefinition();
createSpriteForSystemFont(fontDef);
if (_shadowEnabled)
{
createShadowSpriteForSystemFont(fontDef);
}
}
if(updateFinished){
_contentDirty = false;
}
}
对于自定义字体,其主要工作在alignText内部。1)该过程首先判断:如果有新字加入,则基于该新字的纹理创建对应的SpriteBatchNode,并将其缓存至_batchNodes中,可以发现,_batchNodes是动态维护的,以此保证只为同一个字创建一次SpriteBatchNode,而在后续绘制阶段,只需遍历_batchNodes即可而无需重复的为文本创建SpriteBatchNode。当然,这种动态维护的做法对程序的灵活性有一定要求。
bool Label::alignText()
{
//...
bool ret = true;
do {
_fontAtlas->prepareLetterDefinitions(_utf16Text); // 生成新增字的定义(此处旧的不会被删)
auto& textures = _fontAtlas->getTextures(); // 新增字的纹理也在其内
if (textures.size() > _batchNodes.size()) // 基于新纹理创建 SpriteBatchNode 此处可以发现,_batchNodes用于缓存所有纹理的纹理对应的SpriteBatchNode
{
for (auto index = _batchNodes.size(); index < textures.size(); ++index)
{
auto batchNode = SpriteBatchNode::createWithTexture(textures.at(index));
if (batchNode)
{
_isOpacityModifyRGB = batchNode->getTexture()->hasPremultipliedAlpha();
_blendFunc = batchNode->getBlendFunc();
batchNode->setAnchorPoint(Vec2::ANCHOR_TOP_LEFT);
batchNode->setPosition(Vec2::ZERO);
_batchNodes.pushBack(batchNode);
}
}
}
if (_batchNodes.empty())
{
return true;
}
//...
if (_maxLineWidth > 0.f && !_lineBreakWithoutSpaces)
{
multilineTextWrapByWord(); // 多行文本解缠 实际上就是计算文本中每个字的布局属性letterInfo
}
else
{
multilineTextWrapByChar();
}
//...
if(!updateQuads()){ // 基于文本中每个字的布局属性更新_batchNodes中每个SpriteBatchNode的quad绘制属性
ret = false;
if(_overflow == Overflow::SHRINK){
this->shrinkLabelToContentSize(CC_CALLBACK_0(Label::isHorizontalClamp, this));
}
break;
}
//...
}while (0);
return ret;
}
多行文本解缠用于将Label中的文本解析成多行字符,并计算每个字符的位置及其他属性信息。
bool Label::multilineTextWrap(std::function<int(const std::u16string&, int, int)> nextTokenLen)
{
int textLen = getStringLength();
int lineIndex = 0;
float nextTokenX = 0.f; // 下一个字符起点X位置
float nextTokenY = 0.f; // 下一个字符起点Y位置
float longestLine = 0.f;
float letterRight = 0.f; // 记录当前行长度
auto contentScaleFactor = CC_CONTENT_SCALE_FACTOR();
float lineSpacing = _lineSpacing * contentScaleFactor;
float highestY = 0.f;
float lowestY = 0.f;
FontLetterDefinition letterDef;
Vec2 letterPosition;
for (int index = 0; index < textLen; )
{
auto character = _utf16Text[index];
if (character == '\n') // 换行
{
_linesWidth.push_back(letterRight); // 保存当前行
letterRight = 0.f;
lineIndex++;
nextTokenX = 0.f;
nextTokenY -= _lineHeight*_bmfontScale + lineSpacing;
recordPlaceholderInfo(index, character);
index++;
continue;
}
auto tokenLen = nextTokenLen(_utf16Text, index, textLen); // 计算下一个字|单词字符长度
float tokenRight = letterRight; // 为何新建两个变量:因为下一个单词有可能因为过长超出当前行而导致处理失效
float nextLetterX = nextTokenX; // 单词处理成功 则用新变量更新旧变量;如不成功,基于旧变量重新开始处理
bool newLine = false; // 单词过长 当前行承载不了,就需要切换至下一行
for (int tmp = 0; tmp < tokenLen;++tmp) // 开始处理下一个单词中的所有字符
{
int letterIndex = index + tmp;
character = _utf16Text[letterIndex];
if (character == '\r') // 回至当前行起点处
{
recordPlaceholderInfo(letterIndex, character);
continue;
}
if (_fontAtlas->getLetterDefinitionForChar(character, letterDef) == false)
{
recordPlaceholderInfo(letterIndex, character);
CCLOG("LabelTextFormatter error:can't find letter definition in font file for letter: %c", character);
continue;
}
auto letterX = (nextLetterX + letterDef.offsetX * _bmfontScale) / contentScaleFactor; // 当前单词当前字符起点
if (_enableWrap && _maxLineWidth > 0.f && nextTokenX > 0.f && letterX + letterDef.width * _bmfontScale > _maxLineWidth
&& !StringUtils::isUnicodeSpace(character))
{
_linesWidth.push_back(letterRight);
letterRight = 0.f;
lineIndex++;
nextTokenX = 0.f;
nextTokenY -= (_lineHeight*_bmfontScale + lineSpacing);
newLine = true;
break;
}
else
{
letterPosition.x = letterX;
}
letterPosition.y = (nextTokenY - letterDef.offsetY * _bmfontScale) / contentScaleFactor;
recordLetterInfo(letterPosition, character, letterIndex, lineIndex);
if (_horizontalKernings && letterIndex < textLen - 1)
nextLetterX += _horizontalKernings[letterIndex + 1];
nextLetterX += letterDef.xAdvance * _bmfontScale + _additionalKerning; // 更新下一个字符起点
tokenRight = letterPosition.x + letterDef.width * _bmfontScale; // 更新当前行长度
}
if (newLine)
{
continue; // 单词处理失败 从新行重新开始处理
}
// 当前单词处理完毕 更新旧变量
nextTokenX = nextLetterX;
letterRight = tokenRight;
index += tokenLen;
}
// 保存最后一行(最后一行不会触发上面的 超出当前行后保存当前行宽 的条件)
_linesWidth.push_back(letterRight);
_numberOfLines = lineIndex + 1;
//...
return true;
}
由于_batchNodes只增不减,因此每当文本内容发生变化时都需要对其更新。更新流程是:首先删除所有batchNode的quads,之后基于_letterinfo将新的quad信息输入。
1.2 绘制自身
绘制自身,分为两种情形,一是基于系统自带字体绘制,二是基于自定义字体绘制。绘制系统字体十分简单,直接绘制承载该字体的sprite即可,其方式就是sprite->visit();对于TTF类型的艺术字,需创建一个自定义绘制命令,该命令将处理一些文本设定的特效。关于Render层面如何处理这些绘制命令,可参考之前的博文。
void Label::drawSelf(bool visibleByCamera, Renderer* renderer, uint32_t flags)
{
if (_textSprite)
{
if (_shadowNode)
{
_shadowNode->visit(renderer, _modelViewTransform, flags);
}
_textSprite->visit(renderer, _modelViewTransform, flags);
}
else if (visibleByCamera && !_utf8Text.empty())
{
draw(renderer, _modelViewTransform, flags);
}
}
void Label::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)
{
//...
if (!_shadowEnabled && (_currentLabelType == LabelType::BMFONT || _currentLabelType == LabelType::CHARMAP))
{
// BMFONT|CHARMAP 使用 QuadCommand 进行绘制
auto textureAtlas = _batchNodes.at(0)->getTextureAtlas();
_quadCommand.init(_globalZOrder, textureAtlas->getTexture()->getName(), getGLProgramState(),
_blendFunc, textureAtlas->getQuads(), textureAtlas->getTotalQuads(), transform, flags);
renderer->addCommand(&_quadCommand);
}
else
{
// TTF类型 使用自定义类型绘制
_customCommand.init(_globalZOrder, transform, flags);
_customCommand.func = CC_CALLBACK_0(Label::onDraw, this, transform, transformUpdated);
renderer->addCommand(&_customCommand);
}
}
void Label::onDraw(const Mat4& transform, bool transformUpdated)
{
auto glprogram = getGLProgram();
glprogram->use();
GL::blendFunc(_blendFunc.src, _blendFunc.dst);
glprogram->setUniformsForBuiltins(transform);
if (_currentLabelType == LabelType::TTF)
{
switch (_currLabelEffect) {
case LabelEffect::OUTLINE: // 使用描边特效
glprogram->setUniformLocationWith4f(_uniformTextColor, // 文本颜色 装载至OpenGL
_textColorF.r, _textColorF.g, _textColorF.b, _textColorF.a);
glprogram->setUniformLocationWith4f(_uniformEffectColor, // 描边颜色信息 装载至OpenGL
_effectColorF.r, _effectColorF.g, _effectColorF.b, _effectColorF.a);
for (auto&& batchNode : _batchNodes)
{
batchNode->getTextureAtlas()->drawQuads();
}
//draw text without outline
glprogram->setUniformLocationWith4f(_uniformEffectColor,
_effectColorF.r, _effectColorF.g, _effectColorF.b, 0.f); // alpha置0 用于下方绘制不带描边的文本
break;
case LabelEffect::GLOW: // 发光特效
glprogram->setUniformLocationWith4f(_uniformEffectColor,
_effectColorF.r, _effectColorF.g, _effectColorF.b, _effectColorF.a);
case LabelEffect::NORMAL:
glprogram->setUniformLocationWith4f(_uniformTextColor,
_textColorF.r, _textColorF.g, _textColorF.b, _textColorF.a);
break;
default:
break;
}
}
for (auto&& batchNode : _batchNodes)
{
batchNode->getTextureAtlas()->drawQuads();
}
}