一、带富文本标签的框选是什么
UGUI的InputField提供了selectionAnchorPosition和selectionFocusPosition,开始选择时的光标下标和当前光标下标
对于未添加富文本标签时,直接通过以上两个值,判断一下框选方向(前向后/后向前),进而对inputfield的text内容进行字符串拆分即可
相关基础内容可以看一些参考博客
https://blog.51cto.com/u_15296378/7884559
但是对于有富文本标签的inputfield,这两个值,返回的只是表面看起来的索引,并没有包含富文本标签。
举个栗子:
对于一段普通文本(未添加富文本):今天没有下雨
你想选择“没有下”,那么索引应该是,2和4
但是对于一段富文本内容:今天<b>没有</b>下雨
你想选择“没有下”,那么真实索引应该是,5和12,
那么根据实际索引,拆分后的字符串是这样“没有</b>下”
然而你再使用selectionAnchorPosition和selectionFocusPosition尝试获取时,依然得到的是2和4,
那么根据原索引,拆分后的字符串是这样“<b>”,就完全被富文本标签干扰了
截止目前还没有或者我没找到官方直接可以使用的,支持带有富文本标签的,框选或选中内容定位获取接口。因此只能自己计算
二、获取真实富文本标签定位的大致思路
为了获取真实定位,这里我们先不考虑框选,先只考虑一个字符的位置。
还是以今天<b>没有</b>下雨为例
我想获取“没”的真实定位,只需要在原索引加上其前面富文本标签“<b>”的长度即可
那么2就变成了5
进而扩展一下,今天<b><i><color=yellow>没有</color></i></b>下雨 ,对于这个文本
想获取“没”的真实定位,就需要将“没”前面所有富文本标签的长度都加上
那么写一个方法,去除目标索引前的第一个富文本标签,并记录去除后的字符串和被去除标签的字符数量,然后递归调用自己,直到没有富文本标签之后结束
计算框选时第二个字的定位时,也是同理(这里后面框选时,计算的调节稍有不同,下文讲)
如果想要返回的内容中,不包含富文本标签,类似上图中的打印结果,只要“车10辆”三个字,那么使用一个正则字符串替换即可
//移除选定部分,所有富文本标签
public string RemoveRichTextTags(string text) {
return System.Text.RegularExpressions.Regex.Replace(text, "<.*?>", string.Empty);
}
三、代码实现
在unity创建RichTextTagHandler类
这里先假定,只有一个InputField被编辑,无切换
先创建一个全局变量,用来保存某次计算的真实索引
private int total = 0;
创建RemoveFrontRichTag方法,用于移除,指定字符串,目标索引前,第一个富文本标签
/// <summary>
/// 从前往后,移除第一个富文本标签,计数标签字符数量,并将移除后的字符串返回
/// </summary>
/// <param name="aimString">待移除富文本标签的目标字符串</param>
/// <param name="surfaceIndex">原定位</param>
/// <param name="count">用来计数用的, 标记已经移除的富文本标签字符长度</param>
/// <returns>阶段性返回当前移除的富文本标签字符数量,递归后最终返回所有符合要求的富文本标签字符总数量</returns>
private string RemoveFrontRichTag(string aimString,int surfaceIndex, ref int count) {
//先尝试定位'<'
int meet = aimString.IndexOf('<');
//如果定位不到,或者定位超过了原支付长度,说明标签在我们所选字符后面或者无富文本标签了,方法直接返回,并将数量count设置为0
if (meet == -1 || meet > surfaceIndex) {
count = 0;
return aimString;
}
//成功定位到‘<’后,继续定位‘>’
int leave = aimString.IndexOf('>');
//将字符串拆分成去掉<>及其内部内容
string newString = aimString.Substring(0, meet) + aimString.Substring(leave + 1, aimString.Length - leave - 1);
//计算去掉部分的字符数量
int length = leave - meet + 1;
count += length;
//Debug.Log(newString + " meet=" + meet + " leave=" + leave + " length=" + length);
//把去掉已计数的标签后的字符串,返回
return newString;
}
创建TryGetRealIndex方法,用于递归调用,移除目标索引前的所有富文本标签
/// <summary>
/// 尝试获取真实索引,递归方法,
/// </summary>
/// <param name="richText"></param>
/// <param name="surfaceIndex"></param>
/// <param name="isEndPoint"></param>
/// <returns></returns>
private int TryGetRealIndex(string richText, int surfaceIndex,bool isEndPoint) {
string newString = richText;
int count = 0;
//先尝试定位‘<’,并获取它的定位
int mark = newString.IndexOf('<');
//-1是没找到,没找到或大于,原本的定位,那么代表,原本定位之前已经没有'<'了,直接返回结束
if ((isEndPoint && (mark == -1 || mark >= surfaceIndex)) || //如果是结尾的点,那么判断mark时添加等号,防止后侧遗留标签整体
(!isEndPoint && (mark == -1 || mark > surfaceIndex))) {//如果不是结尾的点,那么判断mark时不加等号,判断前侧遗留标签整体
return total;
}
//如果在原定位前,找到了'<',那么走一遍清除最前的富文本标签并计数的方法
else {
//清除一次标签,并将清除后的字符串保存
newString = RemoveFrontRichTag(newString, surfaceIndex, ref count);
//加入计数
total += count;
//回调,继续判断,原本定位前,是否有富文本标记
TryGetRealIndex(newString, surfaceIndex, isEndPoint);
}
//递归完成后,返回最终的富文本标签字符总数
return total;
}
最后创建GetRealIndex,用于获取最终索引,并返回
/// <summary>
/// 获取选定部分,排除富文本标签后(富文本标签也算作数量),字符的真实位置
/// </summary>
/// <param name="inputField"></param>
/// <param name="surfaceIndex"></param>
/// <param name="isEndPoint">
/// 默认为false,从选定位置往前,所有富文本标签<>全部排除(通常为选定部分的“前面”点)
/// 传入true后,从选定位置开始,不仅往前,而且往后,所有富文本标签<>全部排除(通常为选定部分“后面”点)
/// 注意:选定时,从前往后选和从后往前选,“前面”和“后面”的点要判断一下
/// 扩展:如果“前面”点传入true,而“后面”点传入false,那么可以获取选定区域开始,前后所有的富文本标签都会被选定(默认是清除前后所有富文本标签)
/// </param>
/// <returns></returns>
public int GetRealIndex(TMP_InputField inputField, int surfaceIndex,bool isEndPoint=false) {
//
string richText = inputField.text;
//所有,surfaceIndex前,富文本标签所占的字符总数
int allRichTagCharCount = 0;
total = 0;//每次开始前,重置total为0
//递归获取富文本标签所占的字符总数
allRichTagCharCount = TryGetRealIndex(richText, surfaceIndex,isEndPoint);
//最终实际的定位是,富文本字符总数与surfaceIndex表定位的合
return allRichTagCharCount + surfaceIndex;
}
完整的脚本长这样
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class RichTextTagHandler {
private int total = 0;
/// <summary>
/// 获取选定部分,排除富文本标签后(富文本标签也算作数量),字符的真实位置
/// </summary>
/// <param name="inputField"></param>
/// <param name="surfaceIndex"></param>
/// <param name="isEndPoint">
/// 默认为false,从选定位置往前,所有富文本标签<>全部排除(通常为选定部分的“前面”点)
/// 传入true后,从选定位置开始,不仅往前,而且往后,所有富文本标签<>全部排除(通常为选定部分“后面”点)
/// 注意:选定时,从前往后选和从后往前选,“前面”和“后面”的点要判断一下
/// 扩展:如果“前面”点传入true,而“后面”点传入false,那么可以获取选定区域开始,前后所有的富文本标签都会被选定(默认是清除前后所有富文本标签)
/// </param>
/// <returns></returns>
public int GetRealIndex(TMP_InputField inputField, int surfaceIndex,bool isEndPoint=false) {
//
string richText = inputField.text;
//所有,surfaceIndex前,富文本标签所占的字符总数
int allRichTagCharCount = 0;
total = 0;//每次开始前,重置total为0
//递归获取富文本标签所占的字符总数
allRichTagCharCount = TryGetRealIndex(richText, surfaceIndex,isEndPoint);
//最终实际的定位是,富文本字符总数与surfaceIndex表定位的合
return allRichTagCharCount + surfaceIndex;
}
/// <summary>
/// 尝试获取真实索引,递归方法,
/// </summary>
/// <param name="richText"></param>
/// <param name="surfaceIndex"></param>
/// <param name="isEndPoint"></param>
/// <returns></returns>
private int TryGetRealIndex(string richText, int surfaceIndex,bool isEndPoint) {
string newString = richText;
int count = 0;
//先尝试定位‘<’,并获取它的定位
int mark = newString.IndexOf('<');
//-1是没找到,没找到或大于,原本的定位,那么代表,原本定位之前已经没有'<'了,直接返回结束
if ((isEndPoint && (mark == -1 || mark >= surfaceIndex)) || //如果是结尾的点,那么判断mark时添加等号,防止后侧遗留标签整体
(!isEndPoint && (mark == -1 || mark > surfaceIndex))) {//如果不是结尾的点,那么判断mark时不加等号,判断前侧遗留标签整体
return total;
}
//如果在原定位前,找到了'<',那么走一遍清除最前的富文本标签并计数的方法
else {
//清除一次标签,并将清除后的字符串保存
newString = RemoveFrontRichTag(newString, surfaceIndex, ref count);
//加入计数
total += count;
//回调,继续判断,原本定位前,是否有富文本标记
TryGetRealIndex(newString, surfaceIndex, isEndPoint);
}
//递归完成后,返回最终的富文本标签字符总数
return total;
}
/// <summary>
/// 从前往后,移除第一个富文本标签,计数标签字符数量,并将移除后的字符串返回
/// </summary>
/// <param name="aimString">待移除富文本标签的目标字符串</param>
/// <param name="surfaceIndex">原定位</param>
/// <param name="count">用来计数用的, 标记已经移除的富文本标签字符长度</param>
/// <returns>阶段性返回当前移除的富文本标签字符数量,递归后最终返回所有符合要求的富文本标签字符总数量</returns>
private string RemoveFrontRichTag(string aimString,int surfaceIndex, ref int count) {
//先尝试定位'<'
int meet = aimString.IndexOf('<');
//如果定位不到,或者定位超过了原支付长度,说明标签在我们所选字符后面或者无富文本标签了,方法直接返回,并将数量count设置为0
if (meet == -1 || meet > surfaceIndex) {
count = 0;
return aimString;
}
//成功定位到‘<’后,继续定位‘>’
int leave = aimString.IndexOf('>');
//将字符串拆分成去掉<>及其内部内容
string newString = aimString.Substring(0, meet) + aimString.Substring(leave + 1, aimString.Length - leave - 1);
//计算去掉部分的字符数量
int length = leave - meet + 1;
count += length;
//Debug.Log(newString + " meet=" + meet + " leave=" + leave + " length=" + length);
//把去掉已计数的标签后的字符串,返回
return newString;
}
#region Utility
//移除选定部分,所有富文本标签
public string RemoveRichTextTags(string text) {
return System.Text.RegularExpressions.Regex.Replace(text, "<.*?>", string.Empty);
}
//移除选定部分,所有颜色标签
public string RemoveRichTextTags_color(string text) {
return System.Text.RegularExpressions.Regex.Replace(text, "<color=.*?>|</color>", string.Empty);
}
//移除选定部分,所有加粗标签
public string RemoveRichTextTags_B(string text) {
return System.Text.RegularExpressions.Regex.Replace(text, "<b>|</b>", string.Empty);
}
//移除选定部分,所有Size标签
public string RemoveRichTextTags_Size(string text) {
return System.Text.RegularExpressions.Regex.Replace(text, "<size=.*?>|</size>", string.Empty);
}
#endregion
}
使用时,类似这样
private void Update() {
if (Input.GetMouseButtonUp(0)) {
OnMouseUpFromIpf();
}
}
#region 主要代码
private void OnMouseUpFromIpf() {
if (EventSystem.current.currentSelectedGameObject == ipf.gameObject) {
int startPos = ipf.selectionAnchorPosition;//起始选择的位置
int endPos = ipf.selectionFocusPosition;//结束时的位置
if (startPos == endPos) return;//如果起始结束相等,那么相当于就点了一下,没选择内容
string selectedStr = "";
if (startPos < endPos) {//正常从前往后选的情况
// 获取可见文本的起始位置,从选定Index开始,排除前面所有富文本标签
currentInsideBeginIndex = handler.GetRealIndex(ipf, startPos);
//获取可见文本的结束位置,从选定Index开始,后面的所有富文本标签,也要排除
currentInsideEndIndex = handler.GetRealIndex(ipf, endPos, true);//此处传入true,即可以排除index后面的所有富文本标签
}
else if (startPos > endPos) {//从后往前选的情况
currentInsideBeginIndex = handler.GetRealIndex(ipf, endPos);//反选,endPos是“前面的点”
currentInsideEndIndex = handler.GetRealIndex(ipf, startPos, true);//反选,startPos是“后面的点”
}
//获取选定部分的字符串,排除选定区域开始,前后所有的富文本标签
//注意:如果需要,以选定区域为基准,获取区域前后部分的所有富文本标签(而不是排除),那么需要将BeginIndex的GetRealIndex中IsEndPoint传入true,而EndIndex的GetRealIndex中IsEndPoint传入false
//类似一个加粗区域,<b>A</b>,目前选定A,那么获取到的是“A”,若是以下面的写法反向设置IsEndPoint,那么获取到的是“<b>A</b>”。后面这么获取,可以方便对标签进行判断和修改
selectedStr = ipf.text.Substring(currentInsideBeginIndex, currentInsideEndIndex - currentInsideBeginIndex);
Debug.Log(currentInsideBeginIndex + " " + currentInsideEndIndex + " " + selectedStr+" "+startPos+" "+endPos);
ipf_console.text = selectedStr;
}
}
演示
其他扩展功能,只需要对字符串进行编辑即可