Flutter — 文本为什么可以被编辑?如何自定义编辑的行为?

简介: 文字是人类用符号记录表达信息以传之久远的方式和工具。那么在Flutter中,文本为什么可以被编辑呢?我们又该如何自定义文本编辑的行为呢?

通过阅读本文,您将了解到

  1. 知道在Flutter中关于文本的整体逻辑;
  2. 可编辑文本包含哪些内容;
  3. 如何自定义可编辑行为;
  4. 如何优雅的实现文本表单。

前言:

上一篇文章中,我们讲解了Flutter文本的组成部分和Flutter 文本渲染到屏幕上的逻辑。文本的输出我们已经分析完成了,那么文本的输入又是怎么样的呢?在Flutter中,我们知道文本的输入可以通过TextField等组件将文字输入到App中,但是它背后的原理是什么呢,为什么可以编辑文本呢?在这一篇文章中,就让我们从Flutter的可编辑文本的实现原理,再到自定义可编辑的文本...希望能对你认识Flutter的文本编辑有所帮助。

注:本文的涉及较多文本编辑的核心逻辑,和大量的功能实践,建议收藏!

TextField背后的存在

1.png
在开始具体的分析前,大家可以先看下上面这张流程图,如果你和我一样,好奇Flutter的文本渲染和文本编辑之间有哪些联系,那么当你看完上图后会发现,从TextPainter开始就是相同的了,这也意味着,我们可以只分析TextPainter上层的部分。

组件层

每当我们想要在Flutter中进行文本的输入或者编辑时,我们通常会首先想到TextField这个组件,除了iOS和macOS外的系统都会使用它,它是属于Material库的一部分,和它对应的是Cupertino库中的CupertinoTextField

2.png

3.png

除了这两个组件,大家可能还会想到TextFormField这个组件,但是它其实只是一个能帮助你更快速的实现一些类似保存逻辑的功能,它本质上还是TextField

class TextFormField extends FormField<String> {
  TextFormField({ })
    return UnmanagedRestorationScope(
      ...
       child: TextField(
           .....
                ),
            );
        },
    );
}

TextFieldCupertinoTextField它们是有状态的组件。它们需要处理焦点、手势、鼠标悬停...等内容。但是无论是使用 TextField 还是 CupertinoTextField 最后都会创建EditableText

  • 来自TextField 类 - 材料库 - Dart API

    EditableText,它是 TextField核心的原始文本编辑控件。 EditableText小部件很少直接使用,除非您正在实现完全不同的设计语言,例如 Cupertino。

当本文写到这里时,发现郭哥已经很详细的分析了TextField的内部原理,对于TextField的内部原理本文就不过多赘述了。

推荐阅读:Flutter 快速解析 TextField 的内部原理 @恋猫de小郭

如何自定义编辑的行为?

自定义的编辑行为主要为下面几块部分

  • 格式化输入框的数据
  • 自定义文本选中范围
  • 自定义光标位置
通过 TextInputFormatter格式化输入框的数据

在日常开发中,我们经常会碰到需要用户提交身份证信息,电话号码,银行卡号码等需求,在输入框中获取有效的格式化的数据是必不可少的。在自定义后,能使提交流程简化,大幅度减少错误信息,提高用户体验。

在Flutter中可以使用TextInputFormatter这个类获取到有效的格式化的数据,TextField可以使用它在编辑文本时纠正文本格式。

基本使用:

Flutter提供了两个基础的TextInputFormatter

  • FilteringTextInputFormatter — 创建一个格式化工具,常与正则表达式一起使用。
  • LengthLimitingTextInputFormatter — 只允许输入一定数量的字符。
TextFormField(
      inputFormatters: [
        //.allow是只允许输入xx
        //.deny是不允许输入xx
        //FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z]')),不允许输入字母
        FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z]')),//只允许输入字母
        LengthLimitingTextInputFormatter(5) //只允许输入五个字符
      ],
)

4.gif

自定义TextInputFormatter

在日常开发时,仅仅只靠正则表达式是不够的,我们需要针对需求创建自定义的格式化工具。我们可以扩展TextInputFormatter实现formatEditUpdate方法来实现自定义的TextInputFormatter

为了有更多的扩展性,此处实现一个格式化模板,只需传入xxx-xxxx-xxxx的手机号格式即可,若有其他需求,如银行卡号码等,只需传入它的格式。

class AllFormatter extends TextInputFormatter {
  final String model; //格式
  final String? separator; //识别格式后中间的分割字符
  
  AllFormatter({
    required this.model,
    required this.separator,
  });
  
  //通过TextEditingValue可以读取和写入文本
  @override
  TextEditingValue formatEditUpdate(
      TextEditingValue oldValue, TextEditingValue newValue) {
    var oldText = oldValue.text;
    var newText = newValue.text;
    //判断是否有输入文本
    if (newText.isNotEmpty) {
      if (newText.length > oldText.length) {
        if (newText.length > model.length) return oldValue;
        if (newText.length < model.length &&
            model[newText.length - 1] == separator) {
          return TextEditingValue(
        //text代表用户输入后的文本(用户自己输入的,经过程序逻辑处理后的文本)
              text:
                  "$oldText$separator${newText.substring(newText.length - 1)}",
        //通过selection你可以知道当前所选择的光标位置和选择范围
              selection:
                  TextSelection.collapsed(offset: newValue.selection.end + 1));
        }
      }
    }
    return newValue;
  }
}

使用

TextField(
      inputFormatters: [AllFormatter(model: "xxx-xxxx-xxxx", separator: '-')],
);

5.gif

若需求改变只需要传入不同的model,和separator就可以了。

在开发需求中,碰到需要限制金额等,限制输入数字大小的需求时,我们也一样可以通过自定义TextInputFormatter来实现。设置好限制的大小后,如果输入的值超过这个数字,则值自动等于限制的大小。

class MaxInputFormatter extends TextInputFormatter {
  final double maxValue; //需要限制的大小
​
  MaxInputFormatter({required this.maxValue});
​
  @override
  TextEditingValue formatEditUpdate(
      TextEditingValue oldValue, TextEditingValue newValue) {
    String newText = newValue.text;
    //通过double.tryParse() 检查字符串是否为数字字符串。
    //如果返回值等于null,则输入不是数字字符串。
    double? value = double.tryParse(newText);
    if (value == null) {
      return TextEditingValue(text: newText, selection: newValue.selection);
    }
    if (value > maxValue) {
      newText = maxValue.toString();
    }
    return TextEditingValue(text: newText, selection: newValue.selection);
  }
}

6.gif

通过TextInputFormatter我们可以很容易的实现各种格式化的工具,还有很多的功能大家可以自行探索。

自定义文本选中范围

自定义文本可以很大程度上提高用户的体验(前提是处理好的情况下),例如在一段长文本中,可以通过文本选中范围,快速定位到需要的文本,然后对进行复制、删除、修改...等功能。

在Flutter中,我们可以通过设置TextFieldcontroller中的selection,来实现文本选中。

我们通过实现在一段文本中,快速定位选中姓名的例子,来看下怎样自定义文本选中范围。

int extent = 0;
int base = 0;
​
selectText(String text) {
  String name = "Taxze";
  //判断文本中是否有需要查找的内容
  if (text.contains(name)) {
  //定位到出现内容的第一个位置
    extent = text.indexOf(name);
    base = extent + name.length;
  }
}
​
Widget selectionText() => TextField(
      controller: TextEditingController.fromValue(
        TextEditingValue(
          // 设置内容
          text: "Hello Taxze",
          //设置选中范围
          selection: TextSelection(
            baseOffset: base,
            extentOffset: extent, 
          ),
        ),
      ),
);

7.gif

自定义选中文本范围使用恰当的话,我相信可以给用户带来更好的体验!

自定义光标位置

与自定义选中文本范围一样,自定义光标的位置也会有更多的体验。自定义光标和自定义选中范围类似,这里就不在多说了。

TextEditingValue(
  // 设置内容
  text: "Hello Taxze",
  selection: TextSelection.collapsed(offset: 10), //设置光标位置
),
TextEditingValue

分析了这么多TextEditingValue的应用,现在来分析它本身。

TextEditingValue有三个属性:

  • String textTextField显示的默认值,相当于TextEditingController中的text。因为查看源码就可以发现,TextEditingController里的text最终将会赋值给TextEditingValue.text

    TextEditingController({ String? text })
      : super(text == null ? TextEditingValue.empty : TextEditingValue(text: text));
  • TextSelection selection:通过它可以知道当前选择的光标位置和选择范围,通过它也可以设置光标在换行时的精确位置。
  • TextRange composing:当前编辑单词的偏移量,当你输入某些文本时,它的下方会有下划线,同时,系统键盘的上方会有建议的文本,点击建议的文本即可替换下划线的文本。

可编辑的文本包含哪些内容呢?

我们已经知道在Flutter中,无论是使用 TextField 还是 CupertinoTextField 最后都会创建EditableText。也就是因为这个EditableText它将其他的可编辑的模块都集成了后,才能与系统键盘进行通信,才能在编辑文本时,出现光标、选中文本、可垂直滚动文本...

①具有样式、结构(文本高度)、文本对齐方式、本地化

EditableText中,具有样式:

//可以重写此方法以自定义文本的外观。
TextSpan buildTextSpan({required BuildContext context, TextStyle? style , required bool withComposing}) {
  if (!value.isComposingRangeValid || !withComposing) {
    return TextSpan(style: style, text: text);
  }
  final TextStyle composingStyle = style?.merge(const TextStyle(decoration: TextDecoration.underline))
      ?? const TextStyle(decoration: TextDecoration.underline);
  return TextSpan(
    style: style,
    children: <TextSpan>[
      TextSpan(text: value.composing.textBefore(value.text)),
      TextSpan(
        style: composingStyle,
        text: value.composing.textInside(value.text),
      ),
      TextSpan(text: value.composing.textAfter(value.text)),
    ],
  );
}

通过StrutStyle已确保输入的文本符合分配的空间

StrutStyle get strutStyle {
  //如果为空,将继承style
  if (_strutStyle == null) {
    return StrutStyle.fromTextStyle(style, forceStrutHeight: true);
  }
  return _strutStyle!.inheritFromTextStyle(style);
}

文本具有对齐方式,默认为TextAlign.start,同时具有文本的方向textDirection,用于决定TextAlign.startTextAlign.end的值。

EditableText({
  super.key,
  ...
  this.textAlign = TextAlign.start,
  this.textDirection,
  ...
})

具有Locale,可以根据手机系统语言环境的不同,以不同的方式呈现文本。

②具有文本布局

EditableText的布局取决于maxLinesminLines和是否启用expands

  • 如果最大行数为一(默认为一),则将在一行上水平滚动。
  • 如果最大行数为空,则设置为最小行数,并垂直增长。
  • 如果最大行数大于 1,则按照最小行数进行布局,并垂直一行行增加,直到达到最大行数。
  • 当达到其最大高度,它将垂直滚动。
  • 如果启用了扩展,它会根据传入的约束调整大小。
static TextInputType _inferKeyboardType({
  required Iterable<String>? autofillHints,
  required int? maxLines,
}) {
  if (autofillHints == null || autofillHints.isEmpty) {
    return maxLines == 1 ? TextInputType.text : TextInputType.multiline;
  }
...
  if (maxLines != 1) {
    return TextInputType.multiline;
  }
...
  return inferKeyboardType[effectiveHint] ?? TextInputType.text;
}

EditableTextbuild方法中,嵌套了一层Scrollable,从而使文本可以垂直滚动显示多行文本,水平滚动以显示单行文本。

@override
  Widget build(BuildContext context) {
    ...
    return MouseRegion(
      ...
      child: Scrollable()
     ),
   );
}
③对文本的更改有完整的处理流程

当文本的内容被更改时,EditableText首先会调用onChanged,通常会通知TextFiled去更改文本、光标或选择文本范围。

final ValueChanged<String>? onChanged;

然后当用户按下键盘上的搜索或者发送键时,会调用onEditingComplete,将用户输入的内容提交给controller

EditableText通过_finalizeEditing处理键盘的操作。

@override
void performAction(TextInputAction action) {
  switch (action) {
      ...
      //完成编辑(不代表用户结束了输入文本)
      _finalizeEditing(action, shouldUnfocus: false);
      break;
  }
}

最后当用户确认输入完成后,调用onSubmitted(大部分情况下,onSubmitted会在onChanged后调用)。

④当文本发生更改时,可通过updateEditingValue更新编辑的文本
TextEditingValue? _lastKnownRemoteTextEditingValue;
​
@override
TextEditingValue get currentTextEditingValue => _value;
​
//用于处理文本的编辑更新
@override
void updateEditingValue(TextEditingValue value) {
  if (!_shouldCreateInputConnection) {
    return;
  }
​
  if (widget.readOnly) {
    //如果是只读的模式下,只需要观察选择文本范围,其他都不用关心。
    value = _value.copyWith(selection: value.selection);
  }
  _lastKnownRemoteTextEditingValue = value;
​
  if (value == _value) {
    //如果在输入一个数字后,删除它,这时候引擎会通知两次,所以需要一个判断。
    return;
  }
​
  if (value.text == _value.text && value.composing == _value.composing) {
    //当只有文本选择范围发生变化时
    _handleSelectionChanged(value.selection, (_textInputConnection?.scribbleInProgress ?? false) ? SelectionChangedCause.scribble : SelectionChangedCause.keyboard);
  } else {
    ...
  }
  //无论发生了什么变化,都需要一个showCaretOnScreen,使用户能观察到文本发生的变化。
  _scheduleShowCaretOnScreen(withAnimation: true);
  if (_hasInputConnection) {
    _stopCursorBlink(resetCharTicks: false);
    _startCursorBlink();
  }
}

如何更好的处理输入表单?

表单是我们用于收集用户数据的重要方式,它在应用程序中是不可或缺的组件(不只是移动端)。在用户的登录/注册、地址填写、身份信息填写...等场景中有着很重要的作用,那么在Flutter中,如何使用Form类带来更好的用户体验呢?

①通过Globalkey保存表单状态

Flutter Form组件是用于保存、验证表单文本的。

final _formKey = GlobalKey<FormState>();
​
Widget formText() => Form(
      key: _formKey,
      ...      
);
②将TextFormField添加到表单中

添加两个TextFormField,用于获取姓名和电话号码。

Form(
      key: _formKey,
      child: Column(
        children: <Widget>[
          TextFormField(
            keyboardType: TextInputType.name, //当获取到焦点时,弹出的键盘类型,使其编辑框有更好的用于体验。
            textInputAction: TextInputAction.next, //设置键盘右下角的操作按钮按钮,此处为→按钮
            decoration: const InputDecoration(
              hintText: '请输入姓名',
              labelText: 'Name', //当获取到焦点时显示
            ),
          ),
          TextFormField(
            keyboardType: TextInputType.phone,
            textInputAction: TextInputAction.done,//此处为完成按钮
            decoration: const InputDecoration(
              hintText: '请输入电话号码',
              labelText: 'Phone Number',
            ),
          ),
        ],
      ),
    )
③分配FocusNode,使表单可以提交数据
final _formKey = GlobalKey<FormState>();
//定义两个FocusNode
final FocusNode _nameFocusNode = FocusNode();
final FocusNode _phoneFocusNode = FocusNode();
​
_nextFocus(FocusNode focusNode) { 
  //点击键盘上的next按钮,之间聚焦到下个焦点的输入框,提高用户体验
  FocusScope.of(context).requestFocus(focusNode);
}
​
_submitForm() {
  //底部弹出完成SnackBar
  ScaffoldMessenger.of(context)
      .showSnackBar(const SnackBar(content: Text('完成')));
}
​
Widget formText() => Form(
      key: _formKey,
      child: Column(
        children: <Widget>[
          TextFormField(
            keyboardType: TextInputType.name,
            textInputAction: TextInputAction.next,
            focusNode: _nameFocusNode,
            onFieldSubmitted: (String value) {
              _nextFocus(_phoneFocusNode); //点击按钮触发的回调
            },
            decoration: const InputDecoration(
              hintText: '请输入姓名',
              labelText: 'Name',
            ),
          ),
          TextFormField(
            keyboardType: TextInputType.phone,
            textInputAction: TextInputAction.done,
            focusNode: _phoneFocusNode,
            onFieldSubmitted: (String value) {
              _submitForm(); 
            },
            decoration: const InputDecoration(
              hintText: '请输入电话号码',
              labelText: 'Phone Number',
            ),
          ),
        ],
      ),
    );
④验证数据

在提交数据前,我们可以根据需求对数据进行验证,大家可以根据需求自己定义,例如:

String _dataInput(String value) {
  if (value.trim().isEmpty) {
    return '此为必填项';
  }
  return "";
}

尾述

在这篇文章中,我们知道了文本的编辑是包含了哪些内容,知道了如何自定义编辑的操作,也知道了如何更好的实现一个表单。但这也只是文本的输入编辑、文本的优化的冰山一角。在后续的文章中我也会和大家一起持续探索。希望这篇文章能对你有所帮助,有问题欢迎在评论区留言讨论~

参考&推荐阅读

Flutter中那些你需要知道的文本知识!

Flutter 快速解析 TextField 的内部原理 @恋猫de小郭

相关文章
|
20天前
|
UED 开发者 容器
Flutter&鸿蒙next 的 Sliver 实现自定义滚动效果
Flutter 提供了强大的滚动组件,如 ListView 和 GridView,但当需要更复杂的滚动效果时,Sliver 组件是一个强大的工具。本文介绍了如何使用 Sliver 实现自定义滚动效果,包括 SliverAppBar、SliverList 等常用组件的使用方法,以及通过 CustomScrollView 组合多个 Sliver 组件实现复杂布局的示例。通过具体代码示例,展示了如何实现带有可伸缩 AppBar 和可滚动列表的页面。
85 1
|
22天前
Flutter 自定义组件继承与调用的高级使用方式
本文深入探讨了 Flutter 中自定义组件的高级使用方式,包括创建基本自定义组件、继承现有组件、使用 Mixins 和组合模式等。通过这些方法,您可以构建灵活、可重用且易于维护的 UI 组件,从而提升开发效率和代码质量。
117 1
|
22天前
|
前端开发 开发者
深入探索 Flutter 鸿蒙版的画笔使用与高级自定义动画
本文深入探讨了 Flutter 中的绘图功能,重点介绍了 CustomPainter 和 Canvas 的使用方法。通过示例代码,详细讲解了如何绘制自定义图形、设置 Paint 对象的属性以及实现高级自定义动画。内容涵盖基本绘图、动画基础、渐变动画和路径动画,帮助读者掌握 Flutter 绘图与动画的核心技巧。
68 1
|
22天前
|
Dart UED 开发者
Flutter&鸿蒙next中的按钮封装:自定义样式与交互
在Flutter应用开发中,按钮是用户界面的重要组成部分。Flutter提供了多种内置按钮组件,但有时这些样式无法满足特定设计需求。因此,封装一个自定义按钮组件变得尤为重要。自定义按钮组件可以确保应用中所有按钮的一致性、可维护性和可扩展性,同时提供更高的灵活性,支持自定义颜色、形状和点击事件。本文介绍了如何创建一个名为CustomButton的自定义按钮组件,并详细说明了其样式、形状、颜色和点击事件的处理方法。
69 1
|
22天前
|
Dart 搜索推荐 API
Flutter & 鸿蒙next版本:自定义对话框与表单验证的动态反馈与错误处理
在现代移动应用开发中,用户体验至关重要。本文探讨了如何在 Flutter 与鸿蒙操作系统(HarmonyOS)中创建自定义对话框,并结合表单验证实现动态反馈与错误处理,提升用户体验。通过自定义对话框和表单验证,开发者可以提供更加丰富和友好的交互体验,同时利用鸿蒙next版本拓展应用的受众范围。
70 1
|
2月前
|
前端开发 搜索推荐
Flutter中自定义气泡框效果的实现
Flutter中自定义气泡框效果的实现
86 3
|
3月前
|
前端开发
Flutter快速实现自定义折线图,支持数据改变过渡动画
Flutter快速实现自定义折线图,支持数据改变过渡动画
100 4
Flutter快速实现自定义折线图,支持数据改变过渡动画
|
3月前
|
开发者 监控 开发工具
如何将JSF应用送上云端?揭秘在Google Cloud Platform上部署JSF应用的神秘步骤
【8月更文挑战第31天】本文详细介绍如何在Google Cloud Platform (GCP) 上部署JavaServer Faces (JSF) 应用。首先,确保已准备好JSF应用并通过Maven构建WAR包。接着,使用Google Cloud SDK登录并配置GCP环境。然后,创建`app.yaml`文件以配置Google App Engine,并使用`gcloud app deploy`命令完成部署。最后,通过`gcloud app browse`访问应用,并利用GCP的监控和日志服务进行管理和故障排查。整个过程简单高效,帮助开发者轻松部署和管理JSF应用。
61 0
|
3月前
|
开发者 容器 Java
Azure云之旅:JSF应用的神秘部署指南,揭开云原生的新篇章!
【8月更文挑战第31天】本文探讨了如何在Azure上部署JavaServer Faces (JSF) 应用,充分发挥其界面构建能力和云平台优势,实现高效安全的Web应用。Azure提供的多种服务如App Service、Kubernetes Service (AKS) 和DevOps简化了部署流程,并支持应用全生命周期管理。文章详细介绍了使用Azure Spring Cloud和App Service部署JSF应用的具体步骤,帮助开发者更好地利用Azure的强大功能。无论是在微服务架构下还是传统环境中,Azure都能为JSF应用提供全面支持,助力开发者拓展技术视野与实践机会。
19 0
|
3月前
|
开发框架 API 开发者
Flutter表单控件深度解析:从基本构建到高级自定义,全方位打造既美观又实用的移动端数据输入体验,让应用交互更上一层楼
【8月更文挑战第31天】在构建美观且功能强大的移动应用时,表单是不可或缺的部分。Flutter 作为热门的跨平台开发框架,提供了丰富的表单控件和 API,使开发者能轻松创建高质量表单。本文通过问题解答形式,深入解读 Flutter 表单控件,并通过具体示例代码展示如何构建优秀的移动应用表单。涵盖创建基本表单、处理表单提交、自定义控件样式、焦点管理和异步验证等内容,适合各水平开发者学习和参考。
89 0