1500字范文,内容丰富有趣,写作好帮手!
1500字范文 > Flutter从0到1实现高性能 多功能的富文本编辑器(模块分析篇)

Flutter从0到1实现高性能 多功能的富文本编辑器(模块分析篇)

时间:2019-09-16 19:21:06

相关推荐

Flutter从0到1实现高性能 多功能的富文本编辑器(模块分析篇)

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

了解富文本编辑器需要拥有的功能知道编写富文本编辑器需要的代码模块学会定义富文本配置JSON,并将其解析为富文本

前言:

经过前面两篇文章的基础知识铺垫,我们终于要进入到专栏的核心内容 — 富文本。富文本编辑器可以说是APP中最复杂,但使用场景又极广的组件之一。例如各大笔记APP的核心功能、闲鱼的商品发布功能、还有掘金APP的发布文章&发布沸点功能等,可以说是富文本编辑器让用户能以更简单更便携的方式记录内容。不过Flutter只有最基础的文本编辑组件TextField,在遇到复杂场景时就比较吃力了,例如图片的添加,有序段落…本文通过分析市场上的各大富文本编辑器的功能和Flutter优秀的富文本插件,从而来自定义自己的富文本编辑器,与大家一起探索文本的世界…

注:Flutter目前已经有许多优秀的开源富文本编辑器,例如:flutter_quill。为了更好的实现属于自己的`富文本编辑器,我们必须要先了解&学习这些优秀的开源项目。

对比分析各大APP的富文本编辑器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VOHjSdew-1669544651776)(https://p1-/tos-cn-i-k3u1fbpfcp/e48f2ab9191c4ffd8d70c2cea8f58435~tplv-k3u1fbpfcp-watermark.image?)]

对比各大APP的富文本编辑器后,我们可以将富文本功能总结为这些部分:

协议选择

如今有很多优秀的富文本编辑器,例如QuillwangEditorProsemirror。根据开源协议、可扩展性、生态等方面的对比考虑之后,本专栏选用Quill协议做为我们富文本编辑器的协议。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vl3dKmqU-1669544651778)(https://p3-/tos-cn-i-k3u1fbpfcp/7e527fb6666f4aef9533c8200cc19180~tplv-k3u1fbpfcp-zoom-1.image)]

——图片来源:Quill富文本编辑器的实践 - DevUI

富文本插件基础分析 — FlutterQuill

FlutterQuill是Quill在Flutter的版本,我们来分析下它的基础构成部分、以便更好的实现我们的富文本。

——注:分析的为部分代码,目的在于了解Flutter实现富文本需要哪些部分。

定义配置文件

为了保存输入的文本与样式,需要定义JSON配置文件,在文本需要保存时,只需将JSON提交到服务器。在渲染时,只需通过解析JSON内容进行渲染。

[{"insert": "Flutter Quill" //需要插入的文本},{"attributes": {"header": 1 //h1标题样式},"insert": "\n" //换行},]

定义样式文件

富文本存在许多的样式,例如一二三级的标题样式,字体的颜色,字体的格式…若不定义这些样式文件,那么代码将难以修改维护。

颜色基础样式类:

=====定义颜色基础样式类,包含对JSON颜色的解析方法=====Color stringToColor(String? s, [Color? originalColor]) {//定义基础的颜色switch (s) {case 'transparent':return Colors.transparent;case 'black':return Colors.black;....}//解析JSON数据//"color": "rgba(0, 0, 0, 0.847)"if (s!.startsWith('rgba')) {s = s.substring(5); //取rgba( 五个字符s = s.substring(0, s.length - 1); //取出剩下的字符,并覆盖final arr = s.split(',').map((e) => e.trim()).toList(); //根据","分割出参数return Color.fromRGBO(int.parse(arr[0]), int.parse(arr[1]),int.parse(arr[2]), double.parse(arr[3])); //返回Color值}}

字体大小基础类:

=====定义字体大小基础类=====dynamic getFontSize(dynamic sizeValue) {//选择已定义的字体大小(小,大,超大)if (sizeValue is String && ['small', 'large', 'huge'].contains(sizeValue)) {return sizeValue;}//自定义字体大小if (sizeValue is double) {return sizeValue;}​if (sizeValue is int) {return sizeValue.toDouble();}​assert(sizeValue is String);final fontSize = double.tryParse(sizeValue); //检查字符串是否为数字字符串if (fontSize == null) {//如果不是,抛出一个异常throw 'Invalid size $sizeValue'; }return fontSize;}

样式

=====文本样式=====bold: const TextStyle(fontWeight: FontWeight.bold),italic: const TextStyle(fontStyle: FontStyle.italic),small: const TextStyle(fontSize: 12),...h1: DefaultTextBlockStyle(//h1的字体样式defaultTextStyle.style.copyWith(fontSize: 34,color: defaultTextStyle.style.color!.withOpacity(0.70),height: 1.15,fontWeight: FontWeight.w300,decoration: TextDecoration.none,),const Tuple2(16, 0), //间距const Tuple2(0, 0),null),//自定义的样式类class DefaultTextBlockStyle {DefaultTextBlockStyle(this.style,this.verticalSpacing,this.lineSpacing,this.decoration,);}

自定义控件

自定义光标

在android和在ios上的输入框光标是差距很大的,若没有分开适配,那会出现许多奇怪的问题。

定义光标的样式

class CursorStyle {const CursorStyle({required this.color,required this.backgroundColor,this.width = 1.0,...this.opacityAnimates = false,this.paintAboveText = false,});​...//定义光标的圆角final Radius? radius; //在绘制光标时使用的偏移量(文本的大小、样式、字体格式改变后、偏移量也会改变)final Offset? offset;//光标闪烁动画的绘制,在android平台和ios平台有不同的表现,需要适配final bool opacityAnimates;//判断光标的绘制方式final bool paintAboveText;...}

定义光标控制器

通过ChangeNotifier监听更新光标的动画与样式。

class CursorCont extends ChangeNotifier {CursorCont({required CursorStyle style,...}) :_style = style...CursorStyle _style;CursorStyle get style => _style;//在样式发生变化时,执行notifyListeners()方法set style(CursorStyle value) {if (_style == value) return;_style = value;notifyListeners();}//控制光标是否显示void _cursorTick(Timer timer) {_targetCursorVisibility = !_targetCursorVisibility;//如果需要显示光标,我们需要将不透明度的值设为1.0,如果想要光标消失,我们需要将其设为0.0//当然,这个变化的过程是有曲线动画的,而且android和ios的动画时间会不同。final targetOpacity = _targetCursorVisibility ? 1.0 : 0.0;if (style.opacityAnimates) {_blinkOpa1cityController.animateTo(targetOpacity, curve: Curves.easeOut);} else {_blinkOpacityController.value = targetOpacity;}}...}

绘制光标

class CursorPainter {...//在画布指定位置绘制光标。 void paint(Canvas canvas, Offset offset, TextPosition position, bool lineHasEmbed) {// 光标偏移量var relativeCaretOffset = editable!.getOffsetForCaret(position, prototype);...​if (caretRect.left < 0.0) {//为了避免ios光标因为滚动始终保持在一行的开头。不过会导致光标更靠近右边的字符,但是影响不大。caretRect = caretRect.shift(Offset(-caretRect.left, 0));}​final caretHeight = editable!.getFullHeightForCaret(position);if (caretHeight != null) {//isAppleOS是自定义的基础函数,判断当前系统是macOS还是iOSif (isAppleOS()) {// 将光标垂直居中插入caretRect = Rect.fromLTWH(caretRect.left,caretRect.top + (caretHeight - caretRect.height) / 2,caretRect.width,caretRect.height,);}}...}}

适配iOS浮动光标

//使用浮动光标FloatingCursorPainter get _floatingCursorPainter => FloatingCursorPainter(floatingCursorRect: _floatingCursorRect,style: _cursorController.style,);​class FloatingCursorPainter {FloatingCursorPainter({required this.floatingCursorRect,required this.style,});​CursorStyle style;//使用Rect来存储绘制的位置信息Rect? floatingCursorRect;​final Paint floatingCursorPaint = Paint();void paint(Canvas canvas) {final floatingCursorRect = this.floatingCursorRect;final floatingCursorColor = style.color.withOpacity(0.75);if (floatingCursorRect == null) return;//绘制圆角矩形(绘制浮动光标)canvas.drawRRect(//使用fromRectAndRadius定义一个RectRRect.fromRectAndRadius(floatingCursorRect, _kFloatingCaretRadius),floatingCursorPaint..color = floatingCursorColor,);}}

通过继承RenderObjectWidget自定义输入框控件

为了有更好的可扩展性,对于文本的输入和编辑,就最好不要使用TextField这些Flutter已经提供的组件了。我们通过RenderEditableBox去实现可编辑的输入框。我们可以通过RenderEditableBox实现文本选择、文本光标的操作。

class RenderEditableTextLine extends RenderEditableBox {}

自定义文本选择范围

自定义全局控制器

===基础控制,外部调用大部分都通过这个===class QuillController extends ChangeNotifier {...//基础富文本,提供给外部使用。只需要在使用时定义一个controller即可使用Flutter Quill编辑器//QuillController _controller = QuillController.basic();factory QuillController.basic() {return QuillController(document: Document(),selection: const TextSelection.collapsed(offset: 0),);}//获取选择的样式Style getSelectionStyle() {return document.collectStyle(selection.start, selection.end - selection.start).mergeAll(toggledStyle);}//清空当前富文本中的内容void clear() {replaceText(0, plainTextEditingValue.text.length - 1, '',const TextSelection.collapsed(offset: 0));}...}

定义基础规则(插入、删除、其他)

——因规则较多,此处我们只分析部分规则

保留该行文本的样式。在用户按下回车键时,或粘贴包含多行的文本时,触发该规则。

class PreserveBlockStyleOnInsertRule extends InsertRule {const PreserveBlockStyleOnInsertRule();​@overrideDelta? applyRule(Delta document, int index,{int? len, Object? data, Attribute? attribute}) {//此规则只对包含'\n'的文本起作用if (data is! String || !data.contains('\n')) {return null;}​final itr = DeltaIterator(document)..skip(index);​// 继续查找下一个'\n'final nextNewLine = _getNextNewLine(itr);final lineStyle =Style.fromJson(nextNewLine.item1?.attributes ?? <String, dynamic>{});​final blockStyle = lineStyle.getBlocksExceptHeader();// 多行文本是否在一个区块中,如果不是,就忽略该文本。if (blockStyle.isEmpty) {return null;}​final resetStyle = <String, dynamic>{};//如果这一行文本有文本样式,我们需要将样式保留到新的一行文本中if (lineStyle.containsKey(Attribute.header.key)) {resetStyle.addAll(Attribute.header.toJson());}​// 检查每一行文本,确保在同一块中,使用了相同的样式final lines = data.split('\n');final delta = Delta()..retain(index + (len ?? 0));for (var i = 0; i < lines.length; i++) {final line = lines[i];if (line.isNotEmpty) {delta.insert(line);}if (i == 0) {// 第一行完全继承于lineStyledelta.insert('\n', lineStyle.toJson());} else if (i < lines.length - 1) {final blockAttributes = blockStyle.isEmpty? null: blockStyle.map<String, dynamic>((_, attribute) =>MapEntry<String, dynamic>(attribute.key, attribute.value));//在文本最后插入'\n'delta.insert('\n', blockAttributes);}}​// 如果自定义了换行样式,则替换原始换行样式if (resetStyle.isNotEmpty) {delta..retain(nextNewLine.item2!)..retain((nextNewLine.item1!.data as String).indexOf('\n'))..retain(1, resetStyle);}​return delta;}}

总结

看到这里,是不是觉得还是有点不清晰,毕竟我们是分析了部分内容,对于拥有富文本的模块,我们可以总结成为下面这张图。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V0OVxuVX-1669544651780)(https://p6-/tos-cn-i-k3u1fbpfcp/07b8cc1b46ed425ab53441ed23513f9d~tplv-k3u1fbpfcp-watermark.image?)]

解析JSON,渲染文本

看到这里,相信你已经知道富文本编辑器由哪些模块组成了。那么就让我们开始实现属于我们的富文本编辑器吧。

定义JSON配置文件

好的组件都是一点点迭代起来的,刚开始我们不需要定义太多的参数,后面一点一点迭代优化即可。在这里,我们定义一个基础的JSON文件。用于控制文本的大小和颜色。

[{"message": "Flutter Editor Taxze", //正文"richTexts": { //文本属性"color": "#e60000", "textSize": 32},"insert": "\n" //判断该段落是否已经结束。},{"message": "富文本好像是个大坑","richTexts": {"color": "rgba(32, 54, 190, 1)","textSize": 24},"insert": "\n"}]

编写颜色解析方法

Color stringToColor(String s) {if (s.startsWith('rgba')) {s = s.substring(5); //取rgba( 五个字符s = s.substring(0, s.length - 1); //取出剩下的字符,并覆盖final arr = s.split(',').map((e) => e.trim()).toList(); //根据","分割出参数return Color.fromRGBO(int.parse(arr[0]), int.parse(arr[1]),int.parse(arr[2]), double.parse(arr[3])); //返回Color值} else if (s.startsWith('#')) {s = s.toUpperCase().replaceAll("#", ""); //将字符串转为大写,同时将#号去掉if (s.length == 6) {//判断是否为正确的颜色格式s = "FF$s";}return Color(int.parse(s, radix: 16)); //返回Color值,radix:默认基数10进制,我们需要指定是16进制}return const Color.fromRGBO(0, 0, 0, 0);}

编写JSON实体类

class TextRichInfo {String? message;Map<String, dynamic>? richTexts;String? color;int? textSize;String? insert;​TextRichInfo({this.message, this.richTexts, this.color, this.textSize, this.insert});​TextRichInfo.fromJson(Map<String, dynamic> json) {message = json["message"];richTexts = json["richTexts"];color = richTexts!['color'];textSize = richTexts!['textSize'];insert = json['insert'] ?? null;}}

解析JSON方法

List<TextSpan> _textSpanList() {List<TextSpan> spanList = [];//将网络获取到的Json格式字符串解析var textJsonMap = json.decode(widget.richTextJsonConfig);for (var i in textJsonMap) {var data = TextRichInfo.fromJson(i);String message = data.message!;int textSize = data.textSize!;Color richTextColor = stringToColor(data.color!);spanList.add(TextSpan(text: message,style: TextStyle(color: richTextColor, fontSize: textSize.toDouble()),));//判断该段落是否结束,如果结束添加回车。String? insert = data.insert;if (insert != null) {spanList.add(const TextSpan(text: "\n",));}}return spanList;}

使用

//富文本渲染build@overrideWidget build(BuildContext context) {return Text.rich(TextSpan(children: _textSpanList()));}//使用@overrideWidget build(BuildContext context) {return Scaffold(body: Center(child: RichTextEditor(richTextJsonConfig:r'[{"message": "Flutter Editor Taxze","richTexts": {"color": "#e60000","textSize": 32},"insert": "\n"},{"message": "富文本好像是个大坑","richTexts": {"color": "rgba(32, 54, 190, 1)","textSize": 24}}]'),),);}

这样一番操作下来后,我们就实现了将JSON数据解析为富文本的功能。但需要注意的一点,目前是模拟从服务端获取数据,所以需要确保在上传数据的时候,colortextSize都是有一个默认值的。效果图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a9rcHVBr-1669544651781)(https://p6-/tos-cn-i-k3u1fbpfcp/6eed20dd4dcd4e7ba3d99369b2f6c850~tplv-k3u1fbpfcp-watermark.image?)]

尾述

在这篇文章中,我们分析了一个富文本编辑器需要有哪些功能,也分析了优秀的富文本编辑器Flutter Quill,知道了实现富文本编辑器需要有哪些模块。最后我们对自定义富文本编辑器做了一个开头,简单实现了对富文本JSON配置数据的解析。在下一篇文章中,会详细分析自定义富文本编辑器。希望这篇文章能对你有所帮助,有问题欢迎在评论区留言讨论~

参考

flutter_quill

关于我

Hello,我是Taxze,如果您觉得文章对您有价值,希望您能给我的文章点个❤️,有问题需要联系我的话:我在这里 ,也可以通过掘金的新的私信功能联系到我。如果您觉得文章还差了那么点东西,也请通过关注督促我写出更好的文章——万一哪天我进步了呢?😝

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。