適用場景:手機號、銀行卡號、身份證號、信用卡號、分段輸入格式化
難點:輸入時自動插入空格 + 光標不亂跳 + 刪除也正常

一、為什麼不能只用 onChanged?

很多人第一反應是:

onChanged: (value) {
  controller.text = format(value);
}

但這麼做會出現兩個典型問題:

  1. 光標跳到最後:用户在中間編輯 → 光標突然跑到末尾
  2. 中文輸入法衝突:中文組合輸入被打斷,甚至輸入不了漢字

正確姿勢:用 inputFormatters,這是框架設計給我們用來格式化輸入的入口。

二、目標需求

以手機號為例:

13800138000 → 138 0013 8000
 

要求:

  • 輸入到第 3/7 位時自動插空格
  • 刪除空格時智能回退
  • 在中間插入 / 刪除字符時,光標保持正確位置
  • 不能破壞中文輸入法
  • 支持粘貼內容

三、核心思路

inputFormatters 在每次輸入更新時都會調用:

formatEditUpdate(oldValue, newValue)

我們要做三件事:

  1. 提取用户輸入的純數字
  2. 把數字按規則插入空格
  3. 計算新的光標位置,並返回新的 TextEditingValue

最終返回的值:

TextEditingValue(
  text: 格式化後的文本,
  selection: 光標位置,
)

四、完整可用的手機號空格 Formatter

import 'package:flutter/services.dart';

/// 手機號輸入格式化:3-4-4 分組
class PhoneNumberFormatter extends TextInputFormatter {
  @override
  TextEditingValue formatEditUpdate(
    TextEditingValue oldValue,
    TextEditingValue newValue,
  ) {
    // 1. 輸入為空直接返回
    if (newValue.text.isEmpty) {
      return newValue;
    }

    // 2. 提取純數字
    String digits = newValue.text.replaceAll(RegExp(r'\D'), '');

    // 限制最大長度 11 位
    if (digits.length > 11) {
      digits = digits.substring(0, 11);
    }

    // 3. 格式化 3-4-4
    String formatted = _formatDigits(digits);

    // 4. 計算光標位置
    int cursorPosition = _calculateCursorPosition(
      newValue,
      formatted,
    );

    return TextEditingValue(
      text: formatted,
      selection: TextSelection.collapsed(offset: cursorPosition),
    );
  }

  /// 按 3-4-4 插入空格
  String _formatDigits(String digits) {
    final buffer = StringBuffer();
    for (int i = 0; i < digits.length; i++) {
      buffer.write(digits[i]);
      if (i == 2 || i == 6) buffer.write(' ');
    }
    return buffer.toString();
  }

  /// 根據新文本和格式化後文本,計算正確光標位置
  int _calculateCursorPosition(
    TextEditingValue newValue,
    String formatted,
  ) {
    int cursor = newValue.selection.end;

    // 輸入時,當光標越過第 3 或 8 位置(包含空格),需要往後+1
    if (cursor == 4 || cursor == 9) {
      cursor++;
    }

    // 刪除空格時,光標需要回退
    if (cursor > formatted.length) {
      cursor = formatted.length;
    }

    return cursor;
  }
}

五、使用方式

TextField(
  keyboardType: TextInputType.phone,
  inputFormatters: [
    PhoneNumberFormatter(),
  ],
  decoration: const InputDecoration(
    hintText: '請輸入手機號',
    border: OutlineInputBorder(),
  ),
)

六、效果演示

操作

效果

輸入 1 3 8 0 0 ...

自動變成 138 0013 8000

刪除空格

自動回退,不會卡住

在中間插入數字

光標保持正確位置

粘貼 13800138000

自動格式化

中文輸入法

不受影響

七、為什麼光標會亂跳?(原理解析)

如果你只設置 controller.text

  • Flutter 會認為“你重新賦值了文本”
  • 框架會將光標重置到文本末尾

所以我們必須同時設置 selection,告訴框架光標應該在哪裏

selection: TextSelection.collapsed(offset: cursorPosition)

而 cursorPosition 必須經過計算,不是簡單的 text.length

八、如何擴展為其他格式?

✅ 銀行卡號:4-4-4-4-...

1111 2222 3333 4444

修改 _formatDigits 即可:

if ((i + 1) % 4 == 0 && i != digits.length - 1) {
  buffer.write(' ');
}

✅ 身份證號:6-8-4

✅ 自定義分組:[N, N, N, ...]

你可以把分組規則做成參數:

PhoneFormatter(group: [3, 4, 4])

九、常見坑總結


説明

寫在 onChanged 裏

✅ 光標亂跳,❌ 中文輸入法異常

沒處理刪除行為

刪除空格會卡住

沒處理光標中間插入

光標會跳末尾

沒處理粘貼

粘貼格式錯誤

直接 return newValue

不會格式化,也無法限制長度