Flutter學習:認識CustomPaint組件和Paint對象
Flutter學習:使用CustomPaint繪制路徑
Flutter學習:使用CustomPaint繪制圖形
Flutter學習:使用CustomPaint繪制文字
Flutter學習:使用CustomPaint繪制圖片
drawParagraph
繪制文本。需要傳遞2個參數:
Paragraph paragraph:文本對象Offset offset:文本繪制的位置
ParagraphBuilder paragraphBuilder = ParagraphBuilder(ParagraphStyle())..addText('Hello World');
ParagraphConstraints paragraphConstraints = ParagraphConstraints(width: size.width);
Paragraph paragraph = paragraphBuilder.build() ..layout(paragraphConstraints);
canvas.drawParagraph(paragraph, const Offset(350, 180));

繪制的文字默認大小為14,顏色為白色。如果你直接使用以上代碼並不能繪制成我這樣。這里我用SizedBox.expand包裹了CustomPaint,否則你的size值為0,ParagraphConstraints(width: size.width)設置的也是0,什么都繪制不出來。當然,你也可以使用Center包裹CustomPaint,需要重新設置一下offset,顯示出來的效果如下:
canvas.drawParagraph(paragraph, const Offset(0, -100));

Offset沒有什么好說的,我們着重來研究研究Paragraph對象。
從以上代碼我們可以得知,繪制文字首先需要一個ParagraphBuilder對象。該對象只有一個style屬性,該屬性是一個ParagraphStyle對象,看來我們得把矛頭專項該對象了。
ParagraphBuilder
屬性
通過名稱我們可以得知,該對象是用來設置文本風格的,它有以下多個屬性:
TextAlign? textAlign:文本對齊方式TextDirection? textDirection:文本方向int? maxLines:文本最大行String? fontFamily:文本字體風格double? fontSize:字體大小double? height:文本行高TextHeightBehavior? textHeightBehavior:指定如何將行高應用於第一行的上升和最后一行的下降FontWeight? fontWeight:字重(粗細)FontStyle? fontStyle:繪制字母時使用的字體變體(例如,斜體)StrutStyle? strutStyle:支柱的風格。 Strut 定義了一組最小垂直行高相關的指標,可用於獲得更高級的行間距行為String? ellipsis:用於省略溢出文本的字符串。如果maxLines不為 null,則ellipsis(如果有)將應用於最后渲染的行,如果該行超出寬度約束。如果maxLines為 null,則將ellipsis應用於超出寬度約束的第一行,並刪除后續行。寬度約束是在傳遞給Paragraph.layout方法Locale? locale:用於選擇區域特定字形的語言環境
我們可以發現,大部分屬性和TextStyle是一樣的,我就只講幾個不怎么常用的。
TextHeightBehavior
bool applyHeightToFirstAscent:當為 true 時, TextStyle.height修飾符將應用於第一行的上升。當為 false 時,將使用字體的默認上升。默認為truebool applyHeightToLastDescent:當為 true 時, TextStyle.height修飾符將應用於最后一行的下降。當為 false 時,將使用字體的默認下降。默認為trueTextLeadingDistribution leadingDistribution:前導如何分布在文本之上和之下。默認為TextLeadingDistribution.proportional
ParagraphBuilder paragraphBuilder = ParagraphBuilder(
ParagraphStyle(fontSize: 24, height: 1.5, textHeightBehavior: const TextHeightBehavior()),
)..addText('Hello World' * 10);
ParagraphConstraints paragraphConstraints = ParagraphConstraints(width: size.width / 3 * 2);
Paragraph paragraph = paragraphBuilder.build()..layout(paragraphConstraints);
canvas.drawParagraph(paragraph, Offset(size.width / 3 / 2, 100));
測試了一下,只有applyHeightToFirstAscent這個值對文字的排版有影響,其他兩個屬性沒有效果。可能和字體有關,這個屬性一般也用不到,默認就好。

StrutStyle
String? fontFamily:字體List<String>? fontFamilyFallback:字體列表。當在fontFamily中找不到字體時將搜索該列表double? fontSize:字體大小double? height:字體行高TextLeadingDistribution? leadingDistribution:由height乘數增加的額外垂直空間應該如何分布在文本之上和之下,獨立於leading(它總是均勻分布在文本之上和之下)。默認為段落的TextHeightBehavior的主要分布double? leading:行與行之間的最小行距,是字體大小的倍數。必須提供fontSize才能使該屬性生效。無論 leadingDistribution是什么,此屬性添加的前導均勻分布在文本上方和下方FontWeight? fontWeight:字重(粗細)FontStyle? fontStyle:繪制字母時使用的字體變體(例如,斜體)bool? forceStrutHeight:當為真時,段落將強制所有行從基線到基線完全是(height +leading) * fontSize高。 TextStyle不再能夠影響行高,並且任何高字形都可能與上面的行重疊。如果指定了fontFamily,則第一行的總上升將是fontFamily的Ascent + half-leading和(height +leading) * fontSize的最小值。否則,將由第一個文本的Ascent + half-leading決定。默認為 false

方法
addText:要繪制的文字內容pushStyle:將給定樣式應用於添加的文本pop:刪除最近一次調用pushStyle的效果addPlaceholder:向段落添加內聯占位符空間build:應用給定的段落樣式並返回一個包含添加的文本和相關樣式的Paragraph 。調用此函數后,段落構建器對象無效,無法進一步使用placeholderCount:當前在段落中的占位符的數量placeholderScales:段落中占位符的比例
addText、pushStyle和pop
ui.ParagraphBuilder paragraphBuilder = ui.ParagraphBuilder(
ui.ParagraphStyle(fontSize: 24),
);
paragraphBuilder.pushStyle(ui.TextStyle(
color: Colors.black,
fontSize: 32,
));
paragraphBuilder.pushStyle(ui.TextStyle(
color: Colors.red,
fontSize: 32,
));
paragraphBuilder.pop();
paragraphBuilder.addText('吹夢到西洲');
ui.ParagraphConstraints paragraphConstraints = ui.ParagraphConstraints(width: size.width / 3 * 2);
ui.Paragraph paragraph = paragraphBuilder.build()..layout(paragraphConstraints);
canvas.drawParagraph(paragraph, Offset(size.width / 3 / 2, 100));

注意:pushStyle中的設置會覆蓋ParagraphStyle,addText必須放在后面。
addPlaceholder
paragraphBuilder.addPlaceholder(100, 80, PlaceholderAlignment.top);

PlaceholderAlignment還有 aboveBaseline、baseline、belowBaseline幾個枚舉值,這幾個值的需要設置TextBaseline才能使用,但是我設置里TextBaseline還是沒效果,你們可以自己試一下。
build
調用該方法會返回一個最終會繪制的Paragraph對象。
Paragraph
屬性
height:此段落占的高度width:此段落的寬度alphabeticBaseline:從段落頂部到第一行字母基線的距離,以邏輯像素為單位didExceedMaxLines:如果有更多垂直內容,但文本被截斷,則為真,要么是因為我們達到了maxLines文本行,要么是因為maxLines為空, ellipsis不為空,並且其中一行超出了寬度限制ideographicBaseline:從段落頂部到第一行的表意基線的距離,以邏輯像素為單位longestLine:段落最長的一行的長度maxIntrinsicWidth:返回最小的寬度,超過這個寬度,增加寬度不會減少高度minIntrinsicWidth:該段落可以在不繪制其內容的情況下的最小寬度
ui.ParagraphBuilder paragraphBuilder = ui.ParagraphBuilder(ui.ParagraphStyle());
paragraphBuilder.pushStyle(ui.TextStyle(
color: Colors.red,
fontSize: 32,
));
paragraphBuilder.addText('海水夢悠悠,君愁我亦愁。南風知我意,吹夢到西洲。');
ui.ParagraphConstraints paragraphConstraints = ui.ParagraphConstraints(width: size.width / 3 * 2);
ui.Paragraph paragraph = paragraphBuilder.build();
paragraph.layout(paragraphConstraints);
canvas.drawParagraph(paragraph, Offset(size.width / 3 / 2, 100));

I/flutter ( 7724): height: 138.0
I/flutter ( 7724): width: 261.0
I/flutter ( 7724): alphabeticBaseline: 37.119998931884766
I/flutter ( 7724): didExceedMaxLines: false
I/flutter ( 7724): ideographicBaseline: 46.33599853515625
I/flutter ( 7724): longestLine: 256.0
I/flutter ( 7724): maxIntrinsicWidth: 768.0
I/flutter ( 7724): minIntrinsicWidth: 256.0
方法
layout:計算段落中每個字形的大小和位置computeLineMetrics:返回LineMetrics的完整列表,詳細描述每條布局線的各種指標。這可能會返回大量數據,因此不建議重復調用它。相反,緩存結果getBoxesForPlaceholders:返回包含段落中所有占位符的文本框列表。框的順序與通過ParagraphBuilder.addPlaceholder傳入的順序相同。TextBox的坐標相對於段落的左上角,其中正 y 值表示向下getLineBoundary:返回給定TextPosition處的行的TextRange 。換行符(如果有)作為范圍的一部分返回。這可能會很昂貴,因為它需要計算線路指標,因此請謹慎使用getBoxesForRange:返回包含給定文本范圍的文本框列表。boxHeightStyle和boxWidthStyle參數允許自定義框的垂直和水平綁定方式。兩個樣式參數都默認為tight選項,這將提供緊密貼合的框並且不會考慮任何行間距。TextBox 的坐標相對於段落的左上角,其中正 y 值表示向下getPositionForOffset:返回最接近給定偏移量的文本位置getWordBoundary:返回給定TextPosition處單詞的TextRange
不屬於單詞的字符,例如空格、符號和標點符號,兩邊都有分詞符。在這種情況下,此方法將返回offset, offset+1 。在 Unicode 標准附件 #29 中更精確地定義了單詞邊界
computeLineMetrics
獲取每一行的數據,返回一個行列表
List<ui.LineMetrics> lines = paragraph.computeLineMetrics();
既然最后獲取的是一個LineMetrics數組,我們就來看看LineMetrics對象。它有以下多個屬性:
hardBreak:如果此行以顯式換行符結束(例如 '\n')或者是段落的結尾,則為true。否則為falseascent:以 baseline 為界線,上部分的高descent:以 baseline 為界線,下部分的高unscaledAscent:忽略TextStyle設置的height,上部分的高height:從頂部邊緣到底部邊緣的線的總高度width:從最左側字形的左邊緣到最右側字形的右邊緣的線寬left:線條左邊緣的 x 坐標baseline:此行從段落頂部開始的基線 y 坐標lineNumber:行在整個段落中的編號,第一行索引為零

getBoxesForPlaceholders
獲取所有的占位符的信息
paragraphBuilder.addPlaceholder(100, 80, PlaceholderAlignment.top);
paragraphBuilder.addPlaceholder(20, 40, PlaceholderAlignment.top);
paragraph.layout(paragraphConstraints);
List<TextBox> textBoxes = paragraph.getBoxesForPlaceholders();
[
TextBox.fromLTRBD(0.0, 7.0, 100.0, 87.0, TextDirection.ltr),
TextBox.fromLTRBD(100.0, 7.0, 120.0, 47.0, TextDirection.ltr),
]

getLineBoundary
用來獲取當前索引在繪制文字的第幾行的范圍
該方法需要傳入一個TextPosition對象,TextPosition有兩屬性:
offset:索引affinity:關於 affinity 的介紹可以查看這里
TextRange textRange = paragraph.getLineBoundary(const TextPosition(offset: 10));
TextRange(start: 8, end: 16)

getBoxesForRange
獲取范圍內容所組成的矩形
List<TextBox> textBox = paragraph.getBoxesForRange(4, 12);
[
TextBox.fromLTRBD(128.0, -0.3, 256.0, 46.0, TextDirection.ltr),
TextBox.fromLTRBD(0.0, 45.7, 128.0, 92.0, TextDirection.ltr)
]

getPositionForOffset
返回離坐標最近的字符串的索引
TextPosition textPosition = paragraph.getPositionForOffset(const Offset(100, 20));
TextPosition(offset: 3, affinity: TextAffinity.downstream)

getWordBoundary
如果存在繪制的文本中,就返回位置,不存在就返回-1
TextRange textRange = paragraph.getWordBoundary(const TextPosition(offset: 12));
TextRange(start: 12, end: 13)
layout
需要傳遞一個ParagraphConstraints對象,用來限制文字的顯示寬度
ParagraphConstraints paragraphConstraints = ParagraphConstraints(width: size.width / 3 * 2);
TextPainter
創建一個繪制給定文本的文本畫家。
屬性
InlineSpan? text:設置顯示的元素。雖然是可選,但是在調用layout前不能為nullTextAlign textAlign:文本對齊方式。默認為TextAlign.startTextDirection? textDirection:文字方向。雖然是可選,但是在調用layout前不能為nulldouble textScaleFactor:文字放大倍數。默認為1.0int? maxLines:最多是多少行String? ellipsis:使用省略號表示文本已溢出Locale? locale:用於選擇區域特定字形的語言環境StrutStyle? strutStyle:具體用法查看drawParagraph的用法介紹TextWidthBasis textWidthBasis:測量一行或多行文字寬度的不同方法TextWidthBasis.longestLine:寬度將恰好足以包含最長的線並且不再。一個常見的用例是聊天氣泡TextWidthBasis.parent:多行文本將占據父級給出的全寬。對於單行文本,僅使用包含文本所需的最小寬度。一個常見的用例是標准的段落系列。默認值
ui.TextHeightBehavior? textHeightBehavior:具體用法查看drawParagraph的用法介紹
TextPainter和ParagraphBuilder差不多,繪制的文字默認顏色是白色,大小是14。所以這里我們跳過一些不必要的介紹,先繪制一個最基本的:
TextPainter textPainter = TextPainter(
text: TextSpan(text: text, style: const TextStyle(color: Colors.black)),
textDirection: TextDirection.ltr,
);
textPainter.layout(maxWidth: size.width / 3 * 2);
textPainter.paint(canvas, Offset(size.width / 3 / 2, 120));

可以看到,在TextPainter中,我們需要使用TextSpan組件來設置文字的內容和樣式,關於這個組件的用法就不多做介紹了。
在TextPainter中,我們可以通過ellipsis來自定義顯示文字溢出后的效果:
TextPainter textPainter = TextPainter(
text: TextSpan(text: text, style: const TextStyle(color: Colors.black)),
textDirection: TextDirection.ltr,
maxLines: 3,
ellipsis: "😀😁😂",
);

當然,我們可以在實例化的時候直接給屬性賦值,也可以在實例化之后用.和..來給屬性賦值。
// 方法一
TextPainter textPainter = TextPainter(
text: TextSpan(text: text, style: const TextStyle(color: Colors.black)),
textDirection: TextDirection.ltr,
);
// 方法二
TextPainter textPainter = TextPainter()
..text = TextSpan(text: text, style: const TextStyle(color: Colors.black))
..textDirection = TextDirection.ltr;
// 方法三
TextPainter textPainter = TextPainter();
textPainter.text = TextSpan(text: text, style: const TextStyle(color: Colors.black));
textPainter.textDirection = TextDirection.ltr;
方法
getWordBoundary:具體用法查看drawParagraph的用法介紹getPositionForOffset:具體用法查看drawParagraph的用法介紹getLineBoundary:具體用法查看drawParagraph的用法介紹computeLineMetrics:具體用法查看drawParagraph的用法介紹computeDistanceToActualBaseline:返回從文本頂部到給定類型的第一個基線的距離getBoxesForSelection:返回綁定給定選擇的矩形列表getFullHeightForCaret:{@template flutter.painting.textPainter.getFullHeightForCaret} 返回給定position字形的支柱邊界高度getOffsetForCaret:返回繪制插入符號的偏移量getOffsetAfter:返回輸入光標可以定位的offset量之后最近的偏移量getOffsetBefore:返回輸入光標可以定位的offset量之前最接近的偏移量markNeedsLayout:將此文本繪制器的布局信息標記為臟並刪除緩存信息。在引擎布局發生變化的情況下,使用此方法通知文本繪制器重新布局。在大多數情況下,更新框架中的文本繪制器屬性會自動調用此方法setPlaceholderDimensions:設置text中每個占位符的尺寸。提供的PlaceholderDimensions數量應與文本中PlaceholderSpan的數量相同。傳入一個空值或空value將什么都不做。如果在未設置占位符尺寸的情況下嘗試layout ,則占位符將在文本布局中被忽略,並且不會返回有效的inlinePlaceholderBoxeslayout:計算字形的視覺位置以繪制文本paint:在給定的偏移處將文本繪制到給定的畫布上
computeDistanceToActualBaseline
-
TextBaseline.alphabetic:用於對齊字母字符的字形底部的水平線textPainter.layout(maxWidth: size.width / 3 * 2); textPainter.paint(canvas, Offset(size.width / 3 / 2, 120)); double alphabetic = textPainter.computeDistanceToActualBaseline(TextBaseline.alphabetic); -
TextBaseline.ideographic:用於對齊表意字符的水平線double ideographic = textPainter.computeDistanceToActualBaseline(TextBaseline.ideographic);

getBoxesForSelection
該方法有以下3個屬性:
TextSelection:創建文本選擇。關於該對象的用法請查看這里BoxHeightStyle boxHeightStyle:返回綁定給定選擇的矩形列表。boxHeightStyle和boxWidthStyle參數可用於選擇TextBox的形狀。如果此文本繪制器包含雙向文本,則給定選擇可能具有多個矩形,因為邏輯上連續的文本可能在視覺上不連續。默認是tightBoxWidthStyle boxWidthStyle:同BoxHeightStyle
List<TextBox> textBox = textPainter.getBoxesForSelection(
const TextSelection(baseOffset: 4, extentOffset: 12),
boxHeightStyle: BoxHeightStyle.tight,
boxWidthStyle: BoxWidthStyle.tight,
);

試着修改boxHeightStyle的值,結果沒有影響。
getFullHeightForCaret
該方法需要傳入2個參數:
TextPosition position:創建一個表示字符串中特定位置的對象Rect caretPrototype:創建一個矩形
Rect caretPrototype = const Rect.fromLTWH(100, 100, 40, 120);
double? value = textPainter.getFullHeightForCaret(const TextPosition(offset: 24), caretPrototype);
getOffsetForCaret
同getFullHeightForCaret
markNeedsLayout
將這個文本繪制器的布局信息標記為dirty ,並刪除緩存的信息
setPlaceholderDimensions
需要傳入一個PlaceholderDimensions對象的列表。
PlaceholderDimensions
Size size:占位符的寬度和高度尺寸PlaceholderAlignment alignment:如何將占位符與文本對齊TextBaseline? baseline:文本基線的位置double? baselineOffset:基線與占位符上邊緣的距離
const PlaceholderDimensions(
size: Size(100, 20),
alignment: PlaceholderAlignment.middle,
baseline: TextBaseline.alphabetic,
baselineOffset: 24,
)
提供的PlaceholderDimensions數量應與文本中PlaceholderSpan的數量相同。傳入一個空值或空value將什么都不做。如果在未設置占位符尺寸的情況下嘗試layout ,則占位符將在文本布局中被忽略,並且不會返回有效的inlinePlaceholderBoxes 。
不會🙄
layout
有2個可選參數:
double maxWidth:文字顯示的最大寬度double minWidth:文字顯示的最小寬度
textPainter.layout(maxWidth: size.width / 3 * 2);
paint
textPainter.paint(canvas, Offset(size.width / 3 / 2, 120));
