前言

Text 是 UGUI 中最常用的组件之一了,用于显示文本。无论是 UI 界面还是对话框,都会用到 Text

image.png

可以看到,UGUI 渲染文字的方式与图片类似,一个基本的字符也是由四个顶点及分割出的两个三角形组成的。

Image 类似,Text 也继承自 MaskableGraphic,并且实现了 ILayoutElement 接口。虽然显示文字的功能非常复杂,但 Text 本身却并不涉及到底层的处理,而是把功能全都交给了一个名为 TextGenerator 的类,因此 Text 的代码量并不大。

Text 的属性

1
2
3
[SerializeField] private FontData m_FontData = FontData.defaultFontData;

[TextArea(3, 10)][SerializeField] protected string m_Text = String.Empty;

m_FontData 是一个 FontData 类型的变量,用于存储字体的属性,Text 中的大部分属性都是根据 m_FontData 来设置的。m_Text 是一个字符串,用于存储要显示的文本。

image.png

Text 有非常多的属性可以设置,如字体、字体大小、对齐方式等。为了方便管理,UGUI 使用了 FontData 类来管理这些属性。

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
public class FontData : ISerializationCallbackReceiver
{
private Font m_Font;

private int m_FontSize;

private FontStyle m_FontStyle;

private bool m_BestFit;

private int m_MinSize;

private int m_MaxSize;

private TextAnchor m_Alignment;

private bool m_AlignByGeometry;

private bool m_RichText;

private HorizontalWrapMode m_HorizontalOverflow;

private VerticalWrapMode m_VerticalOverflow;

private float m_LineSpacing;

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;
}
}

// ...
}

defaultFontData 用于返回一个默认的 FontData 对象,这个对象的属性都是默认值。

1
2
3
4
5
6
7
8
9
10
11
12
private TextGenerator m_TextCache;
private TextGenerator m_TextCacheForLayout;

public TextGenerator cachedTextGenerator
{
get { return m_TextCache ?? (m_TextCache = (m_Text.Length != 0 ? new TextGenerator(m_Text.Length) : new TextGenerator())); }
}

public TextGenerator cachedTextGeneratorForLayout
{
get { return m_TextCacheForLayout ?? (m_TextCacheForLayout = new TextGenerator()); }
}

Text 维护了两个 TextGenerator 对象,分别用于显示与布局文本。根据 fontData 生成文本的艰巨任务就是由它们完成的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// <summary>
/// Text's texture comes from the font.
/// </summary>
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;
}
}

mainTextureGraphic 中定义的交给 CanvasRenderer 使用的纹理。在 Text 中,mainTexture 被指定为字体的纹理。

渲染过程方法的重写

Text 重写了 Graphic 中的 UpdateGeometry() 以及 OnPopulateMesh() 方法,用于生成文本的顶点信息。但对于 UpdateMaterial() 方法并没有做额外的处理,因为 Text 仅需要将字体的纹理提交给 CanvasRenderer 即可。

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
protected override void UpdateGeometry()
{
if (font != null)
{
base.UpdateGeometry();
}
}


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;
int vertCount = verts.Count;

// We have no verts to process just return (case 1037923)
if (vertCount <= 0)
{
toFill.Clear();
return;
}

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;
}

OnPopulateMesh() 方法中,最核心的部分依然由 TextGenerator 完成:

1
2
3
4
5
var settings = GetGenerationSettings(extents);
cachedTextGenerator.PopulateWithErrors(text, settings, gameObject);

// Apply the offset to the vertices
IList<UIVertex> verts = cachedTextGenerator.verts;

Text 做的事情仅仅是将 settings 提交给 TextGenerator,便可以得到生成的文本顶点信息。后续便以四个顶点为一组,向 VertexHelper ( toFill ) 中添加顶点信息。这里会额外计算 roundingOffset,用于处理顶点的像素对齐。

ILayoutElement 接口的实现

Image 相同,Text 也实现了 ILayoutElement 接口,因此可以参与到布局系统的计算中。

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
public virtual void CalculateLayoutInputHorizontal() {}
public virtual void CalculateLayoutInputVertical() {}

public virtual float minWidth
{
get { return 0; }
}

public virtual float preferredWidth
{
get
{
var settings = GetGenerationSettings(Vector2.zero);
return cachedTextGeneratorForLayout.GetPreferredWidth(m_Text, settings) / pixelsPerUnit;
}
}

public virtual float flexibleWidth { get { return -1; } }

public virtual float minHeight
{
get { return 0; }
}

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

public virtual float flexibleHeight { get { return -1; } }

public virtual int layoutPriority { get { return 0; } }

在获取 preferredWidthpreferredHeight 时,Text 会根据 TextGenerator 生成的文本信息来计算。这里的 GetGenerationSettings() 方法用于获取 TextGenerator 的设置,GetPixelAdjustedRect() 方法用于获取 Text 的像素调整后的矩形区域。

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;
}

GetGenerationSettings() 方法中,可以看到每一次调用都会 new 一个 TextGenerationSettings 对象,或许有优化的空间?

FontUpdateTracker

FontUpdateTracker 是一个辅助类,用于在 font atlas 更新时 rebuild 所有注册进来的 Text 对象。

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
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);
}
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;
}
}
}

首先,类中定义了一个 Dictionary<Font, HashSet<Text>>,用于分别储存每种字体对应的文本对象。在 TrackText() 方法中,会将 Text 对象注册进 m_Tracked 中。当字体的纹理更新时,会调用 RebuildForFont() 方法,遍历所有注册进来的 Text 对象,调用其 FontTextureChanged() 方法。

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
// Text.cs

/// <summary>
/// Called by the FontUpdateTracker when the texture associated with a font is modified.
/// </summary>
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();
}
}

Text 中也可以看到,FontTextureChanged() 的作用就是重新渲染一次文本。

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
public Font font
{
get
{
return m_FontData.font;
}
set
{
if (m_FontData.font == value)
return;

FontUpdateTracker.UntrackText(this);

m_FontData.font = value;

FontUpdateTracker.TrackText(this);

#if UNITY_EDITOR
// needed to track font changes from the inspector
m_LastTrackedFont = value;
#endif

SetAllDirty();
}
}

protected override void OnEnable()
{
base.OnEnable();
cachedTextGenerator.Invalidate();
FontUpdateTracker.TrackText(this);
}

protected override void OnDisable()
{
FontUpdateTracker.UntrackText(this);
base.OnDisable();
}

TextOnEnable()OnDisable() 方法中,会调用 FontUpdateTrackerTrackText()UntrackText() 方法,用于注册和注销 Text 对象。此外,当字体发生变化时,也会调用 FontUpdateTracker 的方法。

总结

Text 是 UGUI 中最常用的组件之一,用于显示文本。Text 的渲染方式也是为每一个字符生成四个顶点,随后通过 VertexHelper 提交给 CanvasRenderer。但 Text 本身并不直接处理文本的生成,而是通过 TextGenerator 利用 FontData 来生成指定属性的文本。Text 也实现了 ILayoutElement 接口,可以参与到布局系统的计算中。