三方库开源地址:https://atomgit.com/nutpi/flutter_ohos_text_span_field

写在前面

前两篇文章分别介绍了@用户功能和隐藏域值获取,今天来聊聊删除和清空功能。

说实话,删除这个操作看起来简单,但在@功能的场景下还挺有讲究的。普通文本一个字一个字删没问题,但@块呢?删一半算什么?所以就有了"块删除"这个概念。
请添加图片描述

什么是块删除

先解释一下什么是块删除。

假设输入框里的内容是:今天@张三 天气不错

如果用户把光标放在"张"和"三"之间,按删除键,会发生什么?

普通的 TextField 会删掉"张"这一个字,变成 今天@三 天气不错。但这显然不是我们想要的效果,@三 是什么鬼?

FlutterTextSpanField 的处理方式是:只要删除操作涉及到@块的任何一部分,就把整个@块都删掉。所以结果会变成 今天 天气不错

这个行为和微信、QQ的@功能是一样的,用户体验上比较符合直觉。

两种删除方式

这个库提供了两种删除方式:

第一种是用户手动删除。 就是用户在输入框里按删除键,这个库会自动处理块删除的逻辑,不需要我们写代码。

第二种是代码控制删除。 通过调用 delete(start, end) 方法,可以删除指定范围的内容。


先看代码控制删除怎么用:

_textSpanBuilder.delete(0, 5);

这行代码会删除下标0到5之间的内容。注意是左闭右开区间,也就是删除下标0、1、2、3、4这5个字符。


如果删除范围涉及到@块,同样会触发块删除:

// 假设内容是:今天@张三 天气不错
// @张三 的范围是 2-5

_textSpanBuilder.delete(3, 4);  // 只删除"张"
// 实际效果:整个@张三都被删除
// 结果:今天 天气不错

即使你只指定删除"张"这一个字,库也会自动把整个@块删掉。这个逻辑是在库内部处理的。

清空功能

清空就简单多了,一行代码搞定:

_textSpanBuilder.clear();

调用之后,输入框里的所有内容都会被清空,包括普通文本和@块。


有一点要注意,clear() 方法不只是清空文本,还会清空内部维护的@块列表。所以清空之后再调用 getWidgets() 会返回空列表。

_textSpanBuilder.clear();

List<TextSpanWidget> widgets = _textSpanBuilder.getWidgets();
print(widgets.length);  // 输出: 0

实现一个删除功能的演示页面

来写一个完整的演示页面,方便理解这些功能:

class _DeleteDemoState extends State<DeleteDemo> {
  TextSpanBuilder _textSpanBuilder = TextSpanBuilder();
  int _startIndex = 0;
  int _endIndex = 1;
}

先定义好需要的变量。_startIndex_endIndex 用来控制删除范围。


添加一些测试数据的方法:

_addTestData() {
  _textSpanBuilder.appendTextToEnd("今天天气真好,");
  
  _textSpanBuilder.appendToEnd(AtTextSpan(
    id: "10001",
    text: "@张三",
    style: TextStyle(color: Color(0xFF5BA2FF)),
  ));
  
  _textSpanBuilder.appendTextToEnd(" 一起出去玩吧!");
}

这里用了两个方法:appendTextToEnd 添加普通文本,appendToEnd 添加@块。

执行完之后,输入框里的内容是:今天天气真好,@张三 一起出去玩吧!


删除按钮的点击事件:

_onDeletePressed() {
  _textSpanBuilder.delete(_startIndex, _endIndex);
}

很简单,就是调用 delete 方法,传入起始和结束下标。


清空按钮的点击事件:

_onClearPressed() {
  _textSpanBuilder.clear();
}

更简单,直接调用 clear 就行。

获取当前文本长度

在做删除操作之前,最好先知道当前文本有多长,避免下标越界:

_getTextLength() {
  int length = _textSpanBuilder.controller?.text.length ?? 0;
  print("当前文本长度: $length");
}

通过 controller.text.length 可以拿到文本长度。


有了长度信息,就可以做一些边界检查:

_onDeletePressed() {
  int maxLength = _textSpanBuilder.controller?.text.length ?? 0;
  
  if (_startIndex >= maxLength || _endIndex > maxLength) {
    print("下标越界了!");
    return;
  }
  
  if (_startIndex >= _endIndex) {
    print("起始下标必须小于结束下标!");
    return;
  }
  
  _textSpanBuilder.delete(_startIndex, _endIndex);
}

加上这些检查,可以避免一些奇怪的问题。

块删除的内部原理

如果你对原理感兴趣,可以看看库是怎么实现块删除的。

核心逻辑在 _deleteLimit 方法里。大概流程是这样的:

  1. 遍历所有的@块
  2. 检查删除范围是否和@块有交集
  3. 如果有交集,把删除范围扩展到整个@块
  4. 执行删除操作

伪代码大概是这样:

void _deleteLimit(...) {
  for (var item in _customWidgets) {
    // 检查删除范围是否涉及到这个@块
    bool shouldDeleteBlock = false;
    
    for (var i = deleteRange.start; i <= deleteRange.end; i++) {
      if (i > item.range.start && i < item.range.end) {
        shouldDeleteBlock = true;
        break;
      }
    }
    
    // 如果涉及到,扩展删除范围
    if (shouldDeleteBlock) {
      deleteRange = TextRange(
        start: min(deleteRange.start, item.range.start),
        end: max(deleteRange.end, item.range.end),
      );
    }
  }
  
  // 执行删除
  // ...
}

理解了这个原理,就能明白为什么删除@块中间的一个字,会导致整个@块被删除。

光标位置限制

除了块删除,这个库还有一个细节处理:光标位置限制。

用户没办法把光标定位到@块的中间。如果用户点击@张三的"张"和"三"之间,光标会自动跳到@块的前面或后面。

这个行为也是自动的,不需要我们写代码。


判断逻辑大概是这样:如果点击位置在@块范围内,就计算一下离前面近还是离后面近,然后把光标移到近的那一端。

int _calculationCursorPosition(TextRange range, int position, int max) {
  if (position >= range.start && position <= range.end) {
    int median = range.start + (range.end - range.start) ~/ 2;
    if (position < median) {
      return range.start;  // 离前面近,跳到前面
    }
    return range.end;  // 离后面近,跳到后面
  }
  return position;
}

这个细节处理得挺好的,用户体验上很自然。

鸿蒙适配说明

删除和清空功能都是纯 Dart 实现的,在鸿蒙上没有任何兼容性问题。

我测试的时候特意试了一下鸿蒙输入法的删除键,块删除效果和安卓、iOS上完全一致。

踩过的坑

坑1:删除后光标位置

调用 delete 方法之后,光标会自动移到删除位置的起点。如果你想让光标在其他位置,需要手动设置:

_textSpanBuilder.delete(5, 10);

// 手动设置光标位置
_textSpanBuilder.controller?.selection = TextSelection.collapsed(offset: 5);

坑2:连续删除

如果你需要连续删除多个范围,要注意下标会变化。比如删除了0-5之后,原来下标为10的字符现在变成下标5了。

建议从后往前删,这样前面的下标不会受影响:

// 要删除 0-5 和 10-15
// 先删后面的
_textSpanBuilder.delete(10, 15);
// 再删前面的
_textSpanBuilder.delete(0, 5);

坑3:清空后立即添加

clear() 之后如果立即调用 appendToEnd,有时候会有一点延迟问题。建议加一个小延迟:

_textSpanBuilder.clear();

Future.delayed(Duration(milliseconds: 50), () {
  _textSpanBuilder.appendTextToEnd("新内容");
});

写在最后

删除功能看起来简单,但块删除这个细节处理得好不好,直接影响用户体验。

FlutterTextSpanField 在这方面做得还不错,块删除、光标限制这些都帮我们处理好了,省了不少事。

这个系列的三篇文章到这里就结束了,分别介绍了@用户功能、隐藏域值获取、删除与清空功能。希望对你有帮助,有问题欢迎留言讨论~


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

Logo

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

更多推荐