SuperText富文本规划方案
Flutter中要完成富文本,需求运用RichText
或许Text.rich
方法,经过拆分红List<InlineSpan>
来完成,第一感觉上如同还行,但实际运用了才知道,有一个很大的问题便是关于复杂的富文本作用,无法准确拆分出具有实际作用的spans。因此想规划一个具有多种富文本作用,同时便于运用的富文本控件SuperText。
RichText原理
Flutter中的InlineSpan
其实是Tree的结构。例如一段md款式的文字:
这是一段部分粗体的文字
graph TB
A(这是一段部分**粗体**的文字)
B(这是一段)
C(**粗体**)
D(的文字)
A-->B
A-->C
A-->D
其实便是被拆分了3段TextSpan,然后下面绘制的时分,就会运用ParagraphBuilder
别离访问这3个节点,3个节点别离往ParagraphBuilder
中填充对应的文字以及款式。
那么是否这个树的深度一定只要2层?
这个未必,如果一开端就解析拆分出一切的富文本作用,那么可能就只要2层,但实际上就算是多层,也是没有问题的,例如:
这是一段粗体而且部分带着斜体作用的文字
能够拆分红如下:
graph TB
A(这是一段**粗体而且部分带着*斜体作用*的文字**)
B(这是一段)
C(**粗体而且部分带着*斜体作用*的文字**)
D(**粗体而且部分带着**)
E(*斜体作用*)
F(**的文字**)
A-->B
A-->C
C-->D
C-->E
C-->F
需求留意的是TextSpan
中有两个参数,一个是text
,一个是children
。这两个参数是同时生效的, 先用TextSpan中的style和structstyle显现text
,然后再接着显现children。例如:
Text.rich(
TextSpan(
text: '123456',
children: [
TextSpan(
text: 'abcdefg',
style: TextStyle(color: Colors.blue),
),
]
),
)
终究显现的作用是123456abcdefg
,其中abcdefg
是蓝色的。
方案规划
了解了富文本的原理后,封装控件需求完成的方针就确认了,那便是
主动将文本text,转换成inlineSpan组成的树
然后丢给Text控件去显现。
那么怎么去完成这个转化的进程?我的主意是顺次遍历节点,然后衍生出新的节点,终究由叶子节点组成终究的显现作用。
咱们以包括自定义表情和##标签的作用为比如。
#一个[表情]的标签#哈哈哈哈哈
首要初始状态只要文本text的情况下,能够认为是一个树的根节点,里面存在文本text。咱们能够先把标签解析出来,那么就能从这个根节点,拆分出2个节点:
graph TB
A("#一个[表情]的标签#哈哈哈哈哈")
B("#一个[表情]的标签#")
C(哈哈哈哈哈)
A-->B
A-->C
然后再将两个叶子节点解析自定义表情:
graph TB
A("#一个[表情]的标签#哈哈哈哈哈")
B("#一个[表情]的标签#")
C(哈哈哈哈哈)
D(#一个)
E("[表情]")
F(的标签#)
A-->B
A-->C
B-->D
B-->E
B-->F
终究得到4个叶子节点,终究生成的InlineSpan,应该如下:
TextSpan(
children: [
TextSpan(
style: TextStyle(color: Colors.blue),
children: [
TextSpan(
text: '#一个',
),
WidgetSpan(
child: Image.asset(),
),
TextSpan(
text: '的标签#',
),
],
),
TextSpan(
text: '哈哈哈哈哈',
style: TextStyle(color: Colors.black),
),
],
),
上述进程,涉及到三点:1. 遍历;2. 解析拆分;3. 生成节点。等到了终究一切叶子结点都无法再被拆分出新节点时,这颗InlineSpan树便是终究的解析成果。
解析
怎么进行解析。像Emoji表情或许http链接那种,一般都是运用正则便能识别出来,而更加简略的变色彩、改字体大小这种,在Android上都是直接经过设置开端方位和完毕方位来标明范围的,咱们也能够运用这种简略好了解的方式来完成,所以解析的时分,需求能够拿到待解析内容在原始文本中的方位。例如原文“一个需求放大的字”,现已被其他解析器分红了两段“一个需求”和“放大的字”,在斜体解析器解析“放大的字”的时分,需求知道原文第5到第6个字需求变成斜体,在把这5->6转变成相关于“放大的字”这一段而言的第1到第2个字。
代码规划
方案了解了之后,就开端简略的结构编写。
节点定义
依照树结构,定义一个Node
class TextNode {
///该节点文本
String text;
TextStyle style;
late InlineSpan span;
///该节点文本,在原始文本中的开端方位。include
int startPosInOri;
///该节点文本,在原始文本中的完毕方位。include
int endPosInOri;
List<TextNode>? subNodes;
TextNode(this.text, this.style,
{required this.startPosInOri, required this.endPosInOri});
}
Span结构器定义
abstract class BaseSpanBuilder {
bool isSupport(TextNode node);
///
/// 解析生成子节点
///
List<TextNode> parse(TextNode node);
}
SuperText定义
先作为一个简略版的Text控件,接纳text
、TextStyle
和结构器列表
即可。
class SuperText extends StatefulWidget {
final String text;
final TextStyle style;
final List<BaseSpanBuilder>? spanBuilders;
const SuperText(
this.text, {
Key? key,
required this.style,
this.spanBuilders,
}) : super(key: key);
@override
State<StatefulWidget> createState() {
return _SuperTextState();
}
}
对应的build()
方法:
late InlineSpan _textSpan;
@override
Widget build(BuildContext context) {
return Text.rich(
_textSpan,
style: widget.style,
);
}
之后需求做的事便是把传入的text
解析成_textSpan
即可。
InlineSpan _buildSpans() {
if (widget.spanBuilders?.isEmpty ?? true) {
return TextSpan(text: widget.text, style: widget.style);
} else {
//预备根节点
TextNode rootNode = TextNode(widget.text, widget.style,
startPosInOri: 0, endPosInOri: widget.text.length - 1);
rootNode.span = TextSpan(text: widget.text, style: widget.style);
//开端生成子节点
_generateNodes(rootNode, 0);
//深度优先遍历,生成终究的inlineSpan
List<InlineSpan> children = [];
dfs(rootNode, children);
return TextSpan(children: children, style: widget.style);
}
}
void _generateNodes(TextNode node, int builderIndex) {
BaseSpanBuilder spanBuilder = widget.spanBuilders![builderIndex];
if (spanBuilder.isSupport(node)) {
List<TextNode> subNodes = spanBuilder.parse(node);
node.subNodes = subNodes.isEmpty ? null : subNodes;
if (builderIndex + 1 < widget.spanBuilders!.length) {
if (subNodes.isNotEmpty) {
//生成了子节点,那么把子节点抛给下个span结构器
for (TextNode n in subNodes) {
_generateNodes(n, builderIndex + 1);
}
} else {
//没有子节点,说明当时的span结构器不处理当时的节点内容,那么把当时的节点抛给下个span结构器
_generateNodes(node, builderIndex + 1);
}
}
}
}
///
/// 深度优先遍历,构建终究的List<InlineSpan>
///
void dfs(TextNode node, List<InlineSpan> children) {
if (node.subNodes?.isEmpty ?? true) {
children.add(node.span);
} else {
for (TextNode n in node.subNodes!) {
dfs(n, children);
}
}
}
完成逻辑根本便是方案规划中的主意。
能够修正TextStyle的Span结构器
舞台预备好了,那个要练习艺人了。这儿编写一个TextStyleSpanBuilder
,用于承受TextStyle作为富文本款式:
class TextStyleSpanBuilder extends BaseSpanBuilder {
final int startPos;
final int endPos;
final Color? textColor;
final double? fontSize;
final FontWeight? fontWeight;
final Color? backgroundColor;
final TextDecoration? decoration;
final Color? decorationColor;
final TextDecorationStyle? decorationStyle;
final double? decorationThickness;
final String? fontFamily;
final double? height;
final List<Shadow>? shadows;
TextStyleSpanBuilder(
this.startPos,
this.endPos, {
this.textColor,
this.fontSize,
this.fontWeight,
this.backgroundColor,
this.decoration,
this.decorationColor,
this.decorationStyle,
this.decorationThickness,
this.fontFamily,
this.height,
this.shadows,
}) : assert(startPos >= 0 && startPos <= endPos);
@override
List<TextNode> parse(TextNode node) {
List<TextNode> result = [];
if (startPos > node.endPosInOri || endPos < node.startPosInOri) {
return result;
}
if (startPos >= node.startPosInOri) {
//富文本开端方位,在这段文字之内
if (startPos > node.startPosInOri) {
int endRelative = startPos - node.startPosInOri;
String subText = node.text.substring(0, endRelative);
TextNode subNode = TextNode(
subText,
node.style,
startPosInOri: node.startPosInOri,
endPosInOri: startPos - 1,
);
subNode.span = TextSpan(text: subNode.text, style: subNode.style);
result.add(subNode);
}
//富文本在这段文字的开端方位
int startRelative = startPos - node.startPosInOri;
int endRelative;
String subText;
TextStyle textStyle;
if (endPos <= node.endPosInOri) {
//完毕方位在这段文字内
endRelative = startRelative + (endPos - startPos);
} else {
//完毕方位,超出了这段文字。将开端到这段文字完毕,都包括进富文本去
endRelative = node.endPosInOri - node.startPosInOri;
}
subText = node.text.substring(startRelative, endRelative + 1);
textStyle = copyStyle(node.style);
TextNode subNode = TextNode(
subText,
textStyle,
startPosInOri: node.startPosInOri + startRelative,
endPosInOri: node.startPosInOri + endRelative,
);
subNode.span = TextSpan(text: subNode.text, style: subNode.style);
result.add(subNode);
if (endPos < node.endPosInOri) {
//还有剩余的一段
startRelative = endPos - node.startPosInOri + 1;
endRelative = node.endPosInOri - node.startPosInOri;
subText = node.text.substring(startRelative, endRelative + 1);
TextNode subNode = TextNode(
subText,
node.style,
startPosInOri: endPos + 1,
endPosInOri: node.endPosInOri,
);
subNode.span = TextSpan(text: subNode.text, style: subNode.style);
result.add(subNode);
}
} else {
//富文本开端方位不在这段文字之内,那就检查富文本结束的方位,是否在这段文字内
if (node.startPosInOri <= endPos) {
int startRelative = 0;
int endRelative;
String subText;
TextStyle textStyle;
if (endPos <= node.endPosInOri) {
//富文本结束方位,在这段文字内
endRelative = endPos - node.startPosInOri;
} else {
//富文本结束方位,超过了这段文字
endRelative = node.endPosInOri - node.startPosInOri;
}
subText = node.text.substring(startRelative, endRelative + 1);
textStyle = copyStyle(node.style);
TextNode subNode = TextNode(
subText,
textStyle,
startPosInOri: node.startPosInOri + startRelative,
endPosInOri: node.startPosInOri + endRelative,
);
subNode.span = TextSpan(text: subNode.text, style: subNode.style);
result.add(subNode);
if (endPos < node.endPosInOri) {
//还有剩余的一段
startRelative = endPos - node.startPosInOri + 1;
endRelative = node.endPosInOri - node.startPosInOri;
subText = node.text.substring(startRelative, endRelative + 1);
TextNode subNode = TextNode(
subText,
node.style,
startPosInOri: endPos + 1,
endPosInOri: node.endPosInOri,
);
subNode.span = TextSpan(text: subNode.text, style: subNode.style);
result.add(subNode);
}
}
}
return result;
}
TextStyle copyStyle(TextStyle style) {
return style.copyWith(
color: textColor,
fontSize: fontSize,
fontWeight: fontWeight,
backgroundColor: backgroundColor,
decoration: decoration,
decorationColor: decorationColor,
decorationStyle: decorationStyle,
decorationThickness: decorationThickness,
fontFamily: fontFamily,
height: height,
shadows: shadows,
);
}
@override
bool isSupport(TextNode node) {
return node.span is EmojiSpan || node.span is TextSpan;
}
}
parse
方法在做的事,便是将一个TextNode拆分红多段的TextNode。
作用展示
SuperText(
'0123456789',
style: const TextStyle(color: Colors.red, fontSize: 16),
spanBuilders: [
TextStyleSpanBuilder(2, 6, textColor: Colors.blue),
TextStyleSpanBuilder(4, 7, fontSize: 40),
TextStyleSpanBuilder(6, 9, backgroundColor: Colors.green),
TextStyleSpanBuilder(1, 1, decoration: TextDecoration.underline),
],
)
作用如图:
这个用法,如同和原来的也没啥不同啊。其实不然,首要多个作用之间能够交叉堆叠,别的这儿展示的是根本的运用TextStyle
完成的富文本作用。如果是那种需求依靠正则解析拆分后完成的富文本作用,例如自定义表情,只需求一个EmojiSpanBuilder()
即可。
结语
依照这个方案,关于不同的富文本作用,只需求定制不同的spanBuilder
就能够了,运用方法非常类似于Android的SpannableStringBuilder
。
后续也会想在这个控件基础上添加关于overflow作用的定制功用。