Unity移动端系统虚拟键盘调用与封装实践
1. 先把话说清楚什么场景下才该用系统键盘做 Unity 移动端项目但凡涉及让用户敲字这件事绕不开一个选择题自己画一套键盘还是直接喊系统自带的虚拟键盘出来。我见过不少团队一开始雄心勃勃说自绘键盘才能保证 UI 风格统一结果做到一半发现输入法联想、中文候选词、九宫格、手写、语音输入这些功能全得自己补最后工期炸掉又灰溜溜回来接系统键盘。这篇文章就聊 Unity 调用系统自带的虚拟键盘这件事——它是什么、能干什么、哪些坑必须先知道以及怎么把它封装成一个真正能在项目里长期用的东西。适合已经上手 Unity、做过一两个移动端 Demo、准备把输入功能做扎实的开发者看也适合刚接手别人项目、发现输入框在真机上行为诡异想找根因的同学。先把概念对齐。Unity 里说的系统自带虚拟键盘指的是操作系统提供的软键盘Soft Keyboard / On-Screen KeyboardAndroid 上是各家输入法应用iOS 上是系统键盘加第三方输入法桌面端则是系统的触摸键盘。Unity 通过TouchScreenKeyboard这个类去唤起它而 UI 组件InputField和TMP_InputField在底下也是走这条路。它解决的问题很纯粹让用户用自己熟悉的输入法、自己习惯的词库、自己配置的布局来打字你不用管拼音怎么组合、候选词怎么排序、九宫格怎么切。代价是你失去了对键盘外观和部分行为的控制权而且不同平台的表现差异相当大。1.1 自绘键盘在什么情况下会翻车我把自绘键盘的失败案例归成三类。第一类是输入效率问题自绘键盘通常只做全键盘 QWERTY中文用户要么被迫用拼音一个字母一个字母敲要么就得自己实现一套简易的拼音转汉字而词库、联想、纠错这些工作量是天文数字。第二类是适配问题不同地区的用户习惯完全不同九宫格、手写、语音、笔画你不可能全做。第三类是维护成本输入法生态每年都在变自绘键盘等于把这个变化挡在门外同时也把用户的习惯挡在门外。自绘键盘唯一真正站得住的场景是输入内容极度受限的情况。比如只需要输入 4 位数字验证码、只需要在虚拟摇杆旁边敲几个字母当昵称、或者是游戏手柄操作下需要一个方向键选字的极简输入。这种场景下自绘反而更快因为用户不需要在手机上调出输入法再切回游戏。判断标准很简单如果用户输入的内容有明确的字符集边界且长度很短自绘可以否则老老实实调系统键盘。1.2 系统键盘的成本清单调系统键盘不是零成本它的代价体现在三个地方。一是遮挡键盘弹出来会盖住半屏你的 UI 必须做避让否则输入框被挡在键盘后面用户看不见自己打的是什么。二是时序键盘的弹出、收起、文字变化都是异步的你在Update里读到的状态可能落后于用户的实际操作处理不当就会出现输完了但没提交提交了两次这类问题。三是平台差异Android 的输入法有组合输入阶段iOS 的键盘高度会变WebGL 压根没有系统键盘这回事。这三条我会在后面逐个拆开讲。提示如果你的项目只在 PC 上跑、且用户都是用物理键盘那这套东西基本用不上。真正需要认真处理的是移动端和触摸屏一体机场景。2. Unity 里调用系统键盘的三条路径很多人以为调系统键盘就是调一个 API实际上 Unity 给了三层不同抽象级别的东西选错了层后面会一直别扭。搞清楚这三层各自负责什么比背 API 参数重要得多。2.1 TouchScreenKeyboard官方唯一正经入口TouchScreenKeyboard是 UnityEngine 命名空间下的静态类是所有系统键盘调用的最终落点。它的核心方法就一个Open返回一个TouchScreenKeyboard实例你通过轮询这个实例的status和text来获知用户输入了什么。这个类最大的特点是它不参与 UI 渲染——它不画输入框不画光标只负责唤起键盘并把文字回传给你。也就是说屏幕上那个输入框长什么样完全由你自己决定。这个设计的好处是灵活你可以把它接在任意 UI 元素上甚至接在一个 3D 物体上做世界空间输入。坏处是所有细节都得自己管光标闪烁、文字截断、占位符、密码掩码显示统统要自己实现。所以除非你有特殊需求否则不建议直接在业务代码里裸用TouchScreenKeyboard那是重复造轮子。2.2 InputField 家族封装层到底替你做了什么Unity 提供了两套输入框组件老的UnityEngine.UI.InputField和 TextMeshPro 的TMP_InputField。它们在移动端的行为逻辑是检测到聚焦事件后在底层调TouchScreenKeyboard.Open然后把键盘回传的文本同步到自己的text属性上同时触发onValueChanged和onEndEdit事件。除此之外它还替你处理了光标位置、选区、字符限制、内容类型校验这些琐事。这里有个关键开关值得单独拎出来说shouldHideMobileInput老 InputField和shouldHideSoftKeyboard部分版本叫法不同TMP 里是shouldHideMobileInput。当它为true时Unity 会隐藏系统原生输入框只用自己渲染的文本为false时系统会在屏幕某个位置通常是顶部或底部弹出一个原生的输入条里面有系统级的编辑能力。这个开关直接决定了中文输入法的体验好坏后面第 5 节会详细展开。2.3 原生桥接什么时候必须自己写有些需求 Unity 的封装层覆盖不到只能自己写平台代码。典型的三类第一需要在键盘弹出/收起的瞬间拿到精确的高度和动画曲线用来做 UI 联动第二需要控制键盘的 appearance比如深色模式、返回键文字第三需要拿到输入法的组合输入中间状态也就是用户还在敲拼音、没选字的那一刻。前两类可以用AndroidJavaObject和 iOS 的DllImport做桥接第三类在 Unity 层面基本拿不到只能靠原生层拦截。注意写原生桥接的时候一定要做空实现兜底也就是#if UNITY_ANDROID !UNITY_EDITOR这类宏要写全。我踩过最蠢的坑是在编辑器里调 Android 的 Java 类报了一堆找不到类的错排查了半小时才发现是宏没写对。3. 把 TouchScreenKeyboard 的每个参数都掰开既然最终都要落到这个类上那就把它的参数一个一个说清楚。很多人抄代码的时候只改了前两个参数剩下的全用默认值结果在真机上出现自动纠错把用户昵称改掉、单行输入框里能敲回车这类问题。3.1 Open 方法的签名与参数取舍签名大致是这样public static TouchScreenKeyboard Open( string text, TouchScreenKeyboardType keyboardType TouchScreenKeyboardType.Default, bool autocorrection true, bool multiline false, bool secure false, bool alert false, string textPlaceholder , int characterLimit 0);逐个说。第一个text是预填内容用户打开键盘时输入框里已经有的字。第二个keyboardType决定键盘布局枚举里有Default、ASCIICapable、NumbersAndPunctuation、NumberPad、PhonePad、EmailAddress、URL、NamePhonePad等选对了能省掉用户切键盘的动作。这里有个坑NumberPad在 iOS 上确实只有数字但在部分 Android 输入法上会退化成全键盘所以如果你的逻辑强依赖只能输数字别指望键盘类型必须在代码里过滤。第三个autocorrection我的建议是永远设成 false。系统自动纠错在聊天场景是加分项但在游戏昵称、账号、道具名这些场景就是灾难用户打了一串自定义 ID系统给他改成了字典里的词。第四个multiline控制是否允许多行设成 true 后回车键会变成换行而不是提交。第五个secure是密码模式开启后系统会把输入内容显示成圆点并且通常会关闭联想和纠错——但记住它只是显示层掩码text属性返回的仍然是明文别以为开了 secure 就安全了。第六个alert比较冷门开启后键盘会进入一种确认模式主要影响 iOS 上的回车键文案。第七个textPlaceholder是占位符文字只在部分平台生效。第八个characterLimit限制最大字符数0 表示不限制设了之后系统会直接阻止用户继续输入比自己截断体验好。3.2 status 状态机的正确读法TouchScreenKeyboard.status是个枚举四个值Visible、Done、Canceled、LostFocus。看起来简单实际用起来有个反直觉的地方键盘实例在用户点击完成之后并不会立刻失效你还能读到最终的text。所以正确的处理顺序是在Update里先判断status如果是Visible就持续同步text如果是Done就把最终文本提交并置空引用如果是Canceled或LostFocus就回滚到打开前的值。Canceled和LostFocus的区别值得说一句。Canceled通常对应 Android 的物理返回键或者 iOS 的取消操作语义是用户不想输入了。LostFocus则是键盘被其他东西抢走了焦点比如来电、切后台、或者你的代码把焦点移到了另一个输入框。这两种情况下用户的意图不同是否需要保留已输入内容也不同——取消应该回滚失焦通常应该保留。我见过有项目把这两个当成一回事处理结果用户切了个后台回来发现刚打的字全没了。3.3 area 与键盘高度测量TouchScreenKeyboard.area返回一个Rect表示键盘在屏幕上占据的区域单位是像素原点在左下角。这个属性理论上是做 UI 避让的最佳数据源但实际可靠性因平台而异iOS 上比较准Android 上部分输入法会返回全屏或者零尤其是在悬浮键盘、分屏模式下。所以我的做法是不把它当唯一依据而是用它做初值再结合配置里的兜底高度比如屏高的 40%取较大值。还有一个属性TouchScreenKeyboard.visible返回布尔值表示键盘当前是否可见。它比读area更可靠一些适合用来判断要不要做避让这个开关。而isSupported则告诉你当前平台支不支持这套机制桌面端独立运行时它通常返回 false这时候就得走第 4.3 节的方案。提示键盘高度在 iOS 上是有动画的如果你在键盘弹出的第一帧就去读area拿到的可能是 0 或者旧值。稳妥做法是延迟一帧再读或者干脆用visible状态变化作为触发信号。4. 分平台落地Android、iOS、桌面、WebGL同一个 API四个平台四种脾气。这一节按平台拆每个平台给出真正会影响结果的那几个配置点和代码点。4.1 Android清单配置比代码更关键Android 上最常见的问题不是代码写错而是AndroidManifest 里的windowSoftInputMode没配对。这个属性的作用是告诉系统键盘弹出来的时候窗口该怎么调整。可选取值里最常用的是adjustResize和adjustPan。adjustResize会把窗口压缩让出键盘的空间Unity 的屏幕高度会跟着变你的 UI 可以感知到并重新布局adjustPan则是把整个窗口往上推Unity 拿不到高度变化但内容不会被压缩变形。选哪个取决于你的 UI 结构。如果是全屏 Canvas 加锚点布局adjustResize更合适因为你可以监听屏幕尺寸变化做自适应。如果是固定相机视野、UI 跟着相机走的游戏界面adjustPan更省事。这里要注意不同 Unity 版本导出的清单默认值有出入稳妥做法是显式在 Player Settings 的 Custom AndroidManifest 里写死别依赖默认。第二个 Android 特有的点是返回键。键盘弹出来时按物理返回键系统会先收起键盘这个事件 Unity 侧是收不到的所以你别指望在Update里通过Input.GetKeyDown(KeyCode.Escape)捕获到这个动作。如果你的游戏在别处也监听了返回键做退出游戏的逻辑一定要在输入框激活期间屏蔽掉否则用户想收键盘结果游戏退出了。// 输入框激活期间屏蔽返回键逻辑 private bool _inputActive; private void Update() { if (_inputActive) return; if (Input.GetKeyDown(KeyCode.Escape)) { // 正常的返回逻辑 } }另外还有个小技巧如果某些机型上键盘弹出后 Unity 画面被压缩得很奇怪可以尝试在TouchScreenKeyboard.Open之后把TouchScreenKeyboard.hideInput设为 true能减少一层原生输入框的渲染画面会稳定一些。4.2 iOS差异集中在类型映射与回收时机iOS 的键盘整体比 Android 规矩但也有两处要留意。第一处是keyboardType的映射iOS 对键盘类型的支持更严格NumberPad就是纯数字键盘连小数点都没有如果你要做金额输入得用NumbersAndPunctuation。第二处是键盘回收iOS 上如果一个TouchScreenKeyboard实例没有被正确关闭用户切到别的应用再回来键盘可能会残留或者反复弹出。所以务必在OnDisable、OnDestroy里主动把active置 false。iOS 13 之后引入了浮动键盘和快速输入条这带来了一个新变量TouchScreenKeyboard.isInPlaceEditingAllowed。它表示当前是否允许原地编辑也就是键盘不遮挡内容、直接在输入框位置编辑。如果你要做精细的避让逻辑这个属性可以帮你判断这次不用挪 UI。但要注意它只代表系统允许不代表用户真的开成了浮动态所以还是得结合area判断。4.3 桌面端调起系统触摸键盘的两种做法桌面独立构建下TouchScreenKeyboard.isSupported通常返回 false但这不代表触摸屏一体机不需要软键盘。Windows 系统自带的触摸键盘可以通过进程调起最直接的方式是启动TabTip.exeusing System.Diagnostics; using System.IO; public static class WindowsSoftKeyboard { private static Process _proc; public static void Show() { if (_proc ! null !_proc.HasExited) return; string commonFiles Path.Combine( System.Environment.GetFolderPath( System.Environment.SpecialFolder.CommonProgramFiles), microsoft shared\ink\TabTip.exe); if (File.Exists(commonFiles)) { _proc Process.Start(commonFiles); } else { // 兜底老式的屏幕键盘 _proc Process.Start(osk.exe); } } }这里有个实操细节TabTip.exe启动后不会自己消失用户点击输入框外部它也不会自动收起需要你调用Kill或者发送关闭消息。更麻烦的是如果同一时间已经有实例在跑再次Start可能会直接返回已有进程或者什么都不做所以上面代码里加了进程存活判断。另外这个方案只在 Windows 上有效跨平台构建时要用宏隔离。注意调外部进程属于能用但不优雅的方案如果你的项目要上架得考虑某些环境可能限制进程启动。正式项目更稳的做法是自己做一个轻量的屏幕键盘 UI只在检测到触摸设备且当前没有物理键盘输入时才显示。4.4 WebGL系统键盘根本不存在只能借 DOMWebGL 平台是很多人没意识到的一个大坑。Unity 的 WebGL 构建运行在浏览器沙箱里没有权限也没有能力去调起操作系统的软键盘TouchScreenKeyboard.isSupported返回 false。你在手机上用浏览器打开一个 WebGL 游戏点击输入框系统键盘不会出来。但这里有个转机浏览器的原生input元素是能唤起系统键盘的而这个能力通过 JavaScript 桥接是可以借到的。Unity 的老InputField在 WebGL 上其实内部就是这么做的——它会在页面里塞一个隐藏的input元素聚焦它来唤起键盘再把值同步回 Unity。但TMP_InputField在部分版本上这个机制不生效需要自己写.jslib插件// SoftKeyboard.jslib mergeInto(LibraryManager.library, { ShowKeyboard: function (placeholderPtr, defaultPtr) { var placeholder Pointer_stringify(placeholderPtr); var def Pointer_stringify(defaultPtr); var el document.getElementById(unity-soft-input); if (!el) { el document.createElement(input); el.id unity-soft-input; el.type text; el.style.position fixed; el.style.left -9999px; // 移出可视区避免被用户看到 el.style.top 0; document.body.appendChild(el); } el.value def; el.placeholder placeholder; el.focus(); } });隐藏元素的做法是把left设成一个很大的负值而不是用display: none因为display: none的元素无法获得焦点也就唤不起键盘。这个细节坑了不少人我当年就是卡在这一直以为是浏览器不支持。5. 中文输入法才是真正的坑前面那些都算工程配置真正让人掉头发的是中文输入。英文输入是按一个键、屏幕多一个字符一一对应简单直接。中文输入是按一串键、屏幕上先出现拼音、再出现候选词、用户选了才上屏中间有一个**组合输入Composing**阶段。Unity 的输入框在这个阶段的表现直接决定了中文用户骂不骂你。5.1 组合输入阶段的中间态用户敲下nihao这五个字母时屏幕上通常会显示带下划线的nihao此时还没有确定要输入什么字。Unity 的InputField.text在这个阶段会读到什么答案是不确定的取决于shouldHideMobileInput的设置和输入法实现。如果隐藏了原生输入框Unity 自己渲染很多时候读到的是拼音原文用户还没选字onValueChanged就疯狂触发你的业务逻辑如果在这个回调里做实时搜索、实时校验就会收到一堆无意义的拼音串。这个问题的解法有两条路。第一条是把shouldHideMobileInput设成false让系统弹出原生输入条用户在原生输入条里完成组合输入上屏后才同步回 Unity。这条路体验最自然但原生输入条的位置由系统决定可能盖住你的 UI视觉上割裂。第二条路是保留隐藏原生输入框但在业务层做延迟处理不要在onValueChanged里立即响应而是用定时器做 200 毫秒的防抖等用户停止输入再处理。防抖还能顺带解决另一个问题——每次按键都发网络请求或者跑一次完整校验性能开销很大。5.2 多行输入与回车提交的冲突multiline和回车提交是互斥的。开了multiline回车就是换行键盘上的按键文案是换行关了multiline回车就是完成文案变成完成或前往。有些需求既要多行输入又想让用户用某个键提交这在中式输入法下尤其别扭因为中文用户在组合输入时经常用回车来确认选字。这时候按回车到底是选字还是提交我的处理方式是多行输入场景下不提供回车提交只给界面上的一个显式发送按钮。这样用户选字时按回车不会误触发送操作意图清晰。单行场景才依赖键盘的完成键。这个改动看起来小但在实际测试里能减少一大类用户投诉。5.3 密码框与限制字符的边界情况secure模式和中文输入法组合起来会有奇怪的边缘情况。某些 Android 输入法在密码模式下会禁用中文输入用户只能打英文字母另一些则允许中文但候选词不显示用户处于瞎猜状态。所以密码框的输入字符集最好明确限定为 ASCII在onValidateInput里做过滤private char ValidatePassword(string text, int charIndex, char addedChar) { // 只允许可见 ASCII 字符 if (addedChar 0x20 addedChar 0x7E) return addedChar; return \0; // 返回 \0 表示拒绝这次输入 }字符限制还有一个隐性坑characterLimit计的是字符数而一个中文字在 UTF-16 里算一个 char但在视觉上占两个英文宽度。如果你的 UI 布局是按宽度算的用户输入中文时会出现字符数没超但文本溢出框外的情况。解决方式是在显示层用 TextMeshPro 的自动缩放或者 overflow 模式而不是只靠characterLimit卡。提示如果你的需求是昵称最多 12 个字用characterLimit 12是符合用户直觉的但如果需求是最多 12 个字符宽度那就得自己算权重中文算 2、英文算 1。这类需求在表单校验里很常见别想当然。6. 键盘遮挡与焦点管理的实战方案输入框被键盘挡住是玩家反馈里出现频率最高的输入相关问题没有之一。尤其是表单类界面输入框排在页面下半部分键盘一弹出来用户完全看不见自己打的东西。6.1 用 area 做动态避让避让的核心逻辑是算出输入框在屏幕坐标里的位置和键盘占据的矩形做相交判断如果相交就把整个界面往上平移。平移的距离等于相交部分的高度加一点边距。private const float SafetyMargin 24f; private void AdjustForKeyboard(Rect keyboardArea) { if (keyboardArea.height 0f) return; // 键盘区域换算成屏幕坐标系原点左下与 Unity 屏幕坐标一致 float keyboardTop keyboardArea.y keyboardArea.height; // 取当前聚焦输入框在世界空间的包围盒转成屏幕坐标 Vector3[] corners new Vector3[4]; _activeFieldRect.GetWorldCorners(corners); float fieldBottom RectTransformUtility .WorldToScreenPoint(null, corners[0]).y; float overlap keyboardTop - fieldBottom SafetyMargin; if (overlap 0f) return; _canvasRoot.anchoredPosition new Vector2( _canvasRoot.anchoredPosition.x, _canvasRoot.anchoredPosition.y overlap); }这段代码里有几个地方容易写错。第一WorldToScreenPoint的第一个参数是相机Canvas 是 Screen Space - Overlay 时传 null是 Camera 模式时传对应相机传错了坐标全歪。第二corners[0]是左下角这个顺序是GetWorldCorners的约定别记混。第三平移之后一定要在键盘收起时把位置还原否则用户下次打开别的输入框会发现界面莫名其妙飘着。更省事的做法是把所有输入框放在一个ScrollRect里键盘弹出时直接滚动到让输入框可见的位置。这种方式对表单类界面特别友好因为用户本来就需要上下滚动。实现上用ScrollRect.verticalNormalizedPosition配合RectTransformUtility.CalculateRelativeRectTransformBounds算相对位置就行。6.2 焦点切换与防止键盘反复弹出多输入框的界面有个经典 bug用户从输入框 A 点到输入框 B键盘会先收起来再弹出来中间闪烁一下视觉上很难看。原因是 A 失焦时触发了DeactivateInputField键盘开始收起动画B 聚焦时又立刻调Open两次调用打架。解决思路是延迟关闭。在 A 失焦时不立刻关键盘而是等一小段时间比如 0.1 秒如果这段时间内 B 聚焦了就跳过关闭动作直接复用键盘实例。这个技巧在 TMP_InputField 里可以通过重写焦点逻辑实现或者更简单地在业务层维护一个当前键盘实例所有输入框共享它只更新text和keyboardType不重新Open。public class SharedKeyboard : MonoBehaviour { private TouchScreenKeyboard _kb; private TMP_InputField _owner; private float _pendingCloseTime -1f; public void Attach(TMP_InputField field) { // 取消待执行的关闭 _pendingCloseTime -1f; _owner field; if (_kb null) { _kb TouchScreenKeyboard.Open( field.text, MapType(field.contentType), autocorrection: false, multiline: field.lineType TMP_InputField.LineType.MultiLineNewline, secure: field.contentType TMP_InputField.ContentType.Password); } } public void Detach(TMP_InputField field) { if (_owner ! field) return; _pendingCloseTime Time.unscaledTime 0.1f; } private void Update() { if (_kb null) return; if (_kb.status TouchScreenKeyboard.Status.Visible) { if (_owner ! null) _owner.text _kb.text; } else if (_kb.status TouchScreenKeyboard.Status.Done) { _owner?.onEndEdit?.Invoke(_kb.text); Close(); } else if (_kb.status TouchScreenKeyboard.Status.Canceled || _kb.status TouchScreenKeyboard.Status.LostFocus) { Close(); } if (_pendingCloseTime 0f Time.unscaledTime _pendingCloseTime) Close(); } private void Close() { if (_kb ! null) _kb.active false; _kb null; _owner null; _pendingCloseTime -1f; } private void OnDisable() Close(); }用Time.unscaledTime而不是Time.time是有意的因为很多游戏的输入界面会把Time.timeScale设为 0 暂停游戏用缩放时间会导致延迟关闭永远不触发。注意OnDisable里一定要关键盘。我在一个项目里遇到过场景切换后键盘残留的情况用户在加载界面看到一个孤零零的键盘浮在那点了也没反应非常尴尬。根因就是旧场景的键盘实例没被回收。7. 常见问题速查表与排查思路实际排障的时候最怕的是现象描述不清、不知道该查哪一层。我整理了一张表把近几年遇到的典型现象和对应根因列出来遇到问题先对号入座再往下深挖。7.1 问题速查表现象高概率根因排查方向点输入框没反应键盘不弹isSupported为 false或点击事件被上层 UI 拦截打印TouchScreenKeyboard.isSupported检查 Canvas 上有没有透明遮罩层键盘弹出但输入框不显示文字shouldHideMobileInput为 true 但自己没渲染文本检查输入框的 text 组件是否被禁用或层级错误中文输入时文字重复上屏组合阶段text被同步了两次检查onValueChanged里是否有回写 text 的逻辑键盘挡住输入框未做遮挡避让或 Android 清单模式选错检查windowSoftInputMode检查避让代码是否被执行键盘收起后界面位置没还原避让逻辑只做了平移没做还原在关闭回调里恢复原始anchoredPosition按返回键想收键盘游戏却退出了返回键被业务逻辑捕获输入激活期间屏蔽返回键处理场景切换后键盘残留实例没被主动关闭在OnDisable/ 场景卸载回调里调用active false密码框里能输入中文未做字符集过滤在onValidateInput里限定 ASCII 范围WebGL 上点输入框没反应系统键盘在浏览器沙箱里不可用检查是否实现了.jslib的 DOM input 桥接多输入框切换时键盘闪烁关闭和打开打架用共享实例加延迟关闭7.2 排查顺序我个人的习惯是按平台 → 配置 → 代码的顺序查。先确认问题在哪些平台复现如果只有 Android 有问题那大概率是清单配置或者输入法差异如果只有某个品牌的机器有问题那大概率是该机型输入法的特殊行为得单独做兼容。配置层查完再查代码层代码层优先看输入框组件的 Inspector 参数很多人调了半天代码结果发现是contentType设成了IntegerNumber却想输英文。还有一个容易被忽略的点真机日志。编辑器里的行为和真机差得很远尤其是键盘相关的部分。建议在键盘打开、关闭、状态变化的地方都打一条日志真机跑一遍把日志拉出来对着时序看比盲猜快得多。提示Android 上抓日志可以过滤输入法相关的关键字能看到系统层收到的指令有助于判断是 Unity 没发指令还是输入法没响应。8. 封装一个能复用的键盘服务聊了这么多细节最后落到工程实践上别在每个页面里重复写这套逻辑抽一个服务层出来。这个服务层的职责很明确——管理键盘实例的生命周期、把平台差异挡在外面、对外只暴露打开输入读取结果关闭三个语义。8.1 接口设计设计上我用一个回调式的接口而不是让业务代码去轮询状态public interface ISoftKeyboardService { bool IsSupported { get; } void Open(KeyboardRequest request, Actionstring onConfirm, Action onCancel); void Close(); }KeyboardRequest里放标题、预填文本、键盘类型、是否多行、是否密码、最大长度这些参数做成一个结构体避免Open方法的参数列表长到没法看。这样做的好处是后续要加新参数比如 iOS 的返回键文案时不会破坏已有调用方。8.2 代码实现与接入方式实现层按平台分三个子类用一个工厂在运行时选择public static class SoftKeyboardFactory { public static ISoftKeyboardService Create() { #if UNITY_ANDROID || UNITY_IOS return new MobileSoftKeyboardService(); #elif UNITY_WEBGL return new WebGLSoftKeyboardService(); #elif UNITY_STANDALONE_WIN return new WindowsSoftKeyboardService(); #else return new NullSoftKeyboardService(); #endif } }NullSoftKeyboardService是个空实现所有方法什么都不做IsSupported返回 false。它的作用是让不支持键盘的平台上业务代码不至于崩只需要在调用处判断一下IsSupported决定要不要显示请用系统键盘输入的提示。接入方式上我倾向于在游戏的启动入口把服务注册进去页面层通过一个静态访问点拿实例。这里要小心一点编辑器里跑的时候走哪个分支。#if UNITY_ANDROID在编辑器下是不成立的所以如果不额外处理编辑器里永远走 Null 分支测试的时候输入框完全没反应很容易误以为代码坏了。我的做法是在编辑器下也返回移动端实现只是内部对TouchScreenKeyboard的调用做了空判断——这样在编辑器里至少能测通流程真机再验细节。#if UNITY_EDITOR return new MobileSoftKeyboardService(); // 编辑器下走移动端逻辑方便调试 #endif最后再补一个小技巧是我踩坑之后总结的给键盘加一个全局的最后操作时间记录。当游戏进入后台再回来或者长时间没有输入操作时主动检查一下键盘实例是否还存活如果系统已经把它回收了而你的引用还在及时清空避免后续读到脏的text。这个检查成本极低但能规避一类很难复现的偶发问题——尤其是那种用户反馈偶尔输入框里的字变成上一次的内容的诡异 bug。我在几个项目里反复调这套东西最深的一点体会是Unity 的系统键盘调用看起来只是两个 API真正的工作量全在边界情况上输入法组合、平台差异、遮挡避让、焦点切换。这些细节单拎出来都不难难的是它们会同时出现而且只在真机上、只在特定的输入法里、只在用户操作足够快的时候才暴露。所以我的建议永远是输入功能一旦写完别在编辑器里点两下就交付找几台不同品牌的真机切换不同的输入法用中文、英文、数字、表情各试一遍把异常流程中途返回、切后台、来电也走一遍这才是真正能过验收的测试。