Flutter實現(xiàn)編寫富文本Text的示例代碼
SuperText富文本設(shè)計方案
Flutter中要實現(xiàn)富文本,需要使用RichText或者Text.rich方法,通過拆分成List<InlineSpan>來實現(xiàn),第一感覺上好像還行,但實際使用了才知道,有一個很大的問題就是對于復(fù)雜的富文本效果,無法準(zhǔn)確拆分出具有實際效果的spans。因此想設(shè)計一個具有多種富文本效果,同時便于使用的富文本控件SuperText。
RichText原理
Flutter中的InlineSpan其實是Tree的結(jié)構(gòu)。例如一段md樣式的文字:

其實就是被拆分了3段TextSpan,然后下面繪制的時候,就會使用ParagraphBuilder分別訪問這3個節(jié)點,3個節(jié)點分別往ParagraphBuilder中填充對應(yīng)的文字以及樣式。
那么是否這個樹的深度一定只有2層?
這個未必,如果一開始就解析拆分出所有的富文本效果,那么可能就只有2層,但實際上就算是多層,也是沒有問題的,例如:
這是一段粗體并且部分帶著斜體效果的文字
可以拆分成如下:

需要注意的是TextSpan中有兩個參數(shù),一個是text,一個是children。這兩個參數(shù)是同時生效的, 先用TextSpan中的style和structstyle顯示text,然后再接著顯示children。例如:
Text.rich(
TextSpan(
text: '123456',
children: [
TextSpan(
text: 'abcdefg',
style: TextStyle(color: Colors.blue),
),
]
),
)最終顯示的效果是123456abcdefg,其中abcdefg是藍色的。
方案設(shè)計
了解了富文本的原理后,封裝控件需要實現(xiàn)的目標(biāo)就確定了,那就是
自動將文本text,轉(zhuǎn)換成inlineSpan組成的樹
然后丟給Text控件去顯示。
那么如何去實現(xiàn)這個轉(zhuǎn)化的過程?我的想法是依次遍歷節(jié)點,然后衍生出新的節(jié)點,最終由葉子節(jié)點組成最終的顯示效果。
我們以包含自定義表情和##標(biāo)簽的效果為例子。
#一個[表情]的標(biāo)簽#哈哈哈哈哈
首先初始狀態(tài)只有文本text的情況下,可以認(rèn)為是一個樹的根節(jié)點,里面存在文本text。我們可以先把標(biāo)簽解析出來,那么就能從這個根節(jié)點,拆分出2個節(jié)點:

然后再將兩個葉子節(jié)點解析自定義表情:

最終得到4個葉子節(jié)點,最終生成的InlineSpan,應(yīng)該如下:
TextSpan(
children: [
TextSpan(
style: TextStyle(color: Colors.blue),
children: [
TextSpan(
text: '#一個',
),
WidgetSpan(
child: Image.asset(),
),
TextSpan(
text: '的標(biāo)簽#',
),
],
),
TextSpan(
text: '哈哈哈哈哈',
style: TextStyle(color: Colors.black),
),
],
),上述過程,涉及到三點:1. 遍歷;2. 解析拆分;3. 生成節(jié)點。等到了最終所有葉子結(jié)點都無法再被拆分出新節(jié)點時,這顆InlineSpan樹就是最終的解析結(jié)果。
解析
如何進行解析。像Emoji表情或者http鏈接那種,一般都是使用正則便能識別出來,而更加簡單的變顏色、改字體大小這種,在Android上都是直接通過設(shè)置起始位置和結(jié)束位置來標(biāo)明范圍的,我們也可以使用這種簡單好理解的方式來實現(xiàn),所以解析的時候,需要能夠拿到待解析內(nèi)容在原始文本中的位置。例如原文“一個需要放大的字”,已經(jīng)被其他解析器分成了兩段“一個需要”和“放大的字”,在斜體解析器解析“放大的字”的時候,需要知道原文第5到第6個字需要變成斜體,在把這5->6轉(zhuǎn)變成相對于“放大的字”這一段而言的第1到第2個字。
代碼設(shè)計
方案理解了之后,就開始簡單的框架編寫。
節(jié)點定義
按照樹結(jié)構(gòu),定義一個Node
class TextNode {
///該節(jié)點文本
String text;
TextStyle style;
late InlineSpan span;
///該節(jié)點文本,在原始文本中的開始位置。include
int startPosInOri;
///該節(jié)點文本,在原始文本中的結(jié)束位置。include
int endPosInOri;
List<TextNode>? subNodes;
TextNode(this.text, this.style,
{required this.startPosInOri, required this.endPosInOri});
}Span構(gòu)造器定義
abstract class BaseSpanBuilder {
bool isSupport(TextNode node);
///
/// 解析生成子節(jié)點
///
List<TextNode> parse(TextNode node);
}SuperText定義
先作為一個簡單版的Text控件,接收text、TextStyle和構(gòu)造器列表即可。
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();
}
}對應(yīng)的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 {
//準(zhǔn)備根節(jié)點
TextNode rootNode = TextNode(widget.text, widget.style,
startPosInOri: 0, endPosInOri: widget.text.length - 1);
rootNode.span = TextSpan(text: widget.text, style: widget.style);
//開始生成子節(jié)點
_generateNodes(rootNode, 0);
//深度優(yōu)先遍歷,生成最終的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) {
//生成了子節(jié)點,那么把子節(jié)點拋給下個span構(gòu)造器
for (TextNode n in subNodes) {
_generateNodes(n, builderIndex + 1);
}
} else {
//沒有子節(jié)點,說明當(dāng)前的span構(gòu)造器不處理當(dāng)前的節(jié)點內(nèi)容,那么把當(dāng)前的節(jié)點拋給下個span構(gòu)造器
_generateNodes(node, builderIndex + 1);
}
}
}
}
///
/// 深度優(yōu)先遍歷,構(gòu)建最終的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);
}
}
}實現(xiàn)邏輯基本就是方案設(shè)計中的想法。
可以修改TextStyle的Span構(gòu)造器
舞臺準(zhǔn)備好了,那個要訓(xùn)練演員了。這里編寫一個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) {
//富文本開始位置,在這段文字之內(nèi)
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) {
//結(jié)束位置在這段文字內(nèi)
endRelative = startRelative + (endPos - startPos);
} else {
//結(jié)束位置,超出了這段文字。將開始到這段文字結(jié)束,都包含進富文本去
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 {
//富文本開始位置不在這段文字之內(nèi),那就檢查富文本結(jié)尾的位置,是否在這段文字內(nèi)
if (node.startPosInOri <= endPos) {
int startRelative = 0;
int endRelative;
String subText;
TextStyle textStyle;
if (endPos <= node.endPosInOri) {
//富文本結(jié)尾位置,在這段文字內(nèi)
endRelative = endPos - node.startPosInOri;
} else {
//富文本結(jié)尾位置,超過了這段文字
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實現(xiàn)的富文本效果。如果是那種需要依靠正則解析拆分后實現(xiàn)的富文本效果,例如自定義表情,只需要一個EmojiSpanBuilder()即可。
結(jié)語
按照這個方案,對于不同的富文本效果,只需要定制不同的spanBuilder就可以了,使用方法非常類似于Android的SpannableStringBuilder。
以上就是Flutter實現(xiàn)編寫富文本Text的示例代碼的詳細(xì)內(nèi)容,更多關(guān)于Flutter富文本的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
android仿Adapter實現(xiàn)自定義PagerAdapter方法示例
這篇文章主要給大家介紹了關(guān)于android仿Adapter實現(xiàn)自定義PagerAdapter的相關(guān)資料,文中詳細(xì)介紹了關(guān)于PagerAdapter的用法,對大家的理解和學(xué)習(xí)具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-11-11
Android UI設(shè)計系列之HTML標(biāo)簽實現(xiàn)TextView設(shè)置中文字體加粗效果(6)
這篇文章主要介紹了Android UI設(shè)計系列之使用HTML標(biāo)簽,實現(xiàn)在TextView中對中文字體加粗的效果,具有一定的實用性和參考價值,感興趣的小伙伴們可以參考一下2016-06-06
Android 搜索結(jié)果匹配關(guān)鍵字且高亮顯示功能
這篇文章主要介紹了Android 搜索結(jié)果匹配關(guān)鍵字且高亮顯示功能,需要的朋友可以參考下2017-05-05
Android對圖片Drawable實現(xiàn)變色示例代碼
這篇文章主要給大家介紹了關(guān)于Android對圖片Drawable實現(xiàn)變色的相關(guān)資料,文中通過示例代碼將實現(xiàn)的方法介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起看看吧。2017-08-08
Android編程實現(xiàn)AIDL(跨進程通信)的方法詳解
這篇文章主要介紹了Android編程實現(xiàn)AIDL(跨進程通信)的方法,結(jié)合實例形式詳細(xì)分析了Android實現(xiàn)AIDL(跨進程通信)的原理、具體流程與相關(guān)實現(xiàn)技巧,需要的朋友可以參考下2016-06-06
Android中ActionBar和ToolBar添加返回箭頭的實例代碼
這篇文章主要介紹了Android中ActionBar和ToolBar添加返回箭頭的實例代碼,需要的朋友可以參考下2017-09-09
Android實現(xiàn)3D標(biāo)簽云簡單效果
這篇文章主要為大家詳細(xì)介紹了Android實現(xiàn)3D標(biāo)簽云簡單效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-05-05

