cozy logo Moon
thumbnail thumbnail
profile
  • 개발

플러터 위젯 파헤치기 - 3: ConstrainedBox , OverflowBox, SizedBox, ConstraintsTransformBox 등등

2023년 04월 27일 좋아요 0 댓글 0 조회수 385

플러터 위젯 파헤칠 목록

Row, Column, Flexible, Expanded, Spacer

Align, Center

ClipPath, ClipOval, ClipRect

OverflowedBox, ConstrainedBox, ConstraintsBox, UnConstrainedBox, ConstraintsTransformBox, SizedBox

Stack, Postioned

IndexedStack

Padding

Container, DocoratedBox, Contraint Box, Colored Box,

Layout Builder

Baseline

AspectRatio

Gestures Detector

OffState

Opacity

Overflow Bar

Grid, Grid Tile, GridTileBar

Visibility

Wrap

Transform

 

들어가며

여태껏 flutter에서 Container 아니면 SizedBox만 썼는데 차트와 같은 복잡한 위젯을 그릴려고 하니까 다른 위젯들에 대한 학습이 절실한 시점입니다ㅜ. 그래서 오늘은 Box 특집! Constraints를 다루는 Box들에 대해 알아보겠습니다!

ConstraintsTransformBox

A container widget that applies an arbitrary transform to its constraints, and sizes its child using the resulting BoxConstraints, optionally clipping, or treating the overflow as an error. This container sizes its child using a BoxConstraints created by applying constraintsTransform to its own constraints. This container will then attempt to adopt the same size, within the limits of its own constraints. If it ends up with a different size, it will align the child based on alignment. If the container cannot expand enough to accommodate the entire child, the child will be clipped if clipBehavior is not Clip.none.

이 위젯은 부모로부터 전달되는 ConstraintsconstraintsTransform을 통해 변환해서 넘겨 자식의 크기를 결정한다. 위젯의 크기는 자식 위젯의 크기과 같되 부모의 constraints에 제한된다.

아래는 위젯 클래스의 코드이다.

Dart
class ConstraintsTransformBox extends SingleChildRenderObjectWidget { const ConstraintsTransformBox({ super.key, super.child, this.textDirection, this.alignment = Alignment.center, required this.constraintsTransform, this.clipBehavior = Clip.none, String debugTransformType = '', }) final TextDirection? textDirection; final AlignmentGeometry alignment; final BoxConstraintsTransform constraintsTransform; final Clip clipBehavior; }
flutter basic.dart 2529 라인

props로 clipBehavior와 alignment, textDirection을 받는다. 자식이 부모모다 크면 clip, 작으면 align을 적용한다. 기본적으로 alignment는 center, clipBehavior는 none이다.

RenderConstraintsTransformBox

아래 코드를 보면, 모두 자식 위젯과 관련지어 자신의 크기를 결정한다. IntrinsicHeight의 경우도, Constraints를 변형해서 전달하긴 하지만 결국 상위 클래스 구현이 자식 위젯의 IntrinsicHeight을 가져오도록 구현되어 있어 자식의 크기를 쓴다.

paint 코드를 보면, overflow되는 영역은 별도로 clip 처리를 하고 있다. (단 clipBehavior가 none이 아닐경우만)

자식이 부모보다 작은 경우, alignChild()를 통해 위치를 조정한다. alignChild는 RenderAlingingShiftedBox를 통해 얻을 수 있다. 해당 클래스는 앞으로도 자주 등장한다.

Dart
class RenderConstraintsTransformBox extends RenderAligningShiftedBox with DebugOverflowIndicatorMixin { RenderConstraintsTransformBox({ required super.alignment, required super.textDirection, required BoxConstraintsTransform constraintsTransform, super.child, Clip clipBehavior = Clip.none, }) double computeMinIntrinsicHeight(double width) { return super.computeMinIntrinsicHeight( constraintsTransform(BoxConstraints(maxWidth: width)).maxWidth, ); } (...) void performLayout() { final BoxConstraints constraints = this.constraints; final RenderBox? child = this.child; if (child != null) { final BoxConstraints childConstraints = constraintsTransform(constraints); assert(childConstraints != null); assert(childConstraints.isNormalized, '$childConstraints is not normalized'); _childConstraints = childConstraints; child.layout(childConstraints, parentUsesSize: true); size = constraints.constrain(child.size); alignChild(); final BoxParentData childParentData = child.parentData! as BoxParentData; _overflowContainerRect = Offset.zero & size; _overflowChildRect = childParentData.offset & child.size; } else { size = constraints.smallest; _overflowContainerRect = Rect.zero; _overflowChildRect = Rect.zero; } void paint(PaintingContext context, Offset offset) { // There's no point in drawing the child if we're empty, or there is no // child. if (child == null || size.isEmpty) { return; } if (!_isOverflowing) { super.paint(context, offset); return; } // We have overflow and the clipBehavior isn't none. Clip it. _clipRectLayer.layer = context.pushClipRect( needsCompositing, offset, Offset.zero & size, super.paint, clipBehavior: clipBehavior, oldLayer: _clipRectLayer.layer, ); // Display the overflow indicator. assert(() { paintOverflowIndicator(context, offset, _overflowContainerRect, _overflowChildRect); return true; }()); } }

 

ConstrainedBox

ConstrainedBox를 쓸일이 있을까 싶지만, 사실 Container도 내부적으로 ConstrainedBox를 쓸만큼 자주 쓰는 위젯이다. 인자로 받는 constraints를 자식에게 추가적으로 전달한다. 즉 부모의 constraints와 인자로 받은 constraints 의 교집합을 자식에게 전해 자식의 크기를 결정한다. 위젯의 크기도 자식의 크기에 맞춘다.

Dart
class ConstrainedBox extends SingleChildRenderObjectWidget { ConstrainedBox({ super.key, required this.constraints, super.child, }) /// The additional constraints to impose on the child. final BoxConstraints constraints; }

RenderConstrainedBox

performLayout은 ConstraintsTransformBox와 코드가 대응되지 않아보여도 자식의 위젯 크기를 결정하는 방법과 자신의 위젯크기를 결정하는 방법이 논리적으로 일치한다.

그래서 constraintsTransform을 이용해서

ConstrainedBox(constraints: additionalConstraints)ConstraintsTransformBox(constraintsTransform: (constraints) ⇒ addionalConstraints.enforece(constraints)) 로 대체해도 performLayout 동작은 차이가 없을걸로 보인다.

그래서 ConstraintsTransformBox로 ConstrainedBox 대체할 수 있을까 했는데 IntrinsicWidth, IntrinsicHeight 구현이 살짝 다르다. 그래서 IntrinsicWidth와 같은 위젯들 사이에서 다르게 동작할 수 있기 때문에, 대체 가능하지 않다.

Dart
class RenderConstrainedBox extends RenderProxyBox { double computeMinIntrinsicWidth(double height) { if (_additionalConstraints.hasBoundedWidth && _additionalConstraints.hasTightWidth) { return _additionalConstraints.minWidth; } final double width = super.computeMinIntrinsicWidth(height); assert(width.isFinite); if (!_additionalConstraints.hasInfiniteWidth) { return _additionalConstraints.constrainWidth(width); } return width; } (...중략) void performLayout() { final BoxConstraints constraints = this.constraints; if (child != null) { child!.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true); size = child!.size; } else { size = _additionalConstraints.enforce(constraints).constrain(Size.zero); } }

 

UnconstrainedBox

A widget that imposes no constraints on its child, allowing it to render at its "natural" size. This allows a child to render at the size it would render if it were alone on an infinite canvas with no constraints. This container will then attempt to adopt the same size, within the limits of its own constraints. If it ends up with a different size, it will align the child based on alignment. If the box cannot expand enough to accommodate the entire child, the child will be clipped.

자식에게 전달하는 constraints를 풀어준다. 인자로 constrainedAxis를 받는다. ConstrainedAxis.vertical이면 horizontal 방향의 contraints를 해제한다. 그 외에도 clipBehavior, alignment 인자를 받는다.

내부적으로 ConstraintsTransform을 쓰고 있다.

Dart
class UnconstrainedBox extends StatelessWidget { const UnconstrainedBox({ super.key, this.child, this.textDirection, this.alignment = Alignment.center, this.constrainedAxis, this.clipBehavior = Clip.none, }) Widget build(BuildContext context) { return ConstraintsTransformBox( textDirection: textDirection, alignment: alignment, clipBehavior: clipBehavior, constraintsTransform: _axisToTransform(constrainedAxis), child: child, ); } BoxConstraintsTransform _axisToTransform(Axis? constrainedAxis) { if (constrainedAxis != null) { switch (constrainedAxis) { case Axis.horizontal: return ConstraintsTransformBox.heightUnconstrained; case Axis.vertical: return ConstraintsTransformBox.widthUnconstrained; } } else { return ConstraintsTransformBox.unconstrained; } } }

 

LimitedBox

LimitedBox는 maxWidth, maxHeight 인자를 통해 상한 constraints를 지정한다.

Dart
class LimitedBox extends SingleChildRenderObjectWidget { const LimitedBox({ super.key, this.maxWidth = double.infinity, this.maxHeight = double.infinity, super.child, }) final double maxWidth; final double maxHeight; }

주의할 점은 ScrollingView와 같은 위젯이 부모로 있어 부모로부터 받은 contraints가 bounded되지 않을 경우만 동작한다는 점이다. 즉 일반적인 경우에 LimitedBox는 쓰나 안쓰나 별다른 차이가 없다.

아래는 ConstrainedBox와 LimitedBox를 비교한 예시이다.

posting img
ConstrainedBox(maxWidth: 100)
posting img
LimitedBox(maxWidth: 100)

RenderLimitedBox

아래 limitContraints 함수를 보면 알수 있듯 부모의 constraints가 infinite인 경우에는 부모의 constraint를 그대로 전달한다.

Dart
class RenderLimitedBox extends RenderProxyBox { RenderLimitedBox({ RenderBox? child, double maxWidth = double.infinity, double maxHeight = double.infinity, }) : assert(maxWidth != null && maxWidth >= 0.0), Size _computeSize({required BoxConstraints constraints, required ChildLayouter layoutChild }) { if (child != null) { final Size childSize = layoutChild(child!, _limitConstraints(constraints)); return constraints.constrain(childSize); } return _limitConstraints(constraints).constrain(Size.zero); } BoxConstraints _limitConstraints(BoxConstraints constraints) { return BoxConstraints( minWidth: constraints.minWidth, maxWidth: constraints.hasBoundedWidth ? constraints.maxWidth : constraints.constrainWidth(maxWidth), minHeight: constraints.minHeight, maxHeight: constraints.hasBoundedHeight ? constraints.maxHeight : constraints.constrainHeight(maxHeight), ); } }

 

SizedBox

SizedBox는 정확한 크기를 지정하고 싶을 때 쓰는 위젯이다. SizedBox를 처음 접했을때 당황했던 부분은 자식위젯의 크기를 아무리 작게 지정해도 부모 SizedBox의 크기만큼 확장하는 동작이었다.

왜 그런 동작이었는지는 코드를 보니 알 수 있었다.

Dart
class SizedBox extends SingleChildRenderObjectWidget { const SizedBox({ super.key, this.width, this.height, super.child }); RenderConstrainedBox createRenderObject(BuildContext context) { return RenderConstrainedBox( additionalConstraints: _additionalConstraints, ); } BoxConstraints get _additionalConstraints { return BoxConstraints.tightFor(width: width, height: height); } }
flutter basic.dart 2338 라인

SizedBox는 내부적으로 RenderConstrainedBox를 활용해 renderObject를 생성하고 있었다. 그런데 이럴거면 차라리 StatelessWidget으로 만들고 ConstrainedBox를 직접 사용하면 어떨까 하는데 일단 flutter에서는 이렇게 구현되어 있었다.

보면 알겠지만 constraints를 tight 하게 만들어 자식에게 전달하기 때문에 자식은 Align 위젯 같은걸로 감싸져 있지 않는다면 SizedBox만큼 확장하게 된다.

 

OverflowBox

A widget that imposes different constraints on its child than it gets from its parent, possibly allowing the child to overflow the parent.

이 위젯도 ConstraintsTransformBox처럼 자식에게 전달하는 constraints를 변형한다. 하지만 ConstraintsTransformBox와는 조금 다르게 동작해서 대체될 순 없다.

Dart
class OverflowBox extends SingleChildRenderObjectWidget { /// Creates a widget that lets its child overflow itself. const OverflowBox({ super.key, this.alignment = Alignment.center, this.minWidth, this.maxWidth, this.minHeight, this.maxHeight, super.child, }); void performLayout() { if (child != null) { child?.layout(_getInnerConstraints(constraints), parentUsesSize: true); alignChild(); } } }

RenderConstrainedOverflowBox

RenderConstrainedBox와 다르게 자식에게 전달하는 constraints에 부모 constraints는 관여 하지 않는다. IntrinsicHeight과 IntrinsicWidth는 RenderBox를 상속받아 별도로 오버라이딩하지 않는다. 즉 자식 위젯의 크기에 맞춰 자신의 intrinsic 크기를 결정한다. 특이한점이 있다면 computeDryLayout에서 자신의 사이즈 크기를 자식의 위젯 크기가 아니라 부모의 constraints의 maxWidth, maxHeight로 정의했다.

Dart
class RenderConstrainedOverflowBox extends RenderAligningShiftedBox { /// Creates a render object that lets its child overflow itself. RenderConstrainedOverflowBox({ super.child, double? minWidth, double? maxWidth, double? minHeight, double? maxHeight, super.alignment, super.textDirection, } Size computeDryLayout(BoxConstraints constraints) { return constraints.biggest; } void performLayout() { if (child != null) { child?.layout(_getInnerConstraints(constraints), parentUsesSize: true); alignChild(); } } BoxConstraints _getInnerConstraints(BoxConstraints constraints) { return BoxConstraints( minWidth: _minWidth ?? constraints.minWidth, maxWidth: _maxWidth ?? constraints.maxWidth, minHeight: _minHeight ?? constraints.minHeight, maxHeight: _maxHeight ?? constraints.maxHeight, ); } }

 

OverflowSizedBox

아직 작성 못했습니다 ㅎ..

마치며

이번에는 constraints와 관련된 다양한 위젯들에 대해 알아보았습니다. 언뜻 봤을때는 모두 ConstrainedBoxTransform으로 대체가능할줄 알았는데 세부 동작들을 살펴보니 각각이 별개의 동작을 하고 있었습니다. Flutterjs를 구현할때도 이점을 유의해서 구현했습니다.

특히 alignment를 prop으로 받는 위젯들을 단순히 Container처럼 Align 위젯으로 덮어서 만들라고 했다가 큰일날뻔 했습니다. Align위젯의 RenderObject는 RenderAligningShiftedBox가 아니라 그걸 상속받은 RenderPositionedBox 더라구요. 동작은 자식 위젯을 정렬하는 것 이외에 부모의 constraints를 loosen() 하는 역할도 수행하고 있었습니다.

이런 차이를 알게되니 Container에서 alignment의 유무로 자식 위젯이 팽창하거나 안하거나 하는 동작의 이유를 알게되었습니다. 이부분은 Container 를 알아볼때 더 자세하게 이야기 하도록 하겠습니다 😊

 

참고자료

관심 가질만한 포스트