剪裁行为
摘要
#Flutter 现在默认情况下不进行剪裁,除了少数专门的 Widget(例如 ClipRect
)。若要覆盖不剪裁的默认设置,请在 Widget 构造中显式设置 clipBehavior
。
上下文
#Flutter 过去因为裁剪(clip)操作导致速度缓慢。例如,Flutter Gallery 应用的基准测试显示,在 2018 年 5 月,平均帧光栅化时间约为 35 毫秒,而流畅 60fps 渲染的预算为 16 毫秒。通过移除不必要的裁剪及其相关操作,我们实现了近乎 2 倍的速度提升,从 35 毫秒/帧降至 17.5 毫秒/帧。
当时与裁剪相关的最大成本在于,Flutter 习惯在每次裁剪后添加 saveLayer
调用(除非是简单的轴对齐矩形裁剪),以避免像Issue 18057中描述的边缘锯齿伪影。这种行为在通过 Card
、Chip
、Button
等小部件的 Material 应用中普遍存在,导致 PhysicalShape
和 PhysicalModel
裁剪其内容。
saveLayer
调用在旧设备上尤其昂贵,因为它会创建一个离屏渲染目标,而渲染目标切换有时可能花费大约 1 毫秒。
即使没有 saveLayer
调用,裁剪仍然很昂贵,因为它会应用于所有后续绘制操作,直到它被恢复。因此,单个裁剪可能会降低数百个绘制操作的性能。
除了性能问题外,Flutter 还存在一些正确性问题,因为裁剪没有在一个地方进行管理和实现。在一些地方,saveLayer
被插入到错误的位置,因此它只会增加性能成本,而不会修复任何边缘锯齿伪影。
因此,我们在这次重大更改中统一了 clipBehavior
控制及其实现。对于大多数小部件,默认 clipBehavior
为 Clip.none
以节省性能,但以下情况除外:
ClipPath
默认值为Clip.antiAlias
ClipRRect
默认值为Clip.antiAlias
ClipRect
默认值为Clip.hardEdge
Stack
默认值为Clip.hardEdge
EditableText
默认值为Clip.hardEdge
ListWheelScrollView
默认值为Clip.hardEdge
SingleChildScrollView
默认值为Clip.hardEdge
NestedScrollView
默认值为Clip.hardEdge
ShrinkWrappingViewport
默认值为Clip.hardEdge
迁移指南
#您有 4 种代码迁移选择:
- 如果您的内容不需要裁剪(例如,没有一个小部件的子元素扩展到其父元素的边界之外),则可以保留代码原样。这可能会对您应用的整体性能产生积极影响。
- 如果需要裁剪,并且无抗锯齿的裁剪足以满足您(和您的客户)的需求,则添加
clipBehavior: Clip.hardEdge
。当您裁剪矩形或具有非常小的弯曲区域(例如圆角矩形的角)的形状时,这是常见情况。 - 如果需要抗锯齿裁剪,则添加
clipBehavior: Clip.antiAlias
。这会在略微更高的成本下为您提供更平滑的边缘。在处理圆形和弧形时,这是常见情况。 - 如果希望获得与之前(2018 年 5 月)完全相同的行为,则添加
clip.antiAliasWithSaveLayer
。请注意,这在性能方面非常昂贵。这可能很少需要。您可能需要这种情况的一种情况是在非常不同的背景颜色上叠加图像。在这些情况下,请考虑您是否可以避免在一个位置重叠多种颜色(例如,仅在图像不存在的地方显示背景颜色)。
具体到 Stack
小部件,如果您之前使用 overflow: Overflow.visible
,请将其替换为 clipBehavior: Clip.none
。
对于 ListWheelViewport
小部件,如果您之前指定了 clipToSize
,请将其替换为相应的 clipBehavior
:对于 clipToSize = false
使用 Clip.none
,对于 clipToSize = true
使用 Clip.hardEdge
。
迁移前的代码
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: Stack(
overflow: Overflow.visible,
children: const <Widget>[
SizedBox(
width: 100,
height: 100,
),
],
),
),
),
);
迁移后的代码
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: Stack(
clipBehavior: Clip.none,
children: const <Widget>[
SizedBox(
width: 100.0,
height: 100.0,
),
],
),
),
),
);
时间线
#合并版本:不同
稳定版发布:2.0.0
参考
#API 文档
相关问题
相关 PR
- PR 5420:移除不必要的 saveLayer
- PR 18576:向 Material 和相关小部件添加 Clip 枚举
- PR 18616:从 dart 中移除裁剪后的 saveLayer
- PR 5647:向 ClipPath/ClipRRect 和 PhysicalShape 图层添加 ClipMode
- PR 5670:向 canvas 裁剪调用添加抗锯齿开关
- PR 5853:将裁剪模式重命名为裁剪行为
- PR 5868:在 compositing.dart 中将裁剪重命名为 clipBehavior
- PR 5973:如果存在裁剪,则调用 drawPaint 而不是 drawPath
- PR 5952:如果可能,则在没有裁剪的情况下调用 drawPath
- PR 20205:将默认 clipBehavior 设置为 Clip.none 并更新测试
- PR 20538:向更多 Material 按钮公开 clipBehavior
- PR 20751:向 InkWell 添加 customBorder,以便它可以裁剪 ShapeBorder
- PR 20752:再次将默认裁剪设置为 Clip.none
- PR 21012:向更多按钮添加默认无裁剪测试
- PR 21703:将 ClipRect 的默认 clipBehavior 设置为 hardEdge
- PR 21826:ClipRectLayer 缺少默认 hardEdge 裁剪
除非另有说明,否则本网站上的文档反映了 Flutter 的最新稳定版本。页面最后更新于 2024-04-04。 查看源代码 或 报告问题.