Flutter實(shí)現(xiàn)編寫富文本Text的示例代碼
SuperText富文本設(shè)計方案
Flutter中要實(shí)現(xiàn)富文本,需要使用RichText
或者Text.rich
方法,通過拆分成List<InlineSpan>
來實(shí)現(xiàn),第一感覺上好像還行,但實(shí)際使用了才知道,有一個很大的問題就是對于復(fù)雜的富文本效果,無法準(zhǔn)確拆分出具有實(shí)際效果的spans。因此想設(shè)計一個具有多種富文本效果,同時便于使用的富文本控件SuperText。
RichText原理
Flutter中的InlineSpan
其實(shí)是Tree的結(jié)構(gòu)。例如一段md樣式的文字:
其實(shí)就是被拆分了3段TextSpan,然后下面繪制的時候,就會使用ParagraphBuilder
分別訪問這3個節(jié)點(diǎn),3個節(jié)點(diǎn)分別往ParagraphBuilder
中填充對應(yīng)的文字以及樣式。
那么是否這個樹的深度一定只有2層?
這個未必,如果一開始就解析拆分出所有的富文本效果,那么可能就只有2層,但實(shí)際上就算是多層,也是沒有問題的,例如:
這是一段粗體并且部分帶著斜體效果的文字
可以拆分成如下:
需要注意的是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
是藍(lán)色的。
方案設(shè)計
了解了富文本的原理后,封裝控件需要實(shí)現(xiàn)的目標(biāo)就確定了,那就是
自動將文本text,轉(zhuǎn)換成inlineSpan組成的樹
然后丟給Text控件去顯示。
那么如何去實(shí)現(xiàn)這個轉(zhuǎn)化的過程?我的想法是依次遍歷節(jié)點(diǎn),然后衍生出新的節(jié)點(diǎn),最終由葉子節(jié)點(diǎn)組成最終的顯示效果。
我們以包含自定義表情和##標(biāo)簽的效果為例子。
#一個[表情]的標(biāo)簽#哈哈哈哈哈
首先初始狀態(tài)只有文本text的情況下,可以認(rèn)為是一個樹的根節(jié)點(diǎn),里面存在文本text。我們可以先把標(biāo)簽解析出來,那么就能從這個根節(jié)點(diǎn),拆分出2個節(jié)點(diǎn):
然后再將兩個葉子節(jié)點(diǎn)解析自定義表情:
最終得到4個葉子節(jié)點(diǎn),最終生成的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), ), ], ),
上述過程,涉及到三點(diǎn):1. 遍歷;2. 解析拆分;3. 生成節(jié)點(diǎn)。等到了最終所有葉子結(jié)點(diǎn)都無法再被拆分出新節(jié)點(diǎn)時,這顆InlineSpan樹就是最終的解析結(jié)果。
解析
如何進(jìn)行解析。像Emoji表情或者h(yuǎn)ttp鏈接那種,一般都是使用正則便能識別出來,而更加簡單的變顏色、改字體大小這種,在Android上都是直接通過設(shè)置起始位置和結(jié)束位置來標(biāo)明范圍的,我們也可以使用這種簡單好理解的方式來實(shí)現(xiàn),所以解析的時候,需要能夠拿到待解析內(nèi)容在原始文本中的位置。例如原文“一個需要放大的字”,已經(jīng)被其他解析器分成了兩段“一個需要”和“放大的字”,在斜體解析器解析“放大的字”的時候,需要知道原文第5到第6個字需要變成斜體,在把這5->6轉(zhuǎn)變成相對于“放大的字”這一段而言的第1到第2個字。
代碼設(shè)計
方案理解了之后,就開始簡單的框架編寫。
節(jié)點(diǎn)定義
按照樹結(jié)構(gòu),定義一個Node
class TextNode { ///該節(jié)點(diǎn)文本 String text; TextStyle style; late InlineSpan span; ///該節(jié)點(diǎn)文本,在原始文本中的開始位置。include int startPosInOri; ///該節(jié)點(diǎn)文本,在原始文本中的結(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é)點(diǎn) /// 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é)點(diǎn) TextNode rootNode = TextNode(widget.text, widget.style, startPosInOri: 0, endPosInOri: widget.text.length - 1); rootNode.span = TextSpan(text: widget.text, style: widget.style); //開始生成子節(jié)點(diǎn) _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é)點(diǎn),那么把子節(jié)點(diǎn)拋給下個span構(gòu)造器 for (TextNode n in subNodes) { _generateNodes(n, builderIndex + 1); } } else { //沒有子節(jié)點(diǎn),說明當(dāng)前的span構(gòu)造器不處理當(dāng)前的節(jié)點(diǎn)內(nèi)容,那么把當(dāng)前的節(jié)點(diǎn)拋給下個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); } } }
實(shí)現(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é)束,都包含進(jìn)富文本去 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), ], )
效果如圖:
這個用法,好像和原來的也沒啥差別啊。其實(shí)不然,首先多個效果之間可以交叉重疊,另外這里展示的是基本的使用TextStyle
實(shí)現(xiàn)的富文本效果。如果是那種需要依靠正則解析拆分后實(shí)現(xiàn)的富文本效果,例如自定義表情,只需要一個EmojiSpanBuilder()
即可。
結(jié)語
按照這個方案,對于不同的富文本效果,只需要定制不同的spanBuilder
就可以了,使用方法非常類似于Android的SpannableStringBuilder
。
以上就是Flutter實(shí)現(xiàn)編寫富文本Text的示例代碼的詳細(xì)內(nèi)容,更多關(guān)于Flutter富文本的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
android仿Adapter實(shí)現(xiàn)自定義PagerAdapter方法示例
這篇文章主要給大家介紹了關(guān)于android仿Adapter實(shí)現(xiàn)自定義PagerAdapter的相關(guān)資料,文中詳細(xì)介紹了關(guān)于PagerAdapter的用法,對大家的理解和學(xué)習(xí)具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-11-11Android UI設(shè)計系列之HTML標(biāo)簽實(shí)現(xiàn)TextView設(shè)置中文字體加粗效果(6)
這篇文章主要介紹了Android UI設(shè)計系列之使用HTML標(biāo)簽,實(shí)現(xiàn)在TextView中對中文字體加粗的效果,具有一定的實(shí)用性和參考價值,感興趣的小伙伴們可以參考一下2016-06-06Android 搜索結(jié)果匹配關(guān)鍵字且高亮顯示功能
這篇文章主要介紹了Android 搜索結(jié)果匹配關(guān)鍵字且高亮顯示功能,需要的朋友可以參考下2017-05-05Android對圖片Drawable實(shí)現(xiàn)變色示例代碼
這篇文章主要給大家介紹了關(guān)于Android對圖片Drawable實(shí)現(xiàn)變色的相關(guān)資料,文中通過示例代碼將實(shí)現(xiàn)的方法介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起看看吧。2017-08-08Android編程實(shí)現(xiàn)AIDL(跨進(jìn)程通信)的方法詳解
這篇文章主要介紹了Android編程實(shí)現(xiàn)AIDL(跨進(jìn)程通信)的方法,結(jié)合實(shí)例形式詳細(xì)分析了Android實(shí)現(xiàn)AIDL(跨進(jìn)程通信)的原理、具體流程與相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2016-06-06Android實(shí)戰(zhàn)教程第一篇之最簡單的計算器
這篇文章主要為大家詳細(xì)介紹了Android實(shí)戰(zhàn)教程第一篇,如何實(shí)現(xiàn)最簡單的計算器,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-11-11Android中ActionBar和ToolBar添加返回箭頭的實(shí)例代碼
這篇文章主要介紹了Android中ActionBar和ToolBar添加返回箭頭的實(shí)例代碼,需要的朋友可以參考下2017-09-09Android實(shí)現(xiàn)3D標(biāo)簽云簡單效果
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)3D標(biāo)簽云簡單效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-05-05