@charset "UTF-8";.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:15px;overflow-x:hidden;color:#333}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{line-height:1.5;margin-top:35px;margin-bottom:10px;padding-bottom:5px}.markdown-body h1:first-child,.markdown-body h2:first-child,.markdown-body h3:first-child,.markdown-body h4:first-child,.markdown-body h5:first-child,.markdown-body h6:first-child{margin-top:-1.5rem;margin-bottom:1rem}.markdown-body h1:before,.markdown-body h2:before,.markdown-body h3:before,.markdown-body h4:before,.markdown-body h5:before,.markdown-body h6:before{content:"#";display:inline-block;color:#3eaf7c;padding-right:.23em}.markdown-body h1{position:relative;font-size:2.5rem;margin-bottom:5px}.markdown-body h1:before{font-size:2.5rem}.markdown-body h2{padding-bottom:.5rem;font-size:2.2rem;border-bottom:1px solid #ececec}.markdown-body h3{font-size:1.5rem;padding-bottom:0}.markdown-body h4{font-size:1.25rem}.markdown-body h5{font-size:1rem}.markdown-body h6{margin-top:5px}.markdown-body p{line-height:inherit;margin-top:22px;margin-bottom:22px}.markdown-body strong{color:#3eaf7c}.markdown-body img{max-width:100%;border-radius:2px;display:block;margin:auto;border:3px solid rgba(62,175,124,.2)}.markdown-body hr{border:none;border-top:1px solid #3eaf7c;margin-top:32px;margin-bottom:32px}.markdown-body code{word-break:break-word;overflow-x:auto;padding:.2rem .5rem;margin:0;color:#3eaf7c;font-weight:700;font-size:.85em;background-color:rgba(27,31,35,.05);border-radius:3px}.markdown-body code,.markdown-body pre{font-family:Menlo,Monaco,Consolas,Courier New,monospace}.markdown-body pre{overflow:auto;position:relative;line-height:1.75;border-radius:6px;border:2px solid #3eaf7c}.markdown-body pre>code{font-size:12px;padding:15px 12px;margin:0;word-break:normal;display:block;overflow-x:auto;color:#333;background:#f8f8f8}.markdown-body a{font-weight:500;text-decoration:none;color:#3eaf7c}.markdown-body a:active,.markdown-body a:hover{border-bottom:1.5px solid #3eaf7c}.markdown-body a:before{content:"⇲"}.markdown-body table{display:inline-block!important;font-size:12px;width:auto;max-width:100%;overflow:auto;border:1px solid #3eaf7c}.markdown-body thead{background:#3eaf7c;color:#fff;text-align:left}.markdown-body tr:nth-child(2n){background-color:rgba(62,175,124,.2)}.markdown-body td,.markdown-body th{padding:12px 7px;line-height:24px}.markdown-body td{min-width:120px}.markdown-body blockquote{color:#666;padding:1px 23px;margin:22px 0;border-left:.5rem solid;border-color:#42b983;background-color:#f8f8f8}.markdown-body blockquote:after{display:block;content:""}.markdown-body blockquote>p{margin:10px 0}.markdown-body details{outline:none;border:none;border-left:4px solid #3eaf7c;padding-left:10px;margin-left:4px}.markdown-body details summary{cursor:pointer;border:none;outline:none;background:#fff;margin:0 -17px}.markdown-body details summary::-webkit-details-marker{color:#3eaf7c}.markdown-body ol,.markdown-body ul{padding-left:28px}.markdown-body ol li,.markdown-body ul li{margin-bottom:0;list-style:inherit}.markdown-body ol li .task-list-item,.markdown-body ul li .task-list-item{list-style:none}.markdown-body ol li .task-list-item ol,.markdown-body ol li .task-list-item ul,.markdown-body ul li .task-list-item ol,.markdown-body ul li .task-list-item ul{margin-top:0}.markdown-body ol ol,.markdown-body ol ul,.markdown-body ul ol,.markdown-body ul ul{margin-top:3px}.markdown-body ol li{padding-left:6px}.markdown-body ol li::marker{color:#3eaf7c}.markdown-body ul li{list-style:none}.markdown-body ul li:before{content:"•";margin-right:4px;color:#3eaf7c}@media (max-width:720px){.markdown-body h1{font-size:24px}.markdown-body h2{font-size:20px}.markdown-body h3{font-size:18px}}
零、前言
上篇中,通过文本解析,实现了对指定文字的高亮包裹,如下图。今天我们继续完善这个富文本显示的功能,比如文本链接解析
、文本标题
、指定文字加粗、斜体
等。本文会用到一些正则表达式的知识,本系列重点不是正则,不会做过多解释。如果看不懂,可以自己去补补。
以下是 Flutter 文本解读
系列的其他文章:
- 《Flutter 文本解读 1 | 从源码认识 Text 组件》
- 《Flutter 文本解读 2 | Text 是如何画出来的》
- 《Flutter 文本解读 3 | Text 组件使用介绍 》
- 《Flutter 文本解读 4 | TextStyle 文字样式解读 》
- 《Flutter 文本解读 5 | RichText 富文本的使用 (上)》
一、文本链接的处理
1.链接匹配的正则
通过 [.*?)
就可以匹配出 markdown
中的链接,这样就可以通过 StringScanner
获取每个匹配到的起始索引。之后的事就和之前一样了。
2.对数据的抽象与实现
可以看出,需要解析的类型是需要拓展的。不同情况的处理也不相同,这样的话,我们可以创建个枚举类,然后根据类型进行判断处理,但这样很多逻辑都会塞在一块
,不好维护。我们可以定义一层抽象,分离出属性和行为,再根据不同的情况进行不同的实现,使用时使用抽象类完成任务即可。
如下抽象中,需要的数据是一段字符的起止所以,子类需要实现 text
方法返回展示的字符,实现 style
方法获取文字样式。提供 recognizer
属性进行事件处理。
abstract class SpanBean {
SpanBean(this.start, this.end,{this.recognizer});
final int start;
final int end;
String text(String src);
TextStyle get style;
final GestureRecognizer recognizer;
}
复制代码
这样可以通过 WrapSpanBean
实现之前的包裹高亮,代码如下:
//包裹规则: `data`
class WrapSpanBean extends SpanBean {
WrapSpanBean(int start, int end) : super(start, end);
@override
String text(String src) {
return src.substring(start 1, end - 1);
}
@override
TextStyle get style => TextStyleSupport.dotWrapStyle;
}
复制代码
在使用时,使用抽象 SpanBean
,在列表添加对象时使用对应的实现。这便是多态的奥义
。
List _spans = []; // 使用抽象
void parseContent() {
while (!_scanner.isDone) {
if (_scanner.scan(RegExp('`.*?`'))) {
int startIndex = _scanner.lastMatch.start;
int endIndex = _scanner.lastMatch.end;
_spans.add(WrapSpanBean(startIndex, endIndex)); // 添加实现
}
if (!_scanner.isDone) {
_scanner.position ;
}
}
}
3.链接解析
处理链接数据的 LinkSpanBean
实现 SpanBean
。
//链接规则: [data](link)
class LinkSpanBean extends SpanBean {
LinkSpanBean(int start, int end, {GestureRecognizer recognizer})
: super(start, end, recognizer: recognizer);
@override
TextStyle get style => TextStyleSupport.linkStyle;
@override
String text(String src) {
final String target = src.substring(start, end);
return target.split(']')[0].replaceFirst("[", '');
}
}
复制代码
在 parseContent
中收录 LinkSpanBean
,其点击事件通过 url_launcher: ^5.7.10
插件跳转到浏览器。有一点要注意: GestureRecognizer 需要被 dispose,可以在 StringParser 中定义 dispose 来遍历 SpanBean 列表进行释放。
void parseContent() {
while (!_scanner.isDone) {
if (_scanner.scan(RegExp('`.*?`'))) {
int startIndex = _scanner.lastMatch.start;
int endIndex = _scanner.lastMatch.end;
_spans.add(WrapSpanBean(startIndex, endIndex));
}
if (_scanner.scan(RegExp(r'[.*?)'))) {
int startIndex = _scanner.lastMatch.start;
int endIndex = _scanner.lastMatch.end;
final String target = content.substring(startIndex, endIndex);
String link = target.split('(')[1].replaceFirst(')', '');
GestureRecognizer recognizer = TapGestureRecognizer()
..onTap = () {
launch(link);
};
_spans.add(LinkSpanBean(startIndex, endIndex, recognizer: recognizer));
}
if (!_scanner.isDone) {
_scanner.position ;
}
}
}
void dispose() {
_spans.forEach((element) {
element.recognizer?.dispose();
});
}
4.TextSpan 处理
和之前的处理一样,这里我们为 SpanBean
添加了GestureRecognizer
,在生成 TextSpan
时使用一下即可。
InlineSpan parser() {
_scanner = StringScanner(content);
parseContent();
final List spans = [];
int currentPosition = 0;
for (SpanBean span in _spans) {
if (currentPosition != span.start) {
spans.add(
TextSpan(text: content.substring(currentPosition, span.start)));
}
spans.add(TextSpan(
style: span.style,
text: span.text(content),
recognizer: span.recognizer));
currentPosition = span.end;
}
if (currentPosition != content.length)
spans.add(
TextSpan(text: content.substring(currentPosition, content.length)));
return TextSpan(style: TextStyleSupport.defaultStyle, children: spans);
}
5.使用效果
这样便可以实现下面的将文本中的链接高亮。
并且点击链接时可以进行跳转。
二、标题文字的处理
1.标题匹配的正则
通过 ^# .*
来匹配 若干个 #
的开头的行。 在 Dart
正则中多行的开头匹配需要。multiLine: true
。这样如下的 # 777
就不会被误配。
RegExp(r'^# .*',multiLine: true)
2.HeadSpanBean 定义
HeadSpanBean
作为 SpanBean
的实现类,可以完成六个等级的标题,通过 lever
属性表示是几级标题。
//标题规则: # data
class HeadSpanBean extends SpanBean{
HeadSpanBean(int start, int end,this.lever) : super(start, end);
final int lever;
@override
TextStyle get style => TextStyleSupport.headStyleMap[lever];
@override
String text(String src) {
final String target = src.substring(start, end);
return target.replaceRange(0, lever 1, '');
}
}
复制代码
在 TextStyleSupport
中提供一个 headStyleMap
用于根据数字获取样式,这样就不需要用分支结构去逐条返回,让代码看着更舒服些。
class TextStyleSupport {
static const TextStyle defaultStyle =
TextStyle(color: Colors.black, fontSize: 14);
static const TextStyle dotWrapStyle =
TextStyle(color: Colors.purple, fontSize: 14);
static const TextStyle linkStyle = TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
decorationColor: Colors.blue);
static const Map<int,TextStyle> headStyleMap = {
1:h1,
2:h2,
3:h3,
4:h4,
5:h5,
6:h6,
};
static const TextStyle h1 =
TextStyle(color: Colors.black, fontSize: 24, fontWeight: FontWeight.bold);
static const TextStyle h2 =
TextStyle(color: Colors.black, fontSize: 22, fontWeight: FontWeight.bold);
static const TextStyle h3 =
TextStyle(color: Colors.black, fontSize: 20, fontWeight: FontWeight.bold);
static const TextStyle h4 =
TextStyle(color: Colors.black, fontSize: 18, fontWeight: FontWeight.bold);
static const TextStyle h5 =
TextStyle(color: Colors.black, fontSize: 16, fontWeight: FontWeight.bold);
static const TextStyle h6 =
TextStyle(color: Colors.black, fontSize: 14, fontWeight: FontWeight.bold);
}
复制代码
3.标题的解析
这样通过 ^# .*
正则表达式,获取对应字符区间的前后界,再分析有多少个 #
即可。
if (_scanner.scan(RegExp(r'^# .*',multiLine: true))) {
int startIndex = _scanner.lastMatch.start;
int endIndex = _scanner.lastMatch.end;
int lever = content.substring(startIndex, endIndex).split(' ')[0].length;
_spans.add(HeadSpanBean(startIndex, endIndex,lever));
}
这样以 #
开头的标题样式就完成了。在 TextStyleSupport
中你可以修改这些默认的样式。或者提供多组不同的样式,提供切换。知道其中的原理,可操作性就可以大大提高。
三、文字加粗和倾斜
1.文字加粗处理
markdown
加粗的规则是 **data**
,通过之前的那几个,现在应该知道大致流程了。对应的正则是 **.*?**
,
定义 BoldSpanBean 如下 :
代码语言:javascript复制//加粗规则: **data**
class BoldSpanBean extends SpanBean{
BoldSpanBean(int start, int end) : super(start, end);
@override
TextStyle get style => TextStyleSupport.bold;
@override
String text(String src) {
return src.substring(start 2, end-2);
}
}
复制代码
解析内容时,进行添加 BoldSpanBean
即可。
if (_scanner.scan(RegExp(r'**.*?**'))) {
int startIndex = _scanner.lastMatch.start;
int endIndex = _scanner.lastMatch.end;
_spans.add(BoldSpanBean(startIndex, endIndex));
}
这样就可以实现局部文字加粗的效果:
2.文字倾斜处理
markdown
倾斜的规则是 *data*
。对应的正则是 **.*?**
,这时我们会发现,这样加粗的 **data**
会有所干扰,使用在解析时,可以先解析 加粗
,再解析 倾斜
。因为 StringScanner
只会对文本进行一次扫描,加粗
的扫描完后,位置索引会增加,就不会对 倾斜
的正则产生影响。
// 加粗匹配
if (_scanner.scan(RegExp(r'**.*?**'))) {
int startIndex = _scanner.lastMatch.start;
int endIndex = _scanner.lastMatch.end;
_spans.add(BoldSpanBean(startIndex, endIndex));
}
// 倾斜匹配
if (_scanner.scan(RegExp(r'*.*?*'))) {
int startIndex = _scanner.lastMatch.start;
int endIndex = _scanner.lastMatch.end;
_spans.add(LeanSpanBean(startIndex, endIndex));
}
代码语言:javascript复制//加粗规则: *data*
class LeanSpanBean extends SpanBean{
LeanSpanBean(int start, int end) : super(start, end);
@override
TextStyle get style => TextStyleSupport.lean;
@override
String text(String src) {
return src.substring(start 1, end-1);
}
}
复制代码
通过本篇,你应该对富文本的使用多了些了解。这样看来,新加一个规则,最重要的是找到其对应的正则表达式。找到之后,就是一些简单的处理了。本文就到这里,下一篇来看一下,在 Flutter 中如何实现一个代码高亮
显示的富文本。