前一阵子在做UI的时候遇到了这样的一个需求,是在一行里显示这样的东西:
而且我们这个项目要做多语言,也就是前边后边的文字都不一定是什么,有可能是中文有可能是英文有可能是俄语也有可能是泰文…(我们项目虽然不大但是真的设计了12种语言的适配= =)所以就不一定多长。 这时候,我一想,这很简单嘛,建立一个UItext,写文字,建立一个UIImage放图片,把image挂在text上,然后锚点设置为末尾对齐,不就行了吗,这样就可以动态的适应前后的大小了。-------但是,直到我切换到日文这个语言的时候!…他是这样的:
由于日文的语序问题,他这一句话要把宾语写在前边,是“8个星星 需要 解锁这一关”这样的顺序,这样就把文字拦腰截成两段,这我怎么适配呀,没法用锚点解决了,于是,只能搞一个通用的图文混排功能了…
由于本人是史上最菜程序员,纵使网上有大把大把的图文混排讲解,但是能看的懂得都费劲,找了半天终于找了个看得一知半解的,在此分享一下,也记录一下,如果能帮到一些人那就更好了
第一步 准备“图集”
这个图集就是把你要显示的图片整到一起,这个应该是只考虑一样大的图片,不一样大本菜鸡也不知道怎么办…比如我们把这几个图片(都是从网上找的)
命名为skill1-7 都是64x64的 我们按照他这个说明来做:
不想看英文的可以看这里:简单步骤就是要
1.你要有他这个EmojiBuilder这个脚本并且在editor目录下
2.然后你要把你的图片们放在Asset–>emoji–>input这个目录下
3.记得把图片属性的read/write enabled选上,要不然生成不出来
4.点击上边栏的emojiText–>build Emoji 即可在目录下生成这个“图集“啦,在目录emoji–>output下
这地方有一个坑,就是你每个图片的大小要和emojibuilder里面的这个属性对应,不然就生成出来奇奇怪怪的东西了
private const int EmojiSize = 64;//the size of emoji.
(我这图是64x64)
看下生成的东西
一个“图集”,一个txt,还有一个先不用管,很显然这个txt是记录这个图集里图片的位置和大小的
Name Key Frames X Y Size
{skill1} [0] 1 0 0 0.25
{skill2} [1] 1 0.25 0 0.25
{skill3} [2] 1 0.5 0 0.25
{skill4} [3] 1 0.75 0 0.25
{skill5} [4] 1 0 0.25 0.25
{skill6} [5] 1 0.25 0.25 0.25
{skill7} [6] 1 0.5 0.25 0.25
{skill8} [7] 1 0.75 0.25 0.25
frame是他用来做动态表情的,我先不看,其他的就是图片名字,索引{key},X,Y代表的是在这个“图集”上的UV坐标,(0,0)代表的是左下角 size0.25就代表了这个小图的长度是图集大小的四分之一那么大(这里一想,既然有这个size,那么想必不同尺寸的图打在一起应该也没啥问题的吧,嘿嘿,有空试试)
好了这时候第一步就完成了,聪明的同学看看那个emojibuiler脚本就都能明白他是怎么生成的了~
第二步:建立材质
他这个图文混排的思想主要就是 写一个叫“emojiText”类去继承UIText类,并且复写里面的方法,添加上自己的材质球,那么我们就来试试:
1.在unity里新建一个UIText,去掉Text(搞啥子,你这是要一杯全糖白开水,去掉糖)哈哈,其实就是个空物体,挂上他这个厉害的emojiText
2.看到这个组件,他和普通的text不同,他需要显示图,所以他需要一个带有图片资源的材质球,在这里
3.新建一个材质球“UGUIEmoji”,在他的shader里选择他的UI/EmojiFont这个shader(当然你得有这个shader啊)然后把之前生成的,“图集”,拖进去
(下边那个emojidata那个拖不拖都行,如果只是静态表情)
4.最后把这个材质球“UGUIIemoji”拖到组件“emojiText”所需要的material上就大功告成啦
顺利的话,到这里,你在text的输入框里输入XXXXX[0]XXXX就应该能在XXX文字中间显示中你图集里索引为0的图片了
示例:
那么请检查一下你的text所在的canvas
在这个选项里,一定要包含texcoord1这个东西,这个是代表纹理的第一组纹理坐标
这个问题难了我好一阵子…我的就是显示不出来呢(哭
好了 到这里使用说明完了(实际上都是一些废话,谁不会啊)
接下来我们看看他的核心代码emojiText,大概是怎么实现的:
其实这个脚本很简单,就这几个变量和一个函数
private const float ICON_SCALE_OF_DOUBLE_SYMBOLE = 0.7f;
public override float preferredWidth => cachedTextGeneratorForLayout.GetPreferredWidth(emojiText, GetGenerationSettings(rectTransform.rect.size)) / pixelsPerUnit;
public override float preferredHeight => cachedTextGeneratorForLayout.GetPreferredHeight(emojiText, GetGenerationSettings(rectTransform.rect.size)) / pixelsPerUnit;
private string emojiText => Regex.Replace(text, "\\[[a-z0-9A-Z]+\\]", "%%");
private static Dictionary m_EmojiIndexDict = null;
readonly UIVertex[] m_TempVerts = new UIVertex[4];
struct EmojiInfo
{
public float x;
public float y;
public float size;
}
好,我们一点一点看,我们先看看变量声明部分
ICON_SCALE_OF_DOUBLE_SYMBOLE这个用来控制单个图的大小,他实际上代表的是一个“%”的大小,因为他的流程是先把你程序里的【0】,【1】这样的输入转化成两个%来占位,比如,本菜鸡使用了技能【1】,在他的第一步处理后就变成了:本菜鸡使用了技能%%,然后之后再在%%所在的位置显示图片,这样%的大小就影响了图片的大小,我们设置成0.7,那么两个%就是1.4,也就是说一个小图占大概1.4倍的文字大小,好像害挺合适的。
然后我们先看emojiText:这个好理解 就是我们之前说的把输入的text用正则表达式把其中【X】格式的东西替换成%%,emojiText就是替换后的字符串
preferredWidth 这个,这句真的长…这个变量是他override的也就是说他原来在text就有,经过查阅他就是原来这个text所实际占用的宽度。其中cachedTextGeneratorForLayout.GetPreferredWidth()这个函数,我们可以理解为输入字符串内容和这个文本框的配置参数,他给你输出这个文本渲染之后所占的大小(这里指宽)。而后边还除以了pixelsPerUnit这个东西,顾名思义这个应该就是代表“每个unit单位有多少个像素”那么,perferred=X/pixelsPerUnit
讲道理 X的单位就应该是像素,然后除以每个unit单位有多少像素,得出的perferredWidth单位就应该是unit单位,这里我不确定,只是个人理解
高度同理。
下边的字典存储表情的位置,还有一个方便记录表情的结构体,这些都好说我就不废话了,UIVertex是记录定点的一个类我们等下再说。
接下来看这个函数
protected override void OnPopulateMesh(VertexHelper toFill)
这个函数应该就是uitext的核心函数了,参数是一个vertexHelper的东西,这个东西具体是啥本菜鸡也不了解,反正一定包含了你写入text的文字信息。然后呢,这个函数的功能就是把你写入的字符串信息渲染成Mesh,这样才能显示在屏幕上每次输入有变化,或者text的设置有变化,都会自动执行一遍这个函数来刷新,现在他就开始复写这个函数
if (font == null)
{
return;
}
if (m_EmojiIndexDict == null)
{
m_EmojiIndexDict = new Dictionary();
//load emoji data, and you can overwrite this segment code base on your project.
TextAsset emojiContent = Resources.Load("emoji");
string[] lines = emojiContent.text.Split('\n');
for (int i = 1; i < lines.Length; i++)
{
if (!string.IsNullOrEmpty(lines[i]))
{
string[] strs = lines[i].Split('\t');
EmojiInfo info;
info.x = float.Parse(strs[3]);
info.y = float.Parse(strs[4]);
info.size = float.Parse(strs[5]);
m_EmojiIndexDict.Add(strs[1], info);
}
}
}
这段代码非常好理解 就是把之前生成的emoji.txt表格读取到m_EmojiIndexDict这个字典里,key值是序号,value是emoji结构体。
接下来:
if (supportRichText)
{
int nParcedCount = 0;
int nOffset = 0;
MatchCollection matches = Regex.Matches(text, "\\[[a-z0-9A-Z]+\\]");
for (int i = 0; i < matches.Count; i++)
{
EmojiInfo info;
if (m_EmojiIndexDict.TryGetValue(matches[i].Value, out info))
{
emojiDic.Add(matches[i].Index - nOffset + nParcedCount, info);
nOffset += matches[i].Length - 1;
nParcedCount++;
}
}
}
这部分计算有点绕,但是你要看清核心,他就是要建立一个字典 emojiDic,这个字典存储的key是这个图在输入字符串中的序列value还是emojiInfo信息
简单来说因为你字符串中带[0],[1]的都在之前替换成%%了,那么你怎么知道遇到%%该用哪个图片呢?这个字典就是干这个的,这里面具体计算可以这样理解:
matchs[i].index代表这个匹配到的词组在字符串中的序列,比如caijixibibi[1]
那么这个[1]的index就是(数一下)第十三个,可是别忘了数组是从0开始,所以他的index就是12,打印一下log,他确实是12,这验证了我的想法,那不就直接得到这个图在文字中的序列了嘛,
但是你看,如果是有多个,比如caijixiabibi[1]tuwenhunpai[2][1]这个匹配的index依然是12,而[2]的序列是25 可是字符串经过替换实际进入渲染的参数是**caijixiabibi%%tuwenhunpai%%**如果依然按照之前的直接用index来看,第一个参数12,表示我要在第13个字符开始渲染一个图,长度是两个%,这正好把%%渲染成图了,但是第二个我要把第26个渲染成图,数一下,就卡到了第二个百分号那个位置,渲染出来的就是这样
第一个位置正确,第二个图往后错位了一个,那么此时聪明的同学已经大概懂了,他程序里的nOffset和nParcedCount就是调整这些“偏差”的。【X】转化成%%,他的长度始终是要先减去X的长度,因为X不一定是几,如果你图很多,文本里有[256]也不是不可能的,这时候你下一个图的index就会往后延了两个单位,所以要用变量nOffset记录下偏移,[256]长度是5,比平时一个字符多出来4个,所以要减去lengh-1(5-1) 但是我转化完成的也是%%两个字符,所以还要+1,这样综合起来就计算出来一个偏差值,累计到下次循环,和下一个match的index相加,就得到了真正要渲染的时候图所在的序列(emmmm…我也不知道说清楚了没有…- -||)
接下来这些就是一些获取text的配置的代码啦,关于获得文本框的设置(里面包含字号,字体什么的),对齐方式,锚点,我就不细说了,其中主要这句
**cachedTextGenerator.Populate(emojiText, settings);**
这句就是把我们之前说的转化好的字符串真正去渲染的操作了
他渲染之后的结果在哪呢?在下边,
IList verts = cachedTextGenerator.verts;
渲染结果当然是mesh,是网格,是一个一个顶点组成的,此时这里的verts就是渲染好的顶点信息了,我们创建了一个list来接收他
接下来
抱歉这里我实在没太理解,关于像素单位和unit单位之间的转换(呜呜呜呜我就是个菜鸡),查了pixelAdjustpoint这个函数
结果还是没明白他这个prefect adjust到底是怎么的“合适”的 求求大家教教我这个菜鸡。。。。反正这段应该是类似于消除偏差,像素取整这种作用。。?(maybe 多的我看不出来了
接下来这一大段就是最后的绘制了,if里面是绘制图片 下边的else是绘制文字
绘制图片,主要思路就是每四个顶点一个循环,因为一个tempVerts是存储4个顶点的一个结构
然后对tempVerts的每个元素进行赋值,这个赋值分两部分,第一部分是赋值位置坐标,第二部分是赋值UV坐标,也就是给tempVerts.position赋值的那些操作(当然还有前边的计算,一些零零散散的偏差我就不说了)。这里注意的是这几句
m_TempVerts[3] = verts[i];//1
m_TempVerts[2] = verts[i + 1];//2
m_TempVerts[1] = verts[i + 2];//3
m_TempVerts[0] = verts[i + 3];//4
他的数组的顺序和后边verts的顺序是反的,这个是为啥呢。
我们先看vert的坐标,我打了几个log,把vert0-3输出出来,发现是这样子的
vert 0,1,2,3 对应的是图中 A,B,C,D 也就是该字符的左上,右上,右下,左下位置,是一个顺时针顺序。
然后我们在查查 AddUIVertexQuad(m_TempVerts);发现这个函数需要传入的顶端顺序是D–>C---->B—>A 这正好是反过来 逆时针的,于是之前顶点赋值的时候一定要反过来
之后就是UV坐标,就是
m_TempVerts[0].uv1 = new Vector2(emojiDic[index].x + pixelOffset, emojiDic[index].y + pixelOffset);
m_TempVerts[1].uv1 = new Vector2(emojiDic[index].x - pixelOffset + emojiDic[index].size, emojiDic[index].y + pixelOffset);
m_TempVerts[2].uv1 = new Vector2(emojiDic[index].x - pixelOffset + emojiDic[index].size, emojiDic[index].y - pixelOffset + emojiDic[index].size);
m_TempVerts[3].uv1 = new Vector2(emojiDic[index].x + pixelOffset, emojiDic[index].y - pixelOffset + emojiDic[index].size);
这段,这里的uv1就是前边说过的在canvas里要包含的第一套纹理坐标
后边是根据图片字典里的数据得出图片所在的位置,大小,这样就得到他的uv信息啦,说白了就是position是为了让他知道要在哪里画,uv是为了让他知道画什么。
shader的部分有空再细说吧(写了好多…
大体就是这些了,头一次写博客,好长啊…好累…我是史上最菜程序员,祝大家bug少少,头发多多