【Flutter 元件集錄】ClipPath| 8月更文挑戰

張風捷特烈發表於2021-08-05
前言:

這是我參與8月更文挑戰的第 5 天,活動詳情檢視:8月更文挑戰。為應掘金的八月更文挑戰,我準備在本月挑選 31 個以前沒有介紹過的元件,進行全面分析和屬性介紹。這些文章將來會作為 Flutter 元件集錄 的重要素材。希望可以堅持下去,你的支援將是我最大的動力~


一、ClipPath 的使用

1. 認識 ClipPath

ClipPath 繼承自 SingleChildRenderObjectWidget ,說明該元件可以傳入一個元件入參。

ClipPath 的構造方法中可以,傳入 clipperclipBehavior 兩個引數,分別代表裁剪路徑裁剪行為

final CustomClipper<Path>? clipper;
final Clip clipBehavior;
複製程式碼

2. ClipPath 的簡單使用

clipper 型別為 CustomClipper<Path> ,可以看出它是一個 抽象類,所以無法直接例項化物件,所以需要找到可用實現類,或自己實現。在 Flutter 框架中 只有 ShapeBorderClipper 可用。

ShapeBorderClipper 需要傳入一個 ShapeBorder 物件。

ShapeBorder 也是個抽象類,Flutter 中內建了很多的 ShapeBorder 子類。

如下,是通過 CircleBorderRoundedRectangleBorder 兩個形狀進行裁剪的案例。

// 圓形裁剪
ClipPath(
  clipper: ShapeBorderClipper(
    shape: CircleBorder(),
  ),
  child: Image.asset(
    'assets/images/icon_head.jpg',
    width: 100,
    height: 100,
  ),
)
  
// 圓角矩形裁剪
ClipPath(
  clipper: ShapeBorderClipper(
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(20)
    ),
  ),
  child: Image.asset(
    'assets/images/icon_head.jpg',
    width: 100,
    height: 100,
  ),
),
複製程式碼

3.ClipPath 的 shape 方法

既然框架中 CustomClipper 只有 ShapeBorderClipper 子類,那麼就可以簡化使用。如下,通過 shape 方法返回 Widget 元件,只需要傳入 shape 即可。從原始碼中可以看出,其實就是簡單封裝一下 ShapeBorderClipper 而已。

// 使用 ClipPath.shape 簡化程式碼
ClipPath.shape(
  shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(20)),
  child: Image.asset(
    'assets/images/icon_head.jpg',
    width: 100,
    height: 100,
  ),
),
複製程式碼

4. clipBehavior 屬性

clipBehavior 屬性對應的型別為 Clip 列舉,有如下四個元素。它用來表示元件內容裁剪的方式。在這裡中預設是 antiAlias ,這種方式是抗鋸齒的裁剪,也就是說裁剪成曲線時不會產生鋸齒感。

/// Different ways to clip a widget's content.
enum Clip {
  none, // 無
  hardEdge, // 硬邊緣
  antiAlias, // 抗鋸齒
  antiAliasWithSaveLayer, // 抗鋸齒+儲存層
}
複製程式碼

至於其他幾個,none 是不進行裁剪,一般我們預設元件不會超過邊界,但如果內容會溢位邊界,我們需要指定後三種裁剪方式之一。hardEdge 是不抗鋸齒的意思,這種裁剪方式當是曲線路徑裁剪時,會有明顯的鋸齒狀,好處是這種方式要比 antiAlias 快一些,適合用於矩形裁剪。另外 antiAliasWithSaveLayer 模式不僅抗鋸齒,而且還會分配一個緩衝區。後續所有的繪製都在緩衝區上進行,最後被剪下和合成。這種方式要更慢,一般很少使用。


5. 使用 ClipPath 的注意點

原始碼中說,通過路徑裁剪是比較昂貴的,對於一些常規的裁剪,可以考慮其他元件,比如矩形裁剪可以使用 ClipRect,圓或橢圓可以使用 ClipOval ,圓角矩形可以使用 ClipRRect

其實這麼一看 ClipPath 並非用於通常裁剪,對於一些特殊的裁剪需求,如果是按照某些曲線進行裁剪,那 ClipPath 就是可以勝任。


二、自定義裁剪

上面也說過 CustomClipper 在框架中只有一個子類,使用如果我們想要組定義裁剪性質,就需要自定義裁剪器。那首先我們先認識一下 CustomClipper


1. 認識 CustomClipper 裁剪器

CustomClipper 繼承自Listenable可指定泛型,有兩個抽象方法 getClipshouldReclip 。其實看到這裡可以聯絡到 CustomPainter,這兩個抽象在結構上非常類似。都可以通過一個可監聽物件觸發重新裁剪/重繪,都可以通過shouldXXX 判斷讀取類物件更新時是否重新裁剪/重繪


下面先定義一個三角形的路徑裁剪測試一下,主要就是在 getClip 中返回對應裁剪的路徑。

class TriangleClipper extends CustomClipper<Path> {
  @override
  Path getClip(Size size) {
    print(size);
    Path path = Path()
      ..moveTo(0, size.height)
      ..relativeLineTo(size.width, 0)
      ..relativeLineTo(-size.width / 2, -size.height)
      ..close();
    return path;
  }
  @override
  bool shouldReclip(covariant CustomClipper<dynamic> oldClipper) {
    return true;
  }
}
複製程式碼

2. 自定義愛心裁剪

只要是路徑,都可以進行裁剪。如下是一個簡單的愛心路徑裁剪,這裡使用的貝塞爾曲線,正好也來看一下 antiAliashardEdge 的表現效果,你放大一下可以看出使用 hardEdge 型別的裁剪效果周圍有明顯鋸齒。

class LoveClipper extends CustomClipper<Path> {

  @override
  Path getClip(Size size) {
    double fate = 18.5*size.height/100;
    double width = size.width / 2;
    double height = size.height / 4;
    Path path = Path();

    path.moveTo(width, height);
    path.cubicTo(width, height, width + 1.1 * fate, height - 1.5 * fate, width + 2 * fate, height);
    path.cubicTo(width + 2 * fate, height, width + 3.5 * fate, height + 2 * fate, width, height + 4 * fate);

    path.moveTo(width, height);
    path.cubicTo(width, height, width - 1.1 * fate, height - 1.5 * fate, width - 2 * fate, height);
    path.cubicTo(width - 2 * fate, height, width - 3.5 * fate, height + 2 * fate, width, height + 4 * fate);

    return path;
  }

  @override
  bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
    return true;
  }
}
複製程式碼

3.打洞裁剪

【Flutter高階玩法-shape】Path在手,天下我有 一文中介紹過基於 path 自定義 ShapeBorder 的使用,其實這裡也是類似的。你可以操作路徑進行任意地裁剪,當然那篇文章是自定義 ShapeBorder,也可以通過 ShapeBorderClipper 應用到 ClipPath 中。

class HoleClipper extends CustomClipper<Path> {
  final Offset offset;
  final double holeSize;


  HoleClipper({this.offset=const Offset(0.1, 0.1), this.holeSize=20});
  @override
  Path getClip(Size size) {
    Path circlePath = Path();
    circlePath.addRRect(RRect.fromRectAndRadius(Offset.zero&size, Radius.circular(5)));

    double w = size.width;
    double h = size.height;
    Offset offsetXY = Offset( offset.dx*w,offset.dy*h);
    double d = holeSize;
    _getHold(circlePath, 1, d, offsetXY);
    circlePath.fillType = PathFillType.evenOdd;
    return circlePath;
  }

  void _getHold(Path path, int count, double d, Offset offset) {
    var left = offset.dx;
    var top = offset.dy;
    var right = left + d;
    var bottom = top + d;
    path.addOval(Rect.fromLTRB(left, top, right, bottom));
  }

  @override
  bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
    return true;
  }
}
複製程式碼

如果要在 ClipPath 使用自定義路徑裁剪,推薦直接繼承自 CustomClipper 來建立子類。而非自定義 ShapeBorder,再通過 ShapeBorderClipperClipPath 中使用,因為自定義 ShapeBorder 比較複雜,還能進行繪製,但是繪製的東西在 ClipPath 時不會被畫出來,此處只是根據路徑裁剪。通過 CustomClipper比較方便,而且可以控制是否需要重新裁剪,以及通過 Listenable 物件觸發重新裁剪,這樣就可以進行裁剪動畫。


三、ClipPath 的原始碼實現簡看

實現,它繼承自 SingleChildRenderObjectWidget

就說明,該元件需要維護一個 RenderObject 物件的建立及更新,如下是 RenderClipPath

RenderClipPath#paint 時,會觸發 context#pushClipPath 方法,建立一個 layer

pushClipPath 中如果需要合成 needsCompositing,則會建立 ClipPathLayer 執行裁剪工作。

否則,通過 clipPathAndPaint ,通過 canvas.clipPath 進行裁剪。 這裡只是簡單認識一下原始碼,更細節的東西這裡就不展開了。

ClipPath 元件的使用方式到這裡就介紹完畢,那本文到這裡就結束了,謝謝觀看,明天見~

相關文章