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。
备注:内容仅供参考。
