Unity3D UGUI 源码学习 Text

Text用于在UI中绘制文本标签。文字绘制是比较复杂的过程,Text和Unity中的很多其它类共同完成这一绘制过程。本文会围绕UGUI中的Text类并拓展到相关的其它类,以学习和了解文本是如何绘制出来的。

1
public class Text : MaskableGraphic, ILayoutElement

Text继承自MaskableGraphic,并实现接口ILayoutElement

绘制相关

继承自MaskableGraphic,覆写了基类绘制相关的一些方法,同样是分为几何和材质两部分:

几何

使用基类的UpdateGeometry()但覆写了OnPopulateMesh方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
readonly UIVertex[] m_TempVerts = new UIVertex[4];
protected override void OnPopulateMesh(VertexHelper toFill)
{
if (font == null)
return;

// We don't care if we the font Texture changes while we are doing our Update.
// The end result of cachedTextGenerator will be valid for this instance.
// Otherwise we can get issues like Case 619238.
m_DisableFontTextureRebuiltCallback = true;

Vector2 extents = rectTransform.rect.size;

var settings = GetGenerationSettings(extents);
cachedTextGenerator.PopulateWithErrors(text, settings, gameObject);

// Apply the offset to the vertices
IList<UIVertex> verts = cachedTextGenerator.verts;
float unitsPerPixel = 1 / pixelsPerUnit;
//Last 4 verts are always a new line... (\n)
int vertCount = verts.Count - 4;

Vector2 roundingOffset = new Vector2(verts[0].position.x, verts[0].position.y) * unitsPerPixel;
roundingOffset = PixelAdjustPoint(roundingOffset) - roundingOffset;
toFill.Clear();
if (roundingOffset != Vector2.zero)
{
for (int i = 0; i < vertCount; ++i)
{
int tempVertsIndex = i & 3;
m_TempVerts[tempVertsIndex] = verts[i];
m_TempVerts[tempVertsIndex].position *= unitsPerPixel;
m_TempVerts[tempVertsIndex].position.x += roundingOffset.x;
m_TempVerts[tempVertsIndex].position.y += roundingOffset.y;
if (tempVertsIndex == 3)
toFill.AddUIVertexQuad(m_TempVerts);
}
}
else
{
for (int i = 0; i < vertCount; ++i)
{
int tempVertsIndex = i & 3;
m_TempVerts[tempVertsIndex] = verts[i];
m_TempVerts[tempVertsIndex].position *= unitsPerPixel;
if (tempVertsIndex == 3)
toFill.AddUIVertexQuad(m_TempVerts);
}
}

m_DisableFontTextureRebuiltCallback = false;
}

在开始生成网格之前,首先会将m_DisableFontTextureRebuiltCallback置为true,即在此函数执行期间,禁止rebuild的回调。如果被调用了FontTextureChanged会直接返回。FontTextureChanged内会调用UpdateGeometry,增加一个状态判断避免循环调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void FontTextureChanged()
{
// Only invoke if we are not destroyed.
if (!this)
return;

if (m_DisableFontTextureRebuiltCallback)
return;

cachedTextGenerator.Invalidate();

if (!IsActive())
return;

// this is a bit hacky, but it is currently the
// cleanest solution....
// if we detect the font texture has changed and are in a rebuild loop
// we just regenerate the verts for the new UV's
if (CanvasUpdateRegistry.IsRebuildingGraphics() || CanvasUpdateRegistry.IsRebuildingLayout())
UpdateGeometry();
else
SetAllDirty();
}

FontTextureChanged方法是在是在FontUpdateTracker.RebuildForFont(...)中调用的。稍后会展开FontUpdateTracker这个类。

回到OnPopulateMesh,接下来会根据RectTransform的尺寸生成一份TextGenerationSettings

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public TextGenerationSettings GetGenerationSettings(Vector2 extents)
{
var settings = new TextGenerationSettings();

settings.generationExtents = extents;
if (font != null && font.dynamic)
{
settings.fontSize = m_FontData.fontSize;
settings.resizeTextMinSize = m_FontData.minSize;
settings.resizeTextMaxSize = m_FontData.maxSize;
}

// Other settings
settings.textAnchor = m_FontData.alignment;
settings.alignByGeometry = m_FontData.alignByGeometry;
settings.scaleFactor = pixelsPerUnit;
settings.color = color;
settings.font = font;
settings.pivot = rectTransform.pivot;
settings.richText = m_FontData.richText;
settings.lineSpacing = m_FontData.lineSpacing;
settings.fontStyle = m_FontData.fontStyle;
settings.resizeTextForBestFit = m_FontData.bestFit;
settings.updateBounds = false;
settings.horizontalOverflow = m_FontData.horizontalOverflow;
settings.verticalOverflow = m_FontData.verticalOverflow;

return settings;
}

实际上是把自己的m_FontData中的各个字段传给TextGenerationSettings。接下来把文本内容、文字设置传给cachedTextGenerator生成顶点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
cachedTextGenerator.PopulateWithErrors(text, settings, gameObject); 
`

这一过程的源码并没有在UGUI中。接着获取顶点信息,保存到`IList<UIVertex> verts `,供后续的步骤使用。判断是否需要计算偏移,取`verts`中的一个顶点(`verts[0]`),先计算其在`RectTransform`度量单位的坐标,保存在`roundingOffset `中,然后计算其`PixelAdjustPoint`之后的坐标。如果二者相等则无需偏移,否则偏移量为`roundingOffset`。最后是向`VertexHelper`写入顶点数据。在`verts`中存储的顶点坐标,每四个一组(对应一个字符)以`AddUIVertexQuad`的形式填入`VertexHelper`。遍历顶点列表`verts`时使用`i & 3`相当于`i`对4取模。

### 材质

更新材质调用`Graphic`的`UpdateMaterial() `,其中`Text`覆写了`mainTexture`属性:

​```c#
public override Texture mainTexture
{
get
{
if (font != null && font.material != null && font.material.mainTexture != null)
return font.material.mainTexture;

if (m_Material != null)
return m_Material.mainTexture;

return base.mainTexture;
}
}

font.material.mainTexture

其它相关类

可以看到在Text中都是一些比较浅层的逻辑,在其背后还有很多相关的类,以下是其中的一部分:

FontData

也在命名空间UnityEngine.UI中,记录的全部都是标签组件的相关信息,通过FontData.defaultFontData,我们可以看到其中的一些主要的字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static FontData defaultFontData
{
get
{
var fontData = new FontData
{
m_FontSize = 14,
m_LineSpacing = 1f,
m_FontStyle = FontStyle.Normal,
m_BestFit = false,
m_MinSize = 10,
m_MaxSize = 40,
m_Alignment = TextAnchor.UpperLeft,
m_HorizontalOverflow = HorizontalWrapMode.Wrap,
m_VerticalOverflow = VerticalWrapMode.Truncate,
m_RichText = true,
m_AlignByGeometry = false
};
return fontData;
}
}

当外部读取与设置Text的这些属性时,会对应读取与设置FontData的字段,例如以下是Text的字号属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public int fontSize
{
get
{
return m_FontData.fontSize;
}
set
{
if (m_FontData.fontSize == value)
return;
m_FontData.fontSize = value;

SetVerticesDirty();
SetLayoutDirty();
}
}

FontUpdateTracker

包含于命名空间UnityEngine.UI中,是用于更新和重建Text的工具类。主要有三个方法,增加、减少管理的Text,以及重建的回调RebuildForFont。重建的回调会添加给Font.textureRebuilt。其内部维护了一个字典,以Font为key,以使用该FontText组成的HashSet为值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public static class FontUpdateTracker
{
static Dictionary<Font, HashSet<Text>> m_Tracked = new Dictionary<Font, HashSet<Text>>();

public static void TrackText(Text t)
{
if (t.font == null)
return;

HashSet<Text> exists;
m_Tracked.TryGetValue(t.font, out exists);
if (exists == null)
{
// The textureRebuilt event is global for all fonts, so we add our delegate the first time we register *any* Text
if (m_Tracked.Count == 0)
Font.textureRebuilt += RebuildForFont;

exists = new HashSet<Text>();
m_Tracked.Add(t.font, exists);
}

if (!exists.Contains(t))
exists.Add(t);
}

private static void RebuildForFont(Font f)
{
HashSet<Text> texts;
m_Tracked.TryGetValue(f, out texts);

if (texts == null)
return;

foreach (var text in texts)
text.FontTextureChanged();
}

public static void UntrackText(Text t)
{
if (t.font == null)
return;

HashSet<Text> texts;
m_Tracked.TryGetValue(t.font, out texts);

if (texts == null)
return;

texts.Remove(t);

if (texts.Count == 0)
{
m_Tracked.Remove(t.font);

// There is a global textureRebuilt event for all fonts, so once the last Text reference goes away, remove our delegate
if (m_Tracked.Count == 0)
Font.textureRebuilt -= RebuildForFont;
}
}
}

TextGenerationSettings

是一个struct结构,没有在UGUI的源码中,暴露出来的公有属性和FontData几乎完全一样。存储文本的信息,提供给TextGenerator用以生成顶点。

TextGenerator

TextGenerator位于命名空间UnityEngine,UGUI中同样没有源码。在其它途径可以看到它的一些信息:

1
2
3
4
5
6
7
8
9
namespace UnityEngine
{
[UsedByNativeCode]
[StructLayout(LayoutKind.Sequential)]
public sealed class TextGenerator : IDisposable
{
// ...
}
}

前边调用过它的PopulateWithErrors()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public bool PopulateWithErrors(string str, TextGenerationSettings settings, GameObject context)
{
TextGenerationError textGenerationError = this.PopulateWithError(str, settings);
bool result;
if (textGenerationError == TextGenerationError.None)
{
result = true;
}
else
{
if ((textGenerationError & TextGenerationError.CustomSizeOnNonDynamicFont) != TextGenerationError.None)
{
Debug.LogErrorFormat(context, "Font '{0}' is not dynamic, which is required to override its size", new object[]
{
settings.font
});
}
if ((textGenerationError & TextGenerationError.CustomStyleOnNonDynamicFont) != TextGenerationError.None)
{
Debug.LogErrorFormat(context, "Font '{0}' is not dynamic, which is required to override its style", new object[]
{
settings.font
});
}
result = false;
}
return result;
}

里边层层判断和嵌套,最终会调用一个extern方法,其实现封装在动态库里:

1
2
3
[GeneratedByOldBindingsGenerator]
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern bool INTERNAL_CALL_Populate_Internal_cpp(TextGenerator self, string str, Font font, ref Color color, int fontSize, float scaleFactor, float lineSpacing, FontStyle style, bool richText, bool resizeTextForBestFit, int resizeTextMinSize, int resizeTextMaxSize, int verticalOverFlow, int horizontalOverflow, bool updateBounds, TextAnchor anchor, float extentsX, float extentsY, float pivotX, float pivotY, bool generateOutOfBounds, bool alignByGeometry, out uint error);

获取verts时会调用GetVertices()方法,最终也会层层判断和嵌套调用到动态库里的C++方法:

1
2
3
4
5
6
7
8
9
10
11
12
public IList<UIVertex> verts
{
get
{
if (!this.m_CachedVerts)
{
this.GetVertices(this.m_Verts);
this.m_CachedVerts = true;
}
return this.m_Verts;
}
}

Font

Font类位于命名空间UnityEngine

1
2
3
4
5
6
7
namespace UnityEngine
{
public sealed class Font : Object
{
// ...
}
}

在前边提到过Font.textureRebuilt事件,在Font的定义里可以看到对于textureRebuilt的增加和减少使用了Interlocked.CompareExchange的方法来确保操作的原子性(线程安全),非常巧妙:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static event Action<Font> textureRebuilt
{
add
{
Action<Font> action = Font.textureRebuilt;
Action<Font> action2;
do
{
action2 = action;
action = Interlocked.CompareExchange<Action<Font>>(ref Font.textureRebuilt, (Action<Font>)Delegate.Combine(action2, value), action);
}
while (action != action2);
}
remove
{
Action<Font> action = Font.textureRebuilt;
Action<Font> action2;
do
{
action2 = action;
action = Interlocked.CompareExchange<Action<Font>>(ref Font.textureRebuilt, (Action<Font>)Delegate.Remove(action2, value), action);
}
while (action != action2);
}
}

Fontmaterial属性的实现也都是extern,由封装好的动态库来完成:

1
2
3
4
5
6
7
8
9
public extern Material material
{
[GeneratedByOldBindingsGenerator]
[MethodImpl(MethodImplOptions.InternalCall)]
get;
[GeneratedByOldBindingsGenerator]
[MethodImpl(MethodImplOptions.InternalCall)]
set;
}

CharacterInfo

还有一个有关联的类,其内部存储的是字体的结点和字形信息:

1
2
3
4
5
6
7
8
namespace UnityEngine
{
[UsedByNativeCode]
public struct CharacterInfo
{
// ...
}
}

其包含的部分公有字段及属性如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
public int index;

public int size;

public FontStyle style;

public int advance {
get;
set;
}

public int glyphWidth {
get;
set;
}

public int glyphHeight {
get;
set;
}

public int bearing {
get;
set;
}

public int minY {
get;
set;
}

public int maxY {
get;
set;
}

public int minX {
get;
set;
}

public int maxX {
get;
set;
}

internal Vector2 uvBottomLeftUnFlipped {
get;
set;
}

internal Vector2 uvBottomRightUnFlipped {
get;
set;
}

internal Vector2 uvTopRightUnFlipped {
get;
set;
}

internal Vector2 uvTopLeftUnFlipped {
get;
set;
}

public Vector2 uvBottomLeft {
get;
set;
}

public Vector2 uvBottomRight {
get;
set;
}

public Vector2 uvTopRight {
get;
set;
}

public Vector2 uvTopLeft {
get;
set;
}

自动布局

除了绘制文本之外,Text还实现接口ILayoutElement,可作为自动布局元素,相比于基类主要是覆写了两个获取prefer的尺寸的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public virtual float preferredWidth
{
get
{
var settings = GetGenerationSettings(Vector2.zero);
return cachedTextGeneratorForLayout.GetPreferredWidth(m_Text, settings) / pixelsPerUnit;
}
}

public virtual float preferredHeight
{
get
{
var settings = GetGenerationSettings(new Vector2(GetPixelAdjustedRect().size.x, 0.0f));
return cachedTextGeneratorForLayout.GetPreferredHeight(m_Text, settings) / pixelsPerUnit;
}
}

使用的是之前提到过的GetGenerationSettings方法,获取一个TextGenerationSettings,并根据setting计算尺寸,然后转化为RectTransform单位并返回。


本系列其它文章详见Unity3D UGUI 源码学习

REFERENCE

https://docs.unity3d.com/ScriptReference/TextGenerator.html

https://msdn.microsoft.com/zh-cn/library/system.threading.interlocked.compareexchange.aspx