Unity TMP Inputfield 输入框 框选 富文本 获取真实定位

一、带富文本标签的框选是什么

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辆"三个字,那么使用一个正则字符串替换即可

cs 复制代码
//移除选定部分,所有富文本标签
    public string RemoveRichTextTags(string text) {
        return System.Text.RegularExpressions.Regex.Replace(text, "<.*?>", string.Empty);
    }

三、代码实现

在unity创建RichTextTagHandler类

这里先假定,只有一个InputField被编辑,无切换

先创建一个全局变量,用来保存某次计算的真实索引

cs 复制代码
private int total = 0;

创建RemoveFrontRichTag方法,用于移除,指定字符串,目标索引前,第一个富文本标签

cs 复制代码
/// <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方法,用于递归调用,移除目标索引前的所有富文本标签

cs 复制代码
/// <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,用于获取最终索引,并返回

cs 复制代码
/// <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;
}

完整的脚本长这样

cs 复制代码
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
}

使用时,类似这样

cs 复制代码
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;
    }
}

演示

其他扩展功能,只需要对字符串进行编辑即可

相关推荐
向宇it2 小时前
【从零开始入门unity游戏开发之——C#篇25】C#面向对象动态多态——virtual、override 和 base 关键字、抽象类和抽象方法
java·开发语言·unity·c#·游戏引擎
_oP_i4 小时前
unity webgl部署到iis报错
unity
Go_Accepted4 小时前
Unity全局雾效
unity
向宇it4 小时前
【从零开始入门unity游戏开发之——C#篇24】C#面向对象继承——万物之父(object)、装箱和拆箱、sealed 密封类
java·开发语言·unity·c#·游戏引擎
每日出拳老爷子7 小时前
【图形渲染】【Unity Shader】【Nvidia CG】有用的参考资料链接
unity·游戏引擎·图形渲染
北海65168 小时前
Dots 常用操作
unity
YY-nb15 小时前
Unity Apple Vision Pro 开发教程:物体识别跟踪
unity·游戏引擎·apple vision pro
Cool-浩15 小时前
Unity 开发Apple Vision Pro物体识别追踪ObjectTracking
unity·ar·apple vision pro·mr·物体识别·vision pro教程·objecttracking
向宇it1 天前
【从零开始入门unity游戏开发之——C#篇23】C#面向对象继承——`as`类型转化和`is`类型检查、向上转型和向下转型、里氏替换原则(LSP)
java·开发语言·unity·c#·游戏引擎·里氏替换原则
Cool-浩1 天前
Unity 开发Apple Vision Pro空间锚点应用Spatial Anchor
unity·游戏引擎·apple vision pro·空间锚点·spatial anchor·visionpro开发