Unity 来搞一个图文混排吧~

好久没有来更新了,甚至我的账号都掉了,试了半天才搞对密码。
最近也真的是很忙,甚至上周日差点加班,幸好……
废话不多说,开始了!!!

废话

首先说明,这个大标题都是废话,可以跳过。

图文混排,这是一个项目中最常见的功能了。没错,我们的项目也有这个需求,幸运的是,我不会呀!要不也不会来写这片文章了。
不光不会,我甚至觉得这东西贼麻烦,想尽量往后拖。然而他们给我安排的时间实在是太多了,不弄不行了。

于是开始百度一下别人写好的东西。
看到第一种,把要需要的图片打个图集,然后用Mesh贴上去。嗯,局限性很大,根本不是我所能理解的,引用过来也不知道能不能正常运行,还要单打一个图集出来,性能也不了解,有问题我也不会改……
然后是第二种,做一个Asset文件,把需要的图片加进去,然后自己命名,设置尺寸……个人认为还不如第一种,不过他是用来做gif效果的。但是大部分的项目不用这么麻烦的东西,还不如直接弄个Skeleton插件进来了,再套用第三种方法就行了。
最后是第三种,修改Text的OnPopulateMesh方法,在获取顶点信息的过程中,通过正则来把图片的字符串改为宽度合适的空格,并记录图片相关数据。接着在物体上挂载几个Image,把图片信息赋值进去。

第三种方式看起来不错,主要是他给的代码很全,逻辑也很容易看。然而弄着弄着,发现很多问题,特别是位置各种不对。只能默默的改,改了半天,终于看起来差不多了。

就这样平静的过了两个月,来了一个单子【在不同分别率下,图文混排位置不对】
太难受了,只能一点一点的重新看了起来,开始下断点看各个顶点信息。终于解决了一大堆问题,比如说图片占位的空格数量并不对,计算图片位置中心的顶点并不对,没有把换行和原有的空格计算等等……
果然人是不能懒的,之前没有完全看透,现在被迫完全梳理了一下逻辑。-_-

就这样,我以为终于要结束了,然而昨天又给我提了个单子【在手机上,偶现出只有图片没有文字的情况】
这我就很苦恼了,开始各种排查,最终大概知道了,应该是OnPopulateMesh的问题,但是这该咋改……
一直到晚上,我睡觉前也在想这个问题,突然想到,我可以把覆写text的set方法,输入的时候就把文字替换成最终的结果。这样的话在底层逻辑下触发OnPopulateMesh,这总不该有问题了。

于是今天,我来到了公司,去git上找了UGUI2019的源码,看了下text的逻辑。把覆写的OnPopulateMesh方法注掉,在text的set方法里,对m_Text直接做正则处理。试了一下,倒是可以了。
正当我高兴的时候,我发现新的问题了。因为是在set方法做的处理,所以会对m_Text的值做彻底覆盖。这就意味着,如果是直接坐在预制体里的,原来的值就完全丢失了。

emmmm,只好再改回去。看来只能看一下OnPopulateMesh方法是哪里出问题了。我就在源码里继续看OnPopulateMesh的写法。发现和我从网上找的方法有很多不同。
我明白了,因为我复制来的方法是2017的,而现在的项目是2019的,所以底层逻辑是不同的。应该就是这个问题了。
但是新的担忧又来了,因为git上的是2019.1.05的,而我的项目是2019.4的,我不能确保代码是有效的。而且万一后面又要用2018或2020甚至2025那?
我只好重新梳理一下逻辑,发现OnPopulateMesh方法里调用了text,也就是说OnPopulateMesh覆写的核心就是获取将text修改为正确的即可。那我岂不是可以在text的get方法里做计算修改。
试了一下,注掉OnPopulateMesh方法,覆写text的get方法。
就成功了,好家伙,原来只需要改源码就行了。

废话真的结束了,开始搞~

正文

这里才是真的代码逻辑部分

首先说一下这个做法的逻辑。
1.当输入文本的时候,会触发SetVerticesDirty和SetLayoutDirty两个方法,通知底层当前的渲染信息脏了,然后就会主动触发OnPopulateMesh。
2.修改text的get方法,将本来应该返回的m_Text重新修正。但是不要修改m_Text自身,防止污染自身数据。
3.修正的逻辑就是先用正则将<icon=XXX>的部分移除,将其替换为合适数量的空格。再将图片的属性录入到列表中。
4.创建对应数量的Image,通过图片的属性修改显示。

然后说一下图文混排文本的写法

This script can show <icon name=xxx size=40 x=0 y=0/> and string.

在这里插入图片描述

脚本的私有变量

public class MixedPicText : Text
{
    //空格的编码(只读)
    private static readonly string replaceStr = "\u00A0";
    //图片部分的正则表达式(只读)
    private static readonly Regex imageTagRegex = new Regex(@"<icon name=([^>\s]+)([^>]*)/>");//(名字)(属性)
    //图片属性的正则表达式(只读)
    private static readonly Regex imageParaRegex = new Regex(@"(\w+)=([^\s]+)");//(key)=(value)
    //图片属性类列表
    private List<RichTextImageInfo> imageInfoList = new List<RichTextImageInfo>();
    //是否图片有变化的Dirty
    private bool isImageDirty = false;
    //顶点信息,是在计算图片位置时需要的
    private IList<UIVertex> verts = null;
    //当前已经创建的图片
    private List<RectTransform> showImageList = new List<RectTransform>();
}

图片的属性类

public class MixedPicTextImageInfo
{
    public string name;       //名字(路径)
    public Vector2 position;  //位置
    public int startVertex;   //起始顶点
    public int vertexLength;  //占据顶点数

    //标签属性
    public int size = 40;    //尺寸
    public float offsetX = 0f;  //X偏移
    public float offsetY = 0f;  //Y偏移

    public void SetValue(string key, string value)
    {
        switch (key)
        {
            case "size":
                {
                    int.TryParse(value, out size);
                    break;
                }
            case "x":
                {
                    float.TryParse(value, out offsetX);
                    break;
                }
            case "y":
                {
                    float.TryParse(value, out offsetY);
                    break;
                }
            default:
                break;
        }
    }
}

那么就来说一下拆解的方法

    protected string CalculateLayoutWithImage(string richText)
    {
    	//获取填充文本的生成设置
        Vector2 extents = rectTransform.rect.size;
        var settings = GetGenerationSettings(extents);
        //计算空格的宽度
        float unitsPerPixel = 1 / pixelsPerUnit;
        float spaceWidth = cachedTextGenerator.GetPreferredWidth(replaceStr, settings) * unitsPerPixel;

        //解析图片标签,并将标签替换为空格
        imageInfoList.Clear();
        Match match = null;
        //创建一个StringBuilder,用来存储替换后的字符串
        StringBuilder builder = new StringBuilder();
        while ((match = imageTagRegex.Match(richText)).Success)
        {
            //拆解正则数据
            MixedPicTextImageInfo imageInfo = new MixedPicTextImageInfo();
            imageInfo.name = match.Groups[1].Value;
            string paras = match.Groups[2].Value;
            if (!string.IsNullOrEmpty(paras))
            {
                //拆解图片的属性
                var keyValueCollection = imageParaRegex.Matches(paras);
                for (int i = 0; i < keyValueCollection.Count; i++)
                {
                    string key = keyValueCollection[i].Groups[1].Value;
                    string value = keyValueCollection[i].Groups[2].Value;
                    imageInfo.SetValue(key, value);
                }
            }
            imageInfo.startVertex = match.Index * 4;
            //占据几个空格 一般图片和文字间需要间距 所以多一个空格
            int num = Mathf.CeilToInt(imageInfo.size / spaceWidth) + 1;
            imageInfo.vertexLength = num * 4;
            imageInfoList.Add(imageInfo);
            
            //将字符串数据添加
            builder.Length = 0;
            builder.Append(richText, 0, match.Index);
            for (int i = 0; i < num; i++)
            {
                builder.Append(replaceStr);
            }
            builder.Append(richText, match.Index + match.Length, richText.Length - match.Index - match.Length);
            richText = builder.ToString();
        }

        //用新的文本来构成数据,获取新的顶点信息等
        cachedTextGenerator.Populate(richText, settings);
        verts = cachedTextGenerator.verts;
        int vertCount = verts.Count;

        //计算图片位置
        for (int i = imageInfoList.Count - 1; i >= 0; i--)
        {
            MixedPicTextImageInfo imageInfo = imageInfoList[i];

            int charIndex = imageInfo.startVertex / 4;
            string str = richText.Substring(0, charIndex);
            int newLine = str.Split('\n').Length - 1;
            int whiteSpace = str.Split(' ').Length - 1;
            int indexOfTextQuad = (charIndex * 4) - newLine * 4 - whiteSpace * 4;
            if (indexOfTextQuad < vertCount) {
                Vector2 pos = (verts[indexOfTextQuad].position +
                    verts[indexOfTextQuad + 1 + imageInfo.vertexLength - 4].position +
                    verts[indexOfTextQuad + 2 + imageInfo.vertexLength - 4].position +
                    verts[indexOfTextQuad + 3].position) / 4f;
                
                //计算位置的缩放,在Canvas的RenderMode为ScreenSpaceCamera的情况下,不同分辨率的位置修正
                float posScale = 1f;
#if UNITY_EDITOR
                if (Application.isPlaying)
                {
                	//todo UICanvas是我自己项目中的,就是获取canvas而已,根据自己需求来改
                    ---
                    posScale = UICanvas.Get().canvas.scaleFactor;
                    ---
                }
                else
                {
                    if (canvas != null)
                    {
                        posScale = canvas.scaleFactor;
                    }
                }
#else
                posScale = UICanvas.Get().canvas.scaleFactor;
#endif
                pos /= posScale;//适应不同分辨率的屏幕
                pos += new Vector2(imageInfo.offsetX + spaceWidth / 2, imageInfo.size * 0.3f + imageInfo.offsetY);

                imageInfo.position = pos;
            } else {
                imageInfoList.RemoveAt(i);
            }
        }

		//此时认为图片有变化
        isImageDirty = true;

        return richText;
    }

修正的字符串的方法好了,那么就可以开始实装了。

    public override string text {
        get
        {
        	//覆写text的get方法,直接返回修正后的字符串。不要动m_Text,会导致数据修改且不可逆
            return CalculateLayoutWithImage(m_Text);
        }
        set => base.text = value;
    }

接着来说一下生成图片。原文章里是放在Update里,我不知道为什么,也没有试一下放在别的地方,所以就先保持原样。

    protected void Update()
    {
        if (isImageDirty)
        {
            isImageDirty = false;

			//用池的方法来创建图片
            RectTransform imgTrans;
            Image imageComp;
            for (int i = 0; i < imageInfoList.Count; i++)
            {
                if (i < showImageList.Count)
                {
                    imgTrans = showImageList[i];
                    imgTrans.gameObject.SetActive(true);
                    imageComp = imgTrans.GetComponent<Image>();
                }
                else
                {
                    imgTrans = new GameObject("Image", typeof(RectTransform)).transform as RectTransform;
                    imgTrans.SetParent(transform);
                    imageComp = imgTrans.gameObject.AddComponent<Image>();
                    showImageList.Add(imgTrans);
                }

                MixedPicTextImageInfo imageInfo = imageInfoList[i];
                imgTrans.localScale = Vector3.one;
                
                //todo 这里直接使用自己项目中的加载图片的方法
				---
				imageComp.sprite = sprite;
				---
                
                imageComp.SetNativeSize();
                imageComp.raycastTarget = false;
                imgTrans.localScale = Vector3.one * imageInfo.size / imgTrans.rect.width;
                imgTrans.anchoredPosition = imageInfo.position;
            }

            for (int i = imageInfoList.Count; i < showImageList.Count; i++)
            {
                showImageList[i].gameObject.SetActive(false);
            }
        }
    }
}

最后还有两个覆写方法

	//Awake的时候会触发SetDirty,所以先将原有的数据都清理掉,防止污染
    protected override void Awake()
    {
        base.Awake();
        showImageList.Clear();
        for (int i = transform.childCount - 1; i >= 0; i--)
        {
#if UNITY_EDITOR
            if (Application.isPlaying)
            {
#endif
                Destroy(transform.GetChild(i).gameObject);
#if UNITY_EDITOR
            }
            else
            {
                DestroyImmediate(transform.GetChild(i).gameObject);
            }
#endif
        }
    }
	//这个方法是在编辑模式下的处理,不然打开预制体的时候,因为Awake的原因,图片被清理掉了。
    protected override void Start()
    {
        base.Start();
        OnPopulateMesh(new VertexHelper());
    }

结语

这个只是昨天修改的,实际上还会不会有别的问题,还要再验。
如果有新的问题,我会再来改的。
在这里插入图片描述

  • 1
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值