概述

#

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 剪輯