Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
weixin_43355755
engine
提交
599a0728
E
engine
项目概览
weixin_43355755
/
engine
与 Fork 源项目一致
从无法访问的项目Fork
通知
1
Star
0
Fork
0
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
E
engine
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
提交
Issue看板
前往新版Gitcode,体验更适合开发者的 AI 搜索 >>
未验证
提交
599a0728
编写于
1月 06, 2021
作者:
M
Mouad Debbar
提交者:
GitHub
1月 06, 2021
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
[web] Placeholders for rich paragraphs (#23160)
上级
d38aac7a
变更
6
隐藏空白更改
内联
并排
Showing
6 changed file
with
363 addition
and
65 deletion
+363
-65
lib/web_ui/dev/goldens_lock.yaml
lib/web_ui/dev/goldens_lock.yaml
+1
-1
lib/web_ui/lib/src/engine/text/canvas_paragraph.dart
lib/web_ui/lib/src/engine/text/canvas_paragraph.dart
+1
-3
lib/web_ui/lib/src/engine/text/layout_service.dart
lib/web_ui/lib/src/engine/text/layout_service.dart
+239
-56
lib/web_ui/lib/src/engine/text/paint_service.dart
lib/web_ui/lib/src/engine/text/paint_service.dart
+3
-3
lib/web_ui/lib/src/engine/text/paragraph.dart
lib/web_ui/lib/src/engine/text/paragraph.dart
+2
-2
lib/web_ui/test/golden_tests/engine/canvas_paragraph/placeholders_test.dart
...lden_tests/engine/canvas_paragraph/placeholders_test.dart
+117
-0
未找到文件。
lib/web_ui/dev/goldens_lock.yaml
浏览文件 @
599a0728
repository
:
https://github.com/flutter/goldens.git
revision
:
4946ab2de031c14d30502efcaf51220e0be4d1f1
revision
:
7529e9018b11c79334b99d1e7343fcd500c77b08
lib/web_ui/lib/src/engine/text/canvas_paragraph.dart
浏览文件 @
599a0728
...
...
@@ -170,9 +170,7 @@ class CanvasParagraph implements EngineParagraph {
@override
List
<
ui
.
TextBox
>
getBoxesForPlaceholders
()
{
// TODO(mdebbar): After layout, placeholders positions should've been
// determined and can be used to compute their boxes.
return
<
ui
.
TextBox
>[];
return
_layoutService
.
getBoxesForPlaceholders
();
}
// TODO(mdebbar): Check for child spans if any has styles that can't be drawn
...
...
lib/web_ui/lib/src/engine/text/layout_service.dart
浏览文件 @
599a0728
...
...
@@ -104,21 +104,16 @@ class TextLayoutService {
// ********************************* //
if
(
span
is
PlaceholderSpan
)
{
spanometer
.
currentSpan
=
null
;
final
double
lineWidth
=
currentLine
.
width
+
span
.
width
;
if
(
lineWidth
<=
constraints
.
width
)
{
if
(
currentLine
.
widthIncludingSpace
+
span
.
width
<=
constraints
.
width
)
{
// The placeholder fits on the current line.
// TODO(mdebbar):
// (1) adjust the current line's height to fit the placeholder.
// (2) update accumulated line width.
// (3) add placeholder box to line.
currentLine
.
addPlaceholder
(
span
);
}
else
{
// The placeholder can't fit on the current line.
// TODO(mdebbar):
// (1) create a line.
// (2) adjust the new line's height to fit the placeholder.
// (3) update `lineStart`, etc.
// (4) add placeholder box to line.
if
(
currentLine
.
isNotEmpty
)
{
lines
.
add
(
currentLine
.
build
());
currentLine
=
currentLine
.
nextLine
();
}
currentLine
.
addPlaceholder
(
span
);
}
}
else
if
(
span
is
FlatTextSpan
)
{
spanometer
.
currentSpan
=
span
;
...
...
@@ -203,7 +198,7 @@ class TextLayoutService {
while
(
currentLine
.
end
.
type
!=
LineBreakType
.
endOfText
)
{
if
(
span
is
PlaceholderSpan
)
{
// TODO(mdebbar): Do placeholders affect min/max intrinsic width?
currentLine
.
addPlaceholder
(
span
);
}
else
if
(
span
is
FlatTextSpan
)
{
spanometer
.
currentSpan
=
span
;
final
LineBreakResult
nextBreak
=
currentLine
.
findNextBreak
(
span
.
end
);
...
...
@@ -211,26 +206,38 @@ class TextLayoutService {
// For the purpose of max intrinsic width, we don't care if the line
// fits within the constraints or not. So we always extend it.
currentLine
.
extendTo
(
nextBreak
);
}
final
double
widthOfLastSegment
=
currentLine
.
lastSegment
.
width
;
if
(
minIntrinsicWidth
<
widthOfLastSegment
)
{
minIntrinsicWidth
=
widthOfLastSegment
;
}
final
double
widthOfLastSegment
=
currentLine
.
lastSegment
.
width
;
if
(
minIntrinsicWidth
<
widthOfLastSegment
)
{
minIntrinsicWidth
=
widthOfLastSegment
;
}
if
(
currentLine
.
end
.
isHard
)
{
// Max intrinsic width includes the width of trailing spaces.
if
(
maxIntrinsicWidth
<
currentLine
.
widthIncludingSpace
)
{
maxIntrinsicWidth
=
currentLine
.
widthIncludingSpace
;
}
currentLine
=
currentLine
.
nextLine
();
if
(
currentLine
.
end
.
isHard
)
{
// Max intrinsic width includes the width of trailing spaces.
if
(
maxIntrinsicWidth
<
currentLine
.
widthIncludingSpace
)
{
maxIntrinsicWidth
=
currentLine
.
widthIncludingSpace
;
}
currentLine
=
currentLine
.
nextLine
();
}
// Only go to the next span if we've reached the end of this span.
if
(
currentLine
.
end
.
index
>=
span
.
end
&&
spanIndex
<
spanCount
-
1
)
{
span
=
paragraph
.
spans
[++
spanIndex
];
// Only go to the next span if we've reached the end of this span.
if
(
currentLine
.
end
.
index
>=
span
.
end
&&
spanIndex
<
spanCount
-
1
)
{
span
=
paragraph
.
spans
[++
spanIndex
];
}
}
}
List
<
ui
.
TextBox
>
getBoxesForPlaceholders
()
{
final
List
<
ui
.
TextBox
>
boxes
=
<
ui
.
TextBox
>[];
for
(
final
EngineLineMetrics
line
in
lines
)
{
for
(
final
RangeBox
box
in
line
.
boxes
!)
{
if
(
box
is
PlaceholderBox
)
{
boxes
.
add
(
box
.
toTextBox
(
line
));
}
}
}
return
boxes
;
}
List
<
ui
.
TextBox
>
getBoxesForRange
(
...
...
@@ -255,7 +262,7 @@ class TextLayoutService {
for
(
final
EngineLineMetrics
line
in
lines
)
{
if
(
line
.
overlapsWith
(
start
,
end
))
{
for
(
final
RangeBox
box
in
line
.
boxes
!)
{
if
(
box
.
overlapsWith
(
start
,
end
))
{
if
(
box
is
SpanBox
&&
box
.
overlapsWith
(
start
,
end
))
{
boxes
.
add
(
box
.
intersect
(
line
,
start
,
end
));
}
}
...
...
@@ -328,23 +335,133 @@ class TextLayoutService {
/// The box's coordinates are all relative to the line it belongs to. For
/// example, [left] is the distance from the left edge of the line to the left
/// edge of the box.
class
RangeBox
{
RangeBox
.
fromSpanometer
(
this
.
spanometer
,
{
abstract
class
RangeBox
{
LineBreakResult
get
start
;
LineBreakResult
get
end
;
/// The distance from the left edge of the line to the left edge of the box.
double
get
left
;
/// The distance from the left edge of the line to the right edge of the box.
double
get
right
;
/// The direction in which text inside this box flows.
ui
.
TextDirection
get
direction
;
/// Returns a [ui.TextBox] representing this range box in the given [line].
///
/// The coordinates of the resulting [ui.TextBox] are relative to the
/// paragraph, not to the line.
ui
.
TextBox
toTextBox
(
EngineLineMetrics
line
);
/// Returns the text position within this box's range that's closest to the
/// given [x] offset.
///
/// The [x] offset is expected to be relative to the left edge of the line,
/// just like the coordinates of this box.
ui
.
TextPosition
getPositionForX
(
double
x
);
}
/// Represents a box for a [PlaceholderSpan].
class
PlaceholderBox
extends
RangeBox
{
PlaceholderBox
(
this
.
placeholder
,
{
required
LineBreakResult
index
,
required
this
.
left
,
required
this
.
direction
,
})
:
start
=
index
,
end
=
index
;
final
PlaceholderSpan
placeholder
;
@override
final
LineBreakResult
start
;
@override
final
LineBreakResult
end
;
@override
final
double
left
;
@override
double
get
right
=>
left
+
placeholder
.
width
;
@override
final
ui
.
TextDirection
direction
;
ui
.
TextBox
toTextBox
(
EngineLineMetrics
line
)
{
final
double
left
=
line
.
left
+
this
.
left
;
final
double
right
=
line
.
left
+
this
.
right
;
final
double
lineTop
=
line
.
baseline
-
line
.
ascent
;
final
double
top
;
switch
(
placeholder
.
alignment
)
{
case
ui
.
PlaceholderAlignment
.
top
:
top
=
lineTop
;
break
;
case
ui
.
PlaceholderAlignment
.
middle
:
top
=
lineTop
+
(
line
.
height
-
placeholder
.
height
)
/
2
;
break
;
case
ui
.
PlaceholderAlignment
.
bottom
:
top
=
lineTop
+
line
.
height
-
placeholder
.
height
;
break
;
case
ui
.
PlaceholderAlignment
.
aboveBaseline
:
top
=
line
.
baseline
-
placeholder
.
height
;
break
;
case
ui
.
PlaceholderAlignment
.
belowBaseline
:
top
=
line
.
baseline
;
break
;
case
ui
.
PlaceholderAlignment
.
baseline
:
top
=
line
.
baseline
-
placeholder
.
baselineOffset
;
break
;
}
return
ui
.
TextBox
.
fromLTRBD
(
left
,
top
,
right
,
top
+
placeholder
.
height
,
direction
,
);
}
@override
ui
.
TextPosition
getPositionForX
(
double
x
)
{
// See if `x` is closer to the left edge or the right edge of the box.
final
bool
closerToLeft
=
x
-
left
<
right
-
x
;
return
ui
.
TextPosition
(
offset:
start
.
index
,
affinity:
closerToLeft
?
ui
.
TextAffinity
.
upstream
:
ui
.
TextAffinity
.
downstream
,
);
}
}
/// Represents a box in a [FlatTextSpan].
class
SpanBox
extends
RangeBox
{
SpanBox
(
Spanometer
spanometer
,
{
required
this
.
start
,
required
this
.
end
,
required
this
.
left
,
})
:
span
=
spanometer
.
currentSpan
,
required
this
.
direction
,
})
:
this
.
spanometer
=
spanometer
,
span
=
spanometer
.
currentSpan
,
height
=
spanometer
.
height
,
baseline
=
spanometer
.
a
lphabeticBaseline
,
baseline
=
spanometer
.
a
scent
,
width
=
spanometer
.
measureIncludingSpace
(
start
,
end
);
final
Spanometer
spanometer
;
final
Paragraph
Span
span
;
final
FlatText
Span
span
;
final
LineBreakResult
start
;
final
LineBreakResult
end
;
/// The distance from the left edge of the line to the left edge of the box.
@override
final
double
left
;
/// The distance from the left edge to the right edge of the box.
...
...
@@ -357,11 +474,10 @@ class RangeBox {
/// the box.
final
double
baseline
;
/// The direction in which text inside this box flows.
ui
.
TextDirection
get
direction
=>
spanometer
.
paragraph
.
paragraphStyle
.
_effectiveTextDirection
;
@override
final
ui
.
TextDirection
direction
;
/// The distance from the left edge of the line to the right edge of the box.
@override
double
get
right
=>
left
+
width
;
/// Whether this box's range overlaps with the range from [startIndex] to
...
...
@@ -390,14 +506,14 @@ class RangeBox {
if
(
start
<=
this
.
start
.
index
)
{
left
=
this
.
left
;
}
else
{
spanometer
.
currentSpan
=
span
as
FlatTextSpan
;
spanometer
.
currentSpan
=
span
;
left
=
this
.
left
+
spanometer
.
_measure
(
this
.
start
.
index
,
start
);
}
if
(
end
>=
this
.
end
.
indexWithoutTrailingNewlines
)
{
right
=
this
.
right
;
}
else
{
spanometer
.
currentSpan
=
span
as
FlatTextSpan
;
spanometer
.
currentSpan
=
span
;
right
=
this
.
right
-
spanometer
.
_measure
(
end
,
this
.
end
.
indexWithoutTrailingNewlines
);
}
...
...
@@ -414,13 +530,9 @@ class RangeBox {
);
}
/// Returns the text position within this box's range that's closest to the
/// given [x] offset.
///
/// The [x] offset is expected to be relative to the left edge of the line,
/// just like the coordinates of this box.
@override
ui
.
TextPosition
getPositionForX
(
double
x
)
{
spanometer
.
currentSpan
=
span
as
FlatTextSpan
;
spanometer
.
currentSpan
=
span
;
// Make `x` relative to this box.
x
-=
left
;
...
...
@@ -572,11 +684,14 @@ class LineBuilder {
/// The width of trailing white space in the line.
double
get
widthOfTrailingSpace
=>
widthIncludingSpace
-
width
;
/// The alphabetic baseline of the line so far.
double
alphabeticBaseline
=
0.0
;
/// The distance from the top of the line to the alphabetic baseline.
double
ascent
=
0.0
;
/// The distance from the bottom of the line to the alphabetic baseline.
double
descent
=
0.0
;
/// The height of the line so far.
double
height
=
0.0
;
double
get
height
=>
ascent
+
descent
;
/// The last segment in this line.
LineSegment
get
lastSegment
=>
_segments
.
last
;
...
...
@@ -626,13 +741,75 @@ class LineBuilder {
'Cannot extend a line that ends with a hard break.'
,
);
alphabeticBaseline
=
math
.
max
(
alphabeticBaseline
,
spanometer
.
alphabeticBaseline
);
height
=
math
.
max
(
height
,
spanometer
.
height
);
ascent
=
math
.
max
(
ascent
,
spanometer
.
ascent
);
descent
=
math
.
max
(
descent
,
spanometer
.
descent
);
_addSegment
(
_createSegment
(
newEnd
));
}
void
addPlaceholder
(
PlaceholderSpan
placeholder
)
{
// Increase the line's height to fit the placeholder, if necessary.
final
double
ascent
,
descent
;
switch
(
placeholder
.
alignment
)
{
case
ui
.
PlaceholderAlignment
.
top
:
// The placeholder is aligned to the top of text, which means it has the
// same `ascent` as the remaining text. We only need to extend the
// `descent` enough to fit the placeholder.
ascent
=
this
.
ascent
;
descent
=
placeholder
.
height
-
this
.
ascent
;
break
;
case
ui
.
PlaceholderAlignment
.
bottom
:
// The opposite of `top`. The `descent` is the same, but we extend the
// `ascent`.
ascent
=
placeholder
.
height
-
this
.
descent
;
descent
=
this
.
descent
;
break
;
case
ui
.
PlaceholderAlignment
.
middle
:
final
double
textMidPoint
=
this
.
height
/
2
;
final
double
placeholderMidPoint
=
placeholder
.
height
/
2
;
final
double
diff
=
placeholderMidPoint
-
textMidPoint
;
ascent
=
this
.
ascent
+
diff
;
descent
=
this
.
descent
+
diff
;
break
;
case
ui
.
PlaceholderAlignment
.
aboveBaseline
:
ascent
=
placeholder
.
height
;
descent
=
0.0
;
break
;
case
ui
.
PlaceholderAlignment
.
belowBaseline
:
ascent
=
0.0
;
descent
=
placeholder
.
height
;
break
;
case
ui
.
PlaceholderAlignment
.
baseline
:
ascent
=
placeholder
.
baselineOffset
;
descent
=
placeholder
.
height
-
ascent
;
break
;
}
this
.
ascent
=
math
.
max
(
this
.
ascent
,
ascent
);
this
.
descent
=
math
.
max
(
this
.
descent
,
descent
);
_addSegment
(
LineSegment
(
span:
placeholder
,
start:
end
,
end:
end
,
width:
placeholder
.
width
,
widthIncludingSpace:
placeholder
.
width
,
));
// Add the placeholder box.
_boxes
.
add
(
PlaceholderBox
(
placeholder
,
index:
_boxStart
,
left:
_boxLeft
,
direction:
paragraph
.
paragraphStyle
.
_effectiveTextDirection
,
));
}
/// Creates a new segment to be appended to the end of this line.
LineSegment
_createSegment
(
LineBreakResult
segmentEnd
)
{
// The segment starts at the end of the line.
...
...
@@ -822,11 +999,12 @@ class LineBuilder {
return
;
}
_boxes
.
add
(
RangeBox
.
fromSpanometer
(
_boxes
.
add
(
SpanBox
(
spanometer
,
start:
boxStart
,
end:
boxEnd
,
left:
_boxLeft
,
direction:
paragraph
.
paragraphStyle
.
_effectiveTextDirection
,
));
}
...
...
@@ -849,7 +1027,9 @@ class LineBuilder {
widthWithTrailingSpaces:
widthIncludingSpace
+
ellipsisWidth
,
left:
alignOffset
,
height:
height
,
baseline:
accumulatedHeight
+
alphabeticBaseline
,
baseline:
accumulatedHeight
+
ascent
,
ascent:
ascent
,
descent:
descent
,
boxes:
_boxes
,
);
}
...
...
@@ -929,8 +1109,11 @@ class Spanometer {
}
}
/// The alphabetic baseline for the current span.
double
get
alphabeticBaseline
=>
_currentRuler
!.
alphabeticBaseline
;
/// The distance from the top of the current span to the alphabetic baseline.
double
get
ascent
=>
_currentRuler
!.
alphabeticBaseline
;
/// The distance from the bottom of the current span to the alphabetic baseline.
double
get
descent
=>
height
-
ascent
;
/// The line height of the current span.
double
get
height
=>
_currentRuler
!.
height
;
...
...
lib/web_ui/lib/src/engine/text/paint_service.dart
浏览文件 @
599a0728
...
...
@@ -30,11 +30,11 @@ class TextPaintService {
EngineLineMetrics
line
,
RangeBox
box
,
)
{
final
ParagraphSpan
span
=
box
.
span
;
// Placeholder spans don't need any painting. Their boxes should remain
// empty so that their underlying widgets do their own painting.
if
(
span
is
FlatTextSpan
)
{
if
(
box
is
SpanBox
)
{
final
FlatTextSpan
span
=
box
.
span
;
// Paint the background of the box, if the span has a background.
final
SurfacePaint
?
background
=
span
.
style
.
_background
as
SurfacePaint
?;
if
(
background
!=
null
)
{
...
...
lib/web_ui/lib/src/engine/text/paragraph.dart
浏览文件 @
599a0728
...
...
@@ -66,12 +66,12 @@ class EngineLineMetrics implements ui.LineMetrics {
required
this
.
left
,
required
this
.
height
,
required
this
.
baseline
,
required
this
.
ascent
,
required
this
.
descent
,
// Didn't use `this.boxes` because we want it to be non-null in this
// constructor.
required
List
<
RangeBox
>
boxes
,
})
:
displayText
=
null
,
ascent
=
double
.
infinity
,
descent
=
double
.
infinity
,
unscaledAscent
=
double
.
infinity
,
this
.
boxes
=
boxes
;
...
...
lib/web_ui/test/golden_tests/engine/canvas_paragraph/placeholders_test.dart
0 → 100644
浏览文件 @
599a0728
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// @dart = 2.6
import
'dart:async'
;
import
'package:test/bootstrap/browser.dart'
;
import
'package:test/test.dart'
;
import
'package:ui/ui.dart'
hide
window
;
import
'package:ui/src/engine.dart'
;
import
'../scuba.dart'
;
import
'helper.dart'
;
typedef
CanvasTest
=
FutureOr
<
void
>
Function
(
EngineCanvas
canvas
);
const
Rect
bounds
=
Rect
.
fromLTWH
(
0
,
0
,
800
,
600
);
const
Color
white
=
Color
(
0xFFFFFFFF
);
const
Color
black
=
Color
(
0xFF000000
);
const
Color
red
=
Color
(
0xFFFF0000
);
const
Color
green
=
Color
(
0xFF00FF00
);
const
Color
blue
=
Color
(
0xFF0000FF
);
ParagraphConstraints
constrain
(
double
width
)
{
return
ParagraphConstraints
(
width:
width
);
}
CanvasParagraph
rich
(
EngineParagraphStyle
style
,
void
Function
(
CanvasParagraphBuilder
)
callback
,
)
{
final
CanvasParagraphBuilder
builder
=
CanvasParagraphBuilder
(
style
);
callback
(
builder
);
return
builder
.
build
();
}
void
main
(
)
{
internalBootstrapBrowserTest
(()
=>
testMain
);
}
void
testMain
(
)
async
{
setUpStableTestFonts
();
test
(
'draws paragraphs with placeholders'
,
()
{
final
canvas
=
BitmapCanvas
(
bounds
,
RenderStrategy
());
Offset
offset
=
Offset
.
zero
;
for
(
PlaceholderAlignment
placeholderAlignment
in
PlaceholderAlignment
.
values
)
{
final
CanvasParagraph
paragraph
=
rich
(
ParagraphStyle
(
fontFamily:
'Roboto'
,
fontSize:
14.0
),
(
builder
)
{
builder
.
pushStyle
(
TextStyle
(
color:
black
));
builder
.
addText
(
'Lorem ipsum'
);
builder
.
addPlaceholder
(
80.0
,
50.0
,
placeholderAlignment
,
baselineOffset:
40.0
,
baseline:
TextBaseline
.
alphabetic
,
);
builder
.
pushStyle
(
TextStyle
(
color:
blue
));
builder
.
addText
(
'dolor sit amet, consecteur.'
);
},
)..
layout
(
constrain
(
200.0
));
// Draw the paragraph.
canvas
.
drawParagraph
(
paragraph
,
offset
);
// Then fill the placeholders.
final
TextBox
placeholderBox
=
paragraph
.
getBoxesForPlaceholders
().
single
;
final
SurfacePaint
redPaint
=
Paint
()..
color
=
red
;
canvas
.
drawRect
(
placeholderBox
.
toRect
().
shift
(
offset
),
redPaint
.
paintData
);
offset
=
offset
.
translate
(
0.0
,
paragraph
.
height
+
30.0
);
}
return
takeScreenshot
(
canvas
,
bounds
,
'canvas_paragraph_placeholders'
);
});
test
(
'draws paragraphs with placeholders and text align'
,
()
{
final
canvas
=
BitmapCanvas
(
bounds
,
RenderStrategy
());
const
List
<
TextAlign
>
aligns
=
<
TextAlign
>[
TextAlign
.
left
,
TextAlign
.
center
,
TextAlign
.
right
,
];
Offset
offset
=
Offset
.
zero
;
for
(
TextAlign
align
in
aligns
)
{
final
CanvasParagraph
paragraph
=
rich
(
ParagraphStyle
(
fontFamily:
'Roboto'
,
fontSize:
14.0
,
textAlign:
align
),
(
builder
)
{
builder
.
pushStyle
(
TextStyle
(
color:
black
));
builder
.
addText
(
'Lorem'
);
builder
.
addPlaceholder
(
80.0
,
50.0
,
PlaceholderAlignment
.
bottom
);
builder
.
pushStyle
(
TextStyle
(
color:
blue
));
builder
.
addText
(
'ipsum.'
);
},
)..
layout
(
constrain
(
200.0
));
// Draw the paragraph.
canvas
.
drawParagraph
(
paragraph
,
offset
);
// Then fill the placeholders.
final
TextBox
placeholderBox
=
paragraph
.
getBoxesForPlaceholders
().
single
;
final
SurfacePaint
redPaint
=
Paint
()..
color
=
red
;
canvas
.
drawRect
(
placeholderBox
.
toRect
().
shift
(
offset
),
redPaint
.
paintData
);
offset
=
offset
.
translate
(
0.0
,
paragraph
.
height
+
30.0
);
}
return
takeScreenshot
(
canvas
,
bounds
,
'canvas_paragraph_placeholders_align'
);
});
}
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录