引言
cc.RichText功能丰富,支持图片等功能,但是drawcall非常高,大量富文本会导致界面卡顿。
游戏中经常遇到只需要颜色变化的富文本,如抽奖记录列表。使用cc.RichText仅仅显示几条记录,drawcall已经100多,滑动界面非常卡顿。
如何减少这种富文本的drawcall呢?
cc.RichText是通过一个个Label拼接成完成文本的,这种方式能支持多种节点混排,但是没有好的办法优化drawcall。
通过查看cc.Label的渲染代码,发现文本是通过canvas的文字绘制,生成texture来渲染的。
canvas文字绘制功能非常强大,可以用来做像word一样的富文本展示。
我们可以修改文本的canvas绘制过程,给文字加上颜色就可以了。
相关源码为:
//cocos2d/core/renderer/utils/label/ttf.js
_updateTexture () {
_context.clearRect(0, 0, _canvas.width, _canvas.height);
// use round for line join to avoid sharp intersect point
_context.lineJoin = 'round';
//Add a white background to avoid black edges.
if (!_premultiply) {
//TODO: it is best to add alphaTest to filter out the background color.
let _fillColor = _outlineComp ? _outlineColor : _color;
_context.fillStyle = `rgba(${_fillColor.r}, ${_fillColor.g}, ${_fillColor.b}, ${_invisibleAlpha})`;
_context.fillRect(0, 0, _canvas.width, _canvas.height);
_context.fillStyle = `rgba(${_color.r}, ${_color.g}, ${_color.b}, 1)`;
} else {
_context.fillStyle = `rgba(${_color.r}, ${_color.g}, ${_color.b}, ${_color.a / 255.0})`;
}
let startPosition = this._calculateFillTextStartPosition();
let lineHeight = this._getLineHeight();
let drawTextPosX = startPosition.x, drawTextPosY = 0;
// draw shadow and underline
this._drawTextEffect(startPosition, lineHeight);
// draw text and outline
for (let i = 0; i < _splitedStrings.length; ++i) {
drawTextPosY = startPosition.y + i * lineHeight;
if (_outlineComp) {
_context.strokeText(_splitedStrings[i], drawTextPosX, drawTextPosY);
}
_context.fillText(_splitedStrings[i], drawTextPosX, drawTextPosY);
}
if (_shadowComp) {
_context.shadowColor = 'transparent';
}
_texture.handleLoadedTexture();
}
这里用到很多引擎内置的方法和本地变量,如果要扩展该功能,需要自定义引擎,修改该类开放本地变量和方法,新增一个富文本渲染类继承该类,就可以完成功能。
但是这里我们不打算自定义引擎,通过使用一些hack的方法完成目标,最终只需一个组件就可以实现。完整代码:
//
class TextSegment {
text: string;
style?: { color: cc.Color };
start: number;
end: number;
}
let AssemblerClass = null;
function createAssembler() {
if (AssemblerClass) {
return new AssemblerClass();
}
//@ts-ignore
let WebglTTFAssembler = cc.Label.__assembler__?.TTF;
if (!WebglTTFAssembler) {
return null;
}
let _context = null;
let _canvas = null;
let _texture = null;
let _splitedStrings: string[] = [];
let _nodeContentSize = cc.Size.ZERO;
let _string = '';
let _fontDesc = '';
let _color = null;
let _hAlign = 0;
let _textSegmentList: TextSegment[] = [];
//@ts-ignore
let textUtils = cc.textUtils;
class RichColorTextAssembler extends WebglTTFAssembler {
_updateProperties(comp: RichColorText) {
super._updateProperties(comp);
//@ts-ignore
let assemblerData = comp._assemblerData;
_context = assemblerData.context;
_canvas = assemblerData.canvas;
//@ts-ignore
_texture = comp._frame._original ? comp._frame._original._texture : comp._frame._texture;
_nodeContentSize = comp.node.getContentSize();
_color = comp.node.color;
_hAlign = comp.horizontalAlign;
_string = comp.string.toString();
//@ts-ignore
_fontDesc = this._getFontDesc();
_textSegmentList = comp.textSegmentList;
}
_calculateLabelFont() {
let paragraphedStrings = _string.split('\n');
_splitedStrings = paragraphedStrings;
super._calculateLabelFont();
}
_calculateWrapText(paragraphedStrings) {
super._calculateWrapText(paragraphedStrings);
_splitedStrings = [];
let canvasWidthNoMargin = _nodeContentSize.width;
//@ts-ignore
_fontDesc = this._getFontDesc();
for (let i = 0; i < paragraphedStrings.length; ++i) {
let allWidth = textUtils.safeMeasureText(_context, paragraphedStrings[i], _fontDesc);
let textFragment = textUtils.fragmentText(paragraphedStrings[i],
allWidth,
canvasWidthNoMargin,
//@ts-ignore
this._measureText(_context, _fontDesc));
_splitedStrings = _splitedStrings.concat(textFragment);
}
}
_updateTexture() {
_context.clearRect(0, 0, _canvas.width, _canvas.height);
// use round for line join to avoid sharp intersect point
_context.lineJoin = 'round';
//@ts-ignore
let startPosition = this._calculateFillTextStartPosition();
//@ts-ignore
let lineHeight = this._getLineHeight();
let drawTextPosX = startPosition.x, drawTextPosY = 0;
let splitStringsWidth: number[] = [];
for (let i = 0; i < _splitedStrings.length; ++i) {
let curStr = _splitedStrings[i];
drawTextPosY = startPosition.y + i * lineHeight;
let curStrWidth = textUtils.safeMeasureText(_context, curStr, _fontDesc);
splitStringsWidth.push(curStrWidth);
}
// draw text
let startCharIndex = 0;
let startSegmentIndex = 0;
for (let i = 0; i < _splitedStrings.length; ++i) {
drawTextPosY = startPosition.y + i * lineHeight;
let curStr = _splitedStrings[i];
let curStrCount = curStr.length;
let curDrawTextPosX = drawTextPosX;
let curTotalWidth = 0;
let curStrWidth = splitStringsWidth[i];
if (_hAlign === cc.macro.TextAlignment.RIGHT) {
curDrawTextPosX = drawTextPosX - curStrWidth;
} else if (_hAlign === cc.macro.TextAlignment.CENTER) {
curDrawTextPosX = drawTextPosX - curStrWidth / 2;
}
let curStrStart = 0;
for (let i = startSegmentIndex; i < _textSegmentList.length; ++i) {
let segment = _textSegmentList[i];
let colorStart = segment.start;
let colorEnd = segment.end;
let colorRange = colorEnd - startCharIndex - curStrStart;
if (startCharIndex + curStrStart < colorStart) {
continue;
}
let color = segment.style?.color || _color;
_context.fillStyle = `rgba(${color.r}, ${color.g}, ${color.b}, 1)`;
let strCount = 0;
let lastStr = false;
if (colorRange >= curStr.length - curStrStart) {
strCount = curStr.length - curStrStart;
lastStr = true;
} else {
strCount = colorEnd - startCharIndex - curStrStart;
}
let str = curStr.substring(curStrStart, curStrStart + strCount);
let width = 0;
if (lastStr) {
width = curStrWidth - curTotalWidth;
} else {
width = textUtils.safeMeasureText(_context, str, _fontDesc);
}
let strPosX = 0;
if (_hAlign === cc.macro.TextAlignment.RIGHT) {
strPosX = width;
} else if (_hAlign === cc.macro.TextAlignment.CENTER) {
strPosX = width / 2;
}
_context.fillText(str, curDrawTextPosX + curTotalWidth + strPosX, drawTextPosY);
curTotalWidth += width;
curStrStart += strCount;
if (!lastStr) {
startSegmentIndex++;
} else {
break;
}
}
startCharIndex += curStrCount;
}
_texture.handleLoadedTexture();
}
}
AssemblerClass = RichColorTextAssembler;
return new AssemblerClass();
}
class HtmlTextParser {
private _resultObjectArray: TextSegment[] = [];
private _stack = [];
private _curCharCount = 0;
private _resultContent = "";
public parse(htmlString: string) {
this._resultObjectArray = [];
this._stack = [];
this._curCharCount = 0;
this._resultContent = "";
let startIndex = 0;
let length = htmlString.length;
while (startIndex < length) {
let tagEndIndex = htmlString.indexOf('>', startIndex);
let tagBeginIndex = -1;
if (tagEndIndex >= 0) {
tagBeginIndex = htmlString.lastIndexOf('<', tagEndIndex);
let noTagBegin = tagBeginIndex < (startIndex - 1);
if (noTagBegin) {
tagBeginIndex = htmlString.indexOf('<', tagEndIndex + 1);
tagEndIndex = htmlString.indexOf('>', tagBeginIndex + 1);
}
}
if (tagBeginIndex < 0) {
this._stack.pop();
this._processResult(htmlString.substring(startIndex));
startIndex = length;
} else {
let newStr = htmlString.substring(startIndex, tagBeginIndex);
let tagStr = htmlString.substring(tagBeginIndex + 1, tagEndIndex);
if (tagStr === "") newStr = htmlString.substring(startIndex, tagEndIndex + 1);
this._processResult(newStr);
if (tagEndIndex === -1) {
// cc.error('The HTML tag is invalid!');
tagEndIndex = tagBeginIndex;
} else if (htmlString.charAt(tagBeginIndex + 1) === '\/') {
this._stack.pop();
} else {
this._addToStack(tagStr);
}
startIndex = tagEndIndex + 1;
}
}
return { arr: this._resultObjectArray, content: this._resultContent };
}
private _attributeToObject(attribute: string) {
attribute = attribute.trim();
let obj: any = {};
let header = attribute.match(/^(color|size)(\s)*=/);
let tagName;
let nextSpace;
if (header) {
tagName = header[0];
attribute = attribute.substring(tagName.length).trim();
if (attribute === "") return obj;
//parse color
nextSpace = attribute.indexOf(' ');
switch (tagName[0]) {
case 'c':
if (nextSpace > -1) {
obj.color = new cc.Color().fromHEX(attribute.substring(0, nextSpace).trim());
} else {
obj.color = new cc.Color().fromHEX(attribute);
}
break;
}
return obj;
}
return obj;
}
private _addToStack(attribute: string) {
let obj = this._attributeToObject(attribute);
if (this._stack.length === 0) {
this._stack.push(obj);
} else {
//for nested tags
let previousTagObj = this._stack[this._stack.length - 1];
for (let key in previousTagObj) {
if (!(obj[key])) {
obj[key] = previousTagObj[key];
}
}
this._stack.push(obj);
}
}
private _processResult(value: string) {
if (value === "") {
return;
}
let start = this._curCharCount;
let end = start + value.replace(/\n/g, "").length;
this._curCharCount = end;
this._resultContent += value;
if (this._stack.length > 0) {
this._resultObjectArray.push({ text: value, style: this._stack[this._stack.length - 1], start, end });
} else {
this._resultObjectArray.push({ text: value, start, end });
}
}
}
const htmlTextParser = new HtmlTextParser();
const { ccclass, property } = cc._decorator
@ccclass
export default class RichColorText extends cc.Label {
@property
private _richText: string = "";
@property({ multiline: true })
get richText() {
return this._richText;
}
set richText(val: string) {
this._richText = val;
let result = htmlTextParser.parse(val);
this._textSegmentList = result.arr;
this.string = result.content;
}
private _textSegmentList: TextSegment[] = [];
get textSegmentList() {
if (this.string != "" && this._richText != "" && this._textSegmentList.length == 0) {
let result = htmlTextParser.parse(this._richText);
this._textSegmentList = result.arr;
}
return this._textSegmentList;
}
protected _resetAssembler() {
let assembler = createAssembler();
if (!assembler) {
//@ts-ignore
super._resetAssembler();
return;
}
//@ts-ignore
this._assembler = assembler;
//@ts-ignore
this.setVertsDirty();
assembler.init(this);
}
}
最终效果每个富文本仅仅占一个drawcall:
测试中使用的富文本内容:
<color=#FFFFFF>10连抽获得</color><color=#ec81ff>大环刀武器包</color><color=#FFFFFF> 1个</color><color=#FFFFFF>、</color><color=#5aff5a>紫金刀碎片包</color><color=#FFFFFF> 1个</color><color=#FFFFFF>、</color><color=#ffd956>碧水剑碎片包</color><color=#FFFFFF> 1个</color><color=#FFFFFF>、</color><color=#5ee3ff>铁笛碎片包</color><color=#FFFFFF> 1个</color><color=#FFFFFF>、</color><color=#ffd956>白虹剑碎片包</color><color=#FFFFFF> 1个</color><color=#FFFFFF>、</color><color=#ec81ff>大环刀碎片包</color><color=#FFFFFF> 1个</color><color=#FFFFFF>、</color><color=#5ee3ff>金钱镖碎片包</color><color=#FFFFFF> 1个</color><color=#FFFFFF>、</color><color=#5aff5a>鬼头刀碎片包</color><color=#FFFFFF> 1个</color><color=#FFFFFF>、</color><color=#ffd956>屠龙刀碎片包</color><color=#FFFFFF> 1个</color><color=#FFFFFF>、</color><color=#ffd956>匕首碎片包</color><color=#FFFFFF> 1个</color>
用法
该组件继承自cc.Label,新增richText字段,设置richText内容,文字内容自动变色。
richText内容使用cc.RichText的xml语法的color标签。
文字支持整体加粗、斜体。
注意:
-
只有设置richText才支持富文本,设置cc.Label的string不生效。
-
不支持文字大小变化,不支持文字描边、投影、下划线,不支持插入图片。
-
不支持文字缓存模式BITMAP和CHAR,不支持bmfont字体和艺术字。
-
只支持颜色标签,不支持其他任何标签,换行请直接使用’\n’,不要使用xml的br标签。
-
仅仅测试过h5端,原生没有测试。3.x版本请大家自行参考修改,如有完成的,欢迎分享到回帖。