Flutter 框架跨平台鸿蒙开发——TextFormField输入格式化详解
TextFormField输入格式化详解

一、输入格式化概述
输入格式化(Input Formatting)是Flutter中控制用户输入的强大机制。通过TextInputFormatter,可以限制用户只能输入特定类型的字符、自动格式化输入内容、限制输入长度等。这对于创建规范、易用的表单非常重要。输入格式化不仅能够提升用户体验,还能确保输入数据的格式正确性,减少后端验证的压力和错误率。
1.1 格式化的核心场景
输入格式化的应用场景非常广泛。字符过滤用于限制用户只能输入特定类型的字符,比如只允许数字、只允许字母等,这在用户名、验证码、手机号等字段中非常有用。自动格式化用于在用户输入时自动添加格式符号,比如电话号码的分隔符、信用卡号的空格分隔等,提升输入内容的可读性。长度限制用于限制输入的最大或最小长度,确保数据符合业务要求。转换格式用于自动转换输入的大小写、格式等,减少用户的手动操作。
1.2 TextInputFormatter工作原理
TextInputFormatter的工作原理是在用户输入时,拦截输入内容,经过一系列格式化器的处理后,再将结果更新到输入框中。格式化器按照列表顺序依次处理,每个格式化器都可以修改或拒绝输入。通过组合多个格式化器,可以实现复杂的输入控制逻辑。格式化器的处理过程是同步的,必须在格式化器中完成所有处理,不能执行异步操作。
1.3 格式化器类型对比
| 格式化器类型 | 功能 | 复杂度 | 性能 | 适用场景 |
|---|---|---|---|---|
| FilteringTextInputFormatter | 字符过滤 | 低 | 高 | 简单的字符限制 |
| LengthLimitingTextInputFormatter | 长度限制 | 低 | 高 | 固定长度输入 |
| 自定义格式化器 | 自动格式化 | 中 | 中 | 电话号、卡号等 |
| 组合格式化器 | 多功能组合 | 高 | 中 | 复杂输入场景 |
| 正则表达式格式化器 | 正则匹配 | 中 | 中 | 复杂字符模式 |
二、基础格式化器
2.1 FilteringTextInputFormatter详解
FilteringTextInputFormatter是Flutter提供的字符过滤格式化器,可以根据正则表达式允许或拒绝特定的字符。它是最基础也是最常用的格式化器,可以快速实现字符类型的限制。FilteringTextInputFormatter有两个主要方法:allow和deny。allow方法只允许匹配正则表达式的字符通过,deny方法拒绝匹配正则表达式的字符。此外还提供了几个常用的预设方法,比如digitsOnly只允许数字。
// 只允许数字
FilteringTextInputFormatter.digitsOnly
// 只允许字母
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z]'))
// 只允许字母和数字
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9]'))
// 拒绝空格
FilteringTextInputFormatter.deny(RegExp(r'\s'))
// 只允许特定字符集
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9_@.]'))
FilteringTextInputFormatter的工作原理是在用户输入时,检查每个输入的字符是否符合正则表达式规则。如果符合规则,字符被允许通过;如果不符合规则,字符被拒绝,不会出现在输入框中。这种过滤是实时的,用户在输入时就能看到哪些字符被拒绝了。
2.2 数字输入格式化
数字输入格式化是最常见的需求之一,比如年龄、数量、验证码等字段只允许输入数字。使用FilteringTextInputFormatter.digitsOnly可以轻松实现这个需求。
TextFormField(
decoration: const InputDecoration(
labelText: '年龄',
hintText: '请输入年龄',
prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
)
需要注意的是,虽然使用了digitsOnly格式化器,但仍然建议设置keyboardType为TextInputType.number,这样会弹出数字键盘,提升用户体验。数字键盘更适合数字输入,能够减少输入错误的概率。
2.3 字母输入格式化
字母输入格式化用于限制只能输入字母,这在姓名、城市名等字段中很有用。通过正则表达式[a-zA-Z]可以匹配所有大小写字母。
TextFormField(
decoration: const InputDecoration(
labelText: '姓名',
hintText: '请输入姓名',
prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(),
),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z]')),
],
)
如果需要支持中文等其他语言的字母,可以修改正则表达式为更通用的模式,或者使用更宽松的过滤规则。在实际应用中,姓名字段通常需要支持多种字符,包括中文字符、字母、连字符等,这时候就需要自定义更复杂的过滤规则。
2.4 自定义字符集过滤
自定义字符集过滤是指只允许特定的字符集通过。这在用户名、SKU代码、序列号等场景中非常有用。通过正则表达式可以定义任意复杂的字符集规则。
// 用户名:只允许字母、数字、下划线
TextFormField(
decoration: const InputDecoration(
labelText: '用户名',
hintText: '字母、数字、下划线',
prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(),
),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9_]')),
],
)
// SKU代码:只允许大写字母、数字、连字符
TextFormField(
decoration: const InputDecoration(
labelText: 'SKU代码',
hintText: '大写字母、数字、连字符',
prefixIcon: Icon(Icons.inventory),
border: OutlineInputBorder(),
),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[A-Z0-9-]')),
],
)
自定义字符集过滤的核心是正则表达式的使用。正则表达式可以定义非常灵活的字符匹配规则,包括字符范围、否定、选择、重复等。开发者需要根据具体的业务需求,设计合适的正则表达式,既要满足功能需求,又要保证用户体验。
2.5 FilteringTextInputFormatter类型对比
| 类型 | 方法 | 说明 | 使用场景 |
|---|---|---|---|
| digitsOnly | FilteringTextInputFormatter.digitsOnly | 只允许数字 | 电话号码、数量 |
| allow | FilteringTextInputFormatter.allow(RegExp) | 允许指定字符 | 自定义字符集 |
| deny | FilteringTextInputFormatter.deny(RegExp) | 排除指定字符 | 过滤特殊字符 |
三、自动格式化
3.1 自动格式化原理
自动格式化是指在用户输入时,自动添加格式符号或转换格式,比如电话号码的分隔符、信用卡号的空格、货币的小数点等。自动格式化可以大大提升输入内容的可读性,减少用户手动格式化的工作。
自动格式化的核心是继承TextInputFormatter类,实现formatEditUpdate方法。这个方法接收两个参数:oldValue和newValue,分别表示格式化前后的值。在这个方法中,开发者可以根据业务需求,对输入内容进行各种处理,包括过滤字符、添加分隔符、转换格式等。
class CustomFormatter extends TextInputFormatter {
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
// 处理删除操作
if (newValue.text.isEmpty || newValue.text.length < oldValue.text.length) {
return newValue;
}
// 处理添加操作
final processedText = _processText(newValue.text);
return TextEditingValue(
text: processedText,
selection: TextSelection.collapsed(offset: processedText.length),
);
}
String _processText(String text) {
// 自定义处理逻辑
return text;
}
}
3.2 格式化处理流程
格式化处理的基本流程包括:首先判断操作类型,是删除还是添加。删除操作通常不需要格式化,直接返回新值。添加操作则需要先过滤不符合要求的字符,然后应用格式规则,最后调整光标位置,返回格式化后的值。光标位置的调整非常重要,因为格式化通常会改变文本长度,需要确保光标位置符合用户的预期。
3.3 电话号码格式化
class PhoneFormatter extends TextInputFormatter {
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
// 删除操作直接返回
if (newValue.text.isEmpty || newValue.text.length < oldValue.text.length) {
return newValue;
}
// 只允许数字
final digits = newValue.text.replaceAll(RegExp(r'[^\d]'), '');
if (digits.length > 11) return oldValue;
// 格式化为 1xx xxxx xxxx
String formatted = '';
if (digits.isNotEmpty) {
formatted += digits.substring(0, 1);
}
if (digits.length > 3) {
formatted += ' ${digits.substring(1, 4)}';
}
if (digits.length > 7) {
formatted += ' ${digits.substring(4, 8)}';
}
if (digits.length > 11) {
formatted += ' ${digits.substring(8, 11)}';
} else if (digits.length > 7 && digits.length <= 11) {
formatted += ' ${digits.substring(4)}';
}
return TextEditingValue(
text: formatted,
selection: TextSelection.collapsed(offset: formatted.length),
);
}
}
class PhoneFormattingPage extends StatelessWidget {
const PhoneFormattingPage({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('电话号码格式化'),
backgroundColor: Colors.green,
foregroundColor: Colors.white,
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextFormField(
decoration: const InputDecoration(
labelText: '手机号码',
hintText: '输入11位手机号',
prefixIcon: Icon(Icons.phone),
border: OutlineInputBorder(),
),
keyboardType: TextInputType.phone,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(11),
PhoneFormatter(),
],
),
const SizedBox(height: 24),
const Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Text(
'输入框会自动将电话号码格式化为:\n1xx xxxx xxxx\n\n例如:138 1234 5678',
style: TextStyle(fontSize: 14),
),
),
),
],
),
),
);
}
}
信用卡号格式化
class CreditCardFormatter extends TextInputFormatter {
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
// 删除操作直接返回
if (newValue.text.isEmpty || newValue.text.length < oldValue.text.length) {
return newValue;
}
// 只允许数字
final digits = newValue.text.replaceAll(RegExp(r'[^\d]'), '');
if (digits.length > 16) return oldValue;
// 格式化为 xxxx xxxx xxxx xxxx
final buffer = StringBuffer();
for (int i = 0; i < digits.length; i++) {
if (i > 0 && i % 4 == 0) {
buffer.write(' ');
}
buffer.write(digits[i]);
}
final formatted = buffer.toString();
return TextEditingValue(
text: formatted,
selection: TextSelection.collapsed(offset: formatted.length),
);
}
}
class CreditCardPage extends StatelessWidget {
const CreditCardPage({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('信用卡格式化'),
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextFormField(
decoration: const InputDecoration(
labelText: '信用卡号',
hintText: '输入16位卡号',
prefixIcon: Icon(Icons.credit_card),
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(16),
CreditCardFormatter(),
],
),
const SizedBox(height: 16),
TextFormField(
decoration: const InputDecoration(
labelText: '有效期',
hintText: 'MM/YY',
prefixIcon: Icon(Icons.calendar_today),
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(4),
CardExpiryFormatter(),
],
),
const SizedBox(height: 16),
TextFormField(
decoration: const InputDecoration(
labelText: 'CVV',
hintText: '3位安全码',
prefixIcon: Icon(Icons.security),
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(3),
],
obscureText: true,
),
],
),
),
);
}
}
class CardExpiryFormatter extends TextInputFormatter {
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
// 删除操作直接返回
if (newValue.text.isEmpty || newValue.text.length < oldValue.text.length) {
return newValue;
}
// 只允许数字
final digits = newValue.text.replaceAll(RegExp(r'[^\d]'), '');
if (digits.length > 4) return oldValue;
// 格式化为 MM/YY
String formatted = '';
if (digits.isNotEmpty) {
formatted += digits.substring(0, 1);
}
if (digits.length > 1) {
formatted += digits.substring(1, 2);
}
if (digits.length > 2) {
formatted += '/';
formatted += digits.substring(2, 3);
}
if (digits.length > 3) {
formatted += digits.substring(3, 4);
}
return TextEditingValue(
text: formatted,
selection: TextSelection.collapsed(offset: formatted.length),
);
}
}
四、货币格式化
4.1 货币格式化原理
货币格式化是指在用户输入金额时,自动添加货币符号、千分位分隔符、小数点等,使输入的金额符合财务规范。货币格式化在电商、金融、财务等应用中非常重要,可以确保金额数据的准确性和可读性。
货币格式化的关键处理包括:货币符号的添加和定位、千分位分隔符的插入、小数位数的控制、负数的处理等。不同国家和地区有不同的货币格式化规范,开发者需要根据目标市场选择合适的格式化方式。
4.2 金额输入格式化
class CurrencyFormatter extends TextInputFormatter {
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
// 删除操作直接返回
if (newValue.text.isEmpty || newValue.text.length < oldValue.text.length) {
return newValue;
}
// 移除所有非数字
final digits = newValue.text.replaceAll(RegExp(r'[^\d]'), '');
if (digits.isEmpty) return newValue;
// 转换为整数,除以100得到金额
final value = int.tryParse(digits) ?? 0;
final formatted = (value / 100).toStringAsFixed(2);
return TextEditingValue(
text: formatted,
selection: TextSelection.collapsed(offset: formatted.length),
);
}
}
class CurrencyFormattingPage extends StatefulWidget {
const CurrencyFormattingPage({super.key});
State<CurrencyFormattingPage> createState() => _CurrencyFormattingPageState();
}
class _CurrencyFormattingPageState extends State<CurrencyFormattingPage> {
final _controller = TextEditingController();
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('货币格式化'),
backgroundColor: Colors.purple,
foregroundColor: Colors.white,
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextFormField(
controller: _controller,
decoration: const InputDecoration(
labelText: '金额',
hintText: '输入金额',
prefixText: '¥ ',
suffixIcon: Icon(Icons.attach_money),
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
CurrencyFormatter(),
],
),
const SizedBox(height: 24),
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'输入说明',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
const Text('• 输入数字会自动格式化为货币格式'),
const Text('• 小数点后固定两位'),
const Text('• 例如:输入12345显示为123.45'),
const SizedBox(height: 16),
if (_controller.text.isNotEmpty)
Text(
'当前金额:¥${_controller.text}',
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.green,
),
),
],
),
),
),
],
),
),
);
}
}
五、长度限制
5.1 LengthLimitingTextInputFormatter详解
LengthLimitingTextInputFormatter是Flutter提供的长度限制格式化器,可以限制输入框的最大字符数。当输入达到最大长度时,用户将无法继续输入更多字符。这对于验证码、用户名、身份证号等有固定长度要求的字段非常有用。
LengthLimitingTextInputFormatter(6) // 限制为6个字符
LengthLimitingTextInputFormatter的工作原理是在每次输入后检查文本长度,如果超过限制,就拒绝这次输入。需要注意的是,这个格式化器只限制输入,不限制删除,用户可以随时删除已有内容。
5.2 动态长度限制
有些场景下,输入框的最大长度不是固定的,而是根据某些条件动态变化的。比如根据用户选择的类型决定不同的长度限制,或者根据其他输入的值来调整当前字段的长度限制。
class DynamicLengthFormatter extends TextInputFormatter {
final int maxLength;
DynamicLengthFormatter(this.maxLength);
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
return LengthLimitingTextInputFormatter(maxLength).formatEditUpdate(
oldValue,
newValue,
);
}
}
// 使用时动态创建
inputFormatters: [
DynamicLengthFormatter(currentMaxLength),
]
动态长度限制需要在状态变化时重新创建格式化器,确保使用正确的最大长度。这种模式在复杂的表单场景中很有用,可以根据业务逻辑灵活调整输入限制。
5.3 LengthLimitingTextInputFormatter
class LengthLimitingPage extends StatelessWidget {
const LengthLimitingPage({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('长度限制'),
backgroundColor: Colors.teal,
foregroundColor: Colors.white,
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextFormField(
decoration: InputDecoration(
labelText: '验证码',
hintText: '6位数字验证码',
prefixIcon: const Icon(Icons.verified_user),
suffixIcon: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {},
),
],
),
border: const OutlineInputBorder(),
counterText: '6/6',
),
keyboardType: TextInputType.number,
maxLength: 6,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(6),
],
),
const SizedBox(height: 24),
TextFormField(
decoration: InputDecoration(
labelText: '备注',
hintText: '最多100个字符',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
maxLines: 3,
maxLength: 100,
inputFormatters: [
LengthLimitingTextInputFormatter(100),
],
),
const SizedBox(height: 24),
Card(
color: Colors.teal.shade50,
child: const Padding(
padding: EdgeInsets.all(16),
child: Text(
'LengthLimitingTextInputFormatter\n\n限制输入框的最大字符数。\n当达到限制时,用户无法继续输入。',
),
),
),
],
),
),
);
}
}
六、大小写转换
6.1 大小写转换原理
大小写转换格式化器用于在用户输入时自动转换字符的大小写,包括全部大写、全部小写、首字母大写等。这对于SKU代码、用户名、城市名等有统一大小写要求的字段非常有用。
大小写转换的核心是在formatEditUpdate方法中,对输入的文本应用大小写转换函数。Dart的String类提供了toUpperCase()和toLowerCase()方法,可以方便地进行大小写转换。首字母大写则需要手动处理第一个字符和剩余字符。
// 全部大写
class UpperCaseFormatter extends TextInputFormatter {
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
return TextEditingValue(
text: newValue.text.toUpperCase(),
selection: newValue.selection,
);
}
}
// 全部小写
class LowerCaseFormatter extends TextInputFormatter {
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
return TextEditingValue(
text: newValue.text.toLowerCase(),
selection: newValue.selection,
);
}
}
// 首字母大写
class CapitalizeFormatter extends TextInputFormatter {
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
if (newValue.text.isEmpty) return newValue;
final text = newValue.text;
final formatted = text[0].toUpperCase() + text.substring(1).toLowerCase();
return TextEditingValue(
text: formatted,
selection: newValue.selection,
);
}
}
6.2 文本转换格式化器
class UpperCaseFormatter extends TextInputFormatter {
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
return TextEditingValue(
text: newValue.text.toUpperCase(),
selection: newValue.selection,
);
}
}
class LowerCaseFormatter extends TextInputFormatter {
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
return TextEditingValue(
text: newValue.text.toLowerCase(),
selection: newValue.selection,
);
}
}
class CapitalizeFormatter extends TextInputFormatter {
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
if (newValue.text.isEmpty) return newValue;
final text = newValue.text;
final formatted = text[0].toUpperCase() + text.substring(1).toLowerCase();
return TextEditingValue(
text: formatted,
selection: newValue.selection,
);
}
}
class CaseConversionPage extends StatelessWidget {
const CaseConversionPage({super.key});
Widget build(BuildContext context) {
return Scaffold(
AppBar: AppBar(
title: const Text('大小写转换'),
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextFormField(
decoration: const InputDecoration(
labelText: '大写输入',
hintText: '自动转换为大写',
border: OutlineInputBorder(),
),
inputFormatters: [UpperCaseFormatter()],
),
const SizedBox(height: 16),
TextFormField(
decoration: const InputDecoration(
labelText: '小写输入',
hintText: '自动转换为小写',
border: OutlineInputBorder(),
),
inputFormatters: [LowerCaseFormatter()],
),
const SizedBox(height: 16),
TextFormField(
decoration: const InputDecoration(
labelText: '首字母大写',
hintText: '自动首字母大写',
border: OutlineInputBorder(),
),
inputFormatters: [CapitalizeFormatter()],
),
],
),
),
);
}
}
七、组合格式化器
7.1 组合格式化器原理
组合格式化器是指将多个简单的格式化器组合在一起,实现复杂的格式化逻辑。通过组合,可以复用已有的格式化器,避免重复代码,提高开发效率和可维护性。组合格式化器有两种方式:一种是在inputFormatters列表中添加多个格式化器,另一种是创建一个自定义格式化器,在内部调用其他格式化器。
列表方式的优势是简单直观,每个格式化器独立工作。但如果格式化器之间有依赖关系,可能无法满足需求。自定义组合方式可以精确控制格式化器的调用顺序和逻辑,但实现相对复杂。
// 列表方式
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(11),
PhoneFormatter(),
]
// 自定义组合方式
class UsernameFormatter extends TextInputFormatter {
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
// 只允许字母、数字和下划线
final filtered = FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9_]'));
final step1 = filtered.formatEditUpdate(oldValue, newValue);
// 限制长度
final lengthLimited = LengthLimitingTextInputFormatter(16);
final step2 = lengthLimited.formatEditUpdate(oldValue, step1);
// 转换为小写
return TextEditingValue(
text: step2.text.toLowerCase(),
selection: step2.selection,
);
}
}
7.2 多格式化器组合
class UsernameFormatter extends TextInputFormatter {
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
// 只允许字母、数字和下划线
final filtered = FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9_]'));
final step1 = filtered.formatEditUpdate(oldValue, newValue);
// 限制长度
final lengthLimited = LengthLimitingTextInputFormatter(16);
final step2 = lengthLimited.formatEditUpdate(oldValue, step1);
// 转换为小写
return TextEditingValue(
text: step2.text.toLowerCase(),
selection: step2.selection,
);
}
}
class CombinedFormattersPage extends StatelessWidget {
const CombinedFormattersPage({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('组合格式化器'),
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextFormField(
decoration: const InputDecoration(
labelText: '用户名',
hintText: '字母、数字、下划线,最多16位,自动小写',
prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(),
),
inputFormatters: [
UsernameFormatter(),
],
),
const SizedBox(height: 24),
Card(
color: Colors.indigo.shade50,
child: const Padding(
padding: EdgeInsets.all(16),
child: Text(
'UsernameFormatter组合了多个格式化器:\n\n'
'1. 只允许字母、数字、下划线\n'
'2. 限制最大长度为16\n'
'3. 自动转换为小写',
),
),
),
],
),
),
);
}
}
八、格式化器性能优化
8.1 性能优化原则
格式化器在每次输入时都会被调用,因此性能优化非常重要。如果格式化器中有耗时操作,会导致输入卡顿,严重影响用户体验。格式化器的性能优化应该遵循以下原则:
避免复杂正则表达式:复杂的正则表达式会增加处理时间,特别是对于长文本。应该尽量使用简单高效的正则表达式,或者使用其他方式实现相同的功能。如果必须使用复杂正则,可以考虑预先编译正则表达式,避免每次都重新编译。
减少字符串操作:字符串操作在Dart中是相对昂贵的操作,特别是大量的字符串拼接、替换等。应该尽量减少不必要的字符串操作,或者使用更高效的字符串操作方式,比如StringBuffer。
复用格式化器实例:格式化器应该是无状态的,可以安全地复用。不要每次重建时都创建新的格式化器实例,应该在initState中创建并复用,这样可以避免不必要的对象创建和垃圾回收。
优化输入响应:格式化器的处理应该尽可能快,避免阻塞UI线程。如果必须执行复杂计算,可以考虑延迟处理或者使用后台线程。
8.2 格式化器缓存策略
对于一些计算量大的格式化操作,可以使用缓存策略来优化性能。比如将格式化的结果缓存起来,下次遇到相同的输入时直接返回缓存的结果,而不需要重新计算。
class CachedFormatter extends TextInputFormatter {
final Map<String, String> _cache = {};
final int _maxCacheSize = 100;
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
final input = newValue.text;
// 检查缓存
if (_cache.containsKey(input)) {
return TextEditingValue(
text: _cache[input]!,
selection: newValue.selection,
);
}
// 执行格式化
final formatted = _format(input);
// 存入缓存
_cache[input] = formatted;
if (_cache.length > _maxCacheSize) {
_cache.remove(_cache.keys.first);
}
return TextEditingValue(
text: formatted,
selection: newValue.selection,
);
}
String _format(String text) {
// 格式化逻辑
return text;
}
}
缓存策略需要注意缓存的失效和清理。当输入内容变化很大时,缓存可能不再有效,应该及时清理。缓存的规模也需要控制,避免占用过多内存。一般来说,缓存大小限制在100-1000条之间比较合适。
8.3 格式化器测试策略
格式化器的测试非常重要,因为格式化错误会导致用户无法正确输入数据。测试应该覆盖各种输入场景,包括正常输入、边界情况、特殊字符、删除操作、粘贴操作等。
void main() {
test('PhoneFormatter formats correctly', () {
final formatter = PhoneFormatter();
// 正常输入
final result1 = formatter.formatEditUpdate(
const TextEditingValue(text: '', selection: TextSelection.collapsed(offset: 0)),
const TextEditingValue(text: '13812345678', selection: TextSelection.collapsed(offset: 11)),
);
expect(result1.text, equals('138 1234 5678'));
// 删除操作
final result2 = formatter.formatEditUpdate(
const TextEditingValue(text: '138 1234 5678', selection: TextSelection.collapsed(offset: 13)),
const TextEditingValue(text: '138 1234 567', selection: TextSelection.collapsed(offset: 12)),
);
expect(result2.text, equals('138 1234 567'));
// 超过最大长度
final result3 = formatter.formatEditUpdate(
const TextEditingValue(text: '138 1234 5678', selection: TextSelection.collapsed(offset: 13)),
const TextEditingValue(text: '138 1234 56789', selection: TextSelection.collapsed(offset: 14)),
);
expect(result3.text, equals('138 1234 5678')); // 保持原值
});
}
测试用例应该尽可能全面,覆盖所有可能的输入场景。特别是边界情况,比如空输入、单字符输入、最大长度输入、超过最大长度等。删除操作和粘贴操作也应该单独测试,因为这些操作的处理逻辑可能与普通输入不同。
九、格式化器最佳实践
9.1 实践总结
9.2 关键实践要点
-
保持格式化器简单:避免在格式化器中执行复杂的逻辑,保持高效。复杂的格式化器可能导致输入卡顿,影响用户体验。如果必须执行复杂计算,应该考虑使用缓存或延迟处理策略。
-
正确处理删除操作:删除操作应该直接返回新值,不做格式化。删除操作通常不需要格式化,强制格式化可能会导致用户体验下降。应该在格式化器中区分添加和删除操作,分别处理。
-
保持光标位置:格式化后要正确设置光标位置,避免用户输入混乱。格式化通常会改变文本长度,需要根据格式化的字符数量调整光标位置,确保光标位置符合用户的预期。
-
组合使用格式化器:通过组合多个简单格式化器实现复杂功能。组合可以提高代码的可读性和可维护性,避免重复代码。格式化器之间的顺序很重要,应该根据业务逻辑合理安排。
-
全面测试:测试各种输入场景,包括输入、删除、粘贴等操作。测试应该覆盖所有可能的输入场景,特别是边界情况和特殊字符。单元测试、集成测试、手动测试都应该进行。
-
提供用户反馈:对于格式化操作,应该给用户明确的反馈,让用户知道发生了什么。比如自动添加的分隔符、格式化的提示等。良好的用户反馈可以减少用户的困惑。
-
考虑国际化:格式化规则可能因地区而异,比如货币符号、日期格式等。应该根据用户的地区设置选择合适的格式化规则,提供本地化的体验。
-
处理异常情况:格式化器应该能够处理各种异常情况,比如空输入、无效字符、超长输入等。异常情况下应该返回合理的默认值,而不是抛出错误或崩溃。
9.3 常见问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 输入卡顿 | 格式化器中有耗时操作 | 优化正则表达式,使用缓存 |
| 光标跳变 | 格式化后未正确设置光标位置 | 根据格式化后的长度调整光标 |
| 删除格式化 | 格式化器未区分删除操作 | 检测删除操作,直接返回 |
| 粘贴错误 | 未处理粘贴的批量输入 | 支持批量文本的格式化 |
| 内存泄漏 | 格式化器中引用了外部状态 | 确保格式化器无状态 |
| 正则性能差 | 使用了复杂的正则表达式 | 简化正则或使用其他方法 |
| 格式冲突 | 多个格式化器规则冲突 | 调整格式化器顺序或合并规则 |
| 国际化问题 | 未考虑地区差异 | 根据地区选择格式化规则 |
9.4 总结
输入格式化是Flutter中控制用户输入的强大机制,通过TextInputFormatter可以实现字符过滤、自动格式化、长度限制、大小写转换等多种功能。合理使用格式化器可以大大提升用户体验,确保输入数据的格式正确性。
在开发格式化器时,应该遵循最佳实践,包括保持简单、正确处理删除操作、保持光标位置、组合使用格式化器、全面测试等。同时还应该注意性能优化和国际化支持,确保格式化器在各种场景下都能正常工作。
通过掌握格式化器的使用技巧和原理,可以构建出既强大又易用的表单输入系统,为用户提供优秀的输入体验。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)