【Flutter 组件集录】ClipPath| 8月更文挑战

2,373 阅读5分钟
前言:

这是我参与8月更文挑战的第 5 天,活动详情查看:8月更文挑战。为应掘金的八月更文挑战,我准备在本月挑选 31 个以前没有介绍过的组件,进行全面分析和属性介绍。这些文章将来会作为 Flutter 组件集录 的重要素材。希望可以坚持下去,你的支持将是我最大的动力~

本系列组件文章列表
1.NotificationListener2.Dismissible3.Switch
4.Scrollbar5.ClipPath6.CupertinoActivityIndicator
7.Opacity8.FadeTransition9. AnimatedOpacity
10. FadeInImage11. Offstage12. TickerMode
13. Visibility14. Padding15. AnimatedContainer
16.CircleAvatar17.PhysicalShape18.Divider
19.Flexible、Expanded 和 Spacer 20.Card

一、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 组件的使用方式到这里就介绍完毕,那本文到这里就结束了,谢谢观看,明天见~