项目中原来的富文本组件不太好用,做了一些修改,记述主要思路。缺陷很多。

仅适用于没用TextMeshPro,且不打算用的项目,否则请直接用TextMeshPro

原组件特点:

  1. 使用占位符模式,创建新的GameObject,挂载Image组件实现图文混排
  2. 主要通过正则匹配分析语法,扩展不便
  3. 固定RectTransform的anchor、pivot,Text的alignment,修改排版后需要手动计算相关位置,不能实现自动布局

新组件目标

  1. 通过逐步读取的方式分析语法
  2. 实现混排内容位置的自动计算

主要实现思路

需要实现的混排功能

  1. 静态图片(sprite)
  2. 动态表情
  3. 链接点击响应
  4. 颜色(简略代号和#FFFFFF)
  5. 下划线
  6. UGUI原生Text标记(斜体、粗体、大小)

混排位置计算的实现原理

通过Text组件中的字符顶点信息,计算对应位置

主要代码结构

RichText.cs - 接口和生命周期处理

RichText.MarkItem.cs - 定义结构类型和对象池处理

RichText.MarkType.cs - 定义类型枚举

RichText.Utils.cs - 辅助函数

RichText.Analyzor.cs - 语法分析

RichText.Generator.cs - 生成数据结构

RichText.Drawer.cs - 绘制额外内容

RichText.LinkListener.cs - 链接点击响应处理

语法分析

标记结构

private class MarkItem
{
public int markId;
public int markType;
public string value;
public int startIndex; // 起始字符位置
public int endIndex; // 结束字符位置 // 对象池略
}

语法主要模式

#f001#n - 动态表情
#c#FF0000#n红色文字#n - 文字颜色

(即把Text的<>修改为#)

语法分析步骤

  1. 预处理输入的字符串,清除UGUI Text的<>标记内容,进行一些其他需要的前期处理
  2. 清理分析栈、根节点、已存储的数据,根节点入栈
  3. 按顺序读取预处理后的字符串
  4. 如果下一个字符不是'#',读到下一个'#n',存为一个普通字符类型标记
  5. 如果下一个字符是'#',如果是'#n',结束上一个标记,否则读到下一个'#n'
  6. 读取到字符串结束
  7. 检验分析结果,若中间有标记不匹配,或最后分析栈不是只包含根节点,分析结果错误,直接输出原字符串;否则正确,开始生成特殊标记

一些实现细节

[RequireComponent(typeof(Text))]
public partial class RichText : MonoBehavior
{
private MarkItem m_TreeRoot;
private readonly Stack<int> m_AnalyzeStack = new Stack<int>();
private readonly Dictionary<int, int> m_ParentDict = new Dictionary<int, int>();
private readonly Dictionary<int, List<int>> m_ChildrenDict = new Dictionary<int, List<int>>(); private void AnalyzeOriginalText()
{
string tempText = m_OriginalText;
tempText = s_UnityMarkRegex.Replace(tempText, ""); // <.*?>
// 其他处理
Clear(); int pos = 0;
int length = tempText.Length;
bool success = true; while (pos < length)
{
int curLength = 0;
if (tempText[pos] != '#')
{
int nextSharpPos = tempText.IndexOf('#', pos);
if (nextSharpPos < 0)
{
curLength = length - pos;
}
else
{
curLength = nextSharpPos - pos;
} success = CreateNewMarkItem(MARK_NORMAL, tempText.Substring(pos, curLength));
if (!success) break;
}
else
{
if (endMarkPos < 0)
{
curLength = length - pos;
success = CreateNewMarkItem(MARK_NORMAL, tempText.Substring(pos, curLength));
if (!success) break;
}
else {
curLength = endMarkPos - pos + 2;
success = CreateStyleMarkItem(tempText.Substring(pos, curLength));
if (!success) break;
}
} pos += curLength;
} if (m_AnalyzeStack.Count != 1)
{
Clear();
CreateNewMarkItem(MARK_NORAML, tempText);
}
} private bool CreateNewMarkItem(int markType, string value)
{
int markId = m_MarkItemList.Count;
MarkItem item = MarkItem.Get();
item.markType = markType;
item.markId = m_MarkItemList.Count;
item.value = value; m_MarkItemList.Add(item);
if (m_AnalyzeStack.Count == 0)
{
return false; //分析栈中根节点已经弹出,语法错误
} int parentId = m_AnalyzeStack.Peek();
if (!m_ChildrenDict.ContainsKey(parentId))
{
m_ChildrenDict[parentId] = new List<int>();
}
m_ChildrenDict[parentId].Add(item.markId); return true;
} private bool CreateStyleMarkItem(string text)
{
int markType = MARK_NORMAL;
string value = "";
int length = text.Length;
switch(text[1])
{
//...标记类型
} if (length > 4)
{
value = text.Substring(2, length - 4);
} bool success = CreateNewMarkItem(markType, value);
if (!success)
{
return false;
} switch (markType)
{
//可包含子节点的标记类型,入m_AnalyzeStack
} return true;
}
}

为markId使用自增id存入列表,使用字典存储父子关系,还有优化的地方

结构生成

主要遍历上一步生成的语法树

//RichText.Generator.cs

private readonly m_StringBuilder = new StringBuilder();

private void GeneratorDisplayContent()
{
TraversalMarkItemNode(ROOT_ID);
m_Text.text = m_StringBuilder.ToString();
} private void TraversalMarkItemNode(int nodeId)
{
MarkItem node = m_MarkItemList[nodeId];
int startIndex = m_StringBuilder.Length;
node.startIndex = startIndex; switch(node.markType)
{
//普通类型略
case MARK_PHOTO:
case MARK_FACE:
m_StringBuilder.Append("<color=#ffffff00>");
float width, height;
// 即得出需要使用几个占位符
int placeholderCount = GetSpriteParams(node.markType, node.value, out width, out height);
m_StringBuilder.Insert(m_StringBuilder.Length, PLACEHOLDER, placeholderCount);
m_StringBuilder.Append("</color>"); m_ActiveImageCount++;
if (m_ImageItemList == null)
{
m_ImageItemList = new List<ImageItem>();
}
ImageItem iItem = ImageItem.Get();
iItem.markId = node.markId;
iItem.startIndex = startIndex;
iItem.endIndex = m_StringBuilder.Length;
iItem.width = width;
iItem.height = height;
m_ImageItemList.Add(iItem); if (node.markType == MARK_FACE)
{
m_ActiveFaceCount++;
}
break;
} if (m_ChildrenDict.ContainsKey(nodeId))
{
List<int> list = m_ChildrenDict[nodeId];
int size = list.Count;
for (int i = 0; i < size; i++)
{
TraversalMarkItemNode(list[i]);
}
} //标记闭合处理
int endIndex = m_StringBuilder.Length;
node.endIndex = endIndex;
switch(node.markType)
{
//普通类型略
case MARK_LINK:
//和上面Image类似,生成LinkItem,有参数可以做一些处理
break;
case MARK_UNDERLINE:
//同上
break;
}
}

绘制额外内容

首先解决在合适的位置绘制额外内容的问题

// RichText.Drawer.cs

private float m_PlaceholderPixelWidth = 0;
private IList<UICharInfo> m_CurrentUICharInfoList = null;
private IList<UILineInfo> m_CurrentUILineInfoList = null;
private readonly List<int> m_CurrentLineStartIndexList = new List<int>();
private readonly List<int> m_CurrentLineEndIndexList = new List<int>();
private int m_CurrentCharactersCount = 0;
private int m_CUrrentLinesCount = 0; private void RefreshExtraContents()
{
RefreshGeneratorResults();
ResetImageGameObjects();
ResetLinkGameObjects();
ResetUnderlineGameObjects();
} // 修改字体大小时调用,计算占位符宽度
private void RefreshGeneratorParams()
{
TextGenerator textGenerator = new TextGenerator();
Rect rect = m_RectTransform.rect;
Vector2 extents = new Vector2(rect.width, rect.height);
TextGenerationSettings settings = m_Text.GetGenerationSettings(extents);
m_PlaceholderPixelWidth = textGenerator.GetPreferredWidth(PLACEHOLDER, settings);
} // 拷贝生成器结果
private void RefreshGeneratorResults()
{
TextGenerator generator = m_Text.cachedTextGenerator; // 第一次传值时未及时生成
if (generator.characterCount == 0)
{
Rect rect = m_RectTransform.rect;
Vector2 extents = new Vector2(rect.width, rect.height);
TextGenerationSettings settings = m_Text.GetGenerationSettings(extents);
generator.Populate(m_Text.text, setting);
}
m_CurrentCharactersCount = generator.characterCount; //显示部分(生成的)的字符数量,有点坑
m_CurrentUICharInfoList = generator.characters;
m_CurrentUILineInfoList = generator.lines; //刷新m_CurrentLineStartIndexList, m_CurrentLineEndIndexList
} // 对象重用略 //重设图片位置
private void ResetImageGameObjects()
{
if (m_ActiveImageCount == 0)
{
if (m_ImageGo != null)
{
m_ImageGo.SetActive(false);
} return;
} if (m_ImageGo == null)
{
m_ImageGo = CreateUIGameObject(transform, IMAGE_GO_NAME, true);
}
m_ImageGo.SetActive(true); for (int i = 0; i < m_ActiveImageCount; i++)
{
ImageItem item = m_ImageItemList[i];
GameObject go = item.gameObject; if (go == null)
{
go = GetImageGameObject(m_ImageGo.transform, i, i.ToString());
item.gameObject = go;
} Image image = go.GetComponent<Image>();
MarkItem node = m_MarkItemList[item.markId]; image.sprite = GetSprite(node.markType, node.value);
RectTransform rectTransform = go.GetComponent<RectTransform>();
rectTransform.sizeDelta = new Vector2(item.width, item.height);
float sl, sr, st, sb, el, er, et, eb;
// 一些字符是不显示的,如“<color=#ffffff>”, 获取实际位置
int realStartIndex, readEndIndex;
bool success = true;
success &= TryGetNextValidCharPos(item.startIndex, out sl, out sr, out st, out sb, out realStartIndex);
success &= TryGetPrevValidCharPos(item.endIndex, out el, out er, out et, out eb, out realEndIndex);
success &= realStartIndex <= realEndIndex;
if (!success)
{
item.active = false;
go.SetActive(false);
}
else
{
item.active = true;
go.SetActive(true);
} float x = (sl + er) / 2;
float y = (st + sb) / 2;
rectTransform.localPosition = new Vector2(x, y); if (node.markType == MARK_FACE)
{
// 创建动画
}
} Transform container = m_ImageGo.transform;
for (int i = m_ActiveImageCount; i < container.childCount; i++)
{
container.GetChild(i).gameObject.SetActive(false);
}
} //重设链接位置,外层基本和ResetImageGameObjects相同
private void ResetLinkGameObjects()
{
//略 for (int i = 0; i < m_ActiveLinkCount; i++)
{
//略 //多行处理
int curValidLines = 0;
for (int line = 0; line < m_CUrrentLinesCount; line++)
{
int lineStartIndex = m_CurrentLineStartIndexList[line];
int lineEndIndex = m_CurrentLineEndIndexList[line];
if (startIndex > lineEndIndex) continue;
if (lineStartIndex > endIndex) break;
UILineInfo info = m_CurrentUILineInfoList[line];
int curLineStartIndex = startIndex > lineStartIndex ? startIndex : lineStartIndex;
int curLineEndIndex = endIndex < lineEndIndex ? endIndex : lineEndIndex;
float sl, sr, st, sb, el, er, et, eb;
int realStartIndex, realEndIndex;
bool success = true;
success &= TryGetNextValidCharPos(item.startIndex, out sl, out sr, out st, out sb, out realStartIndex);
success &= TryGetPrevValidCharPos(item.endIndex, out el, out er, out et, out eb, out realEndIndex);
success &= realStartIndex <= realEndIndex;
success &= realStartIndex <= curLineEndIndex;
success &= realEndIndex >= curLineStartIndex;
if (!success)
{
continue;
} curValidLines++;
float x = (sl + er) / 2;
float y = info.topY - info.height / 2;
float width = er - sl;
if (width <= 0) continue;
float height = info.height; GameObejct curGo = GetLinkGameObject(curContainer, curValidLines, string.Format("{0}_{1}", i, curValidLines));
curGo.SetActive(true);
RectTransform rectTransform = curGo.GetComponent<RectTransform>();
rectTransform.localPosition = new Vector2(x, y);
rectTransform.sizeDelta = new Vector2(width, height);
} // 略
} // 略
} // ResetUnderlineGameObjects() 和 ResetLinkGameObjects() 基本相同,略
// RichText.Utils.cs

private bool TryGetPrevValidCharPos(int index, out float left, out float right, out float top, out float bottom, out int realIndex)
{
int size = m_CurrentUICharInfoList.Count;
while (true)
{
if (index >= size || index < 0)
{
left = 0;
right = 0;
top = 0;
bottom = 0;
realIndex = 0;
return false;
} UICharInfo info = m_CurrentUICharInfoList[index];
if (info.charWidth == 0)
{
index--;
continue;
} realIndex = index;
left = info.cursorPos.x;
top = info.cursorPos.y;
right = left + info.charWidth;
UILineInfo lineInfo;
if (TryGetUILineInfoByCharacterIndex(realIndex, out linInfo))
{
bottom = top - lineInfo.fontSize;
}
else
{
bottom = top - m_Text.fontSize;
} return true;
}
}

在不设置RectTransform的anchor的情况下,上面的代码基本满足功能,待要修改布局的话,需要针对RectTransform的参数修改做相关处理

private Vector2 m_CachedPivot;
private Vector2 m_CachedAnchorMin;
private Vector2 m_CachedAnchorMax; private void RefreshGameObjectPositions()
{
Vector2 pivot = m_RectTransform.pivot;
Vector2 anchorMin = m_RectTransform.anchorMin;
Vector2 anchorMax = m_RectTransform.anchorMax; if (pivot == m_CachedPivot && anchorMin == m_CachedAnchorMin && anchorMax == m_CachedAnchorMax)
{
return;
} m_CachedPivot = pivot;
m_CachedAnchorMin = anchorMin;
m_CachedAnchorMax = anchorMax; if (m_ImageGo)
{
// 传递下去
} // 后略
} private void OnRectTransformDimensionsChange()
{
if (!m_Inited)
{
return;
} RefreshExtraContents();
}

修改后可以随RectTransform的变化而变化,但发现很容易出现错位现象,原因是unity的自动布局等常在下一帧生效,延时一会儿即可

public int repaintDelayFrame = 3;
private int m_NextRepaintFrame = -1; private void Update()
{
if (!m_Inited)
{
return;
} if (m_Text.cachedTextGenerator.characterCount != m_CurrentCharactersCount)
{
m_NextRepaintFrame = repaintDelayFrame;
m_CurrentCharactersCount = m_Text.cachedTextGenerator.characterCount;
} if (m_NextRepaintFrame > 0)
{
m_NextRepaintFrame--;
}
else if (m_NextRepaintFrame == 0)
{
Repaint();
m_NextRepaintFrame--;
} // 略
} private void OnRectTransformDimensionsChange()
{
if (!m_Inited)
{
return;
} m_NextRepaintFrame = repaintDelayFrame;
}

bug注意

  1. Canvas的RenderMode设置为ScreenSpace - Camera\Overlay时,若因CanvasScaler设置了缩放,TextGenerator.characters得到的坐标、宽度数据会有缩放,位置计算出现偏差,需要除一次缩放比率

一个Unity富文本插件的实现思路的更多相关文章

  1. 小程序快速部署富文本插件wxParser

    为了解决html2wxml在ios下字体过大问题,又发现一个比较好用的富文本插件:wxParser. 目前 wxParser 支持对一般的富文本内容包括标题.字体大小.对齐和列表等进行解析.同时也支持 ...

  2. 【富文本、JS】将接口传来的全部纯URL替换为富文本插件能识别到的img标签

    replaceURLWithImage (text) { var a = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0 ...

  3. web开发实战--弹出式富文本编辑器的实现思路和踩过的坑

    前言: 和弟弟合作, 一起整了个智慧屋的小web站点, 里面包含了很多经典的智力和推理题. 其实该站点从技术层面来分析的话, 也算一个信息发布站点. 因此在该网站的后台运营中, 富文本的编辑器显得尤为 ...

  4. 小程序解析html之富文本插件wxParse

    近期,开发小程序时,遇到后台给我返回了一串html代码,需要我这边来解析,头疼了好久,网上找资料找了变天,终于找到wxParse,然而看到的都是针对于页面中有单个html或者固定数据的,我现在的问题是 ...

  5. 微信小程序开发--富文本插件wxParse的使用

    昨天一位网友问我小程序怎么解析富文本.他尝试过把html转出小程序的组件,但是还是不成功,我说可以把内容剥离出来.但是这两种方法都是不行了.后来找到了wxParse-微信小程序富文本解析组件. 特性 ...

  6. 微信小程序template富文本插件image宽度被js强制设置

    这段时间一直做微信小程序,过程中遇到了一个问题,这个问题一直没有得到完美的解决. 问题描述: 在Web编程中经常会引入template插件,这个插件是封装好,我们通常的做法是直接引入,配置简单,好用, ...

  7. 记录一个Unity播放器插件的开发

    背景 公司最近在做VR直播平台,VR开发我们用到了Unity,而在Unity中播放视频就需要一款视频插件,我们调研了几个视频插件,记录两个,如下: Unity视频插件调研 网上搜了搜,最流行的有以下两 ...

  8. JS 的execCommand 方法 做的一个简单富文本

    execCommand 当一个 HTML 文档切换到设计模式(designMode)时,文档对象暴露 execCommand 方法,该方法允许运行命令来操纵可编辑区域的内容.大多数命令影响文档的选择( ...

  9. 富文本插件KindEditor

    具体用法查看官网http://kindeditor.net/doc.php {% load staticfiles %} <!DOCTYPE html> <html lang=&qu ...

  10. 百度UEditor富文本插件的使用

    这个富文本还是功能挺全的. 官方文档地址 下载地址 常用接口 较完整代码仓库 UEditor下载后直接运行即可访问,但在上传文件时需要单独再做配置. [很详细的SpringBoot整合UEditor教 ...

随机推荐

  1. AI技术在软件测试中的应用和实践

    随着人工智能(AI)技术的快速发展,它在各个领域都展现出了巨大的潜力和影响力.在软件测试领域,AI技术也越来越得到重视和应用.本文将探讨AI技术在软件测试中的应用和实践,重点关注chatGPT如何根据 ...

  2. Open LLM 排行榜近况

    Open LLM 排行榜是 Hugging Face 设立的一个用于评测开放大语言模型的公开榜单.最近,随着 Falcon 的发布并在 Open LLM 排行榜 上疯狂屠榜,围绕这个榜单在推特上掀起了 ...

  3. 无需学习Python,一个公式搞定领导想看的大屏

    摘要:本文由葡萄城技术团队于博客园原创并首发.转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 不要让"做不了"成为数字化转型的障碍 随着 ...

  4. Linux下实现程序开机自启(rc.local 和 systemctl)

    需求: 实现了一个程序,需要在ubuntu上跑起来.能开机自启,且崩溃了也能自己重启.有两种实现方式,个人推荐第二种. 方式1: 实现方式: 直接将要启动程序的运行命令加在 /etc/rc.local ...

  5. Magick.NET跨平台压缩图片的用法

    //首先NuGet安装:Magick.NET.Core,Magick.NET-Q16-AnyCPUusing ImageMagick; /// <summary> /// 压缩图片 /// ...

  6. Go中 net/http 使用

    转载请注明出处: net/http是Go语言标准库中的一个包,提供了实现HTTP客户端和服务器的功能.它使得编写基于HTTP协议的Web应用程序变得简单和方便. net/http包的主要用途包括: 实 ...

  7. python:时间模块dateutil

    安装 pip install python-dateutil dateutil模块主要有两个函数,parser和rrule. 其中parser是根据字符串解析成datetime,而rrule则是根据定 ...

  8. pywintypes.com_error: (-2147418111, '被呼叫方拒绝接收呼叫。', None, None)

    将打开的excel全部关闭,即可解决问题.

  9. Oracle 11g ocm考试内容目录

    Server Configuration Create the database Determine and set sizing parameters for database structures ...

  10. 2023-08-06:小青蛙住在一条河边, 它想到河对岸的学校去学习 小青蛙打算经过河里 的石头跳到对岸 河里的石头排成了一条直线, 小青蛙每次跳跃必须落在一块石头或者岸上 给定一个长度为n的数组ar

    2023-08-06:小青蛙住在一条河边, 它想到河对岸的学校去学习 小青蛙打算经过河里 的石头跳到对岸 河里的石头排成了一条直线, 小青蛙每次跳跃必须落在一块石头或者岸上 给定一个长度为n的数组ar ...