PAYATHON 2026

Flutter 如何创建带千位分隔符和两位小数的支付金额 TextField

支付老李

结论

不要把“限制两位小数”和“添加千位分隔符”交给多个彼此独立的 TextInputFormatter。Flutter 会按数组顺序依次执行 formatter,前一个 formatter 插入的逗号会直接传给后一个 formatter,很容易触发过滤或校验失败。

建议使用一个组合 formatter:先移除千位分隔符并校验原始数字,再格式化整数部分,最后根据格式化后的文本重新计算光标位置。

完整实现

import 'package:flutter/services.dart';

class PaymentAmountInputFormatter extends TextInputFormatter {
  PaymentAmountInputFormatter({
    this.thousandsSeparator = ',',
    this.decimalSeparator = '.',
    this.decimalDigits = 2,
  });

  final String thousandsSeparator;
  final String decimalSeparator;
  final int decimalDigits;

  @override
  TextEditingValue formatEditUpdate(
    TextEditingValue oldValue,
    TextEditingValue newValue,
  ) {
    if (newValue.text.isEmpty) {
      return const TextEditingValue();
    }

    // 记录光标前有多少个真正属于金额的字符,忽略千位分隔符。
    final cursorPosition = newValue.selection.extentOffset.clamp(
      0,
      newValue.text.length,
    );

    final rawCursorPosition = newValue.text
        .substring(0, cursorPosition)
        .replaceAll(thousandsSeparator, '')
        .length;

    // 千位分隔符只是显示格式,不参与金额校验。
    final rawText = newValue.text.replaceAll(thousandsSeparator, '');

    final escapedDecimalSeparator = RegExp.escape(decimalSeparator);
    final amountPattern = RegExp(
      '^\\d*(?:$escapedDecimalSeparator\\d{0,$decimalDigits})?\$',
    );

    // 输入包含非法字符、多个小数点或超过两位小数时,拒绝本次修改。
    if (!amountPattern.hasMatch(rawText)) {
      return oldValue;
    }

    final decimalIndex = rawText.indexOf(decimalSeparator);

    final integerPart = decimalIndex == -1
        ? rawText
        : rawText.substring(0, decimalIndex);

    final decimalPart = decimalIndex == -1
        ? null
        : rawText.substring(decimalIndex + decimalSeparator.length);

    final formattedInteger = _formatInteger(integerPart);

    final formattedText = decimalPart == null
        ? formattedInteger
        : '$formattedInteger$decimalSeparator$decimalPart';

    final newCursorPosition = _calculateCursorPosition(
      formattedText,
      rawCursorPosition,
    );

    return TextEditingValue(
      text: formattedText,
      selection: TextSelection.collapsed(offset: newCursorPosition),
      composing: TextRange.empty,
    );
  }

  String _formatInteger(String value) {
    if (value.isEmpty) {
      return '';
    }

    final buffer = StringBuffer();

    for (var index = 0; index < value.length; index++) {
      final remaining = value.length - index;

      if (index > 0 && remaining % 3 == 0) {
        buffer.write(thousandsSeparator);
      }

      buffer.write(value[index]);
    }

    return buffer.toString();
  }

  int _calculateCursorPosition(
    String formattedText,
    int rawCursorPosition,
  ) {
    if (rawCursorPosition <= 0) {
      return 0;
    }

    var rawCharacters = 0;

    for (var index = 0; index < formattedText.length; index++) {
      if (formattedText[index] != thousandsSeparator) {
        rawCharacters++;
      }

      if (rawCharacters == rawCursorPosition) {
        return index + 1;
      }
    }

    return formattedText.length;
  }
}

TextField 用法

金额输入需要带小数点的数字键盘,应使用 TextInputType.numberWithOptions,不能只使用 TextInputType.number:

TextField(
  controller: controller,
  keyboardType: const TextInputType.numberWithOptions(
    decimal: true,
  ),
  inputFormatters: [
    PaymentAmountInputFormatter(),
  ],
  decoration: const InputDecoration(
    hintText: '0.00',
  ),
)

输入效果:

1          -> 1
1000       -> 1,000
1234567    -> 1,234,567
1234.5     -> 1,234.5
1234.56    -> 1,234.56
1234.567   -> 拒绝最后一次输入

为什么多个 formatter 容易冲突

inputFormatters 并不是各自独立执行的。例如:

inputFormatters: [
  ThousandsSeparatorInputFormatter(),
  FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}$')),
],

第一个 formatter 会把 1000 转成 1,000,第二个 formatter 接收到的就是带逗号的 1,000。如果它的规则只允许数字和小数点,逗号就会使过滤或校验失败。

调换顺序也不一定能解决问题。千位分隔 formatter 可能会把整段文本当成整数,输入 1234.56 时,小数部分也可能被纳入分组。组合 formatter 能明确区分整数和小数部分,从而避免这类冲突。

获取实际金额

controller.text 保存的是格式化后的显示值,例如:

1,234.56

提交给后端或参与计算前,需要移除千位分隔符:

final rawAmount = controller.text.replaceAll(',', '');

如果只需要解析为普通数值:

final amount = double.tryParse(rawAmount);

支付金额不适合长期直接使用二进制浮点数做精确计算。可以将金额转换为最小货币单位,例如把元转换为分:

int? parseAmountInCents(String text) {
  final raw = text.replaceAll(',', '');

  if (!RegExp(r'^\d+(?:\.\d{0,2})?$').hasMatch(raw)) {
    return null;
  }

  final parts = raw.split('.');
  final yuan = int.parse(parts[0]);
  final fraction = parts.length == 1
      ? '00'
      : parts[1].padRight(2, '0');

  return yuan * 100 + int.parse(fraction);
}

注意事项

  • keyboardType 只控制键盘样式,不能代替输入校验。
  • formatter 需要允许 123. 这样的临时输入,否则用户无法顺利继续输入小数。
  • 示例使用 , 作为千位分隔符,. 作为小数点。支持其他地区格式时,需要同时调整分隔符、键盘输入行为和金额解析逻辑。
  • 最终提交时仍需校验非空、金额范围和业务规则,不能只依赖输入 formatter。

备注:内容仅供参考。