개요
ClipRect는 사각형 영역으로 자식 위젯을 클리핑하는 위젯입니다.
이 위젯은 clipper 콜백 함수를 받아 Rect 객체를 반환하도록 하여, 자식 위젯을 원하는 사각형 영역으로 자를 수 있습니다. 내부적으로는 ClipPath를 사용하여 클리핑을 수행합니다.
참조: https://api.flutter.dev/flutter/widgets/ClipRect-class.html
언제 사용하나요?
- 위젯의 일부분만 보여주고 싶을 때
- 오버플로우되는 콘텐츠를 숨기고 싶을 때
- 스크롤 가능한 영역의 경계를 정의할 때
- 이미지나 비디오의 특정 영역만 표시할 때
- 사용자 정의 크롭(crop) 기능을 구현할 때
기본 사용법
// 전체 영역 클리핑 (기본)
ClipRect({
clipper: (size) => Rect.fromLTWH({
left: 0,
top: 0,
width: size.width,
height: size.height
}),
child: Image({
src: 'https://example.com/large-image.jpg',
width: 400,
height: 400
})
})
// 중앙 부분만 클리핑
ClipRect({
clipper: (size) => Rect.fromCenter({
center: { x: size.width / 2, y: size.height / 2 },
width: size.width * 0.5,
height: size.height * 0.5
}),
child: Container({
width: 200,
height: 200,
color: 'blue'
})
})
// 상단 절반만 표시
ClipRect({
clipper: (size) => Rect.fromLTRB({
left: 0,
top: 0,
right: size.width,
bottom: size.height / 2
}),
child: child
})
Props 상세 설명
clipper (필수)
값: (size: Size) => Rect
클리핑 영역을 정의하는 콜백 함수입니다. 위젯의 크기(Size)를 매개변수로 받아 Rect 객체를 반환해야 합니다.
// 다양한 클리핑 패턴 예제
ClipRect({
// 왼쪽 상단 1/4 영역만
clipper: (size) => Rect.fromLTWH({
left: 0,
top: 0,
width: size.width / 2,
height: size.height / 2
}),
child: child
})
// 가로 중앙 스트립
ClipRect({
clipper: (size) => Rect.fromLTRB({
left: 0,
top: size.height * 0.25,
right: size.width,
bottom: size.height * 0.75
}),
child: child
})
// 여백을 둔 클리핑
ClipRect({
clipper: (size) => Rect.fromLTWH({
left: 20,
top: 20,
width: size.width - 40,
height: size.height - 40
}),
child: child
})
clipped
값: boolean (기본값: true)
클리핑 효과를 활성화/비활성화합니다.
true
: 클리핑 적용false
: 클리핑 비활성화 (자식 위젯이 정상적으로 표시됨)
ClipRect({
clipped: isClippingEnabled,
clipper: (size) => Rect.fromLTWH({
left: 0,
top: 0,
width: size.width,
height: size.height
}),
child: content
})
child
값: Widget
클리핑될 자식 위젯입니다.
Rect API 이해하기
Rect 생성 메서드
// 왼쪽, 상단, 너비, 높이로 생성
Rect.fromLTWH({
left: 10,
top: 10,
width: 100,
height: 100
});
// 왼쪽, 상단, 오른쪽, 하단 좌표로 생성
Rect.fromLTRB({
left: 10,
top: 10,
right: 110,
bottom: 110
});
// 중심점과 크기로 생성
Rect.fromCenter({
center: { x: 60, y: 60 },
width: 100,
height: 100
});
// 원을 감싸는 사각형
Rect.fromCircle({
center: { x: 50, y: 50 },
radius: 50
});
// 두 점으로 생성
Rect.fromPoints(
{ x: 10, y: 10 }, // 첫 번째 점
{ x: 110, y: 110 } // 두 번째 점
);
실제 사용 예제
예제 1: 이미지 크롭 도구
class ImageCropper extends StatefulWidget {
imageUrl: string;
constructor({ imageUrl }: { imageUrl: string }) {
super();
this.imageUrl = imageUrl;
}
createState(): State<ImageCropper> {
return new ImageCropperState();
}
}
class ImageCropperState extends State<ImageCropper> {
cropRect = {
x: 0,
y: 0,
width: 200,
height: 200
};
build(): Widget {
return Container({
width: 400,
height: 400,
child: Stack({
children: [
// 원본 이미지 (어둡게)
Opacity({
opacity: 0.3,
child: Image({
src: this.widget.imageUrl,
width: 400,
height: 400,
objectFit: 'cover'
})
}),
// 크롭된 영역
Positioned({
left: this.cropRect.x,
top: this.cropRect.y,
child: ClipRect({
clipper: (size) => Rect.fromLTWH({
left: 0,
top: 0,
width: this.cropRect.width,
height: this.cropRect.height
}),
child: Transform.translate({
offset: { x: -this.cropRect.x, y: -this.cropRect.y },
child: Image({
src: this.widget.imageUrl,
width: 400,
height: 400,
objectFit: 'cover'
})
})
})
}),
// 크롭 영역 테두리
Positioned({
left: this.cropRect.x,
top: this.cropRect.y,
child: Container({
width: this.cropRect.width,
height: this.cropRect.height,
decoration: BoxDecoration({
border: Border.all({
color: 'white',
width: 2
})
})
})
})
]
})
});
}
}
예제 2: 텍스트 오버플로우 처리
function TextPreview({ text, maxLines = 3, lineHeight = 24 }): Widget {
const maxHeight = maxLines * lineHeight;
return Container({
width: 300,
child: Stack({
children: [
ClipRect({
clipper: (size) => Rect.fromLTWH({
left: 0,
top: 0,
width: size.width,
height: Math.min(size.height, maxHeight)
}),
child: Text(text, {
style: TextStyle({
fontSize: 16,
lineHeight: lineHeight
})
})
}),
// 페이드 아웃 효과
Positioned({
bottom: 0,
left: 0,
right: 0,
child: Container({
height: 30,
decoration: BoxDecoration({
gradient: LinearGradient({
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: ['rgba(255,255,255,0)', 'rgba(255,255,255,1)']
})
})
})
})
]
})
});
};
예제 3: 진행률 표시기
function ProgressBar({ progress, height = 20 }): Widget {
return Container({
width: 300,
height: height,
decoration: BoxDecoration({
borderRadius: BorderRadius.circular(height / 2),
color: '#E0E0E0'
}),
child: ClipRRect({
borderRadius: BorderRadius.circular(height / 2),
child: Stack({
children: [
// 진행률 바
ClipRect({
clipper: (size) => Rect.fromLTWH({
left: 0,
top: 0,
width: size.width * progress,
height: size.height
}),
child: Container({
decoration: BoxDecoration({
gradient: LinearGradient({
colors: ['#4CAF50', '#8BC34A'],
begin: Alignment.centerLeft,
end: Alignment.centerRight
})
})
})
}),
// 텍스트
Center({
child: Text(`${Math.round(progress * 100)}%`, {
style: TextStyle({
color: progress > 0.5 ? 'white' : 'black',
fontWeight: 'bold',
fontSize: 12
})
})
})
]
})
})
});
};
예제 4: 뷰포트 시뮬레이션
function ViewportSimulator({ content, viewportSize, scrollOffset }): Widget {
return Container({
width: viewportSize.width,
height: viewportSize.height,
decoration: BoxDecoration({
border: Border.all({
color: '#333',
width: 2
})
}),
child: ClipRect({
clipper: (size) => Rect.fromLTWH({
left: 0,
top: 0,
width: size.width,
height: size.height
}),
child: Transform.translate({
offset: {
x: -scrollOffset.x,
y: -scrollOffset.y
},
child: content
})
})
});
};
// 사용 예
ViewportSimulator({
viewportSize: { width: 300, height: 400 },
scrollOffset: { x: 0, y: 100 },
content: Container({
width: 300,
height: 1000,
child: Column({
children: Array.from({ length: 20 }, (_, i) =>
Container({
height: 50,
margin: EdgeInsets.all(5),
color: i % 2 === 0 ? '#E3F2FD' : '#BBDEFB',
child: Center({
child: Text(`Item ${i + 1}`)
})
})
)
})
})
});
예제 5: 이미지 비교 슬라이더
class ImageComparisonSlider extends StatefulWidget {
beforeImage: string;
afterImage: string;
constructor({ beforeImage, afterImage }: { beforeImage: string; afterImage: string }) {
super();
this.beforeImage = beforeImage;
this.afterImage = afterImage;
}
createState(): State<ImageComparisonSlider> {
return new ImageComparisonSliderState();
}
}
class ImageComparisonSliderState extends State<ImageComparisonSlider> {
sliderPosition = 0.5;
build(): Widget {
return GestureDetector({
onHorizontalDragUpdate: (details) => {
const newPosition = Math.max(0, Math.min(1,
details.localPosition.x / 400
));
this.setState(() => {
this.sliderPosition = newPosition;
});
},
child: Container({
width: 400,
height: 300,
child: Stack({
children: [
// After 이미지 (전체)
Image({
src: this.widget.afterImage,
width: 400,
height: 300,
objectFit: 'cover'
}),
// Before 이미지 (클리핑)
ClipRect({
clipper: (size) => Rect.fromLTWH({
left: 0,
top: 0,
width: size.width * this.sliderPosition,
height: size.height
}),
child: Image({
src: this.widget.beforeImage,
width: 400,
height: 300,
objectFit: 'cover'
})
}),
// 슬라이더 라인
Positioned({
left: 400 * this.sliderPosition - 2,
top: 0,
bottom: 0,
child: Container({
width: 4,
color: 'white',
child: Center({
child: Container({
width: 40,
height: 40,
decoration: BoxDecoration({
color: 'white',
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow({
color: 'rgba(0,0,0,0.3)',
blurRadius: 4,
offset: { x: 0, y: 2 }
})
]
}),
child: Center({
child: Icon({
icon: Icons.dragHandle,
color: '#666'
})
})
})
})
})
})
]
})
})
});
}
}
주의사항
- ClipRect는 렌더링 성능에 영향을 줄 수 있으므로 필요한 경우에만 사용하세요
- 클리핑된 영역 밖의 터치 이벤트는 감지되지 않습니다
- clipper 함수는 위젯 크기가 변경될 때마다 호출되므로 복잡한 계산은 피하세요
- 애니메이션과 함께 사용할 때는 성능 최적화를 고려하세요
- 중첩된 클리핑은 성능 문제를 일으킬 수 있으므로 주의하세요
관련 위젯
- ClipOval: 타원형으로 클리핑
- ClipRRect: 둥근 모서리 사각형으로 클리핑
- ClipPath: 사용자 정의 경로로 클리핑
- CustomClipper: 사용자 정의 클리핑 로직 구현
- Viewport: 스크롤 가능한 영역 정의