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

仅适用于没用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. 【环境搭建】phpstudy显示目录列表

    问题来源 新版本的PHPStudy访问127.0.0.1不再像以前版本一样显示目录列表了 解决办法 打开vhosts.conf 将图中标记出来的一行Options FollowSymLinks Exe ...

  2. Windows安装SSH服务器

    1.打开Win的设置并在设置中找到应用 2.在应用中依次选择应用和功能 可选功能 3.在可选功能中选择添加功能 (OpenSSH客户端默认已存在) 选中OpenSSH服务器后点击下方的安装 4.快捷键 ...

  3. KVM 虚拟机 热插拔硬盘

    新建硬盘 lvm 命令 lvcreate -L 200G -n lv02 ssd01 qemu-img 命令 qemu-img create -f raw test1G.raw 1G dd 命令 dd ...

  4. 2021-7-11 Vue的自定义指令简单实例

    获取焦点简单实例,用Vue.directive(ps:指令)定义,命名不要是关键字,用v-加自定义指令名称调用,而内部用钩子函数inserted来实现 <!DOCTYPE html> &l ...

  5. 根据图片搜索excel

    问题描述:在excel使用中,当我们用大量的excel记录图文信息的时候,如果excel过多,比如成百上千个,里面都是包含大量的图片.这个时候如果想要根据图片快速找到这张图片可能被哪些excel包含, ...

  6. mysql系列03---事务

    一.事务简介 事务是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销 操作请求,即这些操作要么同时成功,要么同时失败. 二.事物的四大特性 一个事务具有A ...

  7. 基于Linux的三种防火墙(IPtables、Firewall、UFW)

    学而不思则罔,思而不学则殆. 导航 IPtables Firewall UFW 对比总结 IPtables部分 1.IPtables 四表五链. 四表:filter.nat.raw.mangle. 五 ...

  8. RocketMQ Linux单机测试:简易快速部署指南及Dashboard控制台部署

    目录 简介 开始 下载 增加环境变量 修改启动文件jvm大小 修改rocketmq配置文件 启动 快速测试 关闭 Dashboard 下载Dashboard 已编译jar包网盘下载 启动命令 可能遇到 ...

  9. golang1.21新特性速览

    经过了半年左右的开发,golang 1.21 在今天早上正式发布了. 这个版本中有不少重要的新特性和变更,尤其是在泛型相关的代码上. 因为有不少大变动,所以建议等第一个patch版本也就是1.21.1 ...

  10. Java内存溢出时,还能正常处理请求吗?

    当你被问到"当Java程序发生内存溢出时,进程还能正常处理请求吗?"这样的面试题,会不会很懵?这里分享一次网友车辙在当初刚毕业那几年,意义风发,总觉得天下没有自己不会的面试题.然后 ...