Flutter应用TextInputFormatter实现带分隔符的文本输入

在手机app开发经常用到手机号、身份证号、银行卡号等长串字符的输入,为增加客户体验感,往往对输入字符作分割,官方提供的类FilteringTextInputFormatter和LengthLimitingTextInputFormatter不能完美满足需求,对抽象类TextInputFormatter继承后自定义个性化的输入格式类。

​ 涉及到知识点有以下:

  • 抽象类TextInputFormatter
  • 类TextEditingValue
  • 正则类RegExp
  • 类TextRange和类TextSelection
  • 类TextPosition
  • 枚举TextAffinity

​ 复写抽象类TextInputFormatter的方法formatEditUpdate即可实现继承,方法的定义中有两个参数,返回TextEditingValue。

//需要override,这是重点
formatEditUpdate(
	TextEditingValue oldValue, 
	TextEditingValue newValue
)TextEditingValue

​ TextInputFormatter有两个子类是FilteringTextInputFormatter和LengthLimitingTextInputFormatter,在常规需求结合正则基本满足需求,用法不在这里赘述,抽象类还有一个静态方法withFunction,其定义:

withFunction(
TextInputFormatFunction formatFunction
)TextInputFormatter
//预定义TextInputFormatFunction
TextInputFormatFunction = TextEditingValue Function(
TextEditingValue oldValue,
TextEditingValue newValue
)  

也可以用类的静态方法直接返回TextEditingValue,为后期使用,并且可以方便收录到自己的库中管理,这里使用继承的方式写个自定义类DividerInputFormatter,分隔符默认空白符。

​ 输入格式的自定义关键类是TextEditingValue,其构造函数

TextEditingValue({
String text: '',
//显示的文本
TextSelection selection: const TextSelection.collapsed(offset: -1),
//选中的范围,collapsed为开始和结束一致是无选中的,即光标位置,-1和0是最左侧位置
TextRange composing: TextRange.empty
//范围会有一条下划线,如TextRange(start: 8, end: 16)
})

类TextRange和TextSelection是父子关系,可以简单看一下构造函数

//TextRange构造
TextRange({required int start, required int end})
//没有范围选中,开始和结束一致,光标的位置
TextRange.collapsed(int offset)
//常规方法
textAfter(String text)String
textBefore(String text)String
textInside(String text)String
TextSelection({
	required int baseOffset, //开始位置
	required int extentOffset, //结束位置
	TextAffinity affinity = TextAffinity.downstream, 
	bool isDirectional = false
	//是否消除了其基础和范围的歧义
})

/*
TextSelection.fromPosition是把TextSelection对象
的 extentOffset 和 baseOffset赋值给了同一个数,
而TextSelection.collapsed又是TextSelection.fromPosition
的简化版
*/
TextSelection.collapsed({
	required int offset, 
	TextAffinity affinity = TextAffinity.downstream
})
//光标移动到第几位,-1或0为第一个
TextSelection.fromPosition(TextPosition position)
//
TextPosition({
	required int offset, 
	//枚举TextAffinity,换行后光标在上还是下
	TextAffinity affinity = TextAffinity.downstream
})

​ 对上面的涉及到类能看明白理解作用,就开始进入正式话题,需求是输入自动分割,默认空白符,分隔符是短线,输入后界面显示:135-1234-1234,随着字数增加光标位置受到影响,可以分为两种情况来分析,第一种光标在最右侧也就是文本尾部,这是常见的情况,那么只要对override方法formatEditUpdate的入参newValue.text作分隔符的适当位置插入,光标始终在text.length的位置,返回对应TextEditingValue

///光标在文字最右侧(尾部)的情况,光标始终在最后
if (cursorPosition >= newValue.text.length) {
  return TextEditingValue(
    text: allTextDeal,//文本带分隔符
    selection: TextSelection.collapsed(offset: allTextDeal.length),
    //光标在最右侧
  );
}

另一种情况,文本框现在是:136-1234-1,光标在4后面的位置cursorPosition=8,随即输入5,文本框显示136-12345-1,光标在5后面cursorPosition=9,Flutter给你的newValue值里text为136-12345-1,newValue.selection.baseOffset(光标位置)为9,当你拿到newValue处理完新文本应该是136-1234-51,原来光标位置被新增的分隔符占位,光标应该在5后面,因此cursorPosition++为光标新位置值。

​ 光标不在最右侧时输入,增加的一个字符是在左侧,Flutter给的参数newValue的是新增后的光标位置,所有不用额外加光标位置值cursorPosition,只有一种特殊情况光标左侧文本(相对原来带分隔符的文本)会增加一个分隔符,而且新增的分隔符位置必定在cursorPosition位置上,因此需要判断带分隔符的新文本在cursorPosition位置上的字符是否等于分隔符,如果是则光标后移一位cursorPosition++,代码如下

///光标不在文字尾部的情况
//如果原光标位置变为分割符,则说明左侧增加一位,光标也加一位
if (allTextDeal.substring(cursorPosition - 1, cursorPosition) == pattern) {
  cursorPosition++;
}
return TextEditingValue(
  text: allTextDeal,
  selection: TextSelection.collapsed(offset: cursorPosition),
);

​ 根据思路撸代码,落笔较匆忙,代码也较随意,类名没有完全按照官方命名规则,嫌名字太长。如有错误还请斧正,自定义类DividerInputFormatter代码:

/*
 * create by 行云流水
 * 空格符--RegExp(r'\s')
 * 注意点--需要对分隔符允许输入
 * 注意点--后期对文本内容清除分隔符
 * 官方类--LengthLimitingTextInputFormatter,FilteringTextInputFormatter
 * 只允许数字和X和x和空格--FilteringTextInputFormatter.allow(RegExp(r'[0-9Xx\s]')),
 */
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() => runApp(MaterialApp(home: DividerFormatterPage()));

class DividerFormatterPage extends StatelessWidget {
  const DividerFormatterPage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) => Scaffold(body: _buildBody());

  _buildBody() {
    return Padding(
      padding: EdgeInsets.symmetric(horizontal: 15, vertical: 100),
      child: TextField(
        inputFormatters: [
          DividerInputFormatter(pattern: '-'),
          FilteringTextInputFormatter.allow(RegExp(r'[0-9\s\-]')),
          LengthLimitingTextInputFormatter(13),
        ],
      ),
    );
  }
}

class DividerInputFormatter extends TextInputFormatter {
  final int first, rear; //第一个分割位数,后面分割位,,数
  final String pattern; //分割符

  DividerInputFormatter({this.first = 3, this.rear = 4, this.pattern = ' '});

  
  TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
    //不含分隔符的文本
    String allTextPure = newValue.text.replaceAll(RegExp(pattern), '');
    //处理后含分隔符的文本
    String allTextDeal = '';
    //光标位置
    int cursorPosition = newValue.selection.baseOffset;
    for (int i = 0; i < allTextPure.length; i++) {
      if ((i == first || (i - first) % rear == 0) && allTextPure[i] != pattern) {
        allTextDeal = '$allTextDeal$pattern';
      }
      allTextDeal += allTextPure[i];
    }

    ///光标在文字最右侧(尾部)的情况,光标始终在最后
    if (cursorPosition >= newValue.text.length) {
      return TextEditingValue(
        text: allTextDeal,
        selection: TextSelection.collapsed(offset: allTextDeal.length),
      );
    }

    ///光标不在文字尾部的情况
    //如果原光标位置变为分割符,则说明左侧增加一位,光标也加一位
    if (allTextDeal.substring(cursorPosition - 1, cursorPosition) == pattern) {
      cursorPosition++;
    }
    return TextEditingValue(
      text: allTextDeal,
      selection: TextSelection.collapsed(offset: cursorPosition),
    );
  }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Flutter实现本地数据持久化有多种方式,比如使用shared_preferences、sqflite、path_provider等插件。下面以shared_preferences为例,介绍如何实现输入数据的本地存储和读取。 首先,在`pubspec.yaml`文件中添加依赖: ```yaml dependencies: shared_preferences: ^2.0.6 ``` 然后,在需要存储数据的页面,我们可以使用如下代码将输入的数据存储到本地: ```dart import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; class InputPage extends StatefulWidget { @override _InputPageState createState() => _InputPageState(); } class _InputPageState extends State<InputPage> { TextEditingController _textEditingController = TextEditingController(); String _inputData = ''; @override void initState() { super.initState(); _loadData(); } @override void dispose() { _textEditingController.dispose(); super.dispose(); } Future<void> _loadData() async { SharedPreferences prefs = await SharedPreferences.getInstance(); setState(() { _inputData = prefs.getString('input_data') ?? ''; _textEditingController.text = _inputData; }); } Future<void> _saveData(String data) async { SharedPreferences prefs = await SharedPreferences.getInstance(); await prefs.setString('input_data', data); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('输入页面'), ), body: Padding( padding: EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '请输入数据:', style: TextStyle( fontSize: 16.0, ), ), SizedBox(height: 10.0), TextField( controller: _textEditingController, onChanged: (value) { _saveData(value); }, decoration: InputDecoration( hintText: '请输入', ), ), SizedBox(height: 20.0), Text( '输入的数据是:$_inputData', style: TextStyle( fontSize: 16.0, ), ), ], ), ), ); } } ``` 在上面的代码中,我们首先在`initState`方法中调用了`_loadData`方法,从本地存储中加载数据并将其设置到文本输入框中。`_loadData`方法中,我们使用`SharedPreferences`插件获取实例,并调用`getString`方法获取之前存储的数据,如果之前没有存储数据则返回默认值''。然后,我们在`TextField`的`onChanged`回调中调用`_saveData`方法,将输入的数据保存到本地。`_saveData`方法中,我们同样使用`SharedPreferences`插件获取实例,并调用`setString`方法将数据存储到本地。 这样,输入的数据就可以实现本地持久化了。每次打开应用时,都会从本地存储中加载之前输入的数据,并将其设置到文本输入框中。如果用户修改了输入的数据,就会自动保存到本地。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值