Unity UGUI动态伸缩输入框实现:基于TextMeshPro与ContentSizeFitter

发布时间:2026/7/11 5:12:32
Unity UGUI动态伸缩输入框实现:基于TextMeshPro与ContentSizeFitter 1. 项目概述为什么我们需要一个会“呼吸”的输入框在Unity的UI开发中InputField或TextMeshPro的TMP_InputField是处理用户文本输入的核心组件。然而原生的输入框有一个不大不小的问题它的尺寸是固定的。当用户输入的文字超过预设的显示区域时要么出现滚动条在多行模式下要么文字被截断。这在很多需要动态内容展示的场景下显得非常笨拙比如聊天框、笔记应用、可编辑的标签或者任何希望输入框能随着内容“生长”的界面。这就是“自定义输入框实现伸缩”要解决的核心痛点。我们需要的不是一个僵硬的方框而是一个能根据输入内容动态调整高度或宽度的、有“呼吸感”的交互元素。想象一下手机备忘录里新建一条内容输入框随着你敲击段落逐渐变长的体验那就是我们想要在Unity中复现的效果。这个需求在移动端应用、工具类编辑器、游戏内的社交系统等场景中尤为常见。实现它不仅能提升产品的交互质感更是对Unity UI系统理解深度的一次实践。2. 核心思路拆解从静态框到动态容器的转变实现一个可伸缩的输入框其核心思想是将输入框的文本内容与一个用于测量文本尺寸的“标尺”关联起来然后根据“标尺”的测量结果动态调整输入框背景或容器的大小。听起来简单但里面有几个关键的技术点需要厘清。2.1 方案选型UGUI Text vs TextMeshPro首先我们得决定基于哪个文本系统来构建。Unity提供了两套主流的UI文本方案传统UGUI Text内置在Unity的UGUI系统中易于使用但功能相对基础在高清屏幕或需要复杂文本效果时表现一般。TextMeshPro (TMP)Unity官方收购的顶级文本渲染方案提供了无与伦比的清晰度、丰富的字体特性如字距调整、连字和强大的性能。对于新项目尤其是对文本质量有要求的项目TMP几乎是唯一的选择。注意由于TMP已成为Unity UI的事实标准且其TMP_InputField功能更为强大本文后续的实现将主要围绕TextMeshPro - TextMeshProUGUI和TMP_InputField展开。但核心原理同样适用于传统的UGUI Text。选择TMP意味着我们将使用TMP_InputField作为输入组件并利用TextMeshProUGUI来获取文本的精确尺寸。2.2 伸缩的驱动逻辑谁在控制大小输入框的伸缩本质上是一个布局Layout问题。在Unity UGUI中控制元素尺寸通常有几种方式直接修改RectTransform的sizeDelta这是最直接的方法通过代码改变UI元素的宽高。使用ContentSizeFitter组件这是一个自动布局组件可以设置子物体根据其内容Content自动调整宽或高。结合Layout Group如VerticalLayoutGroup在复杂的嵌套布局中可能需要布局组来协调多个动态元素。对于单个输入框的伸缩“ContentSizeFitter 一个用于测量的TextMeshProUGUI”的组合是最高效、最符合UGUI设计哲学的方案。ContentSizeFitter负责响应尺寸变化并驱动布局更新而我们只需要在文本变化时更新那个测量用Text的文本内容即可。2.3 架构设计分离测量与显示一个健壮的可伸缩输入框不能简单地在TMP_InputField自带的Text组件上开启ContentSizeFitter。因为输入框的Text组件还承担着光标显示、文本选择、富文本解析等复杂职责直接修改它可能会引发不可预料的冲突。因此更优雅的设计是角色分离显示文本TMP_InputField自带的textComponent一个TextMeshProUGUI它只负责最终显示给用户看的文本、处理输入和光标。我们通常不直接改变它的大小。测量文本一个独立的、隐藏的TextMeshProUGUI组件我们称之为“Size Calculator”或“Placeholder Text”。它拥有与显示文本完全相同的字体、字号、边距等样式设置。它的唯一职责就是接收相同的文本内容并计算出该文本渲染出来所需的理想尺寸。背景/容器一个作为输入框视觉背景的Image组件或一个空的GameObject它上面挂载着ContentSizeFitter组件。这个背景的尺寸将由ContentSizeFitter根据其子物体——也就是那个“测量文本”——的内容尺寸来自动调整。联动控制器一个自定义的脚本例如AutoSizeInputField挂载在输入框根物体上。它监听TMP_InputField的文本变化事件onValueChanged每当文本改变就将新的文本同步给“测量文本”组件从而触发ContentSizeFitter重新计算背景容器的大小。同时它还需要确保TMP_InputField的可视区域Viewport与新的背景大小匹配。3. 步步为营构建可伸缩输入框的完整流程下面我将以一个从上到下的垂直伸缩输入框为例详细拆解每一步的操作和代码实现。3.1 第一步搭建UI层级结构在Unity编辑器中按照以下结构创建UI元素创建一个Canvas在其下创建一个空GameObject命名为DynamicInputField。这就是我们的根节点。在DynamicInputField下添加一个Image组件作为背景命名为Background。设置你喜欢的颜色如浅灰色。在Background下创建两个子物体Text_Display这是一个TextMeshPro - TextMeshProUGUI。它将作为TMP_InputField的显示组件。清空其默认文本设置好对齐方式如左上对齐Upper Left并设置合适的字体、颜色和字号。关键一步将其Raycast Target属性取消勾选。这是因为点击检测应由上层的TMP_InputField组件处理避免射线被文本阻挡。Text_SizeCalculator这是另一个TextMeshPro - TextMeshProUGUI。它就是我们的“隐形标尺”。将其文本颜色设置为完全透明Alpha0或者直接将Canvas Renderer组件禁用。确保其所有样式设置字体、字号、字间距、边距Margin与Text_Display完全一致。特别是Margin它直接影响文本占用的空间必须保持一致测量才准确。回到根节点DynamicInputField为其添加TMP_InputField组件。在TMP_InputField组件的Text Component属性中拖入Text_Display。通常TMP_InputField需要一个Viewport来定义文本显示的区域。默认情况下它会将父物体Background作为Viewport。这在我们动态调整Background大小时会产生问题。一个更稳妥的做法是在Background下专门创建一个子物体Viewport可以是一个空的RectTransform或者带Mask/RectMask2D的组件并将其拖拽到TMP_InputField的TextViewport属性中。这样文本显示区域就被限定在这个固定的Viewport内不受背景伸缩的影响。对于简单的单行/多行伸缩也可以暂时不指定用背景做Viewport但需要在脚本中处理视口更新。3.2 第二步配置自动布局组件选中Background物体为其添加ContentSizeFitter组件。将Horizontal Fit设置为Unconstrained因为我们实现垂直伸缩宽度固定。将Vertical Fit设置为Preferred Size。这告诉Unity这个物体的高度应该自动匹配其子物体即Text_SizeCalculator的“首选高度”。确保Background的RectTransform锚点Anchors设置为顶部拉伸Top-Stretch或根据你的布局需求设置。例如锚点在上边Pivot在(0.5, 1)顶部中心这样它向下伸缩时位置更可控。3.3 第三步编写核心控制脚本创建一个名为AutoSizeTMPInputField的C#脚本并将其挂载到根节点DynamicInputField上。using TMPro; using UnityEngine; using UnityEngine.UI; [RequireComponent(typeof(TMP_InputField))] public class AutoSizeTMPInputField : MonoBehaviour { [Header(组件引用)] [SerializeField] private TMP_InputField inputField; [SerializeField] private TextMeshProUGUI sizeCalculatorText; // 隐藏的测量文本 [SerializeField] private RectTransform backgroundRect; // 背景的RectTransform [SerializeField] private ContentSizeFitter backgroundSizeFitter; // 背景上的ContentSizeFitter [Header(伸缩设置)] [SerializeField] private bool autoSizeHeight true; // 是否自动调整高度 [SerializeField] private float heightPadding 10f; // 高度内边距 [SerializeField] private float minHeight 40f; // 最小高度 [SerializeField] private float maxHeight 300f; // 最大高度防止无限变大 private float originalBackgroundWidth; // 记录背景初始宽度用于计算 private void Awake() { // 自动获取组件引用 if (inputField null) inputField GetComponentTMP_InputField(); if (backgroundRect null inputField ! null) { // 通常背景是InputField的父物体或Viewport的父物体 backgroundRect inputField.GetComponentInParentRectTransform(); } if (backgroundSizeFitter null backgroundRect ! null) { backgroundSizeFitter backgroundRect.GetComponentContentSizeFitter(); } if (backgroundRect ! null) { originalBackgroundWidth backgroundRect.sizeDelta.x; } // 验证关键组件 if (sizeCalculatorText null) { Debug.LogError(SizeCalculatorText is not assigned!, this); enabled false; return; } if (inputField null) { Debug.LogError(TMP_InputField is not assigned!, this); enabled false; return; } } private void Start() { // 监听输入框的文本变化事件 inputField.onValueChanged.AddListener(OnInputValueChanged); // 初始同步一次文本并更新尺寸 SyncTextToCalculator(); UpdateLayout(); } private void OnDestroy() { // 移除事件监听防止内存泄漏 if (inputField ! null) { inputField.onValueChanged.RemoveListener(OnInputValueChanged); } } /// summary /// 当输入框文本变化时调用 /// /summary private void OnInputValueChanged(string newText) { SyncTextToCalculator(); UpdateLayout(); } /// summary /// 将输入框的文本同步到测量文本组件 /// /summary private void SyncTextToCalculator() { if (sizeCalculatorText ! null) { // 关键测量文本需要设置与显示文本完全一致的内容包括可能的富文本标签如果允许 // 如果输入框关闭了富文本这里也应同步关闭。 sizeCalculatorText.text inputField.text; // 如果输入框是多行且允许回车测量文本也必须启用多行支持 sizeCalculatorText.enableWordWrapping inputField.multiLine; // 确保测量文本的宽度约束与背景一致 sizeCalculatorText.rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, originalBackgroundWidth); } } /// summary /// 强制更新布局计算并应用新尺寸 /// /summary private void UpdateLayout() { if (!autoSizeHeight || backgroundRect null || sizeCalculatorText null) return; // 立即强制重建测量文本的网格获取最新的首选尺寸 // 直接访问textInfo在某些情况下可能未更新使用ForceMeshUpdate更可靠 sizeCalculatorText.ForceMeshUpdate(); // 获取测量文本的首选高度包含字体本身的上下间隙 float preferredHeight sizeCalculatorText.preferredHeight; // 计算最终背景高度加上内边距并限制在最小最大值之间 float targetHeight Mathf.Clamp(preferredHeight heightPadding, minHeight, maxHeight); // 应用新的高度到背景 backgroundRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, targetHeight); // 强制ContentSizeFitter立即生效有时需要延迟一帧但这里通常直接生效 if (backgroundSizeFitter ! null) { LayoutRebuilder.ForceRebuildLayoutImmediate(backgroundRect); } // 可选如果输入框的Viewport是背景本身可能需要通知InputField更新其内部布局 // inputField.Rebuild(CanvasUpdate.Layout); } }3.4 第四步脚本配置与参数说明在Unity编辑器中将脚本拖到DynamicInputField上并关联各个引用Input Field拖入该物体自身的TMP_InputField组件。Size Calculator Text拖入我们创建的隐藏文本Text_SizeCalculator。Background Rect拖入Background物体的RectTransform。Background Size Fitter拖入Background物体上的ContentSizeFitter组件。然后调整伸缩设置参数Auto Size Height总开关。Height Padding文本区域与背景边框之间的上下间距。这个值很重要需要根据你的视觉设计来调整否则文字会紧贴边缘。Min Height输入框为空或内容很少时的最小高度。Max Height防止用户输入过多内容导致界面失控。达到最大高度后输入框可以配合滚动条使用。4. 进阶优化与深度问题排查一个基础的伸缩输入框已经完成但要投入生产环境还需要考虑更多细节和边界情况。4.1 处理多行与单行模式多行模式这是伸缩输入框最常用的场景。确保TMP_InputField的Line Type设置为Multi Line Newline或Multi Line Submit。同时Text_Display和Text_SizeCalculator的TextMeshProUGUI组件必须勾选Enable Word Wrapping启用自动换行。这样文本到达背景边界宽度固定时会自动换行preferredHeight才会正确增加。单行模式如果希望单行输入框的宽度随内容伸缩思路类似但需要将ContentSizeFitter的Horizontal Fit也设置为Preferred Size并在脚本中计算和应用preferredWidth。注意单行模式下通常要禁用换行。4.2 精准的尺寸测量与内边距处理上面脚本中直接使用preferredHeight heightPadding是一个简化模型。实际上文本的渲染区域textInfo和RectTransform的尺寸计算可能受到字体资源、行间距lineSpacing、段落间距、文本组件自身的Margin属性影响。更精确的计算方法private void UpdateLayoutPrecise() { sizeCalculatorText.ForceMeshUpdate(); // 获取文本信息 TMP_TextInfo textInfo sizeCalculatorText.textInfo; if (textInfo.lineCount 0) // 空文本 { ApplyHeight(minHeight); return; } // 计算文本内容的总高度从第一行的上缘到最后一行下缘 float ascender textInfo.lineInfo[0].ascender; // 第一行的上升部分如‘h’的顶部 float descender textInfo.lineInfo[textInfo.lineCount - 1].descender; // 最后一行的下降部分如‘y’的底部 float textContentHeight Mathf.Abs(ascender - descender); // 加上文本组件自身设置的上下边距Margin float totalMargin sizeCalculatorText.margin.y sizeCalculatorText.margin.w; // y是上边距w是下边距TMP的Vector4 margin float preferredHeightFromText textContentHeight totalMargin; // 再加上我们自定义的视觉内边距 float targetHeight Mathf.Clamp(preferredHeightFromText heightPadding, minHeight, maxHeight); ApplyHeight(targetHeight); }这种方法更复杂但能避免因字体度量标准不同导致的几个像素偏差在追求像素级精准的UI中非常必要。4.3 性能优化避免每帧重建onValueChanged事件在用户快速输入时触发非常频繁。如果每次触发都直接ForceMeshUpdate和SetSizeWithCurrentAnchors可能会带来性能压力尤其是在低端移动设备上。优化策略使用协程延迟处理可以设置一个标志位和一个短延迟如0.1秒在用户停止输入后再触发一次尺寸更新。private Coroutine m_ResizeCoroutine; private void OnInputValueChanged(string newText) { SyncTextToCalculator(); // 防抖处理停止之前的协程开启一个新的 if (m_ResizeCoroutine ! null) StopCoroutine(m_ResizeCoroutine); m_ResizeCoroutine StartCoroutine(DelayedUpdateLayout(0.1f)); } private System.Collections.IEnumerator DelayedUpdateLayout(float delay) { yield return new WaitForSeconds(delay); UpdateLayout(); m_ResizeCoroutine null; }缓存尺寸只有当计算出的新高度与当前背景高度差值超过一个阈值如0.5像素时才实际执行SetSizeWithCurrentAnchors和LayoutRebuilder避免无意义的布局计算。4.4 与Scroll View的协同工作当可伸缩输入框被放置在一个Scroll Rect滚动视图内时情况会变得复杂。输入框变大会将父滚动视图的内容撑开。你需要确保滚动视图的Content物体上有一个VerticalLayoutGroup并且Child Controls Size设置为Height这样它才能感知到子物体我们的输入框的高度变化。在输入框尺寸更新后可能需要调用Canvas.ForceUpdateCanvases()或LayoutRebuilder.ForceRebuildLayoutImmediate在滚动视图的Content上以触发整个滚动区域的重新布局。一个更好的用户体验是当输入框增长导致光标移出当前视口时自动滚动ScrollRect以确保光标可见。这需要额外的逻辑来监听输入框的onEndEdit事件或光标位置并计算所需的滚动量。5. 常见问题与实战排坑记录在实际项目中我遇到了不少坑这里总结一下希望能帮你节省时间。5.1 问题输入框伸缩时文本显示错位或闪烁可能原因1显示文本Text_Display和测量文本Text_SizeCalculator的样式设置不完全一致。必须逐项检查字体Asset、字体大小、字体样式加粗、斜体、字符间距、行间距、字间距、对齐方式、边距Margin特别是Extra Settings里的Padding。任何一个不同都会导致测量失真。可能原因2ContentSizeFitter的更新帧率与脚本更新不同步。尝试在UpdateLayout方法末尾手动调用Canvas.ForceUpdateCanvases()强制所有Canvas在同一帧内完成布局计算。可能原因3TMP_InputField自身的Viewport设置问题。如果Viewport是背景背景大小变化后Viewport的裁剪区域可能没有立即更新。可以尝试在更新背景尺寸后调用inputField.Rebuild(CanvasUpdate.Layout)。5.2 问题输入框获得焦点时背景尺寸突然跳变排查检查是否在输入框获得焦点/失去焦点时改变了测量文本的样式例如Placeholder文本的消失/出现。确保测量文本在任何状态下有焦点、无焦点、有占位符、无占位符其text属性和样式都是稳定的。一个技巧是永远让测量文本同步输入框的text属性而不是placeholder。5.3 问题在滚动视图内输入框伸缩不流畅或卡顿优化如前所述为尺寸更新添加防抖Debounce机制。同时检查滚动视图的Movement Type是否为Elastic弹性弹性效果在频繁尺寸变化时可能造成卡顿可以考虑改为Clamped夹紧。检查布局计算深度过于复杂的嵌套布局多层LayoutGroup和ContentSizeFitter会导致一次尺寸变化引发整个UI树的连锁重建。尽量简化UI结构或者将频繁变化的元素放在布局树的末端。5.4 问题中文换行或标点符号处测量高度不准确原因与解决TMP对中文等复杂文本的换行规则避头尾和标点挤压处理非常精细。确保测量文本的Enable Word Wrapping、Text Wrapping模式以及Overflow模式与显示文本完全一致。如果问题依旧考虑使用前面提到的基于textInfo.lineInfo的精确高度计算方法它直接基于渲染后的行信息最为可靠。实现一个健壮的、可伸缩的Unity输入框远不止是挂几个组件那么简单。它涉及到对UGUI布局系统、TMP文本渲染、以及Unity事件机制的深入理解。从最基础的同步文本到处理多行换行、内边距计算再到与复杂滚动视图的集成和性能优化每一步都需要仔细考量。但一旦你掌握了这套方法并将其封装成一个预制体或可复用的UI组件它就能极大地提升你项目中所有文本输入场景的用户体验。