如何低成本实现Flutter富文本,看这一篇就够了!
作者:閑魚技術-玄川
背景
閑魚是國內最早使用Flutter 的團隊,作為一個電商App商品詳情頁是非常重要場景,其中最主要的技術能力是文字混排。
我們面對文本類的需求是復雜而且多變,然而Flutter歷史的幾個版本,Text只能顯示簡單樣式文本,它只有包含一些控制文本樣式顯示的屬性,而通過TextSpan連接實現的RichText也只能顯示多種文本樣式(例如:一個基礎文本片段和一個鏈接片段),這些遠遠達不到設計需要的能力。被產品和設計慫為啥別人別的平臺能做,Flutter為何做不了,不管,必須支持。
因此,需要開發一個能力更強的文字混排組件就變得迫在眉睫。
富文本的原理
再講文字混批組件設計實現前,先來講講系統RichText的富文本的原理。
-
創建過程
創建RichText節點的時候其實會創建以下幾個對象:
RenderParagraph實例最后會將自身登記到渲染模塊的Dirty Nodes當中去,渲染模塊會遍歷Dirty Nodes 將進入RenderParagraph 渲染環節。
-
渲染過程
RenderParagraph 方法當中封裝的是將文本繪制到 canvas 上面的邏輯,主要是用了一個叫做 TextPainter 的模塊,其調用過程遵循RenderObject 調用。
- PerfromLayout 過程通過調用TextPaint的Layout,在期過程中通過TextSpan 結構樹,依次通過AddText 添加各個階段的文本,最后通過Paragraph的Layout 計算文本高度。
- Paint 過程,先繪制clipRect,接著通過TextPaint的Paint函數調用,Paragraph的Paint繪制文本,最后繪制drawRect。
設計思路
通過RichText的文本繪制原理,我們不難發現TextSpan記錄了各段文本信息,TextPaint通過記錄的信息調用Native接口計算寬高,以及將文本繪制到canvas上面。傳統的方案實現復雜的混排,會通過HTML去做一個WebView的富文本,使用WebView在性能上自然不及原生實現,出于性能的考慮,我們設想通過通過原生的方式去實現圖文混排。一開始的方案是設計幾種特殊的Span(例如:ImageSpan,EmojiSpan等),通過Span記錄的信息,在TextPaint的Layout 重新根據各種類型重新計算布局,在Paint過程再分別繪制特殊的Widget,然而這種方案對上面幾個涉及的類封裝破壞的特別大,需要將RichText、RenderParagraph 源碼Copy 出來重新修改。最后設想是后可以通過特殊的文字先占位置,(例如:空字符串),然后在這個文字的位置上面把特殊的Span分別獨立移動到上面。
然而上面這種方案會帶來兩個難點:
- 難點一:如何在文本中先占位,并且能制定任意想要的寬高。
通過Google 發現u200B字符代表ZERO WIDTH SPACE(寬帶為0的空白),結合對TextPainter測試,我們發現layout出來的Width總是0,fontSize只決定了高度,結合TextStyle里面的letterSpacing
/// The amount of space (in logical pixels) to add between each letter /// A negative value can be used to bring the letters closer. final double letterSpacing;這樣我們就能任意的控制這個特殊文字的寬高度。
- 難點二:如何將特殊的Span移動到位置上面。
通過上面的測試不難發現,特殊的Span其實還是獨立Widget和RichText并不融合。所以我們需要知道當前widget相對RichText空間的相對位置,并且結合Stack將其融合。結合TextPaint里面的getOffsetForCaret方法
/// Returns the offset at which to paint the caret.////// Valid only after [layout] has been called.Offset getOffsetForCaret(TextPosition position, Rect caretPrototype)可以天然的獲取到當前占位符相對位置。
實現方案
關鍵部分代碼實現如下:
-
統一的占位SpaceSpan
SpaceSpan({this.contentWidth,this.contentHeight,this.widgetChild,GestureRecognizer recognizer, }) : super(style: TextStyle(color: Colors.transparent,letterSpacing: contentWidth,height: 1.0,fontSize:contentHeight),text: '\u200B',recognizer: recognizer); -
SpaceSpan 相對位置獲取
for (TextSpan textSpan in widget.text.children) {if (textSpan is SpaceSpan) {final SpaceSpan targetSpan = textSpan;Offset offsetForCaret = painter.getOffsetForCaret(TextPosition(offset: textIndex),Rect.fromLTRB(0.0, targetSpan.contentHeight, targetSpan.contentWidth, 0.0),);........}textIndex += textSpan.toPlainText().length;} -
RichtText和SpaceSpan融合
Stack(children: <Widget>[RichText(),Positioned(left: position.dx, top: position.dy, child: child),],);}
效果
先上圖看看效果
這種方案的優點是任意Widget可通過SpaceSpan和RichText進行組合,無論是圖片、自定義標簽、甚至是按鈕都可以融合進來,同時對RichText本身封裝性破壞較小。
未來
上面只是富文本顯示的部分,依然存在著很多局限,還有較多需要優化的點,目前通過SpaceSpan 控件,必需要指定寬高,另外對于文本選擇、自定義文字背景這些都是無法支持,其次對富文本編輯器的支持,可以使其編輯文字時,讓圖片、貨幣格式化等控件輸入等。
原文鏈接
本文為云棲社區原創內容,未經允許不得轉載。
總結
以上是生活随笔為你收集整理的如何低成本实现Flutter富文本,看这一篇就够了!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数据人看Feed流-架构实践
- 下一篇: Swift 在 GAIA 平台云端一体化