Skip to main content

裁剪行为

摘要

#

Flutter 现在默认情况下 裁剪,除非是一些特殊的 Widget(例如 ClipRect)。要覆盖不裁剪的默认设置,请在 Widget 构造中显式设置 clipBehavior

背景

#

由于裁剪,Flutter 过去运行速度很慢。例如,Flutter 图库应用基准测试在 2018 年 5 月的平均帧光栅化时间约为 35 毫秒,而流畅 60fps 渲染的预算为 16 毫秒。通过移除不必要的裁剪及其相关操作,我们看到速度提高了近 2 倍,从 35 毫秒/帧提高到 17.5 毫秒/帧。

过去,与裁剪相关的最大成本在于 Flutter 过去会在每次裁剪后添加 saveLayer 调用(除非是简单的轴对齐矩形裁剪),以避免问题 18057 中描述的边缘溢出伪影。这种行为通过诸如 CardChipButton 等 Widget 普遍存在于 Material 应用中,导致 PhysicalShapePhysicalModel 裁剪其内容。

saveLayer 调用在旧设备中尤其昂贵,因为它会创建一个离屏渲染目标,而渲染目标切换有时可能花费约 1 毫秒。

即使没有 saveLayer 调用,裁剪仍然很昂贵,因为它会应用于所有后续绘制,直到它被恢复。因此,单个裁剪可能会降低数百个绘制操作的性能。

除了性能问题外,Flutter 还存在一些正确性问题,因为裁剪没有在一个地方进行管理和实现。在多个地方,saveLayer 被插入到错误的位置,因此它只会增加性能成本而不会修复任何边缘溢出伪影。

因此,在这个重大更改中,我们统一了 clipBehavior 控制及其实现。对于大多数 Widget,默认的 clipBehaviorClip.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 种代码迁移选择:

  1. 如果您的内容不需要裁剪(例如,没有任何 Widget 的子元素扩展到其父元素边界之外),则保留您的代码不变。这可能会对您的应用程序的整体性能产生积极影响。
  2. 如果您需要裁剪,并且不带抗锯齿的裁剪对您(和您的客户)来说足够好,则添加 clipBehavior: Clip.hardEdge。当您裁剪矩形或具有非常小的弯曲区域(例如圆角矩形的角)的形状时,这是常见的情况。
  3. 如果您需要抗锯齿裁剪,则添加 clipBehavior: Clip.antiAlias。这会在略高的成本下为您提供更平滑的边缘。在处理圆形和弧形时,这是常见的情况。
  4. 如果您希望获得与之前(2018 年 5 月)完全相同的行为,则添加 clip.antiAliasWithSaveLayer。请注意,这在性能方面非常昂贵。这可能很少需要。如果您有一个叠加在非常不同的背景颜色上的图像,则可能需要这种情况。在这些情况下,请考虑您是否可以避免在一个位置重叠多种颜色(例如,只在图像不存在的地方显示背景颜色)。

对于 Stack Widget,如果您以前使用的是 overflow: Overflow.visible,请将其替换为 clipBehavior: Clip.none

对于 ListWheelViewport Widget,如果您以前指定了 clipToSize,请将其替换为相应的 clipBehavior:对于 clipToSize = false 使用 Clip.none,对于 clipToSize = true 使用 Clip.hardEdge

迁移前的代码:

dart
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Center(
          child: Stack(
            overflow: Overflow.visible,
            children: const <Widget>[
              SizedBox(
                width: 100,
                height: 100,
              ),
            ],
          ),
        ),
      ),
    );

迁移后的代码:

dart
    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:将 Clip 枚举添加到 Material 和相关 Widget
  • PR 18616:从 dart 中移除裁剪后的 saveLayer
  • PR 5647:将 ClipMode 添加到 ClipPath/ClipRRect 和 PhysicalShape 层
  • PR 5670:将抗锯齿开关添加到画布裁剪调用
  • PR 5853:将裁剪模式重命名为裁剪行为
  • PR 5868:将 compositing.dart 中的裁剪重命名为 clipBehavior
  • PR 5973:如果存在裁剪,则调用 drawPaint 而不是 drawPath
  • PR 5952:如果可能,则在没有裁剪的情况下调用 drawPath
  • PR 20205:将默认 clipBehavior 设置为 Clip.none 并更新测试
  • PR 20538:将 clipBehavior 公开给更多 Material 按钮
  • PR 20751:将 customBorder 添加到 InkWell,以便它可以裁剪 ShapeBorder
  • PR 20752:再次将默认裁剪设置为 Clip.none
  • PR 21012:将默认无裁剪测试添加到更多按钮
  • PR 21703:将 ClipRect 的默认 clipBehavior 设置为 hardEdge
  • PR 21826:缺少 ClipRectLayer 的默认 hardEdge 裁剪