前言通過閱讀本文,您將了解到
創(chuàng)新互聯(lián)為客戶提供專業(yè)的成都網站設計、成都網站制作、程序、域名、空間一條龍服務,提供基于WEB的系統(tǒng)開發(fā). 服務項目涵蓋了網頁設計、網站程序開發(fā)、WEB系統(tǒng)開發(fā)、微信二次開發(fā)、成都手機網站制作等網站方面業(yè)務。
- 文本的組成部分;
- Flutter對于文本&段落是如何繪制的;
- 明白Flutter Text 背后的邏輯;
- 在業(yè)務中碰到一些文本顯示的問題時,知道從哪些地方去嘗試修改。
文字是記錄語言的書寫符號系統(tǒng),是形、音、義的統(tǒng)一體,是人類最重要的輔助性 交際工具。作為一個Flutter開發(fā)者,我們都知道可以通過Text()
這個文本組件將文字顯示出來。但是這其中的Flutter的字體是怎么組成的?Flutter文本是怎么構建的?Render Tree
是怎樣繪制文本的…作為本專欄(整個專欄都在與文本打交道)的第一篇文章,讓我們從這些原理細節(jié)講起。希望能對你認識Flutter的文本渲染有所幫助。
注:本文的目的在于讓大家了解Flutter中的基本文本知識,快速的帶大家了解渲染流程,但并未很深入的分析Flutter文本渲染的原理。
字體基礎理論通用部分在整個網絡世界中,大家可以將字體理解為一個數字文件,它是一個包含特定大小、粗細和樣式的文件。它定義了每個字的形狀、大小和圖形。
例如Bariol_Regular.otf。.otf
是字體文件格式。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-SLGvJ6LK-1669544494694)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6b43356354404639bd7b54526a2970da~tplv-k3u1fbpfcp-watermark.image?)]
有了字體格式后,我們會碰到相同的字體大小卻有不同的顯示布局這個問題。因為每一個字體格式都定義了它自己的參考大小,每一個字符都是基于這個大小設計的。所以即使設置同樣的字體大小,也會有不同的布局。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-M19SgzcN-1669544494696)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/541c8653d5964a3b9358d941b2ff3661~tplv-k3u1fbpfcp-watermark.image?)]
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-flIXYZLi-1669544494697)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/06896234fa3846469fb647a380bc607b~tplv-k3u1fbpfcp-watermark.image?)]
在Flutter中文本由哪些部分組成? BaselineBaseline
(基線)上。有了這個基線后,就算是不同大小的文字也可以處于同一水平線上。Baseline
是非常重要的,因為可以通過它測量文本和元素之間的垂直距離。其他還有Middleline
、Bottomline
、Topline
。[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-VZO6UXAO-1669544494698)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2853b80fe819407b928a6fd93e29a859~tplv-k3u1fbpfcp-watermark.image?)]
TextStyle
下的wordSpacing
設置單詞與單詞之間的間距,通過letterSpacing
設置字符與字符之間的間距。[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-uyChiBG1-1669544494698)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/28a543c5d32840ee9f4a48d2ea333688~tplv-k3u1fbpfcp-watermark.image?)]
WeightWeight是指字體筆畫的粗細,在Flutter中通過fontWeight
設置。
常見的有:normal
、bold
,其他還有FontWeight.w100
…等粗細值
TextStyle(
fontWeight: FontWeight.bold
),
TextSpan在Flutter中,我們經常會使用Text()
這個組件,但是我們通過閱讀Text()
的源碼后就可以知道,它的build
方法返回的就是RichText
組件。所以它會呈現為TextSpan
。Span指的是字符之間的行距。
@override
Widget build(BuildContext context) {
...
Widget result = RichText(
...
text: TextSpan(
style: effectiveTextStyle,
text: data,
children: textSpan != null ?[textSpan!] : null,
),
);
...
return result;
}
Height在Flutter中,定義了一個TextStyle.height
,用于給呈現文本的TextSpan
一個準確的行高。
TextStyle(height: 1)
但是我們需要注意,每一種字體格式都定義了自己的字體度量默認高度,這也是為什么即使設置了相同的字體高度,也會有不同的TextSpan
的高度。
讓我們來看下這個例子:
紅色是Flutter默認的字體,藍色是Bariol_Regular字體,綠色是Bellota-Regular字體,看看他們在相同height
下不同的框高度。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ikr2c80A-1669544494699)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6f47d0f38c7443b8bbaad4ac3e15cc04~tplv-k3u1fbpfcp-watermark.image?)]
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-qe9aiK52-1669544494700)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fa54e9cf2b28475ea39155ede0583499~tplv-k3u1fbpfcp-watermark.image?)]
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-tbGTBpwJ-1669544494701)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1733f3d05cb2482fb868d92c7e14d8dc~tplv-k3u1fbpfcp-watermark.image?)]
這個例子也很好的驗證了:
那么關于Flutter的字體組成我們也可以得到一個結論:使用多種字體大概率會因為基線的不同導致布局不協(xié)調!
Flutter中是如何繪制文本的?通過Paragraph
,Flutter
最后繪制文本時都是通過Paragraph
完成的!
// Paragraph paragraph:文本對象
// Offset offset:文本繪制的位置
void drawParagraph(Paragraph paragraph, Offset offset)
舉個例子: 通過drawParagraph
繪制一段文字
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-C7TkjSos-1669544494702)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fc53245c2bbd4a9c86207b2cf58afbfd~tplv-k3u1fbpfcp-watermark.image?)]
import 'dart:ui' as ui;
class TextPainter extends CustomPainter {
//創(chuàng)建段落構建器
ParagraphBuilder paragraphBuilder = ParagraphBuilder(
ParagraphStyle(fontWeight: FontWeight.bold, fontSize: 16))
..pushStyle(ui.TextStyle(color: Colors.black))
..addText('通過drawParagraph繪制的 Hello Taxze');
?
@override
void paint(Canvas canvas, Size size) {
//設置段落寬度
ParagraphConstraints paragraphConstraints =
ParagraphConstraints(width: size.width);
//計算繪制的文本位置及尺寸
Paragraph paragraph = paragraphBuilder.build()
..layout(paragraphConstraints);
//繪制
canvas.drawParagraph(paragraph, const Offset(40.0, 50.0));
}
?
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) =>false;
}
使用:
@override
Widget build(BuildContext context) {
return Scaffold(
...
body: SizedBox.expand(child: CustomPaint(painter: TextPainter())),
);
}
SizedBox.expand
包裹CustomPaint
是為了給ParagraphConstraints(width: size.width)
一個size
。你也可以用其他的組件包裹它。
關于Flutter使用CustomPaint
繪制文字的實踐較為復雜,若要講清楚繪制的主要知識點,則需要另開一篇文章來講述。若對這個部分感興趣的朋友可以閱讀下這篇文章:Flutter學習:使用CustomPaint繪制文字 — @菠蘿橙子丶
你有沒有想過,Flutter是如何把一段長文字生成下面的這樣一個段落的呢?
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-K8zsfS9R-1669544494703)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a6aa369fa78f4762b66bfa5f64026cee~tplv-k3u1fbpfcp-watermark.image?)]
這張效果圖的代碼:
Container(
color: Colors.red,
width: 200,
height: 100,
margin: EdgeInsets.all(30),
child: Text(
"通過drawParagraph繪制的 Taxze Hello....")),
那么其中的自動換行是怎么實現的呢?
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-hQ2ezYSG-1669544494704)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a4fe3f2943234c1ca5372dc902c53b57~tplv-k3u1fbpfcp-watermark.image?)]
我們知道,段落指的就是一段文本,我們要給每個字符一個合適的大小和位置。那么Flutter是如何計算這些參數的呢?
在前文說到過,Flutter最后繪制文本時都是通過Paragraph
完成的。Flutter就是通過Paragraph.layout
來計算這些參數,而且ParagraphBuilder
給每個字符都在渲染前分配了一個偏移量。通過Paragraph
可以知道所有占位符的位置和尺寸大小。
class TextPosition {
//創(chuàng)建一個表示字符串中特定位置的對象。
const TextPosition({
required this.offset,
this.affinity = TextAffinity.downstream,
}) : assert(offset != null),
assert(affinity != null);
//舉個例子:有一個“Hello”字符,offset = 0表示光標在字符H之前,offset = 5表示光標在字符o之后。
final int offset;
final TextAffinity affinity;
?
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType)
return false;
return other is TextPosition
&& other.offset == offset
&& other.affinity == affinity;
}
?
@override
int get hashCode =>Object.hash(offset, affinity);
?
@override
String toString() {
return 'TextPosition(offset: $offset, affinity: $affinity)';
}
}
Text()背后的大哥有哪些?–文本的渲染流程
從之前講述的知識點,Text()
組件它的build
方法返回的就是RichText
,但是Flutter
最后繪制文本時又都是通過Paragraph
完成的!那么其中的完整的一個流程是怎么樣的呢?話不多說,先上圖!
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-dDMUcy7t-1669544494704)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dbce4fff785640e78c6c743b6ed6c409~tplv-k3u1fbpfcp-watermark.image?)]
組件層
如圖所示,每當我們使用Text
組件時,它實際上創(chuàng)建的是RichText
組件。但是RichText
和Text
不同的是,Text
將String
作為參數,而RichText
將InlinSpan
作為參數(或者說是TextSpan
)。
const Text(String this.data)
//通過Text.rich構造函數傳給RichText
const Text.rich(InlineSpan this.textSpan)
RichText(
...
text: TextSpan(
style: effectiveTextStyle,
text: data,
children: textSpan != null ?[textSpan!] : null,
),
)
//TextSpan繼承于InlineSpan
class TextSpan extends InlineSpan implements HitTestTarget, MouseTrackerAnnotation {}
因RichText
接收TextSpan
,而每一個TextSpan
都有更多的子TextSpan
,這些子TextSpan
會 繼承父TextSpan
的樣式。例如:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-PF2spzd5-1669544494705)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/233e496530c94552aca073ef9b90a60c~tplv-k3u1fbpfcp-watermark.image?)]
RichText(
text: TextSpan(
style: Theme.of(context)
.textTheme
.bodyText1
?.copyWith(fontSize: 24),
children: [
TextSpan(
text: 'Taxze ',
),
TextSpan(text: 'blog', style: TextStyle(color: Colors.blue)),
TextSpan(
text: ' Flutter',
),
TextSpan(text: '稀土掘金', style: TextStyle(color: Colors.blue)),
]))
不過,RichText
本身是MultiChildRenderObjectWidget
的子類。它們之間有這樣的繼承關系:
class RichText extends MultiChildRenderObjectWidget {}
abstract class MultiChildRenderObjectWidget extends RenderObjectWidget {}
而MultiChildRenderObjectWidget
產生的MultiChildRenderObjectElement
則是這樣的關系:
class MultiChildRenderObjectElement extends RenderObjectElement {}
abstract class RenderObjectElement extends Element {}
RichText
實際上是需要一個InlineSpan
,而InlineSpan
可以是TextSpan
或者是WidgetSpan
。對WidgetSpan有興趣的朋友,可以參考官方的文檔WidgetSpan。
到這里為止,我們可以將RichText
(包括RichText)之前的所有劃分為組件層,那么我們現在就要進入渲染層了。
渲染層
我們已經知道了RichText
會創(chuàng)建一個渲染對象—RenderParagraph
,那么RenderParagraph
是干什么的呢?
RichText
是MultiChildRenderObjectWidget
的子類,它會把MultiChildRenderObjectElement
往下傳遞,但是此時MultiChildRenderObjectElement
沒有渲染,它還沒有什么作用。這個時候RichText
會給它一個RenderParagraph
,RenderParagraph
會收到RenderPadding
的指令,這個時候MultiChildRenderObjectElement
就準備好了一切,就可以開始工作了。
這樣解釋可能有點抽象,那么我們來看下這個例子:
body: Container(
alignment: Alignment.center,
child: Text("Taxze Hello"), ,
)
很簡單的一個小例子,它的結構也很清晰:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-yhsYCYUC-1669544494705)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ef477b4af1b849f3a3cf5aa3f68c8466~tplv-k3u1fbpfcp-watermark.image?)]
當Flutter把三棵樹都構建完后:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-zinkstt2-1669544494706)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/58bc0cb3d88a4dc1b08305026eef423a~tplv-k3u1fbpfcp-watermark.image?)]
那么當我們改變文本時,又會發(fā)生什么呢?
最先改變的當然是組件層:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-94R0jawX-1669544494706)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/313481d32f85479c8eb3dd8ffe47b545~tplv-k3u1fbpfcp-watermark.image?)]
我們會有一個 “新” 的組件樹。不過你真的認為都是新的嗎?Flutter會充分利用現有的元素,讓我們來看下這個名為canUpdate
的方法吧。
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
通過這個方法,Flutter可以檢查一個老的組件的Type
和key
,并把它和新的組件進行比較。如果它們都相同的話,就不需要更新。
所以就算更新后,Container
更新之后它還是存在的,而且我們沒有給它一個Key,所以OldContainer
和NewContainer
是完全相同的。Align、Text、以及RichText它們的Type和Key都沒有變化,重新構建它們沒有什么意義,所以它們都不會有更新。
到這里,我猜你肯定會問,都沒有更新,那么文本是如何改變的呢?
那么我們就要講到組件中的屬性了。組件除了具有Type和Key之外,還有屬性。屬性的改變會使RenderParagraph
顯示新的文本。
不過關于文本的更改渲染到現在我們都是在紙上談兵,那么我們現在就來用一個簡單的例子去驗證之前的結論。
bool _isFirst = true;
?
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.swap_horiz),
onPressed: () {
setState(() {
_isFirst = !_isFirst;
});
},
),
body: _isFirst ? first() : second());
}
}
?
Widget first() =>Container(
alignment: Alignment.center,
child: const Text("Taxze First"),
);
?
Widget second() =>Container(
alignment: Alignment.center,
child: const Text("Taxze Second"),
);
非常簡單的一個例子,點擊按鈕更改顯示文字。當我們點下按鈕時,文本改變后,所有的組件都會重用,Flutter只會重建RenderPadding
。
繪制層
在渲染層中,我們最后發(fā)生文本變化都在RenderParagraph
上,不過RenderParagraph
并不會直接的繪制文本,而是會創(chuàng)建一TextPainter
來管理繪制的工作。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-JgUMZp4Z-1669544494706)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f358e123221c48ef8891ea7a5ef49620~tplv-k3u1fbpfcp-watermark.image?)]
不過,TextPainter
做的事和它的名字完全不一樣,你以為就是它來繪制文本的嗎?No~
實際上,它只是負責管理繪制的事,但它自己不會去繪制(當老板)。
基礎層
到現在為止,你會發(fā)現,講了那么多,但是還是沒有那個ta去真正的繪制文本,就好像之前的所有的組件都在當中間商,把活外包了出去。
到了Flutter的最底層,你會發(fā)現有一個ParagraphBuilder
和Paragraph
,在前面關于Flutter如何繪制文本中,我們也提到了Flutter
最后繪制文本時都是通過Paragraph
完成的,而TextPainter
是負責創(chuàng)建ParagraphBuilder
的,但是當你翻看Paragraph
類的源碼時,你會發(fā)現,大部分的函數都是空函數,原來這哥們也沒干活?。?/p>
@pragma('vm:entry-point')
class Paragraph extends NativeFieldWrapperClass1 {
@pragma('vm:entry-point')
Paragraph._();
?
bool _needsLayout = true;
?
double get width native 'Paragraph_width';
?
double get height native 'Paragraph_height';
?
double get longestLine native 'Paragraph_longestLine';
?
double get minIntrinsicWidth native 'Paragraph_minIntrinsicWidth';
?
double get maxIntrinsicWidth native 'Paragraph_maxIntrinsicWidth';
?
double get alphabeticBaseline native 'Paragraph_alphabeticBaseline';
...
}
引擎層
當Paragraph
和ParagraphBuilder
這兩個類都將繪制的工作交給了Flutter Engine
后,我們也要將視線放到SkParagraph
上了,在以前Flutter Engine
處理文本繪制的庫是LibText
。后面切換成了SkParagraph
,但是也實現了和Libtext
相同的API。對于Flutter引擎在這篇文章中只做一個簡單的說明,若對引擎感興趣的朋友可以自己編譯FlutterEngine進行學習,或者在線閱讀。
–更詳細更深入的Flutter文本渲染原理有興趣的朋友可以閱讀這篇文章
解決Flutter文本基線不對齊的問題經常在各大Flutter交流群中看到有哥們問這樣的問題:Row中,兩個文本沒有對齊,這怎么處理呀?
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-jLL0YCuo-1669544494707)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/327583bd15264122a5b1e9c4bc2c0aca~tplv-k3u1fbpfcp-watermark.image?)]
展示圖代碼:
Center(
child: Row(
children: [
ColoredBox(
color: Colors.amber,
child: Text.rich(TextSpan(children: [
TextSpan(text: "¥999", style: TextStyle(fontSize: 28)),
TextSpan(text: ".9", style: TextStyle(fontSize: 14)),
])),
),
ColoredBox(
color: Colors.red,
child: Text.rich(TextSpan(children: [
TextSpan(text: "123", style: TextStyle(fontSize: 12)),
])),
),
],
),
)
其實處理這個問題很簡單,只需要給Row加上:
textBaseline: TextBaseline.alphabetic,
crossAxisAlignment: CrossAxisAlignment.baseline,
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-JFwhozn4-1669544494707)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/add602d2e218444e8ccf7c3f157b108b~tplv-k3u1fbpfcp-watermark.image?)]
關于更多有關文本的布局問題大家可以查看官方這篇文檔。
尾述在這篇文章中,我們知道了文本是由什么組成的,Flutter是怎樣將文本顯示到屏幕上的。但是這也只是Flutter關于文本的一小部分,關于文本的編輯…等內容將會在后續(xù)的文章中繼續(xù)探索。希望這篇文章能對你有所幫助,有問題歡迎在評論區(qū)留言討論~
參考&推薦閱讀Flutter Text Rendering — @Jonathan Sande
書后拓展:Flutter 中一行文字到屏幕上,渲染全過程! — @MeandNi
Flutter 小技巧之玩轉字體渲染和問題修復 — @戀貓de小郭
Flutter學習:使用CustomPaint繪制文字 — @菠蘿橙子丶
關于我Hello,我是Taxze,如果您覺得文章對您有價值,希望您能給我的文章點個??,有問題需要聯(lián)系我的話:我在這里
如果您覺得文章還差了那么點東西,也請通過關注督促我寫出更好的文章——萬一哪天我進步了呢?😝
— 文字是人類用符號記錄表達信息以傳之久遠的方式和工具。
你是否還在尋找穩(wěn)定的海外服務器提供商?創(chuàng)新互聯(lián)www.cdcxhl.cn海外機房具備T級流量清洗系統(tǒng)配攻擊溯源,準確流量調度確保服務器高可用性,企業(yè)級服務器適合批量采購,新人活動首月15元起,快前往官網查看詳情吧