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小郭

相关文章
|
1天前
Flutter 组件(二)文本 与 输入框组件
Flutter 组件(二)文本 与 输入框组件
86 0
|
7月前
|
缓存 Java 开发工具
Flutter的文本、图片和按钮使用
Flutter的文本、图片和按钮使用
68 0
|
1天前
|
前端开发 搜索推荐 开发者
【Flutter前端技术开发专栏】Flutter中的自定义主题与暗黑模式
【4月更文挑战第30天】本文介绍了如何在Flutter中自定义主题和实现暗黑模式。通过`ThemeData`类定义应用的外观,包括颜色、字体和样式。示例展示了如何设置主色、强调色及文本、按钮主题。暗黑模式可通过`darkTheme`属性启用,结合`ThemeData.dark()`方法定制。利用`MediaQuery`监听系统亮度变化,动态调整暗黑模式状态。Flutter的这些特性有助于开发者创建独特且用户友好的界面。
【Flutter前端技术开发专栏】Flutter中的自定义主题与暗黑模式
|
1天前
|
缓存 前端开发 搜索推荐
【Flutter前端技术开发专栏】Flutter中的自定义绘制与Canvas API
【4月更文挑战第30天】Flutter允许开发者通过`CustomPaint`和`CustomPainter`进行自定义绘制,以实现丰富视觉效果。`CustomPaint` widget将`CustomPainter`应用到画布,而`CustomPainter`需实现`paint`和`shouldRepaint`方法。`paint`用于绘制图形,如示例中创建的`MyCirclePainter`绘制蓝色圆圈。Canvas API提供绘制形状、路径、文本和图片等功能。注意性能优化,避免不必要的重绘和利用缓存提升效率。自定义绘制让Flutter UI更具灵活性和个性化,但也需要图形学知识和性能意识。
【Flutter前端技术开发专栏】Flutter中的自定义绘制与Canvas API
|
1天前
|
开发框架 前端开发 搜索推荐
【Flutter前端技术开发专栏】Flutter中的自定义Widget与渲染流程
【4月更文挑战第30天】探索Flutter的自定义Widget与渲染流程。自定义Widget是实现复杂UI设计的关键,优点在于个性化设计、功能扩展和代码复用,但也面临性能优化和复杂性管理的挑战。创建步骤包括设计结构、定义Widget类、实现构建逻辑和处理交互。Flutter渲染流程涉及渲染对象树、布局、绘制和合成阶段。实践案例展示如何创建带渐变背景和阴影的自定义按钮。了解这些知识能提升应用体验并应对开发挑战。查阅官方文档以深入学习。
【Flutter前端技术开发专栏】Flutter中的自定义Widget与渲染流程
|
1天前
|
前端开发 开发者 UED
Flutter的自定义Painter:深入探索自定义绘制Widget的技术实现
【4月更文挑战第26天】Flutter的自定义Painter允许开发者根据需求绘制独特UI,通过继承`CustomPaint`类和重写`paint`方法实现。在`paint`中使用`Canvas`绘制图形、路径等。创建自定义Painter类后,将其作为`CustomPaint` Widget的`painter`属性使用。此技术可实现自定义形状、渐变、动画等复杂效果,提升应用视觉体验。随着Flutter的进化,自定义Painter将提供更丰富的功能。
|
1天前
|
运维 监控 定位技术
应用研发平台EMAS常见问题之flutter插件不支持自定义图标如何解决
应用研发平台EMAS(Enterprise Mobile Application Service)是阿里云提供的一个全栈移动应用开发平台,集成了应用开发、测试、部署、监控和运营服务;本合集旨在总结EMAS产品在应用开发和运维过程中的常见问题及解决方案,助力开发者和企业高效解决技术难题,加速移动应用的上线和稳定运行。
80 0
|
1天前
Flutter 自定义ICON库
Flutter 自定义ICON库 Flutter提供了一些内置的ICON库,但在实际开发中,可能需要一些自定义的ICON图标。Flutter允许我们使用自定义图标,本文将介绍如何创建和使用自定义ICON库。
|
1天前
|
UED
Flutter之自定义路由切换动画
Flutter之自定义路由切换动画 在Flutter中,我们可以通过Navigator来实现路由管理,包括路由的跳转和返回等。默认情况下,Flutter提供了一些简单的路由切换动画,但是有时候我们需要自定义一些特殊的动画效果来提高用户体验。本文将介绍如何在Flutter中实现自定义的路由切换动画。
|
1天前
|
开发框架 Dart 容器
Flutter 自定义渐变按钮 GradientButton
Flutter 自定义渐变按钮 GradientButton Flutter 是一种流行的跨平台移动应用开发框架。Flutter 提供了许多内置的小部件,但有时您可能需要创建自己的小部件以满足特定的需求。这个文档将介绍如何创建一个自定义渐变按钮小部件 GradientButton。

热门文章

最新文章

相关实验场景

更多
http://www.vxiaotou.com