flutter如何实现富文本?

前言

做了个富文本编辑器,先给大家看看效果: 【潜心研究一年,开发的笔记软件】

之前有个朋友私信我说,既然你的笔记软件收费了,能不能出点富文本的教学?

不是我不想出,实在是太复杂了呀 >_>!

我先写一篇,看看大家能不能看懂,如果写得都能看懂的话,那后面出一期教学又有何妨。

富文本编辑器需要解决以下几个难题(不仅仅):

  • 编辑协议(数据结构)
  • 选择器
  • 数据渲染
  • 富文本html解析
  • 撤销重做
  • 输入法对接
  • 复制粘贴

富文本元素

首先看看有哪些富文本元素:

  • 文本
  • 标题
  • 链接
  • 公式
  • 代码块
  • 表格
  • 分割线
  • 图片
  • 列表
  • 有序列表
  • 引用

对这些元素进行分类,行内元素有:

  • 文本
  • 标题
  • 链接
  • 公式

块元素有:

  • 代码块
  • 表格
  • 图片
  • 列表
  • 有序列表
  • 引用

对于行内元素,使用flutter自带的 RichText 即可实现,对于块元素,就需要单独处理。

例如图片元素,可以自定义一个图片 Widget。

数据渲染

为了提高渲染性能,渲染部分设计成局部渲染,按需加载。

即将内容分成行,内容由一行行块元素组成,块元素内部会包含行内元素。

渲染时,每个行块给个预设尺寸,等元素出现到视图中时,才会计算元素的真实尺寸。

当然真实尺寸其实也可以缓存到数据结构中,增加性能,但也会增加数据体积。

对于块元素,用一个抽象类 Block 表示。

整个界面通过 Scrollable + Stack 组件实现。

在滚动视图 Scollable 进行滚动时,需要根据 Block 尺寸计算出现在视野中的 Block。

在 Widget 的 layout 阶段,通过 ViewportOffset 的 applyViewportDimension 和 applyContentDimensions 通知滚动视图刷新视野尺寸和内容尺寸。

因此,我们可以在layout阶段根据预设尺寸计算可能出现在视野中的Block,再去计算出可能现在视野中的Block真实尺寸,进而刷新真实内容尺寸,然后将尺寸交给 ViewportOffset 即可处理滚动了。

有点类似微积分,在滚动过程中不断计算尺寸,ViewportOffset 会自动处理滚动逻辑,无需我们多做什么。

选择器

选择富文本内容时,需要根据点击坐标计算得到光标位置。

如何描述光标位置?不妨想想如何确定光标位置。

光标在哪一行?在这行的那个位置?

对于文本元素,就是第几行 + 第几个文字。

对于图片元素,就是第几行 + 0/1,0指的时图片前面,1指的是图片后面。

对于表格元素,就是第几行 + 第几表格列 + 第几表格行 + 第几个文字,但可以通过数学索引包含表格内行列位置,化简成第几行 + 第几个文字。

其它的元素也都可以化简成第几行+第几个文字的形式,不一一列举。

输入法对接

在 flutter 中,通过 TextInput.attach 即可完成输入法的接入。

在 attach 后,会返回一个 TextInputConnection 对象,通过这个 connect 对象与输入法完成交互。

需要结合 FocusNode,监听 Focus 变化,在聚焦时打开输入法,在非聚焦时关闭输入法。

通过以下代码更新内容到拼音输入法


void updateEditingValue(TextEditingValue value) {
  if (!value.composing.isValid) {
    if (value.text.isNotEmpty) {
      inputCallback?.call(value);
      connection?.setEditingState(const TextEditingValue());
    } else {
      inputStartCallback?.call(value);
    }
  } else {
    inputComposingCallback?.call(value);
  }
}

对于桌面端,还需要通过以下代码更新拼音输入法显示位置

void updateInputPosition(
    Size editableSize, Rect? caretRect, Rect? composingRect) {
  if (caretRect != null && composingRect != null) {
    Offset? offset = _snapToPhysicalPixel(caretRect.topLeft);
    if (offset != null) {
      connection?.setCaretRect(caretRect.shift(offset));
      connection?.setComposingRect(composingRect.shift(offset));
    }
    Matrix4? to = getTransformTo();
    if (to != null) {
      connection?.setEditableSizeAndTransform(editableSize, to);
    }
    if (connection?.attached == true) {
      connection?.show();
    }
  }
}

另外,对于ios也要做特殊处理,我还没处理,各位有兴趣的可以去测试下。

复制粘贴

粘贴需要读取系统剪切板,通过 RichClipboard 得到 text 和 html。

如果有 html,证明是富文本,需要解析 html 到自定义的 Block 格式。

通过 Pasteboard.image 得到内存图,如果有内存图片,就需要读取内存图数据,存到自己设计好的文件夹中,然后转换为 ImageBlock,插入到行管理器中即可。

复制时,则将每行的 Block 写到 html 里面,然后将 html 交给 RichClipboard 即可。

Future<void> paste({
  bool pasteText = false,
  bool pasteHtml = false,
  bool pasteMarkdown = false,
}) async {
  var data = await RichClipboard.getData();
  var text = data.text;
  var html = data.html;
  Uint8Lis? image;
  if (!isMobile) {
    image = await Pasteboard.image;
  }
}

撤销重做

撤销重做需要定义一个撤销管理器 UndoManager 来记录每一步的动作。

在 UndoManager 用一个 List 存储每一步的 oldInfo 和 newInfo。

用一个 index 存储当前位置用于处理撤销和重做动作。

如果是撤销,则将newInfo转换为oldInfo,index–;

如果是重做,则将oldInfo转换为newInfo,index++;

index为-1,不可撤销;

index=list.length,不可重做。

当然,我做的富文本采用了yjs翻译过来的ydart,就没有做的那么麻烦,不过我做的思维导图组件是这样实现的= =。


class UndoManager {
  MindMapController controller;

  UndoManager(this.controller);

  List<NodeDelta> deltaList = [];

  int index = -1;

  void add(NodeDelta delta) {
    if (index < deltaList.length - 1) {
      deltaList.removeRange(index + 1, deltaList.length);
    }
    deltaList.add(delta);
    index++;
  }

  void undo() {
    if (index >= 0) {
      deltaList[index].undo(controller);
      index--;
    }
  }

  void redo() {
    if (index < deltaList.length - 1) {
      index++;
      deltaList[index].redo(controller);
    }
  }

  bool get canUndo => index >= 0;

  bool get canRedo => index < deltaList.length - 1;

}

总结

本期简单介绍了如何 flutter 实现一个富文本,具体要实现其实还是很费脑筋的。

我之前开源的温知笔记有具体实现,虽然代码比较乱,但也勉强可以看一看,代码地址:https://github.com/lyming99/wenznote

可以点个关注,我会不定期分享一些flutter知识,感谢各位!。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值