概述

#

Flutter 现在默认不进行剪辑,除了少数特殊的小部件(如 ClipRect)。要覆盖不剪辑的默认行为,请在构建小部件时显式设置 clipBehavior

背景

#

以前,Flutter 因为剪辑而变慢。例如,在 2018 年 5 月,Flutter Gallery 应用的基准测试平均帧光栅化时间约为 35 毫秒,而实现流畅 60fps 渲染的预算是 16 毫秒。通过移除不必要的剪辑及其相关操作,我们将每帧时间从 35 毫秒缩短到 17.5 毫秒,速度提升了近一倍。

当时剪辑带来的最大开销是,Flutter 会在每次剪辑后(除非是简单的轴对齐矩形剪辑)调用 saveLayer,以避免出现 Issue 18057 中描述的边缘溢出伪影。这种行为通过 CardChipButton 等小部件在 Material 应用中普遍存在,导致 PhysicalShapePhysicalModel 对其内容进行剪辑。

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

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

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

因此,我们在这次重大更改中统一了 clipBehavior 的控制及其实现。为了提高性能,大多数小部件的默认 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. 如果您的内容不需要剪辑(例如,小部件的子项没有超出其父边界),请将代码原样保留。这很可能会对您应用的整体性能产生积极影响。
  2. 如果您需要剪辑,并且没有抗锯齿的剪辑对您(和您的客户)的眼睛来说已经足够,请添加 clipBehavior: Clip.hardEdge。这是剪辑矩形或具有非常小曲线区域(如圆角矩形的角)的形状时的常见情况。
  3. 如果您需要抗锯齿剪辑,请添加 clipBehavior: Clip.antiAlias。这会以略高的成本提供更平滑的边缘。这是处理圆形和弧形时的常见情况。
  4. 如果您想获得与之前(2018 年 5 月)完全相同的行为,请添加 clip.antiAliasWithSaveLayer。请注意,这会带来很高的性能成本。这可能很少需要。您可能需要这种行为的一个例子是,当您有一个图像叠加在非常不同的背景颜色上时。在这些情况下,请考虑是否可以在一个点避免多种颜色重叠(例如,仅在图像不存在的地方显示背景颜色)。

对于 Stack 小部件,如果您之前使用了 overflow: Overflow.visible,请将其替换为 clipBehavior: Clip.none

对于 ListWheelViewport 小部件,如果您之前指定了 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:向 Material 和相关小部件添加 Clip 枚举
  • PR 18616:从 dart 中移除剪辑后的 saveLayer
  • PR 5647:向 ClipPath/ClipRRect 和 PhysicalShape 层添加 ClipMode
  • PR 5670:向 canvas 剪辑调用添加抗锯齿开关
  • PR 5853:将剪辑模式重命名为剪辑行为
  • PR 5868:在 compositing.dart 中将 clip 重命名为 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 剪辑