TmpRichText.ts 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944
  1. import TextMeshPro from "./TextMeshPro";
  2. import { HtmlTextParser } from "./utils/HtmlParser";
  3. import TmpUtils from "./utils/TmpUtils";
  4. const { ccclass, property, disallowMultiple, executeInEditMode, menu } = cc._decorator;
  5. const RichTextChildName = "RICHTEXT_CHILD";
  6. const RichTextChildImageName = "RICHTEXT_Image_CHILD";
  7. const _htmlTextParser = new HtmlTextParser();
  8. let pool = new cc.js.Pool(function (node) {
  9. if (CC_EDITOR) {
  10. cc.isValid(node) && node.destroy();
  11. return false;
  12. }
  13. if (CC_DEV) {
  14. cc["assert"](!node._parent, "Recycling node\'s parent should be null!");
  15. }
  16. if (!cc.isValid(node)) {
  17. return false;
  18. }
  19. return true;
  20. }, 20);
  21. pool.get = function (string: string, richtext: TmpRichText) {
  22. let labelNode = this._get();
  23. if (!labelNode) {
  24. labelNode = new cc.PrivateNode(RichTextChildName);
  25. labelNode._objFlags |= cc.Object["Flags"].DontSave;
  26. }
  27. labelNode.setPosition(0, 0);
  28. labelNode.setAnchorPoint(0.5, 0.5);
  29. let labelComponent: TextMeshPro = labelNode.getComponent(TextMeshPro);
  30. if (!labelComponent) {
  31. labelComponent = labelNode.addComponent(TextMeshPro);
  32. }
  33. labelComponent.string = "";
  34. labelComponent.horizontalAlign = cc.Label.HorizontalAlign.LEFT;
  35. labelComponent.verticalAlign = cc.Label.VerticalAlign.CENTER;
  36. return labelNode;
  37. };
  38. /**
  39. * TextMeshPro富文本组件
  40. */
  41. @ccclass
  42. @disallowMultiple
  43. @executeInEditMode
  44. @menu("TextMeshPro组件/TmpRichText")
  45. export default class TmpRichText extends cc.Component {
  46. @property
  47. private _string: string = "";
  48. @property({ multiline: true })
  49. public get string(): string { return this._string; }
  50. public set string(v: string) {
  51. if (this._string === v) { return; }
  52. this._string = v;
  53. // this._layoutDirty = true;
  54. this._updateRichText();
  55. }
  56. @property(cc.JsonAsset)
  57. private _font: cc.JsonAsset = null;
  58. @property({ tooltip: CC_DEV && "字体资源\n依赖的纹理请勿打入图集\n在编辑器内拖拽此文件时,纹理必须和此文件处于同一目录下", type: cc.JsonAsset })
  59. private get font(): cc.JsonAsset { return this._font; }
  60. private set font(v: cc.JsonAsset) {
  61. if (this._font === v) { return; }
  62. this._font = v;
  63. if (CC_EDITOR) {
  64. this.editorInit();
  65. } else {
  66. this._layoutDirty = true;
  67. this._updateRichText();
  68. }
  69. }
  70. @property({ type: cc.Label.HorizontalAlign })
  71. private _horizontalAlign: cc.Label.HorizontalAlign = cc.Label.HorizontalAlign.LEFT;
  72. @property({ type: cc.Label.HorizontalAlign })
  73. public get horizontalAlign(): cc.Label.HorizontalAlign { return this._horizontalAlign; }
  74. public set horizontalAlign(v: cc.Label.HorizontalAlign) {
  75. if (this._horizontalAlign === v) { return; }
  76. this._horizontalAlign = v;
  77. this._layoutDirty = true;
  78. this._updateRichText();
  79. }
  80. @property({ type: cc.Label.VerticalAlign })
  81. private _verticalAlign: cc.Label.VerticalAlign = cc.Label.VerticalAlign.TOP;
  82. @property({ type: cc.Label.VerticalAlign })
  83. public get verticalAlign(): cc.Label.VerticalAlign { return this._verticalAlign; }
  84. public set verticalAlign(v: cc.Label.VerticalAlign) {
  85. if (this._verticalAlign === v) { return; }
  86. this._verticalAlign = v;
  87. this._layoutDirty = true;
  88. this._updateRichText();
  89. }
  90. @property
  91. private _fontSize: number = 32;
  92. @property({ range: [0, 1024] })
  93. public get fontSize(): number { return this._fontSize; }
  94. public set fontSize(v: number) {
  95. if (this._fontSize === v) { return; }
  96. this._fontSize = v;
  97. this._layoutDirty = true;
  98. this._updateRichText();
  99. }
  100. @property
  101. private _maxWidth: number = 0;
  102. @property({ tooltip: CC_DEV && "富文本的最大宽度" })
  103. public get maxWidth(): number { return this._maxWidth; }
  104. public set maxWidth(v: number) {
  105. if (this._maxWidth === v) { return; }
  106. this._maxWidth = v;
  107. this._layoutDirty = true;
  108. this._updateRichText();
  109. }
  110. @property
  111. private _lineHeight: number = 32;
  112. @property
  113. public get lineHeight(): number { return this._lineHeight; }
  114. public set lineHeight(v: number) {
  115. if (this._lineHeight === v) { return; }
  116. this._lineHeight = v;
  117. this._layoutDirty = true;
  118. this._updateRichText();
  119. }
  120. @property(cc.SpriteAtlas)
  121. private _imageAtlas: cc.SpriteAtlas = null;
  122. @property(cc.SpriteAtlas)
  123. public get imageAtlas(): cc.SpriteAtlas { return this._imageAtlas; }
  124. public set imageAtlas(v: cc.SpriteAtlas) {
  125. if (this._imageAtlas === v) { return; }
  126. this._imageAtlas = v;
  127. this._layoutDirty = true;
  128. this._updateRichText();
  129. }
  130. @property
  131. private _handleTouchEvent: boolean = true;
  132. @property
  133. public get handleTouchEvent(): boolean { return this._handleTouchEvent; }
  134. public set handleTouchEvent(v: boolean) {
  135. if (this._handleTouchEvent === v) { return; }
  136. this._handleTouchEvent = v;
  137. if (this.enabledInHierarchy) {
  138. this.handleTouchEvent ? this._addEventListeners() : this._removeEventListeners();
  139. }
  140. }
  141. @property(cc.Material)
  142. public material: cc.Material = null;
  143. @property({ tooltip: CC_DEV && "字体所依赖的纹理", type: cc.Texture2D, readonly: true })
  144. public textures: cc.Texture2D[] = [];
  145. private _textArray = null;
  146. private _labelSegments: cc.PrivateNode[] = [];
  147. private _labelSegmentsCache: cc.PrivateNode[] = [];
  148. private _linesWidth: number[] = [];
  149. private _lineOffsetX: number = 0;
  150. private _lineCount: number = 1;
  151. private _labelWidth: number = 0;
  152. private _labelHeight: number = 0;
  153. private _layoutDirty: boolean = true;
  154. // 文本父节点
  155. private _labelContent: cc.PrivateNode = null;
  156. private get labelContent(): cc.PrivateNode {
  157. if (!this._labelContent) {
  158. const content = "TMP_LABEL_CONTENT";
  159. this._labelContent = this.node.getChildByName(content) ?? new cc.PrivateNode(content);
  160. this._labelContent["_objFlags"] |= cc.Object["Flags"].DontSave;
  161. this.node.insertChild(this._labelContent, this._imageContent ? 1 : 0);
  162. }
  163. return this._labelContent;
  164. }
  165. // 图片父节点
  166. private _imageContent: cc.PrivateNode = null;
  167. private get imageContent(): cc.PrivateNode {
  168. if (!this._imageContent) {
  169. const content = "TMP_IMAGE_CONTENT";
  170. this._imageContent = this.node.getChildByName(content) ?? new cc.PrivateNode(content);
  171. this._imageContent["_objFlags"] |= cc.Object["Flags"].DontSave;
  172. this.node.insertChild(this._imageContent, 0);
  173. }
  174. return this._imageContent;
  175. }
  176. private editorInit(): void {
  177. if (CC_EDITOR) {
  178. // 加载图集
  179. if (!this._font || !this._font["_uuid"]) {
  180. this.textures = [];
  181. this._layoutDirty = true;
  182. this._updateRichText();
  183. return;
  184. }
  185. Editor.assetdb.queryUrlByUuid(this._font["_uuid"], (error: any, url: string) => {
  186. if (!url) {
  187. return;
  188. }
  189. let start = 12;
  190. let end = url.lastIndexOf("/");
  191. let dir = url.slice(start, end + 1);
  192. let arr: Promise<cc.Texture2D>[] = [];
  193. this._font.json.pageData.forEach((v) => {
  194. let imgUrl = dir + v.file;
  195. arr.push(TmpUtils.load<cc.Texture2D>(imgUrl));
  196. });
  197. Promise.all(arr).then((v) => {
  198. this.textures = v;
  199. this._layoutDirty = true;
  200. this._updateRichText();
  201. });
  202. });
  203. }
  204. }
  205. protected resetInEditor(): void {
  206. if (CC_EDITOR) {
  207. TmpUtils.load<cc.Material>(TmpUtils.TMP_MAT).then((mat) => {
  208. if (mat) {
  209. this.material = mat;
  210. }
  211. });
  212. }
  213. }
  214. public onRestore(): void {
  215. if (CC_EDITOR) {
  216. // Because undo/redo will not call onEnable/onDisable,
  217. // we need call onEnable/onDisable manually to active/disactive children nodes.
  218. if (this.enabledInHierarchy) {
  219. this.onEnable();
  220. }
  221. else {
  222. this.onDisable();
  223. }
  224. }
  225. }
  226. protected onEnable(): void {
  227. if (this.handleTouchEvent) {
  228. this._addEventListeners();
  229. }
  230. this._onFontLoaded();
  231. this._activateChildren(true);
  232. }
  233. protected onDisable(): void {
  234. if (this.handleTouchEvent) {
  235. this._removeEventListeners();
  236. }
  237. this._activateChildren(false);
  238. }
  239. protected onDestroy(): void {
  240. for (let i = 0; i < this._labelSegments.length; ++i) {
  241. this._labelSegments[i].removeFromParent();
  242. // @ts-ignore
  243. pool.put(this._labelSegments[i]);
  244. }
  245. }
  246. private _onColorChanged(parentColor): void {
  247. this.node.children.forEach((content) => {
  248. content.children.forEach((childNode) => {
  249. childNode.color = parentColor;
  250. });
  251. });
  252. }
  253. private _addEventListeners(): void {
  254. this.node.on(cc.Node.EventType.TOUCH_END, this._onTouchEnded, this);
  255. this.node.on(cc.Node.EventType.COLOR_CHANGED, this._onColorChanged, this);
  256. }
  257. private _removeEventListeners(): void {
  258. this.node.off(cc.Node.EventType.TOUCH_END, this._onTouchEnded, this);
  259. this.node.off(cc.Node.EventType.COLOR_CHANGED, this._onColorChanged, this);
  260. }
  261. private _updateLabelSegmentTextAttributes(): void {
  262. this._labelSegments.forEach(function (item) {
  263. this._applyTextAttribute(item, null, true);
  264. }.bind(this));
  265. }
  266. private _createFontLabel(string: string): cc.Node {
  267. let node = pool.get(string, this);
  268. let tmp: TextMeshPro = node.getComponent(TextMeshPro);
  269. if (tmp && tmp.getMaterial(0) !== this.material) {
  270. tmp.setMaterial(0, this.material);
  271. }
  272. return node;
  273. }
  274. private _onFontLoaded(): void {
  275. this._layoutDirty = true;
  276. this._updateRichText();
  277. }
  278. private _measureText(styleIndex: number, string?: string): number | ((s: string) => number) {
  279. let self = this;
  280. let func = function (string) {
  281. let label: cc.Node;
  282. if (self._labelSegmentsCache.length === 0) {
  283. label = self._createFontLabel(string);
  284. self._labelSegmentsCache.push(label);
  285. } else {
  286. label = self._labelSegmentsCache[0];
  287. }
  288. label["_styleIndex"] = styleIndex;
  289. self._applyTextAttribute(label, string, true);
  290. let labelSize = label.getContentSize();
  291. return labelSize.width;
  292. };
  293. if (string) {
  294. return func(string);
  295. } else {
  296. return func;
  297. }
  298. }
  299. private _onTouchEnded(event): void {
  300. let components = this.node.getComponents(cc.Component);
  301. for (let i = 0; i < this._labelSegments.length; ++i) {
  302. let labelSegment = this._labelSegments[i];
  303. let clickHandler = labelSegment["_clickHandler"];
  304. let clickParam = labelSegment["_clickParam"];
  305. if (clickHandler && this._containsTouchLocation(labelSegment, event.touch.getLocation())) {
  306. components.forEach(function (component) {
  307. if (component.enabledInHierarchy && component[clickHandler]) {
  308. component[clickHandler](event, clickParam);
  309. }
  310. });
  311. event.stopPropagation();
  312. }
  313. }
  314. }
  315. private _containsTouchLocation(label: cc.Node, point: cc.Vec2): boolean {
  316. let myRect = label.getBoundingBoxToWorld();
  317. return myRect.contains(point);
  318. }
  319. private _resetContent(node: cc.Node): void {
  320. if (!node) {
  321. return;
  322. }
  323. const children = node.children;
  324. for (let i = children.length - 1; i >= 0; i--) {
  325. const child = children[i];
  326. if (child.name === RichTextChildName || child.name === RichTextChildImageName) {
  327. if (child.parent === node) {
  328. child.parent = null;
  329. }
  330. else {
  331. // In case child.parent !== this.node, child cannot be removed from children
  332. children.splice(i, 1);
  333. }
  334. if (child.name === RichTextChildName) {
  335. // @ts-ignore
  336. pool.put(child);
  337. }
  338. }
  339. }
  340. }
  341. private _resetState(): void {
  342. for (let i = 0; i < this.node.childrenCount; i++) {
  343. this._resetContent(this.node.children[i]);
  344. }
  345. this._labelSegments.length = 0;
  346. this._labelSegmentsCache.length = 0;
  347. this._linesWidth.length = 0;
  348. this._lineOffsetX = 0;
  349. this._lineCount = 1;
  350. this._labelWidth = 0;
  351. this._labelHeight = 0;
  352. this._layoutDirty = true;
  353. }
  354. private _activateChildren(active: boolean): void {
  355. this.node.children.forEach((content) => {
  356. for (let i = content.children.length - 1; i >= 0; i--) {
  357. let child = content.children[i];
  358. if (child.name === RichTextChildName || child.name === RichTextChildImageName) {
  359. child.active = active;
  360. }
  361. }
  362. });
  363. }
  364. private _addLabelSegment(stringToken: string, styleIndex: number): cc.Node {
  365. let labelSegment;
  366. if (this._labelSegmentsCache.length === 0) {
  367. labelSegment = this._createFontLabel(stringToken);
  368. } else {
  369. labelSegment = this._labelSegmentsCache.pop();
  370. }
  371. const tmp: TextMeshPro = labelSegment.getComponent(TextMeshPro);
  372. if (tmp.verticalAlign !== this._verticalAlign) {
  373. tmp.verticalAlign = this._verticalAlign;
  374. }
  375. labelSegment._styleIndex = styleIndex;
  376. labelSegment._lineCount = this._lineCount;
  377. labelSegment.active = this.node.active;
  378. labelSegment.setAnchorPoint(0, 0);
  379. this._applyTextAttribute(labelSegment, stringToken, !!CC_EDITOR);
  380. this.labelContent.addChild(labelSegment);
  381. this._labelSegments.push(labelSegment);
  382. return labelSegment;
  383. }
  384. private _updateRichTextWithMaxWidth(labelString, labelWidth, styleIndex): void {
  385. let fragmentWidth = labelWidth;
  386. let labelSegment;
  387. if (this._lineOffsetX > 0 && fragmentWidth + this._lineOffsetX > this.maxWidth) {
  388. //concat previous line
  389. let checkStartIndex = 0;
  390. while (this._lineOffsetX <= this.maxWidth) {
  391. let checkEndIndex = this._getFirstWordLen(labelString,
  392. checkStartIndex,
  393. labelString.length);
  394. let checkString = labelString.substr(checkStartIndex, checkEndIndex);
  395. let checkStringWidth: number = this._measureText(styleIndex, checkString) as number;
  396. if (this._lineOffsetX + checkStringWidth <= this.maxWidth) {
  397. this._lineOffsetX += checkStringWidth;
  398. checkStartIndex += checkEndIndex;
  399. }
  400. else {
  401. if (checkStartIndex > 0) {
  402. let remainingString = labelString.substr(0, checkStartIndex);
  403. this._addLabelSegment(remainingString, styleIndex);
  404. labelString = labelString.substr(checkStartIndex, labelString.length);
  405. fragmentWidth = this._measureText(styleIndex, labelString);
  406. }
  407. this._updateLineInfo();
  408. break;
  409. }
  410. }
  411. }
  412. if (fragmentWidth > this.maxWidth) {
  413. let fragments = cc["textUtils"].fragmentText(labelString,
  414. fragmentWidth,
  415. this.maxWidth,
  416. this._measureText(styleIndex));
  417. for (let k = 0; k < fragments.length; ++k) {
  418. let splitString = fragments[k];
  419. labelSegment = this._addLabelSegment(splitString, styleIndex);
  420. let labelSize = labelSegment.getContentSize();
  421. this._lineOffsetX += labelSize.width;
  422. if (fragments.length > 1 && k < fragments.length - 1) {
  423. this._updateLineInfo();
  424. }
  425. }
  426. }
  427. else {
  428. this._lineOffsetX += fragmentWidth;
  429. this._addLabelSegment(labelString, styleIndex);
  430. }
  431. }
  432. private _isLastComponentCR(stringToken: string): boolean {
  433. return stringToken.length - 1 === stringToken.lastIndexOf("\n");
  434. }
  435. private _updateLineInfo(): void {
  436. this._linesWidth.push(this._lineOffsetX);
  437. this._lineOffsetX = 0;
  438. this._lineCount++;
  439. }
  440. private _needsUpdateTextLayout(newTextArray): boolean {
  441. if (this._layoutDirty || !this._textArray || !newTextArray) {
  442. return true;
  443. }
  444. if (this._textArray.length !== newTextArray.length) {
  445. return true;
  446. }
  447. for (let i = 0; i < this._textArray.length; ++i) {
  448. let oldItem = this._textArray[i];
  449. let newItem = newTextArray[i];
  450. if (oldItem.text !== newItem.text) {
  451. return true;
  452. }
  453. else {
  454. let oldStyle = oldItem.style, newStyle = newItem.style;
  455. if (oldStyle) {
  456. if (newStyle) {
  457. if (!oldStyle.outline !== !newStyle.outline) {
  458. return true;
  459. }
  460. if (oldStyle.size !== newStyle.size
  461. || !oldStyle.italic !== !newStyle.italic
  462. || oldStyle.isImage !== newStyle.isImage) {
  463. return true;
  464. }
  465. if (oldStyle.src !== newStyle.src ||
  466. oldStyle.imageAlign !== newStyle.imageAlign ||
  467. oldStyle.imageHeight !== newStyle.imageHeight ||
  468. oldStyle.imageWidth !== newStyle.imageWidth ||
  469. oldStyle.imageOffset !== newStyle.imageOffset) {
  470. return true;
  471. }
  472. }
  473. else {
  474. if (oldStyle.size || oldStyle.italic || oldStyle.isImage || oldStyle.outline) {
  475. return true;
  476. }
  477. }
  478. }
  479. else {
  480. if (newStyle) {
  481. if (newStyle.size || newStyle.italic || newStyle.isImage || newStyle.outline) {
  482. return true;
  483. }
  484. }
  485. }
  486. }
  487. }
  488. return false;
  489. }
  490. private _addRichTextImageElement(richTextElement): void {
  491. let spriteFrameName = richTextElement.style.src;
  492. let spriteFrame = this.imageAtlas.getSpriteFrame(spriteFrameName);
  493. if (spriteFrame) {
  494. let spriteNode = new cc.PrivateNode(RichTextChildImageName);
  495. spriteNode["_objFlags"] |= cc.Object["Flags"].DontSave;
  496. let spriteComponent = spriteNode.addComponent(cc.Sprite);
  497. switch (richTextElement.style.imageAlign) {
  498. case "top":
  499. spriteNode.setAnchorPoint(0, 1);
  500. break;
  501. case "center":
  502. spriteNode.setAnchorPoint(0, 0.5);
  503. break;
  504. default:
  505. spriteNode.setAnchorPoint(0, 0);
  506. break;
  507. }
  508. if (richTextElement.style.imageOffset) spriteNode["_imageOffset"] = richTextElement.style.imageOffset;
  509. spriteComponent.type = cc.Sprite.Type.SLICED;
  510. spriteComponent.sizeMode = cc.Sprite.SizeMode.CUSTOM;
  511. this.imageContent.addChild(spriteNode);
  512. this._labelSegments.push(spriteNode);
  513. let spriteRect = spriteFrame.getRect();
  514. let scaleFactor = 1;
  515. let spriteWidth = spriteRect.width;
  516. let spriteHeight = spriteRect.height;
  517. let expectWidth = richTextElement.style.imageWidth;
  518. let expectHeight = richTextElement.style.imageHeight;
  519. if (expectHeight > 0) {
  520. scaleFactor = expectHeight / spriteHeight;
  521. spriteWidth = spriteWidth * scaleFactor;
  522. spriteHeight = spriteHeight * scaleFactor;
  523. }
  524. else {
  525. scaleFactor = this.lineHeight / spriteHeight;
  526. spriteWidth = spriteWidth * scaleFactor;
  527. spriteHeight = spriteHeight * scaleFactor;
  528. }
  529. if (expectWidth > 0) spriteWidth = expectWidth;
  530. if (this.maxWidth > 0) {
  531. if (this._lineOffsetX + spriteWidth > this.maxWidth) {
  532. this._updateLineInfo();
  533. }
  534. this._lineOffsetX += spriteWidth;
  535. }
  536. else {
  537. this._lineOffsetX += spriteWidth;
  538. if (this._lineOffsetX > this._labelWidth) {
  539. this._labelWidth = this._lineOffsetX;
  540. }
  541. }
  542. spriteComponent.spriteFrame = spriteFrame;
  543. spriteNode.setContentSize(spriteWidth, spriteHeight);
  544. spriteNode["_lineCount"] = this._lineCount;
  545. if (richTextElement.style.event) {
  546. if (richTextElement.style.event.click) {
  547. spriteNode["_clickHandler"] = richTextElement.style.event.click;
  548. }
  549. if (richTextElement.style.event.param) {
  550. spriteNode["_clickParam"] = richTextElement.style.event.param;
  551. }
  552. else {
  553. spriteNode["_clickParam"] = "";
  554. }
  555. }
  556. else {
  557. spriteNode["_clickHandler"] = null;
  558. }
  559. }
  560. else {
  561. cc["warnID"](4400);
  562. }
  563. }
  564. private _updateRichText(): void {
  565. if (!this.enabledInHierarchy) return;
  566. let newTextArray = _htmlTextParser.parse(this.string);
  567. if (!this._needsUpdateTextLayout(newTextArray)) {
  568. this._textArray = newTextArray.slice();
  569. this._updateLabelSegmentTextAttributes();
  570. return;
  571. }
  572. this._textArray = newTextArray.slice();
  573. this._resetState();
  574. let lastEmptyLine = false;
  575. let label;
  576. let labelSize;
  577. for (let i = 0; i < this._textArray.length; ++i) {
  578. let richTextElement = this._textArray[i];
  579. let text = richTextElement.text;
  580. //handle <br/> <img /> tag
  581. if (text === "") {
  582. if (richTextElement.style && richTextElement.style.newline) {
  583. this._updateLineInfo();
  584. continue;
  585. }
  586. if (richTextElement.style && richTextElement.style.isImage && this.imageAtlas) {
  587. this._addRichTextImageElement(richTextElement);
  588. continue;
  589. }
  590. }
  591. let multilineTexts = text.split("\n");
  592. for (let j = 0; j < multilineTexts.length; ++j) {
  593. let labelString = multilineTexts[j];
  594. if (labelString === "") {
  595. //for continues \n
  596. if (this._isLastComponentCR(text)
  597. && j === multilineTexts.length - 1) {
  598. continue;
  599. }
  600. this._updateLineInfo();
  601. lastEmptyLine = true;
  602. continue;
  603. }
  604. lastEmptyLine = false;
  605. if (this.maxWidth > 0) {
  606. let labelWidth = this._measureText(i, labelString);
  607. this._updateRichTextWithMaxWidth(labelString, labelWidth, i);
  608. if (multilineTexts.length > 1 && j < multilineTexts.length - 1) {
  609. this._updateLineInfo();
  610. }
  611. }
  612. else {
  613. label = this._addLabelSegment(labelString, i);
  614. labelSize = label.getContentSize();
  615. this._lineOffsetX += labelSize.width;
  616. if (this._lineOffsetX > this._labelWidth) {
  617. this._labelWidth = this._lineOffsetX;
  618. }
  619. if (multilineTexts.length > 1 && j < multilineTexts.length - 1) {
  620. this._updateLineInfo();
  621. }
  622. }
  623. }
  624. }
  625. if (!lastEmptyLine) {
  626. this._linesWidth.push(this._lineOffsetX);
  627. }
  628. if (this.maxWidth > 0) {
  629. this._labelWidth = this.maxWidth;
  630. }
  631. this._labelHeight = (this._lineCount + cc["textUtils"].BASELINE_RATIO) * this.lineHeight;
  632. // trigger "size-changed" event
  633. this.node.setContentSize(this._labelWidth, this._labelHeight);
  634. this._updateRichTextPosition();
  635. this._layoutDirty = false;
  636. }
  637. private _getFirstWordLen(text, startIndex, textLen): number {
  638. let character = text.charAt(startIndex);
  639. if (cc["textUtils"].isUnicodeCJK(character)
  640. || cc["textUtils"].isUnicodeSpace(character)) {
  641. return 1;
  642. }
  643. let len = 1;
  644. for (let index = startIndex + 1; index < textLen; ++index) {
  645. character = text.charAt(index);
  646. if (cc["textUtils"].isUnicodeSpace(character)
  647. || cc["textUtils"].isUnicodeCJK(character)) {
  648. break;
  649. }
  650. len++;
  651. }
  652. return len;
  653. }
  654. private _updateRichTextPosition(): void {
  655. let nextTokenX = 0;
  656. let nextLineIndex = 1;
  657. let totalLineCount = this._lineCount;
  658. for (let i = 0; i < this._labelSegments.length; ++i) {
  659. let label = this._labelSegments[i];
  660. let lineCount = label["_lineCount"];
  661. if (lineCount > nextLineIndex) {
  662. nextTokenX = 0;
  663. nextLineIndex = lineCount;
  664. }
  665. let lineOffsetX = 0;
  666. switch (this.horizontalAlign) {
  667. case cc.Label.HorizontalAlign.LEFT:
  668. lineOffsetX = - this._labelWidth / 2;
  669. break;
  670. case cc.Label.HorizontalAlign.CENTER:
  671. lineOffsetX = - this._linesWidth[lineCount - 1] / 2;
  672. break;
  673. case cc.Label.HorizontalAlign.RIGHT:
  674. lineOffsetX = this._labelWidth / 2 - this._linesWidth[lineCount - 1];
  675. break;
  676. default:
  677. break;
  678. }
  679. label.x = nextTokenX + lineOffsetX;
  680. label.y = this.lineHeight * (totalLineCount - lineCount) - this._labelHeight / 2;
  681. if (lineCount === nextLineIndex) {
  682. let labelSize = label.getContentSize();
  683. nextTokenX += labelSize.width;
  684. // 排版根据TextMeshPro字符信息适配
  685. let tmp: TextMeshPro = label.getComponent(TextMeshPro);
  686. if (tmp && tmp.richTextDeltaX) {
  687. nextTokenX += tmp.richTextDeltaX;
  688. }
  689. }
  690. let sprite = label.getComponent(cc.Sprite);
  691. if (sprite) {
  692. // adjust img align (from <img align=top|center|bottom>)
  693. let lineHeightSet = this.lineHeight;
  694. let lineHeightReal = this.lineHeight * (1 + cc["textUtils"].BASELINE_RATIO); //single line node height
  695. switch (label.anchorY) {
  696. case 1:
  697. label.y += (lineHeightSet + ((lineHeightReal - lineHeightSet) / 2));
  698. break;
  699. case 0.5:
  700. label.y += (lineHeightReal / 2);
  701. break;
  702. default:
  703. label.y += ((lineHeightReal - lineHeightSet) / 2);
  704. break;
  705. }
  706. // adjust img offset (from <img offset=12|12,34>)
  707. if (label["_imageOffset"]) {
  708. let offsets = label["_imageOffset"].split(",");
  709. if (offsets.length === 1 && offsets[0]) {
  710. let offsetY = parseFloat(offsets[0]);
  711. if (Number.isInteger(offsetY)) label.y += offsetY;
  712. }
  713. else if (offsets.length === 2) {
  714. let offsetX = parseFloat(offsets[0]);
  715. let offsetY = parseFloat(offsets[1]);
  716. if (Number.isInteger(offsetX)) label.x += offsetX;
  717. if (Number.isInteger(offsetY)) label.y += offsetY;
  718. }
  719. }
  720. }
  721. //adjust y for label with outline
  722. // let outline = label.getComponent(cc.LabelOutline);
  723. // if (outline && outline.width) label.y = label.y - outline.width;
  724. }
  725. }
  726. /**
  727. * 16进制颜色转换
  728. * @param color
  729. */
  730. private _convertLiteralColorValue(color: string): cc.Color {
  731. let colorValue = color.toUpperCase();
  732. if (cc.Color[colorValue]) {
  733. return cc.Color[colorValue];
  734. }
  735. else {
  736. let hexString = (color.indexOf("#") === 0) ? color.substring(1) : color;
  737. let r = parseInt(hexString.substring(0, 2), 16) || 0;
  738. let g = parseInt(hexString.substring(2, 4), 16) || 0;
  739. let b = parseInt(hexString.substring(4, 6), 16) || 0;
  740. let a = parseInt(hexString.substring(6, 8), 16);
  741. if (Number.isNaN(a)) {
  742. a = 255;
  743. }
  744. return cc.color(r, g, b, a);
  745. }
  746. }
  747. /**
  748. * 更新字体样式
  749. * @param labelNode
  750. * @param string
  751. * @param force
  752. * @param needSpaceW 临时计算宽度时需要考虑空格字符串的宽度
  753. */
  754. private _applyTextAttribute(labelNode: cc.Node, string: string, force: boolean): void {
  755. let labelComponent: TextMeshPro = labelNode.getComponent(TextMeshPro);
  756. if (!labelComponent) {
  757. return;
  758. }
  759. let index = labelNode["_styleIndex"];
  760. let textStyle = null;
  761. if (this._textArray[index]) {
  762. textStyle = this._textArray[index].style;
  763. }
  764. if (textStyle && textStyle.color) {
  765. labelNode.color = this._convertLiteralColorValue(textStyle.color);
  766. } else {
  767. labelNode.color = this.node.color;
  768. }
  769. labelComponent.setFont(this.font, this.textures);
  770. labelComponent.lineHeight = this.lineHeight;
  771. labelComponent.colorGradient = Boolean(textStyle && textStyle.colorGradient);
  772. if (labelComponent.colorGradient) {
  773. labelComponent.colorLB = this._convertLiteralColorValue(textStyle.colorGradient.lb);
  774. labelComponent.colorRB = this._convertLiteralColorValue(textStyle.colorGradient.rb);
  775. labelComponent.colorLT = this._convertLiteralColorValue(textStyle.colorGradient.lt);
  776. labelComponent.colorRT = this._convertLiteralColorValue(textStyle.colorGradient.rt);
  777. }
  778. if (textStyle && textStyle.face) {
  779. labelComponent.tmpUniform.faceColor = this._convertLiteralColorValue(textStyle.face.color);
  780. labelComponent.tmpUniform.faceDilate = textStyle.face.dilate;
  781. labelComponent.tmpUniform.faceSoftness = textStyle.face.softness;
  782. } else {
  783. labelComponent.tmpUniform.faceColor = cc.Color.WHITE;
  784. labelComponent.tmpUniform.faceDilate = 0.5;
  785. labelComponent.tmpUniform.faceSoftness = 0.01;
  786. }
  787. labelComponent.enableItalic = Boolean(textStyle && textStyle.italic);
  788. labelComponent.enableUnderline = Boolean(textStyle && textStyle.underline);
  789. if (labelComponent.enableUnderline) {
  790. labelComponent.underlineOffset = textStyle.offset || 0;
  791. }
  792. labelComponent.enableStrikethrough = Boolean(textStyle && textStyle.strikethrough);
  793. if (labelComponent.enableStrikethrough) {
  794. labelComponent.strikethroughOffset = textStyle.offset || 0;
  795. }
  796. labelComponent.tmpUniform.enableOutline = Boolean(textStyle && textStyle.outline);
  797. if (textStyle && textStyle.outline) {
  798. labelComponent.tmpUniform.outlineColor = this._convertLiteralColorValue(textStyle.outline.color);
  799. labelComponent.tmpUniform.outlineThickness = textStyle.outline.thickness;
  800. }
  801. labelComponent.tmpUniform.enableUnderlay = Boolean(textStyle && textStyle.underlay);
  802. if (labelComponent.tmpUniform.enableUnderlay) {
  803. labelComponent.tmpUniform.underlayColor = this._convertLiteralColorValue(textStyle.underlay.color);
  804. labelComponent.tmpUniform.underlayOffset = cc.v2(textStyle.underlay.x, textStyle.underlay.y);
  805. labelComponent.tmpUniform.underlayDilate = textStyle.underlay.dilate;
  806. labelComponent.tmpUniform.underlaySoftness = textStyle.underlay.softness;
  807. }
  808. labelComponent.tmpUniform.enableGlow = Boolean(textStyle && textStyle.glow);
  809. if (labelComponent.tmpUniform.enableGlow) {
  810. labelComponent.tmpUniform.glowColor = this._convertLiteralColorValue(textStyle.glow.color);
  811. labelComponent.tmpUniform.glowOffset = textStyle.glow.offset;
  812. labelComponent.tmpUniform.glowInner = textStyle.glow.inner;
  813. labelComponent.tmpUniform.glowOuter = textStyle.glow.outer;
  814. labelComponent.tmpUniform.glowPower = textStyle.glow.power;
  815. }
  816. if (textStyle && textStyle.size) {
  817. labelComponent.fontSize = textStyle.size;
  818. } else {
  819. labelComponent.fontSize = this.fontSize;
  820. }
  821. if (string !== null) {
  822. if (typeof string !== "string") {
  823. string = "" + string;
  824. }
  825. labelComponent.string = string;
  826. }
  827. force && labelComponent.forceUpdateRenderData();
  828. if (textStyle && textStyle.event) {
  829. if (textStyle.event.click) {
  830. labelNode["_clickHandler"] = textStyle.event.click;
  831. }
  832. if (textStyle.event.param) {
  833. labelNode["_clickParam"] = textStyle.event.param;
  834. }
  835. else {
  836. labelNode["_clickParam"] = "";
  837. }
  838. }
  839. else {
  840. labelNode["_clickHandler"] = null;
  841. }
  842. }
  843. }