文章目录
-
- [0. 效果预览](#0. 效果预览)
- [1. 需求分析](#1. 需求分析)
- [2. Hierarchy 搭建](#2. Hierarchy 搭建)
- [3. 核心组件配置](#3. 核心组件配置)
-
- [3.1 TMP_InputField 组件面板](#3.1 TMP_InputField 组件面板)
- [3.2 Content Type 详解](#3.2 Content Type 详解)
- [3.3 Line Type 三种模式](#3.3 Line Type 三种模式)
- [3.4 RectTransform 建议](#3.4 RectTransform 建议)
- [4. 完整代码](#4. 完整代码)
-
- [4.1 InputFieldDemo.cs](#4.1 InputFieldDemo.cs)
- [4.2 聊天输入的进阶:Shift+Enter 换行](#4.2 聊天输入的进阶:Shift+Enter 换行)
- [5. 使用方法](#5. 使用方法)
-
- [5.1 基础 InputField](#5.1 基础 InputField)
- [5.2 密码输入框](#5.2 密码输入框)
- [5.3 聊天输入框](#5.3 聊天输入框)
- [6. 参数说明](#6. 参数说明)
-
- [InputFieldDemo.cs 可调参数](#InputFieldDemo.cs 可调参数)
- [7. 变体与扩展](#7. 变体与扩展)
-
- [7.1 正则表达式校验](#7.1 正则表达式校验)
- [7.2 数值输入 + 范围限制](#7.2 数值输入 + 范围限制)
- [7.3 输入框聚焦时弹出自定义面板](#7.3 输入框聚焦时弹出自定义面板)
- [8. 常见问题](#8. 常见问题)
- [9. 附:遇到 Legacy InputField 怎么迁移](#9. 附:遇到 Legacy InputField 怎么迁移)
- [10. 性能 / 适配建议](#10. 性能 / 适配建议)
0. 效果预览


InputField 是 UGUI 的文本输入组件,覆盖登录注册、聊天输入、搜索栏、数值编辑等场景。看似简单,但移动端键盘适配、输入校验、TMP 版差异这些坑,踩一次就够喝一壶。
1. 需求分析
核心思路:InputField = 一个可编辑的 Text + 光标 + 选中高亮 + 事件回调,搞清楚它的组成结构和事件时机就够应付 90% 的场景。
典型使用场景:
- 登录/注册界面的账号密码输入
- 游戏内聊天输入栏
- 搜索框 / 筛选框
- Inspector 风格的数值编辑字段
- 控制台 / 指令输入
需要实现的功能点:
- 单行输入与多行输入
- 密码遮挡模式
- 输入类型限制(纯数字、字母、自定义正则)
- 字符数量限制
- 占位文本(Placeholder)
- 事件回调:内容变化、提交、选中/失焦
- 移动端软键盘适配
前置知识:建议先阅读本系列 文章 1(总领篇),了解 Canvas、RectTransform、EventSystem 的基本概念。
2. Hierarchy 搭建
原生 InputField 和 TMP 版的层级略有不同,这里以推荐的 TMP_InputField 为准:
Canvas
└── InputField (TMP) ← Image(背景)+ TMP_InputField 组件
└── Text Area(RectMask2D) ← 裁剪区域,防止文字溢出输入框
├── Placeholder(TMP_Text) ← 占位提示文字
└── Tmp_Content(TMP_Text) ← 实际输入内容
-
InputField (TMP) :父对象,挂
Image(背景)和TMP_InputField组件 -
Text Area :挂
RectMask2D,作用是裁剪超出输入框范围的文字。输入内容很长时,文字只在这个区域内可见,不会画到外面去。不要删除这个组件。Q:为什么原生 InputField 没有 RectMask2D 也不会溢出? A:两者的裁剪机制不同。 -- 原生 Text 组件在生成网格阶段就按 RectTransform 范围裁掉了超出的字符------顶点根本没生成,自然画不出去。 -- 而 TMP 会把所有字符的网格都生成出来,裁剪交给渲染阶段处理,所以需要 RectMask2D在 Shader 层面丢弃超出区域的像素。代价是多一个组件,好处是能支持更灵活的文字滚动和溢出省略号等高级功能。 -
Placeholder:占位提示文字,输入内容为空时显示(如"请输入用户名...")
-
Tmp_Content:真正显示用户输入内容的文本组件

快捷创建:在 Hierarchy 右键 → UI → InputField - TextMeshPro,Unity 会自动帮你搭好这套结构(含 Text Area)。原生版选 UI → InputField,层级少一层 Text Area,但渲染质量不如 TMP。
3. 核心组件配置
3.1 TMP_InputField 组件面板

| 参数 | 说明 |
|---|---|
| Text Component | 拖入子对象 Tmp_Content(TMP_Text),用于显示输入内容 |
| Text | 当前输入内容(运行时读写) |
| Character Limit | 最大字符数,0 = 不限制 |
| Content Type | 输入类型预设(Standard / Integer / Decimal / Alphanumeric / Password 等) |
| Line Type | 单行 / 多行自动换行 / 多行带回车提交 |
| Placeholder | 拖入子对象 Placeholder(TMP_Text) |
| Caret Blink Rate | 光标闪烁频率(0 = 不闪) |
| Caret Width | 光标像素宽度 |
| Caret Color | 光标颜色 |
| Selection Color | 选中文字的高亮背景色 |
| Hide Mobile Input | 移动端是否隐藏原生输入框(iOS 顶部横条) |
| Rich Text | 是否支持富文本标签(TMP 独有) |
| Read Only | 只读模式,可选中复制但不能编辑 |
3.2 Content Type 详解
Content Type 是最常用的配置项,它本质上是一组预设,控制了三个底层属性的组合:
| Content Type | Input Type | Keyboard Type | Character Validation |
|---|---|---|---|
| Standard | Standard | Default | None |
| Autocorrected | AutoCorrect | Default | None |
| Integer Number | Standard | NumberPad | Integer |
| Decimal Number | Standard | NumbersAndPunctuation | Decimal |
| Alphanumeric | Standard | Default | Alphanumeric |
| Name | Standard | Default | Name(首字母自动大写) |
| Email Address | Standard | EmailAddress | EmailAddress |
| Password | Password | Default | None |
| Pin | Password | NumberPad | Integer |
| Custom | 手动选 | 手动选 | 手动选 |
选 Custom 的时机:当预设不满足需求时------比如你想要"数字键盘 + 密码遮挡",就得选 Custom 自己组合。
3.3 Line Type 三种模式
| Line Type | 行为 | 回车键效果 |
|---|---|---|
| Single Line | 只能输入一行,回车 = 提交 | 触发 onEndEdit / onSubmit |
| Multi Line Submit | 可以显示多行,但回车仍 = 提交 | 触发 onEndEdit / onSubmit |
| Multi Line Newline | 回车 = 换行,不会触发提交 | 插入 \n |
聊天输入框通常用 Single Line 或 Multi Line Submit (回车发送消息)。备注/日志输入框用 Multi Line Newline。
3.4 RectTransform 建议
| 对象 | 锚点 | 建议尺寸 |
|---|---|---|
| InputField (TMP) | 按需求定位 | 宽 200-400,高 30-40(单行) |
| Text Area | Stretch All(四角拉满) | 跟随父对象,四边各留 10px padding |
| Placeholder | Stretch All(四角拉满) | 跟随 Text Area |
| Tmp_Content | Stretch All(四角拉满) | 跟随 Text Area |
Text Area 的 RectTransform 四边留 10px padding(通过 Left/Right/Top/Bottom 偏移实现),Placeholder 和 Tmp_Content 拉满 Text Area 即可。
4. 完整代码
以下脚本演示 InputField 的常用操作:事件监听、输入校验、密码框切换、聊天提交。
4.1 InputFieldDemo.cs
csharp
using UnityEngine;
using UnityEngine.UI; // Toggle 仍在此命名空间
using TMPro; // TMP_InputField、TMP_Text
/// <summary>
/// TMP_InputField 基础用法演示:事件监听、字符校验、密码框切换
/// </summary>
public class InputFieldDemo : MonoBehaviour
{
// ===== Inspector 引用 =====
[Header("输入框引用")]
[SerializeField] private TMP_InputField usernameInput; // 用户名输入框
[SerializeField] private TMP_InputField passwordInput; // 密码输入框
[SerializeField] private TMP_InputField chatInput; // 聊天输入框
[Header("UI 反馈")]
[SerializeField] private TMP_Text feedbackText; // 校验提示文本
[SerializeField] private Toggle showPasswordToggle; // 显示/隐藏密码的 Toggle
[Header("校验规则")]
[SerializeField] private int minUsernameLength = 3; // 用户名最短长度
[SerializeField] private int maxUsernameLength = 16; // 用户名最长长度
// ===== 生命周期 =====
private void Start()
{
// 监听用户名输入变化------每输入一个字符都会触发
usernameInput.onValueChanged.AddListener(OnUsernameChanged);
// 监听用户名输入结束(失焦或回车)
usernameInput.onEndEdit.AddListener(OnUsernameEndEdit);
// 监听聊天输入框提交(回车)
chatInput.onEndEdit.AddListener(OnChatSubmit);
// 监听密码显示/隐藏切换
if (showPasswordToggle != null)
showPasswordToggle.onValueChanged.AddListener(OnTogglePasswordVisibility);
// 自定义字符校验:用户名只允许字母、数字、下划线
usernameInput.onValidateInput += ValidateUsernameChar;
// 设置初始状态
SetupInputFields();
}
private void OnDestroy()
{
// 移除监听,防止内存泄漏
usernameInput.onValueChanged.RemoveListener(OnUsernameChanged);
usernameInput.onEndEdit.RemoveListener(OnUsernameEndEdit);
chatInput.onEndEdit.RemoveListener(OnChatSubmit);
if (showPasswordToggle != null)
showPasswordToggle.onValueChanged.RemoveListener(OnTogglePasswordVisibility);
usernameInput.onValidateInput -= ValidateUsernameChar;
}
// ===== 初始化 =====
private void SetupInputFields()
{
// 用户名:字母数字,限制长度
usernameInput.characterLimit = maxUsernameLength;
usernameInput.contentType = TMP_InputField.ContentType.Custom;
usernameInput.inputType = TMP_InputField.InputType.Standard;
usernameInput.keyboardType = TouchScreenKeyboardType.Default;
// 密码:默认遮挡模式
passwordInput.contentType = TMP_InputField.ContentType.Password;
passwordInput.characterLimit = 20;
// 聊天:单行,回车提交
chatInput.lineType = TMP_InputField.LineType.SingleLine;
}
// ===== 事件回调 =====
/// <summary>
/// 用户名每次输入变化时的实时反馈
/// </summary>
private void OnUsernameChanged(string value)
{
if (value.Length < minUsernameLength)
{
feedbackText.text = $"至少输入 {minUsernameLength} 个字符";
feedbackText.color = Color.yellow;
}
else
{
feedbackText.text = "✓ 用户名格式正确";
feedbackText.color = Color.green;
}
}
/// <summary>
/// 用户名输入结束时的完整校验
/// </summary>
private void OnUsernameEndEdit(string value)
{
// 注意:按 Esc 取消也会触发 onEndEdit
// 通过 wasCanceled 区分是提交还是取消
if (usernameInput.wasCanceled)
{
Debug.Log("用户取消了输入");
return;
}
if (value.Length < minUsernameLength)
{
feedbackText.text = $"用户名至少 {minUsernameLength} 个字符";
feedbackText.color = Color.red;
}
else
{
Debug.Log($"用户名确认: {value}");
}
}
/// <summary>
/// 聊天输入框回车提交
/// </summary>
private void OnChatSubmit(string message)
{
// 忽略空消息和取消操作
if (chatInput.wasCanceled || string.IsNullOrWhiteSpace(message))
return;
Debug.Log($"发送消息: {message}");
// 清空输入框并重新聚焦,方便连续输入
chatInput.text = "";
chatInput.ActivateInputField();
}
/// <summary>
/// 切换密码可见性
/// </summary>
private void OnTogglePasswordVisibility(bool showPassword)
{
// 记住当前内容
string currentText = passwordInput.text;
if (showPassword)
{
// 明文显示
passwordInput.contentType = TMP_InputField.ContentType.Standard;
}
else
{
// 密码遮挡
passwordInput.contentType = TMP_InputField.ContentType.Password;
}
// 切换 contentType 后必须调用 ForceLabelUpdate 才能立即刷新显示
passwordInput.text = currentText;
passwordInput.ForceLabelUpdate();
}
// ===== 自定义字符校验 =====
/// <summary>
/// 逐字符校验:只允许字母、数字、下划线
/// 返回 '\0' 表示拒绝该字符
/// </summary>
private char ValidateUsernameChar(string text, int charIndex, char addedChar)
{
// 字母
if (char.IsLetterOrDigit(addedChar))
return addedChar;
// 下划线
if (addedChar == '_')
return addedChar;
// 其他字符一律拒绝
return '\0';
}
}
4.2 聊天输入的进阶:Shift+Enter 换行
如果希望聊天输入框支持 Shift+Enter 换行、单独 Enter 发送,需要自己处理按键逻辑:
csharp
using UnityEngine;
using UnityEngine.EventSystems;
using TMPro;
/// <summary>
/// 聊天输入框:Enter 发送,Shift+Enter 换行
/// </summary>
public class ChatInputHandler : MonoBehaviour, ISubmitHandler
{
[SerializeField] private TMP_InputField chatInput;
private void Start()
{
// 多行模式才能插入换行符
chatInput.lineType = TMP_InputField.LineType.MultiLineNewline;
}
private void Update()
{
// 只在输入框聚焦时处理
if (!chatInput.isFocused) return;
// Enter 按下(不含 Shift)= 发送
if (Input.GetKeyDown(KeyCode.Return) || Input.GetKeyDown(KeyCode.KeypadEnter))
{
bool shiftHeld = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift);
if (!shiftHeld)
{
// 发送消息
SendChatMessage(chatInput.text);
}
// Shift+Enter 由 InputField 自身的 MultiLineNewline 模式处理换行,不需要额外代码
}
}
private void SendChatMessage(string message)
{
if (string.IsNullOrWhiteSpace(message)) return;
// 去掉末尾可能多出的换行符
message = message.TrimEnd('\n', '\r');
Debug.Log($"发送: {message}");
chatInput.text = "";
chatInput.ActivateInputField();
}
// ISubmitHandler 接口------防止默认的 Submit 行为
public void OnSubmit(BaseEventData eventData)
{
// 留空,阻止默认行为
}
}
5. 使用方法
5.1 基础 InputField
- Hierarchy 右键 → UI → InputField - TextMeshPro 创建输入框
- 选中 InputField (TMP) 对象,在 Inspector 中设置:
Content Type选 Standard(通用文本)Character Limit填 16(最大字符数)- 展开子对象 Text Area → Placeholder,修改文字为 "请输入用户名..."
- 创建空物体 InputFieldDemo ,挂载
InputFieldDemo.cs - 把 InputField (TMP) 拖入
usernameInput字段 - 创建一个 TextMeshPro 文本(UI → Text - TextMeshPro)用于显示校验反馈,拖入
feedbackText - 运行,输入字符观察实时校验反馈
5.2 密码输入框
- 再创建一个 InputField - TextMeshPro,
Content Type选 Password - 拖入
passwordInput字段 - (可选)创建 Toggle 控制密码显示/隐藏,拖入
showPasswordToggle - 运行,输入密码看到星号遮挡;勾选 Toggle 切换明文
5.3 聊天输入框
- 再创建一个 InputField - TextMeshPro,
Line Type选 Single Line - 展开子对象 Text Area → Placeholder,修改文字为 "按 Enter 发送..."
- 拖入
chatInput字段 - 运行,输入内容后按 Enter,查看 Console 的发送日志
- 确认发送后输入框自动清空并重新聚焦
6. 参数说明
InputFieldDemo.cs 可调参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| usernameInput | TMP_InputField | --- | 用户名输入框引用 |
| passwordInput | TMP_InputField | --- | 密码输入框引用 |
| chatInput | TMP_InputField | --- | 聊天输入框引用 |
| feedbackText | TMP_Text | --- | 校验反馈文本引用 |
| showPasswordToggle | Toggle | --- | 密码明文切换 Toggle |
| minUsernameLength | int | 3 | 用户名最短长度 |
| maxUsernameLength | int | 16 | 用户名最长长度 |
7. 变体与扩展
7.1 正则表达式校验
onValidateInput 是逐字符校验,有时需要对完整输入做正则校验(比如邮箱格式)。这种情况在 onEndEdit 中做:
csharp
using System.Text.RegularExpressions;
private void OnEmailEndEdit(string value)
{
// 简单的邮箱正则
string pattern = @"^[\w\.-]+@[\w\.-]+\.\w+$";
if (!Regex.IsMatch(value, pattern))
{
feedbackText.text = "邮箱格式不正确";
feedbackText.color = Color.red;
}
}
onValidateInput管"每个字符能不能输入",onEndEdit管"最终结果是否合法"。两者配合使用。
7.2 数值输入 + 范围限制
Content Type 设为 Integer / Decimal 只能限制输入字符类型,无法限制数值范围。范围限制在 onEndEdit 里做:
csharp
[SerializeField] private TMP_InputField hpInput;
private void OnHpEndEdit(string value)
{
if (int.TryParse(value, out int hp))
{
// 限制在 0-9999
hp = Mathf.Clamp(hp, 0, 9999);
hpInput.text = hp.ToString();
}
else
{
hpInput.text = "0";
}
}
7.3 输入框聚焦时弹出自定义面板
有些场景不用系统键盘,而是弹出自定义的虚拟键盘或选择面板:
csharp
[SerializeField] private TMP_InputField codeInput;
[SerializeField] private GameObject customKeyboard; // 自定义键盘面板
private void Start()
{
// 监听选中事件
codeInput.onSelect.AddListener(OnInputSelected);
codeInput.onDeselect.AddListener(OnInputDeselected);
}
private void OnInputSelected(string value)
{
customKeyboard.SetActive(true);
}
private void OnInputDeselected(string value)
{
customKeyboard.SetActive(false);
}
8. 常见问题
Q1: onEndEdit 为什么点击空白处也会触发?
onEndEdit 在输入框失去焦点时 触发,不管是回车提交还是点击了别处。用 inputField.wasCanceled 区分:
csharp
private void OnEndEdit(string value)
{
if (usernameInput.wasCanceled)
{
// 用户按 Esc 或点击了别处
return;
}
// 正常提交逻辑
}
注意:点击别处导致的失焦,
wasCanceled在不同 Unity 版本行为略有差异。如果需要严格区分"回车提交",可以用onSubmit(仅回车触发)替代。
Q2: 切换 Content Type 后显示没变化?
切换 contentType 后必须手动刷新。最可靠的做法:
csharp
string temp = inputField.text;
inputField.contentType = TMP_InputField.ContentType.Password;
inputField.text = temp;
inputField.ForceLabelUpdate(); // 强制刷新显示
只改 contentType 不调 ForceLabelUpdate(),文本显示不会立即更新。
Q3: 移动端软键盘遮挡了输入框怎么办?
iOS / Android 弹出软键盘时,输入框可能被键盘遮住。常见解决方案:
csharp
private void Update()
{
// 软键盘可见时,将 Canvas 上移
if (TouchScreenKeyboard.visible)
{
float keyboardHeight = GetKeyboardHeight();
// 将输入框所在区域上移 keyboardHeight
inputArea.anchoredPosition = new Vector2(0, keyboardHeight);
}
else
{
inputArea.anchoredPosition = Vector2.zero;
}
}
/// <summary>
/// 获取软键盘高度(像素)
/// </summary>
private float GetKeyboardHeight()
{
#if UNITY_ANDROID
using (var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
{
var activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
var view = activity.Call<AndroidJavaObject>("getWindow")
.Call<AndroidJavaObject>("getDecorView");
var rect = new AndroidJavaObject("android.graphics.Rect");
view.Call("getWindowVisibleDisplayFrame", rect);
int screenHeight = Screen.height;
int visibleHeight = rect.Call<int>("height");
return screenHeight - visibleHeight;
}
#elif UNITY_IOS
return (float)TouchScreenKeyboard.area.height;
#else
return 0;
#endif
}
另一个思路:把输入框放在屏幕上半区域,大多数情况下键盘不会遮到。
Q4: InputField 中文输入法(IME)组合输入时 onValueChanged 触发异常?
中文输入法在组合拼音阶段会不断触发 onValueChanged,拼音确认前的内容可能是半成品。解决方案:
- 不要在
onValueChanged中做重量级操作(如网络请求、数据库查询) - 把"搜索"之类的操作放到
onEndEdit或加一个延迟(用户停止输入 0.3s 后再触发)
csharp
private Coroutine _debounceCoroutine;
private void OnSearchValueChanged(string value)
{
// 每次输入变化,重置延迟计时
if (_debounceCoroutine != null)
StopCoroutine(_debounceCoroutine);
_debounceCoroutine = StartCoroutine(DebounceSearch(value, 0.3f));
}
private System.Collections.IEnumerator DebounceSearch(string query, float delay)
{
yield return new WaitForSeconds(delay);
// 延迟后才真正执行搜索
Debug.Log($"搜索: {query}");
}
Q5: InputField 和 TMP_InputField 用哪个?
详见下一节。简短回答:新项目用 TMP_InputField,除非你有特殊理由坚持用原生 Text。
9. 附:遇到 Legacy InputField 怎么迁移
本文全部代码基于 TMP_InputField。如果你在老项目中遇到了原生 InputField(Legacy),以下是两者的差异和迁移要点:
| 对比项 | InputField(Legacy) | TMP_InputField(本文使用) |
|---|---|---|
| 文字渲染 | 位图字体,缩放模糊 | SDF 字体,任意缩放清晰 |
| 富文本 | 基础 <b>、<i>、<color> |
完整富文本标签体系 |
| 命名空间 | UnityEngine.UI |
TMPro |
| 组件引用 | InputField、Text |
TMP_InputField、TMP_Text |
| 创建方式 | UI → InputField | UI → InputField - TextMeshPro |
| Hierarchy | 没有 Text Area 层 | 多一层 Text Area(RectMask2D) |
| 性能 | 字符多时顶点数暴增 | 顶点数更可控,大段文本更优 |
迁移只需要改三样东西:
csharp
// Legacy 版
using UnityEngine.UI;
InputField input;
Text text;
// TMP 版------改 using、改类型,API 几乎一样
using TMPro;
TMP_InputField input;
TMP_Text text;
两者的事件签名一致:
onValueChanged、onEndEdit、onSelect、onDeselect、onValidateInput。迁移时基本只需要换using声明、字段类型、枚举前缀(InputField.ContentType→TMP_InputField.ContentType)。
10. 性能 / 适配建议
-
不要在
onValueChanged里做重操作 。每输入一个字符都会触发,如果里面有字符串拼接、网络请求或大量 UI 更新,帧率直接起飞(往下飞)。加 debounce 或把逻辑移到onEndEdit。 -
中文输入场景优先用 TMP_InputField。原生 Text 的中文渲染在高 DPI 屏幕上容易糊,TMP 的 SDF 渲染清晰度好得多。
-
Raycast Target 只开在需要交互的对象上。InputField 的 Placeholder 和 Text 子对象默认开启 Raycast Target,但它们不需要接收射线。关掉可以减少 GraphicRaycaster 的遍历开销。
-
移动端注意
Hide Mobile Input。iOS 默认会在屏幕顶部显示一个原生输入条,勾选Hide Mobile Input可以隐藏它,但部分旧 iOS 版本上可能导致中文输入异常。建议在目标设备上实测。 -
多个输入框的 Tab 切换。Unity 原生不支持 Tab 键在 InputField 之间切换焦点。如果需要(登录表单场景很常见),自己写一个简单的 Tab 导航:
csharp
[SerializeField] private Selectable[] tabOrder; // 按顺序拖入各输入框
private void Update()
{
if (Input.GetKeyDown(KeyCode.Tab))
{
// 找到当前聚焦的对象
GameObject current = EventSystem.current.currentSelectedGameObject;
for (int i = 0; i < tabOrder.Length; i++)
{
if (tabOrder[i].gameObject == current)
{
// 跳到下一个
int next = (i + 1) % tabOrder.Length;
tabOrder[next].Select();
// 如果目标是 TMP_InputField,激活它
var inputField = tabOrder[next].GetComponent<TMP_InputField>();
if (inputField != null)
inputField.ActivateInputField();
break;
}
}
}
}