普通视图

发现新文章,点击刷新页面。
今天 — 2025年12月28日首页

Flutter限制输入框只能输入中文,iOS拼音打不出来?

作者 淡写成灰
2025年12月28日 14:33

中文输入必踩的 Flutter 坑合集:iOS 拼音打不出来,其实是你 Formatter 写错了

如果你在 Flutter 里做过「只允许中文 / 中英文校验」,并且只在 iOS 上翻过车,那这篇文章大概率能帮你节省半天 Debug 时间。

这不是 iOS 的锅,也不是 Flutter 的 Bug,而是 TextInputFormatter 和中文输入法(IME)之间的理解偏差


一、血iOS 上拼音怎么都打不出来

常见反馈包括:

  • iOS 中文拼音键盘
  • 输入 bei jing
  • 键盘有拼音显示
  • 输入框内容完全不变
  • 无法选词、无法上屏

👉 Android 正常
👉 模拟器正常
👉 真机 iOS 不行

很多人第一反应是:
“Flutter 对中文支持不好?”

结论先行:不是。


二、罪魁祸首:TextInputFormatter 的「中文校验」

下面这种 Formatter,你一定写过或见过:

class NameInputFormatter extends TextInputFormatter {
  @override
  TextEditingValue formatEditUpdate(
    TextEditingValue oldValue,
    TextEditingValue newValue,
  ) {
    final chineseOnly = RegExp(r'^[\u4E00-\u9FFF]+$');

    if (newValue.text.isEmpty) return newValue;

    if (!chineseOnly.hasMatch(newValue.text)) {
      return oldValue; // 
    }

    return TextEditingValue(
      text: newValue.text,
      selection: TextSelection.collapsed(
        offset: newValue.text.length,
      ),
    );
  }
}

逻辑看起来非常合理:

  • 只允许中文
  • 非法字符直接回退

但在 iOS 上,这段代码等于封死了中文输入法的入口


三、核心原理:iOS 中文输入法有「组字阶段」

1️ composing 是什么?

iOS 拼音输入法的输入过程分为两步:

  1. 组字(composing)

    • 输入:bei
    • 输入框里是拼音(未确认)
  2. 提交

    • 选择「北」
    • 中文字符真正上屏

在组字阶段:

newValue.text == "bei"
newValue.composing.isCollapsed == false

"bei" 必然无法通过「只允许中文」的正则校验


2️ Formatter 提前“否决”了输入

当 Formatter 在 composing 阶段做了以下任意一件事:

  • return oldValue
  • 修改 text
  • 强制重置 selection

iOS 输入法就会认为:
「当前输入不合法,终止组字」

于是出现经典现象:

拼音能打,但永远无法选字


四、隐藏更深的坑:selection 会杀死输入法

很多 Formatter 里都有这行:

selection: TextSelection.collapsed(offset: text.length),

在普通输入下没问题,但在中文输入中:

  • selection 是 IME 状态的一部分
  • 每次重置 selection = 重启组字流程

哪怕你放行了拼音,也可能出现:

  • 候选词异常
  • 游标跳动
  • 输入体验极差

五、那为什么 Android 没这个问题?

这是一个非常关键、也最容易误判的点

Android 的行为差异

  • Android 输入法对 composing 的暴露不一致
  • 很多键盘在 字符提交后才触发 Formatter
  • 即使 composing 存在,也更“宽容”

结果就是:

错误的 Formatter 在 Android 上“看起来能用”

但这并不代表代码是对的,只是 Android 没那么严格

真相

Android 是侥幸没炸,iOS 是严格把问题暴露出来。


六、正确原则

1. composing 阶段必须放行

if (!newValue.composing.isCollapsed) {
  return newValue;
}

2. 校验只在 composing 结束后做

3. 不要无脑重置 selection

4. Formatter ≠ 表单最终校验


七、正确示例

下面是一个安全、可扩展、iOS / Android 双端稳定的 Formatter 示例:

class UniversityNameInputFormatter extends TextInputFormatter {
  UniversityNameInputFormatter({this.maxLength = 40});

  final int maxLength;

  static final RegExp _disallowed =
      RegExp(r'[^a-zA-Z0-9\u4E00-\u9FFF-\s]');
  static final RegExp _multiHyphen = RegExp(r'-{2,}');
  static final RegExp _leadingHyphen = RegExp(r'^-+');
  static final RegExp _trailingHyphen = RegExp(r'-+$');

  @override
  TextEditingValue formatEditUpdate(
    TextEditingValue oldValue,
    TextEditingValue newValue,
  ) {
    // iOS 中文拼音组字阶段
    if (!newValue.composing.isCollapsed) {
      return newValue;
    }

    var text = newValue.text;
    if (text.isEmpty) return newValue;

    text = text.replaceAll(_disallowed, '');
    text = text.replaceAll(_multiHyphen, '-');
    text = text.replaceAll(_leadingHyphen, '');
    text = text.replaceAll(_trailingHyphen, '');

    if (text.length > maxLength) {
      text = text.substring(0, maxLength);
    }

    if (text == newValue.text) return newValue;

    int clamp(int o) => o.clamp(0, text.length);

    return TextEditingValue(
      text: text,
      selection: TextSelection(
        baseOffset: clamp(newValue.selection.baseOffset),
        extentOffset: clamp(newValue.selection.extentOffset),
      ),
      composing: TextRange.empty,
    );
  }
}

八、中文输入必踩的 Flutter 坑合集(Checklist)

❌ 坑 1:Formatter 里直接做中文正则校验

后果:iOS 拼音无法输入

❌ 坑 2:忽略 newValue.composing

后果:IME 组字被打断

❌ 坑 3:每次都把 selection 移到末尾

后果:候选词异常、游标乱跳

❌ 坑 4:以为 Android 正常 = 代码正确

后果:iOS 真机翻车


九、一句话总结

TextInputFormatter 是 IME 输入流程的一部分,不是简单的字符串过滤器。

❌
❌