Flutter: TextEditingValue的实现

news/2025/2/24 9:29:29

文章目录

    • TextEditingValue
    • 一、fromJSON
    • 二、text、selection、composing、empty
    • 三、isComposingRangeValid
    • 四、replaced

TextEditingValue

/// The current text, selection, and composing state for editing a run of text.

class TextEditingValue {
  const TextEditingValue({
    this.text = '',
    this.selection = const TextSelection.collapsed(offset: -1),
    this.composing = TextRange.empty,
  });

创建用于编辑文本段落的信息。
选择范围和正在编辑的范围必须位于文本之内。这一点在构建时不会被检查,
必须由调用者确保。
[selection] 的默认值是 TextSelection.collapsed(offset: -1)。
这表示没有任何选择。

一、fromJSON

class TextEditingValue {
  // ... 省略大量代码
  factory TextEditingValue.fromJSON(Map<String, dynamic> encoded) {
    final String text = encoded['text'] as String;
    final TextSelection selection = TextSelection(
      baseOffset: encoded['selectionBase'] as int? ?? -1,
      extentOffset: encoded['selectionExtent'] as int? ?? -1,
      affinity: _toTextAffinity(encoded['selectionAffinity'] as String?) ?? TextAffinity.downstream,
      isDirectional: encoded['selectionIsDirectional'] as bool? ?? false,
    );
    final TextRange composing = TextRange(
      start: encoded['composingBase'] as int? ?? -1,
      end: encoded['composingExtent'] as int? ?? -1,
    );
    assert(_textRangeIsValid(selection, text));
    assert(_textRangeIsValid(composing, text));
    return TextEditingValue(
      text: text,
      selection: selection,
      composing: composing,
    );
  }

从 JSON 对象创建此类的实例。
final String text = encoded[‘text’] as String; : JSON 中提取文本内容
TextSelection :从 JSON 中提取选择范围信息并创建 TextSelection 对象
baseOffset: encoded['selectionBase'] as int? ?? -1, // 选择起始位置,默认为 -1
extentOffset: encoded['selectionExtent'] as int? ?? -1, // 选择结束位置,默认为 -1
affinity: _toTextAffinity(encoded['selectionAffinity'] as String?) ?? TextAffinity.downstream, // 选择的光标方向,默认为下游
isDirectional: encoded['selectionIsDirectional'] as bool? ?? false,// 选择是否为方向性选择,默认为 false

TextRange : 从 JSON 中提取正在编辑的范围信息并创建 TextRange 对象
final TextRange composing = TextRange(
start: encoded['composingBase'] as int? ?? -1, // 编辑范围起始位置,默认为 -1
end: encoded['composingExtent'] as int? ?? -1, // 编辑范围结束位置,默认为 -1
);
断言检查选择范围和编辑范围是否在文本范围内,如果_textRangeIsValid(selection, text)为true,assert(true)就抛出错误,不再往下执行。
assert(_textRangeIsValid(selection, text));
assert(_textRangeIsValid(composing, text));
如果检查没问题,就返回赋值后的TextEditingValue。
return TextEditingValue(text: text, selection: selection, composing: composing);

二、text、selection、composing、empty

class TextEditingValue {
  // ... 省略大量代码
  /// text 是当前正在编辑的文本。
  final String text;

  /// selection是当前选中的文本范围。
  /// 当 [selection] 是一个 [TextSelection],并且其 baseOffset 和 extentOffset 相同且为非负数时,
  /// [selection] 属性表示光标的位置。
  /// 如果当前 [selection] 的 baseOffset 或 extentOffset 为负数,
  /// 则表示当前文本没有选中内容或光标位置,大多数依赖于当前选择的文本编辑操作
  /// (例如,在光标位置插入字符)将不会执行任何操作。
final TextSelection selection;

  /// 正在被组合输入的文本范围。
  /// 组合输入区域由输入法(IME)创建,用于指示某个范围内的文本是临时的。
  /// 例如,Android Gboard 应用的英文键盘会将光标下的当前单词放入组合输入区域,
  /// 以指示该单词可能会受到自动更正或预测更改的影响。
  /// 组合输入区域还可用于执行多阶段输入,这通常由为拼音键盘设计的输入法使用,
  /// 用于输入表意符号。例如,许多中日韩(CJK)键盘要求用户输入拉丁字母序列,
  /// 然后将其转换为 CJK 字符。在 iOS 上,默认的软件键盘没有专门的视图来显示未完成的拉丁字母序列,
  /// 因此它直接显示在文本字段中,位于组合输入区域内。
  /// 组合输入区域通常只应由输入法或用户通过与输入法的交互来更改。
  /// 如果此属性表示的文本范围是 [TextRange.empty],则表示当前没有文本正在被组合输入。
  final TextRange composing;

  /// 一个表示空字符串、没有选择且没有组合输入范围的值。
  static const TextEditingValue empty = TextEditingValue();
}

text: 当前编辑的文字
selection: 当前选中文字的范围
composing:正在被组合输入的文本范围。例如拼音输入法,在输入字母出现候选字还未确认选择的阶段,composing就为true,当确认选中后,composing就为false。下图就是composing: true的示例。
在这里插入图片描述
empty: 一个表示空字符串、没有选择且没有组合输入范围的值
使用:_textEditController.value = TextEditingVlaue.empty; 给一个输入赋值一个空值。

class TextEditingValue {
  // ... 省略大量代码
  
  TextEditingValue copyWith({
    String? text,
    TextSelection? selection,
    TextRange? composing,
  }) {
    return TextEditingValue(
      text: text ?? this.text,
      selection: selection ?? this.selection,
      composing: composing ?? this.composing,
    );
  }
}

创建一个当前 TextEditingValue 的副本,并允许部分字段被替换为新值。
在需要修改 TextEditingValue部分属性时,避免直接修改原对象,而是返回一个新的对象。这是不可变对象设计的常见模式。

参数:text、selection 和 composing 都是可选参数。如果调用者提供了新值,则使用新值;否则,保留当前对象的值。

空值合并运算符 (??):用于判断参数是否为 null。如果为 null,则使用当前对象的对应值。

返回新对象:通过 TextEditingValue 构造函数创建一个新对象,确保原对象不会被修改。

text: text ?? this.text, 如果提供了新的文本,则使用新文本,否则保留原文本
selection: selection ?? this.selection,如果提供了新的选择范围,则使用新范围,否则保留原范围
composing: composing ?? this.composing, 如果提供了新的组合输入范围,则使用新范围,否则保留原范围

三、isComposingRangeValid

class TextEditingValue {
  // ... 省略大量代码
  
  bool get isComposingRangeValid => composing.isValid && composing.isNormalized && composing.end <= text.length;
}

isComposingRangeValid :通过get 关键字,给 isComposingRangeValid 提供了一个只读的访问方式,使外部能直接读取isComposingRangeValid 值,而无法直接修改它。
用于判断 composing 范围是否text的有效范围。
当且仅当 composing 范围是规范化的、其起始位置大于或等于 0,并且其结束位置小于或等于 [text] 的长度时,返回 true
如果此属性为 false,而 [composing] 范围的 isValid 为 true,通常表示当前 [composing] 范围由于编程错误而无效。
composing.isValid:检查 composing 范围是否有效(即 start 和 end 都是非负数)。
composing.isNormalized:检查 composing 范围是否规范化(即 start <= end)。
composing.end <= text.length:检查 composing 的结束位置是否不超过文本的长度。

返回值:只有当所有条件都满足时,返回 true,否则返回 false。
composing 是 TextRange,在TextRange中有以下定义:

class TextRange {
  // ... 省略大量代码
  
  /// 范围的起始位置
  /// 如果 [start] 和 [end] 都为 -1,则表示一个空的文本范围
  final int start;

  /// 范围结束位置的下一个索引
  /// 如果 [start] 和 [end] 都为 -1,则表示一个空的文本范围
  final int end;
  
  /// 判断该范围是否表示文本中的有效位置
  bool get isValid => start >= 0 && end >= 0;

  /// 判断该范围是否为空(但仍可能位于文本中),未选中一段范围的文本。
  bool get isCollapsed => start == end;

  /// 判断该范围的起始位置是否在结束位置之前。
  bool get isNormalized => end >= start;
}

isCollapsed: true时,光标未选中范围,例如在“本”“。”前的光标,如下图:
在这里插入图片描述

四、replaced

class TextEditingValue {
  // ... 省略大量代码
  
  TextEditingValue replaced(TextRange replacementRange, String replacementString) {
    if (!replacementRange.isValid) {
      return this;
    }
    final String newText = text.replaceRange(replacementRange.start, replacementRange.end, replacementString);

    if (replacementRange.end - replacementRange.start == replacementString.length) {
      return copyWith(text: newText);
    }

    int adjustIndex(int originalIndex) {
      // The length added by adding the replacementString.
      final int replacedLength = originalIndex <= replacementRange.start && originalIndex < replacementRange.end ? 0 : replacementString.length;
      // The length removed by removing the replacementRange.
      final int removedLength = originalIndex.clamp(replacementRange.start, replacementRange.end) - replacementRange.start;
      return originalIndex + replacedLength - removedLength;
    }

    final TextSelection adjustedSelection = TextSelection(
      baseOffset: adjustIndex(selection.baseOffset),
      extentOffset: adjustIndex(selection.extentOffset),
    );
    final TextRange adjustedComposing = TextRange(
      start: adjustIndex(composing.start),
      end: adjustIndex(composing.end),
    );
    assert(_textRangeIsValid(adjustedSelection, newText));
    assert(_textRangeIsValid(adjustedComposing, newText));
    return TextEditingValue(
      text: newText,
      selection: adjustedSelection,
      composing: adjustedComposing,
    );
  }
}
  1. if (!replacementRange.isValid) { return this; }
    检查 replacementRange 是否有效:
    如果 replacementRange 无效(如 start 或 end 为负数),则直接返回当前对象,不做任何操作。

  2. final String newText = text.replaceRange(replacementRange.start, replacementRange.end, replacementString);
    替换文本:
    使用 text.replaceRange 方法将 replacementRange 指定的文本替换为 replacementString,生成新的文本 newText。

  3. 处理特殊情况:
    if (replacementRange.end - replacementRange.start == replacementString.length) { return copyWith(text: newText); }
    如果替换前后的文本长度相同(即 replacementRange.end - replacementRange.start == replacementString.length),则直接调用 copyWith 方法返回新的 TextEditingValue,因为选择范围和组合输入范围不需要调整。

  4. int adjustIndex(int originalIndex) {}
    调整索引:
    定义 adjustIndex 方法,用于计算替换后 selection 和 composing 范围的新位置。
    final int replacedLength = originalIndex <= replacementRange.start && originalIndex < replacementRange.end ? 0 : replacementString.length;
    replacedLength:如果原始索引在替换范围内,则不增加长度;否则,增加 replacementString 的长度。
    final int removedLength = originalIndex.clamp(replacementRange.start, replacementRange.end) - replacementRange.start;
    removedLength:计算被替换文本的长度。
    调整公式:originalIndex + replacedLength - removedLength。当前索引 + 替换文本的长度 - 被替换删除的文本的长度 = 最终光标的索引下标

clamp 是 Dart 中用于限制一个数值在指定范围内的工具方法。它的定义如下:
int clamp(int lowerLimit, int upperLimit);
功能:将当前值限制在 lowerLimit 和 upperLimit 之间。
如果当前值小于 lowerLimit,则返回 lowerLimit
如果当前值大于 upperLimit,则返回 upperLimit
当前值处于lowerLimit~upperLimit之间,返回当前值
上述的originalIndex.clamp(replacementRange.start, replacementRange.end),表示返回替换文案在长度范围内的合法索引。

  1. final TextSelection adjustedSelection = TextSelection( baseOffset: adjustIndex(selection.baseOffset), extentOffset: adjustIndex(selection.extentOffset), );
    通过adjustIndex计算 selection 和 composing 的最终索引范围,并通过TextSelection重新赋值。

  2. 断言检查:_textRangeIsValid(adjustedSelection, newText) 是否为true
    使用 assert 检查调整后的 selection 和 composing 范围是否有效。

  3. 返回新对象
    返回一个新的 TextEditingValue,包含替换后的文本、调整后的选择范围和组合输入范围。


http://www.niftyadmin.cn/n/5864180.html

相关文章

C++——模版(二)

前言 我们前面讲过模版的一&#xff0c;不知道大家还有没有所印象&#xff0c;如果大家不太能回忆起来可以再去前面看一下&#xff0c;那通过我们讲解了几个容器之后&#xff0c;相信大家现在应该已经对模版很熟悉了&#xff0c;那模版还剩下一些其他的内容我们就在这里进行讲…

vllm部署LLM(qwen2.5,llama,deepseek)

目录 环境 qwen2.5-1.5b-instruct 模型下载 vllm 安装 验证安装 vllm 启动 查看当前模型列表 OpenAI Completions API&#xff08;文本生成&#xff09; OpenAI Chat Completions API&#xff08;chat 对话&#xff09; vllm 进程查看&#xff0c;kill llama3 deep…

2025年- G15-Lc89-383.赎金记录-java版

1.题目描述 给定两个字符串 ransomNote 和 magazine&#xff0c;如果 ransomNote 可以通过使用 magazine 中的字母构造出来&#xff0c;返回 true&#xff0c;否则返回 false。 magazine 中的每个字母只能使用一次。 示例 1&#xff1a; 输入&#xff1a;ransomNote “a”, …

Element UI日期选择器默认显示1970年解决方案

目录 问题背景 问题根源 1. 数据绑定类型错误 2. 初始化逻辑错误 解决方案 核心思路 步骤 1&#xff1a;正确初始化日期对象 步骤 2&#xff1a;处理数据交互 步骤 3&#xff1a;处理年份切换事件 完整代码示例 注意事项 1. 时区问题 2. 格式化绑定值 常见问题 1. 为什…

C++之旅-C++11的深度剖析(1)

目录 前言/背景 1.C11的发展历史 2.列表初始化 2.1 C98传统的{} 2.2 C11中的{} 2.3 C11中的std::initializer_list 3.右值引用 3.1 左值和右值 3.2 左值引用和右值引用 3.3 引用延长生命周期 3.4 左值和右值的参数匹配 结束语 前言/背景 随着现代软件开发的快速发展…

改进A*算法并用于城市无人机路径规划

独家原创&#xff01;改进A*算法进行城市无人机路径规划&#xff0c;考虑碰撞&#xff0c;飞行高度等优化启发式搜索。所有指标超过A*和A算法&#xff01;附有完整的文档说明 算法设计、毕业设计、期刊专利&#xff01;感兴趣可以联系我。 &#x1f3c6;代码获取方式1&#xff…

UE_C++ —— Logging in Unreal

目录 一&#xff0c;UE_LOG Log Verbosity Console Commands Logging Fundamental Data Types Define Your Own Log Category 二&#xff0c;UE_LOGFMT On-screen debug messages 日志是一种非常实用的调试工具&#xff0c;可以详细说明代码当前的执行逻辑&#xff1b;可…

美颜相机1.0

项目开发步骤 1 界面开发 美颜相机界面构成&#xff1a; 标题 尺寸 关闭方式 位置 可视化 2 创建主函数调用界面方法 3 添加两个面板 一个是按钮面板一个是图片面板 用JPanel 4 添加按钮到按钮面吧【注意&#xff1a;此时要用初始化按钮面板的方法initBtnPanel 并且将按钮添…