thumbnail thumbnail
  • flitter
  • flutter

Unpacking Flutter Widgets - 2: ClipPath, ClipOval, ClipRect, ClipRRect,

Moon

Introduction

Recently, I've been working on a framework that mimics Flutter using JavaScript. While fully replicating the original is beyond my current abilities, I'm giving it my best effort to emulate it closely. As part of this process, I plan to document the layout rules of Flutter widgets in a series of articles, aiming to publish one each week.

ClipPath

The ClipPath widget takes a clipper argument to cut the screen into various shapes. The clipper’s getClip method receives the size of the widget as its parameter. An example is shown where a square is clipped into a triangle.

ClipPath 적용

ClipPath applies cutting based on the clipBehavior parameter, which includes none, hardEdge, antiAlias, and antiAliasWithSaveLayer. With none, the clipper has no effect. From hardEdge onwards, the cutting appears to be more precise, though the differences are subtle.

Dart
class ClipPath extends SingleChildRenderObjectWidget { /// Creates a path clip. /// /// If [clipper] is null, the clip will be a rectangle that matches the layout /// size and location of the child. However, rather than use this default, /// consider using a [ClipRect], which can achieve the same effect more /// efficiently. /// /// The [clipBehavior] argument must not be null. If [clipBehavior] is /// [Clip.none], no clipping will be applied. const ClipPath({ super.key, this.clipper, this.clipBehavior = Clip.antiAlias, super.child, }) : assert(clipBehavior != null); /// If non-null, determines which clip to use. /// /// The default clip, which is used if this property is null, is the /// bounding box rectangle of the widget. [ClipRect] is a more /// efficient way of obtaining that effect. final CustomClipper<Path>? clipper; final Clip clipBehavior; RenderClipPath createRenderObject(BuildContext context) { return RenderClipPath(clipper: clipper, clipBehavior: clipBehavior); } } /// Different ways to clip a widget's content. enum Clip { none, hardEdge, antiAlias, antiAliasWithSaveLayer, }

ClipPath is a RenderObjectWidget, primarily focusing on clipping. The hitTest and paint methods prominently feature the clip, derived from the clipper’s getClip

Dart
class RenderClipPath extends _RenderCustomClip<Path> { RenderClipPath({ super.child, super.clipper, super.clipBehavior, }) : assert(clipBehavior != null); Path get _defaultClip => Path()..addRect(Offset.zero & size); void _updateClip() { _clip ??= _clipper?.getClip(size) ?? _defaultClip; } bool hitTest(BoxHitTestResult result, { required Offset position }) { if (_clipper != null) { _updateClip(); assert(_clip != null); if (!_clip!.contains(position)) { return false; } } return super.hitTest(result, position: position); } void paint(PaintingContext context, Offset offset) { if (child != null) { if (clipBehavior != Clip.none) { _updateClip(); layer = context.pushClipPath( needsCompositing, offset, Offset.zero & size, _clip!, super.paint, clipBehavior: clipBehavior, oldLayer: layer as ClipPathLayer?, ); } else { context.paintChild(child!, offset); layer = null; } } else { layer = null; } } }

ClipPath의 clip 타입은 Path이다. Path 코드를 보면 아래와 같다.

Dart
class Path extends NativeFieldWrapperClass1 { (...) void close() native 'Path_close'; void contains(Offset offset) native 'Path_contains'; void moveTo(double x, double y) native 'Path_moveTo'; void relativeMoveTo(double dx, double dy) native 'Path_relativeMoveTo'; void lineTo(double x, double y) native 'Path_lineTo'; void relativeLineTo(double dx, double dy) native 'Path_relativeLineTo'; void quadraticBezierTo(double x1, double y1, double x2, double y2) native 'Path_quadraticBezierTo'; void relativeQuadraticBezierTo(double x1, double y1, double x2, double y2) native 'Path_relativeQuadraticBezierTo'; void cubicTo(double x1, double y1, double x2, double y2, double x3, double y3) native 'Path_cubicTo'; void relativeCubicTo(double x1, double y1, double x2, double y2, double x3, double y3) native 'Path_relativeCubicTo'; void _arcToPoint(double arcEndX, double arcEndY, double radiusX, double radiusY, double rotation, bool largeArc, bool clockwise) native 'Path_arcToPoint'; (...) void _addRect(double left, double top, double right, double bottom) native 'Path_addRect'; void _addOval(double left, double top, double right, double bottom) native 'Path_addOval'; void _addRRect(Float32List rrect) native 'Path_addRRect'; (...) }
painting.dart 2325 라인

함수 구현부분은 플랫폼별로 빌드할 때 생성되는 듯 보인다.

아래에 addRect, addOval, addRRectmoveTo, lineTo, artToPoint, close의 조합으로 구현 가능해 보인다. 그러나 flutter는 이부분도 native 빌드할때 생성하는걸로 남겨두었다.

Path를 조작하는 법은 아래 블로그를 참고하시길

웹에서 canvas나 svg로 그릴 때랑 비슷한 인터페이스라 반가웠다ㅎㅎ

ClipRect

Similar to ClipPath, ClipRect changes its generic type from Path to Rect but functions in the same manner.

The Rect class offers various factory constructors like fromLTRB, fromLTWH, fromCircle, and fromCenter to create a rectangle. The contains method, unlike in Path, is not part of a native build.

Dart
class Rect { const Rect.fromLTRB(this.left, this.top, this.right, this.bottom) : assert(left != null), assert(top != null), assert(right != null), assert(bottom != null); const Rect.fromLTWH(double left, double top, double width, double height) : this.fromLTRB(left, top, left + width, top + height); Rect.fromCircle({ required Offset center, required double radius }) : this.fromCenter( center: center, width: radius * 2, height: radius * 2, ); Rect.fromCenter({ required Offset center, required double width, required double height }) : this.fromLTRB( center.dx - width / 2, center.dy - height / 2, center.dx + width / 2, center.dy + height / 2, ); Rect.fromPoints(Offset a, Offset b) : this.fromLTRB( math.min(a.dx, b.dx), math.min(a.dy, b.dy), math.max(a.dx, b.dx), math.max(a.dy, b.dy), ); bool contains(Offset offset) { return offset.dx >= left && offset.dx < right && offset.dy >= top && offset.dy < bottom; } }

ClipRect, also a RenderObjectWidget, distinguishes itself from ClipPath by having a separate widget implementation, even though the hitTest and paint methods utilize the rectangle directly.

Dart
class RenderClipRect extends _RenderCustomClip<Rect> { Rect get _defaultClip => Offset.zero & size; bool hitTest(BoxHitTestResult result, { required Offset position }) { if (_clipper != null) { _updateClip(); assert(_clip != null); if (!_clip!.contains(position)) { return false; } } return super.hitTest(result, position: position); } void paint(PaintingContext context, Offset offset) { if (child != null) { if (clipBehavior != Clip.none) { _updateClip(); layer = context.pushClipRect( needsCompositing, offset, _clip!, super.paint, clipBehavior: clipBehavior, oldLayer: layer as ClipRectLayer?, ); } else { context.paintChild(child!, offset); layer = null; } } else { layer = null; } } }

ClipOval

ClipOval operates similarly to ClipRect, with its clipper type remaining unchanged. It's another RenderObjectWidget where hitTest is uniquely implemented, and paint leverages a Path obtained through a specific method, suggesting optimization reasons behind Flutter's choice to implement these as separate widgets.

Dart
class RenderClipOval extends _RenderCustomClip<Rect> { RenderClipOval({ super.child, super.clipper, super.clipBehavior, }) : assert(clipBehavior != null); Rect? _cachedRect; late Path _cachedPath; Path _getClipPath(Rect rect) { if (rect != _cachedRect) { _cachedRect = rect; _cachedPath = Path()..addOval(_cachedRect!); } return _cachedPath; } Rect get _defaultClip => Offset.zero & size; bool hitTest(BoxHitTestResult result, { required Offset position }) { _updateClip(); assert(_clip != null); final Offset center = _clip!.center; // convert the position to an offset from the center of the unit circle final Offset offset = Offset( (position.dx - center.dx) / _clip!.width, (position.dy - center.dy) / _clip!.height, ); // check if the point is outside the unit circle if (offset.distanceSquared > 0.25) { // x^2 + y^2 > r^2 return false; } return super.hitTest(result, position: position); } void paint(PaintingContext context, Offset offset) { if (child != null) { if (clipBehavior != Clip.none) { _updateClip(); layer = context.pushClipPath( needsCompositing, offset, _clip!, _getClipPath(_clip!), super.paint, clipBehavior: clipBehavior, oldLayer: layer as ClipPathLayer?, ); } else { context.paintChild(child!, offset); layer = null; } } else { layer = null; } }

Conclusion

The purpose of this code analysis was to decide whether to implement ClipOval, ClipRect, and ClipRRect in Flitter as separate RenderObjectWidgets like Flutter, or to reuse ClipPath. The speculation is that Flutter's separate implementations for these widgets might be for optimization related to hit testing. The reasoning is that calculating containment within a Path might be more computationally intensive compared to the direct implementations in RenderClipRect or RenderClipOval. Ultimately, since SVG is used and hit testing doesn't need to be implemented manually, and there's no significant performance concern between using <rect/> and <path/>, it was decided to implement ClipRect, ClipOval, and ClipRRect using ClipPath.

https://flitter.pages.dev/ - ClipPath

 

참고자료