TextFormField输入格式化详解

在这里插入图片描述

一、输入格式化概述

输入格式化(Input Formatting)是Flutter中控制用户输入的强大机制。通过TextInputFormatter,可以限制用户只能输入特定类型的字符、自动格式化输入内容、限制输入长度等。这对于创建规范、易用的表单非常重要。输入格式化不仅能够提升用户体验,还能确保输入数据的格式正确性,减少后端验证的压力和错误率。

1.1 格式化的核心场景

输入格式化

字符过滤

自动格式化

长度限制

转换格式

只允许数字

只允许字母

只允许特定字符

排除特定字符

电话号码

信用卡号

货币金额

日期时间

最大长度

最小长度

动态长度

字数统计

大写转换

小写转换

首字母大写

自定义转换

输入格式化的应用场景非常广泛。字符过滤用于限制用户只能输入特定类型的字符,比如只允许数字、只允许字母等,这在用户名、验证码、手机号等字段中非常有用。自动格式化用于在用户输入时自动添加格式符号,比如电话号码的分隔符、信用卡号的空格分隔等,提升输入内容的可读性。长度限制用于限制输入的最大或最小长度,确保数据符合业务要求。转换格式用于自动转换输入的大小写、格式等,减少用户的手动操作。

1.2 TextInputFormatter工作原理

控制器 格式化器列表 输入框 用户 控制器 格式化器列表 输入框 用户 alt [有多个格式化器] 输入字符 调用格式化器1 过滤/转换字符 返回处理后的值 调用格式化器2 进一步处理 返回最终值 更新文本值 触发重建显示新值

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 关键实践要点

  1. 保持格式化器简单:避免在格式化器中执行复杂的逻辑,保持高效。复杂的格式化器可能导致输入卡顿,影响用户体验。如果必须执行复杂计算,应该考虑使用缓存或延迟处理策略。

  2. 正确处理删除操作:删除操作应该直接返回新值,不做格式化。删除操作通常不需要格式化,强制格式化可能会导致用户体验下降。应该在格式化器中区分添加和删除操作,分别处理。

  3. 保持光标位置:格式化后要正确设置光标位置,避免用户输入混乱。格式化通常会改变文本长度,需要根据格式化的字符数量调整光标位置,确保光标位置符合用户的预期。

  4. 组合使用格式化器:通过组合多个简单格式化器实现复杂功能。组合可以提高代码的可读性和可维护性,避免重复代码。格式化器之间的顺序很重要,应该根据业务逻辑合理安排。

  5. 全面测试:测试各种输入场景,包括输入、删除、粘贴等操作。测试应该覆盖所有可能的输入场景,特别是边界情况和特殊字符。单元测试、集成测试、手动测试都应该进行。

  6. 提供用户反馈:对于格式化操作,应该给用户明确的反馈,让用户知道发生了什么。比如自动添加的分隔符、格式化的提示等。良好的用户反馈可以减少用户的困惑。

  7. 考虑国际化:格式化规则可能因地区而异,比如货币符号、日期格式等。应该根据用户的地区设置选择合适的格式化规则,提供本地化的体验。

  8. 处理异常情况:格式化器应该能够处理各种异常情况,比如空输入、无效字符、超长输入等。异常情况下应该返回合理的默认值,而不是抛出错误或崩溃。

9.3 常见问题与解决方案

问题 原因 解决方案
输入卡顿 格式化器中有耗时操作 优化正则表达式,使用缓存
光标跳变 格式化后未正确设置光标位置 根据格式化后的长度调整光标
删除格式化 格式化器未区分删除操作 检测删除操作,直接返回
粘贴错误 未处理粘贴的批量输入 支持批量文本的格式化
内存泄漏 格式化器中引用了外部状态 确保格式化器无状态
正则性能差 使用了复杂的正则表达式 简化正则或使用其他方法
格式冲突 多个格式化器规则冲突 调整格式化器顺序或合并规则
国际化问题 未考虑地区差异 根据地区选择格式化规则

9.4 总结

输入格式化是Flutter中控制用户输入的强大机制,通过TextInputFormatter可以实现字符过滤、自动格式化、长度限制、大小写转换等多种功能。合理使用格式化器可以大大提升用户体验,确保输入数据的格式正确性。

在开发格式化器时,应该遵循最佳实践,包括保持简单、正确处理删除操作、保持光标位置、组合使用格式化器、全面测试等。同时还应该注意性能优化和国际化支持,确保格式化器在各种场景下都能正常工作。

通过掌握格式化器的使用技巧和原理,可以构建出既强大又易用的表单输入系统,为用户提供优秀的输入体验。

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐