개요
ClipRRect는 둥근 모서리 사각형(rounded rectangle)을 사용하여 자식 위젯을 클리핑하는 위젯입니다.
기본적으로 ClipRRect는 자체 경계를 기본 사각형으로 사용하지만, 사용자 정의 clipper를 사용하여 클립의 크기와 위치를 커스터마이징할 수 있습니다. borderRadius 속성을 통해 각 모서리의 둥근 정도를 개별적으로 설정할 수 있습니다.
참조: https://api.flutter.dev/flutter/widgets/ClipRRect-class.html
언제 사용하나요?
- 이미지나 콘테이너에 둥근 모서리를 적용하고 싶을 때
- 카드 UI나 버튼에 부드러운 모서리 효과를 주고 싶을 때
- 오버플로우되는 콘텐츠를 둥근 모서리로 자르고 싶을 때
- 각 모서리의 둥근 정도를 다르게 설정하고 싶을 때
- 프로필 이미지나 썸네일에 둥근 효과를 적용할 때
기본 사용법
// 모든 모서리가 동일한 반경
ClipRRect({
borderRadius: BorderRadius.circular(16),
child: Image({
src: 'https://example.com/image.jpg',
width: 200,
height: 200,
objectFit: 'cover'
})
})
// 특정 모서리만 둥글게
ClipRRect({
borderRadius: BorderRadius.only({
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
bottomLeft: Radius.zero,
bottomRight: Radius.zero
}),
child: Container({
width: 300,
height: 200,
color: 'blue'
})
})
// 타원형 모서리
ClipRRect({
borderRadius: BorderRadius.all(Radius.elliptical(40, 20)),
child: Container({
width: 150,
height: 100,
color: 'green'
})
})
Props 상세 설명
borderRadius
값: BorderRadius (기본값: BorderRadius.zero)
각 모서리의 둥근 정도를 정의합니다. BorderRadius 클래스는 다양한 팩토리 메서드를 제공합니다:
BorderRadius.circular(radius)
: 모든 모서리에 동일한 원형 반경 적용BorderRadius.all(Radius)
: 모든 모서리에 동일한 Radius 적용BorderRadius.only({topLeft?, topRight?, bottomLeft?, bottomRight?})
: 각 모서리 개별 설정BorderRadius.vertical({top?, bottom?})
: 상단과 하단 모서리 설정BorderRadius.horizontal({left?, right?})
: 좌측과 우측 모서리 설정
// 원형 반경
ClipRRect({
borderRadius: BorderRadius.circular(20),
child: child
})
// 타원형 반경
ClipRRect({
borderRadius: BorderRadius.all(Radius.elliptical(30, 15)),
child: child
})
// 개별 모서리 설정
ClipRRect({
borderRadius: BorderRadius.only({
topLeft: Radius.circular(30),
topRight: Radius.circular(10),
bottomLeft: Radius.circular(5),
bottomRight: Radius.circular(20)
}),
child: child
})
clipper
값: (size: Size) => RRect (선택)
사용자 정의 클리핑 영역을 정의하는 콜백 함수입니다. 위젯의 크기를 받아 RRect(둥근 사각형) 객체를 반환해야 합니다.
ClipRRect({
clipper: (size) => {
// 중앙에 작은 둥근 사각형 클리핑
return RRect.fromRectAndRadius({
rect: Rect.fromCenter({
center: { x: size.width / 2, y: size.height / 2 },
width: size.width * 0.8,
height: size.height * 0.8
}),
radius: Radius.circular(10)
});
},
child: child
})
clipped
값: boolean (기본값: true)
클리핑 효과를 활성화/비활성화합니다.
true
: 클리핑 적용false
: 클리핑 비활성화 (자식 위젯이 정상적으로 표시됨)
ClipRRect({
clipped: isClippingEnabled,
borderRadius: BorderRadius.circular(16),
child: content
})
child
값: Widget
클리핑될 자식 위젯입니다.
실제 사용 예제
예제 1: 카드 UI 구성
const CardWithImage = ({ image, title, description, onTap }) => {
return GestureDetector({
onTap: onTap,
child: Container({
width: 320,
margin: EdgeInsets.all(16),
decoration: BoxDecoration({
color: 'white',
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow({
color: 'rgba(0,0,0,0.1)',
blurRadius: 10,
offset: { x: 0, y: 4 }
})
]
}),
child: Column({
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 상단 이미지
ClipRRect({
borderRadius: BorderRadius.only({
topLeft: Radius.circular(16),
topRight: Radius.circular(16)
}),
child: Image({
src: image,
width: 320,
height: 180,
objectFit: 'cover'
})
}),
// 콘텐츠 영역
Padding({
padding: EdgeInsets.all(16),
child: Column({
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, {
style: TextStyle({
fontSize: 20,
fontWeight: 'bold'
})
}),
SizedBox({ height: 8 }),
Text(description, {
style: TextStyle({
fontSize: 14,
color: '#666'
})
})
]
})
})
]
})
})
});
};
예제 2: 프로필 아바타 변형
const ProfileAvatar = ({ imageUrl, size, status }) => {
const getStatusColor = () => {
switch (status) {
case 'online': return '#4CAF50';
case 'away': return '#FF9800';
case 'busy': return '#F44336';
default: return '#9E9E9E';
}
};
return Stack({
children: [
// 프로필 이미지
Container({
width: size,
height: size,
decoration: BoxDecoration({
border: Border.all({
color: getStatusColor(),
width: 3
}),
borderRadius: BorderRadius.circular(size * 0.25)
}),
padding: EdgeInsets.all(3),
child: ClipRRect({
borderRadius: BorderRadius.circular(size * 0.25 - 3),
child: Image({
src: imageUrl,
width: size - 6,
height: size - 6,
objectFit: 'cover'
})
})
}),
// 상태 표시기
Positioned({
right: 0,
bottom: 0,
child: Container({
width: size * 0.3,
height: size * 0.3,
decoration: BoxDecoration({
color: getStatusColor(),
borderRadius: BorderRadius.circular(size * 0.15),
border: Border.all({
color: 'white',
width: 2
})
})
})
})
]
});
};
// 사용 예
ProfileAvatar({
imageUrl: 'https://example.com/avatar.jpg',
size: 80,
status: 'online'
});
예제 3: 메시지 버블
const MessageBubble = ({ message, isMe, time, hasImage }) => {
return Container({
margin: EdgeInsets.symmetric({ horizontal: 16, vertical: 4 }),
alignment: isMe ? Alignment.centerRight : Alignment.centerLeft,
child: Container({
constraints: BoxConstraints({ maxWidth: 280 }),
child: Column({
crossAxisAlignment: isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [
ClipRRect({
borderRadius: BorderRadius.only({
topLeft: Radius.circular(isMe ? 16 : 4),
topRight: Radius.circular(isMe ? 4 : 16),
bottomLeft: Radius.circular(16),
bottomRight: Radius.circular(16)
}),
child: Container({
color: isMe ? '#007AFF' : '#E5E5EA',
child: Column({
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (hasImage)
ClipRRect({
borderRadius: BorderRadius.only({
topLeft: Radius.circular(isMe ? 12 : 0),
topRight: Radius.circular(isMe ? 0 : 12)
}),
child: Image({
src: hasImage,
width: 200,
height: 150,
objectFit: 'cover'
})
}),
Padding({
padding: EdgeInsets.all(12),
child: Text(message, {
style: TextStyle({
color: isMe ? 'white' : 'black',
fontSize: 16
})
})
})
]
})
})
}),
SizedBox({ height: 4 }),
Text(time, {
style: TextStyle({
fontSize: 12,
color: '#666'
})
})
]
})
})
});
};
예제 4: 그라데이션 버튼
const GradientButton = ({ text, onPressed, disabled = false }) => {
return GestureDetector({
onTap: disabled ? null : onPressed,
child: Opacity({
opacity: disabled ? 0.5 : 1.0,
child: ClipRRect({
borderRadius: BorderRadius.circular(30),
child: Container({
width: 200,
height: 60,
decoration: BoxDecoration({
gradient: LinearGradient({
colors: ['#667eea', '#764ba2'],
begin: Alignment.topLeft,
end: Alignment.bottomRight
})
}),
child: Stack({
children: [
// 호버 효과를 위한 오버레이
AnimatedContainer({
duration: Duration.milliseconds(200),
decoration: BoxDecoration({
color: 'rgba(255,255,255,0.1)'
})
}),
// 텍스트
Center({
child: Text(text, {
style: TextStyle({
color: 'white',
fontSize: 18,
fontWeight: 'bold'
})
})
})
]
})
})
})
})
});
};
예제 5: 이미지 갤러리 썸네일
const GalleryThumbnail = ({ images, title }) => {
const gridSize = Math.ceil(Math.sqrt(images.length));
return Column({
children: [
ClipRRect({
borderRadius: BorderRadius.circular(12),
child: Container({
width: 200,
height: 200,
color: '#f0f0f0',
child: images.length === 1
? Image({
src: images[0],
width: 200,
height: 200,
objectFit: 'cover'
})
: Grid({
crossAxisCount: gridSize,
gap: 2,
children: images.slice(0, gridSize * gridSize).map((img, index) =>
ClipRRect({
borderRadius: BorderRadius.circular(
index === 0 ? 12 : 0
),
child: Image({
src: img,
width: 200 / gridSize - 2,
height: 200 / gridSize - 2,
objectFit: 'cover'
})
})
)
})
})
}),
SizedBox({ height: 8 }),
Container({
width: 200,
child: Row({
children: [
Expanded({
child: Text(title, {
style: TextStyle({
fontSize: 14,
fontWeight: 'bold'
}),
overflow: TextOverflow.ellipsis
})
}),
if (images.length > gridSize * gridSize)
Container({
padding: EdgeInsets.symmetric({ horizontal: 8, vertical: 2 }),
decoration: BoxDecoration({
color: 'rgba(0,0,0,0.6)',
borderRadius: BorderRadius.circular(12)
}),
child: Text(`+${images.length - gridSize * gridSize}`, {
style: TextStyle({
color: 'white',
fontSize: 12
})
})
})
]
})
})
]
});
};
RRect API 이해하기
RRect 생성 메서드
// 기본 생성자들
RRect.fromLTRBR({
left: 0,
top: 0,
right: 100,
bottom: 100,
radius: Radius.circular(10)
});
RRect.fromRectAndRadius({
rect: Rect.fromLTWH({ left: 0, top: 0, width: 100, height: 100 }),
radius: Radius.circular(10)
});
RRect.fromRectAndCorners({
rect: rect,
topLeft: Radius.circular(10),
topRight: Radius.circular(20),
bottomLeft: Radius.circular(5),
bottomRight: Radius.circular(15)
});
// 확대/축소
const inflatedRRect = rrect.inflate(10); // 10픽셀 확대
const deflatedRRect = rrect.deflate(5); // 5픽셀 축소
주의사항
- 클리핑은 렌더링 성능에 영향을 줄 수 있으므로 필요한 경우에만 사용하세요
- borderRadius 값이 위젯 크기보다 크면 예상치 못한 결과가 발생할 수 있습니다
- 클리핑된 영역 밖의 터치 이벤트는 감지되지 않습니다
- 복잡한 레이아웃에서는 Container의 decoration과 ClipRRect를 함께 사용할 때 주의하세요
- 이미지 로딩 중에는 플레이스홀더를 사용하여 레이아웃 이동을 방지하세요
관련 위젯
- ClipRect: 사각형으로 클리핑
- ClipOval: 타원형으로 클리핑
- ClipPath: 사용자 정의 경로로 클리핑
- Container: decoration 속성으로 둥근 모서리 구현 가능
- Card: 기본적으로 둥근 모서리를 가진 머티리얼 디자인 카드