好久没有来更新了,甚至我的账号都掉了,试了半天才搞对密码。
最近也真的是很忙,甚至上周日差点加班,幸好……
废话不多说,开始了!!!
废话
首先说明,这个大标题都是废话,可以跳过。
图文混排,这是一个项目中最常见的功能了。没错,我们的项目也有这个需求,幸运的是,我不会呀!要不也不会来写这片文章了。
不光不会,我甚至觉得这东西贼麻烦,想尽量往后拖。然而他们给我安排的时间实在是太多了,不弄不行了。
于是开始百度一下别人写好的东西。
看到第一种,把要需要的图片打个图集,然后用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());
}
结语
这个只是昨天修改的,实际上还会不会有别的问题,还要再验。
如果有新的问题,我会再来改的。