InputField是UGUI的重要组件,可以提供文本输入功能,是与用户交互的一个重要手段。我们可以在编辑器里,为OnValueChanged和OnEndEdit两个事件添加监听,这样就可以获得用户输入的文本。本文就探究一下InputField的实现原理。
在编辑器新建一个InputField,它会附带两个子对象Placeholder和Text。
Placeholder(占位符)是当输入文字为空时显示的提示文字,Text显示的便是输入文字。
我们先看一下InputField的继承关系:
public class InputField
: Selectable,
IUpdateSelectedHandler,
IBeginDragHandler,
IDragHandler,
IEndDragHandler,
IPointerClickHandler,
ISubmitHandler,
ICanvasElement
InputField重写了OnEnable(调用时机参见Untiy3D组件小贴士(一)OnEnabled与OnDisabled)方法,方法里设置了一些变量,然后调用了m_TextComponent的RegisterDirtyVerticesCallback方法两次,添加MarkGeometryAsDirty和UpdateLabel回调,最后再调用一下UpdateLabel。m_TextComponent对应的是Text对象的Text组件,而RegisterDirtyVerticesCallback则是Graphic的方法(Text间接继承自Graphic),当Graphic需要重建Mesh(将顶点数据设为脏的)的时候,会回调方法。MarkGeometryAsDirty方法将自己注册到CanvasUpdateRegistry的图像重建序列。
/// <summary>
/// Update the visual text Text.
/// </summary>
protected void UpdateLabel()
{
if (m_TextComponent != null && m_TextComponent.font != null && !m_PreventFontCallback)
{
// TextGenerator.Populate invokes a callback that's called for anything
// that needs to be updated when the data for that font has changed.
// This makes all Text components that use that font update their vertices.
// In turn, this makes the InputField that's associated with that Text component
// update its label by calling this UpdateLabel method.
// This is a recursive call we want to prevent, since it makes the InputField
// update based on font data that didn't yet finish executing, or alternatively
// hang on infinite recursion, depending on whether the cached value is cached
// before or after the calculation.
//
// This callback also occurs when assigning text to our Text component, i.e.,
// m_TextComponent.text = processed;
m_PreventFontCallback = true;
string fullText;
if (Input.compositionString.Length > 0)
fullText = text.Substring(0, m_CaretPosition) + Input.compositionString + text.Substring(m_CaretPosition);
else
fullText = text;
string processed;
if (inputType == InputType.Password)
processed = new string(asteriskChar, fullText.Length);
else
processed = fullText;
bool isEmpty = string.IsNullOrEmpty(fullText);
if (m_Placeholder != null)
m_Placeholder.enabled = isEmpty;
// If not currently editing the text, set the visible range to the whole text.
// The UpdateLabel method will then truncate it to the part that fits inside the Text area.
// We can't do this when text is being edited since it would discard the current scroll,
// which is defined by means of the m_DrawStart and m_DrawEnd indices.
if (!m_AllowInput)
{
m_DrawStart = 0;
m_DrawEnd = m_Text.Length;
}
if (!isEmpty)
{
// Determine what will actually fit into the given line
Vector2 extents = m_TextComponent.rectTransform.rect.size;
var settings = m_TextComponent.GetGenerationSettings(extents);
settings.generateOutOfBounds = true;
cachedInputTextGenerator.Populate(processed, settings);
SetDrawRangeToContainCaretPosition(caretSelectPositionInternal);
processed = processed.Substring(m_DrawStart, Mathf.Min(m_DrawEnd, processed.Length) - m_DrawStart);
SetCaretVisible();
}
m_TextComponent.text = processed;
MarkGeometryAsDirty();
m_PreventFontCallback = false;
}
}
UpdateLabel方法会为字符串添加光标,然后根据字符串是否为空来设置Placeholder是否显示。如果是密码格式,把字符改为*,接着调用TextGenerator的Populate生成Mesh数据,然后调用SetDrawRangeToContainCaretPosition方法,根据TextGenerator和caretPos(光标位置)来计算m_DrawStart和m_DrawEnd,并根据这两个值取字符串的子字符串,设置光标可见(使用协程闪烁)。最后把加工过的字符串赋值给m_TextComponent的text属性,并调用MarkGeometryAsDirty。
InputField重写OnDisable方法,方法停止了光标闪烁,反激活了输入区域,解除了m_TextComponent的两个回调,解除了CanvasUpdateRegistry的注册,设置m_CachedInputRenderer(绘制光标和选中区域)的mesh为null,并Destroy了m_Mesh(光标和选中区域的Mesh)。
OnPointerDown也被重写,将自己的对象设为EventSystem的选中对象(会调用对象上的OnSelect方法),如果自己已经被选中了,那么就设置光标的位置,并调用UpdateLabel更新文本。
OnSelect方法激活输入区域,OnDeselect反激活输入区域。
Selectable的DoStateTransition也被重写,会让点击过后的输入区域一直处于高亮状态,直到输入区域被反激活。
OnUpdateSelected继承自IUpdateSelectedHandler接口,输入模块切换到StandaloneInputModule时会被调用。
OnBeginDrag继承自IBeginDragHandler接口,设置m_UpdateDrag为true。
OnDrag继承自IDragHandler接口,会根据鼠标位置设置选中区域,如果拖拽事件在InputField之外,便开启协程调用MouseDragOutsideRect方法,根据方向不断移动选中区域,并调用UpdateLabel方法更新。m_UpdateDrag为false或者拖拽事件回到InputField里面时,便停止协程。
OnEndDrag继承自IEndDragHandler接口,设置m_UpdateDrag为false。
OnPointerClick继承自IPointerClick接口,调用ActivateInputField激活输入区域。
OnSubmit继承自ISubmit接口(当确认键按下),设置m_ShouldActivateNextUpdate为true,会在LateUpdate里激活输入区域。
Rebuild继承自ICanvasElement接口,CanvasUpdateRegistry重建图像是会回调Rebuild方法。方法里调用了UpdateGeometry方法。
ActivateInputField方法会激活m_Keyboard(触屏键盘),并设置text。设置
m_ShouldActivateNextUpdate为true。
DeactivateInputField会反激活m_Keyboard,并发送onEndEdit事件。然后调用MarkGeometryAsDirty等待重建。
UpdateGeometry方法会为InputField对象新建一个InputField Input Caret子对象,子对象添加了CanvasRenderer组件,用于显示光标或者选中区域。调用OnFillVBO方法为CanvasRenderer生成Mesh。
OnFillVBO会调用GenerateCursor生成光标Mesh,或者GenerateHightlight生成选中Mesh。GenerateCursor根据光标位置计算出四个顶点,GenerateHightlight根据选中区域的起始点和结束点计算出四个顶点。
LateUpdate方法会每一帧调用,在所有对象Update执行过后,如果m_ShouldActivateNextUpdate为true,便执行ActivateInputFieldInternal方法开启触屏键盘,并设置光标可见。如果m_Keyboard的输入文本与之前的不同,便会遍历字符根据内容类型(ContentType)判断输入是否合法。并根据文本长度限制裁剪字符串,然后发送onValueChange时间,并调用UpdateLabel方法更新文本。最后如果键盘结束输入,便会反激活输入区域。