개요
AnimatedPadding은 위젯의 패딩(padding)이 변경될 때 자동으로 애니메이션을 적용하여 부드럽게 전환하는 위젯입니다. Flutter의 AnimatedPadding 위젯에서 영감을 받아 구현되었습니다.
Animated version of Padding which automatically transitions the indentation over a given duration whenever the given inset changes.
Flutter 참조: https://api.flutter.dev/flutter/widgets/AnimatedPadding-class.html
언제 사용하나요?
- 사용자 인터랙션에 따라 위젯의 여백을 동적으로 조절할 때
- 카드나 컨테이너의 확장/축소 시 내부 여백을 부드럽게 변경할 때
- 비번박 리스트에서 아이템 간격을 동적으로 조절할 때
- 반응형 디자인에서 화면 크기에 따라 여백을 조정할 때
- 폴더블 컨텐츠의 여백을 애니메이션으로 전환할 때
기본 사용법
import { AnimatedPadding, Container, EdgeInsets, StatefulWidget } from '@meursyphus/flitter';
class PaddingExample extends StatefulWidget {
createState() {
return new PaddingExampleState();
}
}
class PaddingExampleState extends State<PaddingExample> {
isExpanded = false;
build() {
return Column({
children: [
ElevatedButton({
onPressed: () => {
this.setState(() => {
this.isExpanded = !this.isExpanded;
});
},
child: Text(this.isExpanded ? "축소" : "확대"),
}),
Container({
width: 300,
height: 300,
color: "lightgray",
child: AnimatedPadding({
padding: this.isExpanded
? EdgeInsets.all(50)
: EdgeInsets.all(10),
duration: 500, // 0.5초
child: Container({
color: "blue",
child: Center({
child: Text("애니메이션 패딩", {
style: TextStyle({ color: "white" })
}),
}),
}),
}),
}),
],
});
}
}
Props
duration (필수)
값: number
애니메이션 지속 시간을 밀리초 단위로 지정합니다.
padding (선택)
값: EdgeInsetsGeometry (기본값: EdgeInsets.all(0))
위젯의 내부 여백을 설정합니다. 사용 가능한 메서드:
EdgeInsets 메서드
EdgeInsets.all(value)
: 모든 방향에 동일한 여백EdgeInsets.symmetric({ horizontal, vertical })
: 가로/세로 대칭 여백EdgeInsets.only({ top, bottom, left, right })
: 각 방향별 여백 지정EdgeInsets.fromLTRB({ left, top, right, bottom })
: 좌/상/우/하 순서로 지정
사용 예시
// 모든 방향 20의 여백
EdgeInsets.all(20)
// 가로 16, 세로 8의 여백
EdgeInsets.symmetric({ horizontal: 16, vertical: 8 })
// 각 방향별 다른 여백
EdgeInsets.only({ top: 10, left: 20, right: 20, bottom: 30 })
// LTRB 순서로 지정
EdgeInsets.fromLTRB({ left: 10, top: 20, right: 10, bottom: 20 })
curve (선택)
값: Curve (기본값: Curves.linear)
애니메이션의 진행 곡선을 지정합니다. 사용 가능한 곡선:
Curves.linear
: 일정한 속도Curves.easeIn
: 천천히 시작Curves.easeOut
: 천천히 종료Curves.easeInOut
: 천천히 시작하고 종료Curves.circIn
: 원형 가속 시작Curves.circOut
: 원형 감속 종료Curves.circInOut
: 원형 가속/감속Curves.backIn
: 뒤로 갔다가 시작Curves.backOut
: 목표를 지나쳤다가 돌아옴Curves.backInOut
: backIn + backOutCurves.anticipate
: 예비 동작 후 진행Curves.bounceIn
: 바운스하며 시작Curves.bounceOut
: 바운스하며 종료Curves.bounceInOut
: 바운스 시작/종료
child (선택)
값: Widget | undefined
패딩이 적용될 자식 위젯입니다.
key (선택)
값: any
위젯의 고유 식별자입니다.
실제 사용 예제
예제 1: 폴더블 카드
import { AnimatedPadding, Container, Card, EdgeInsets, Curves } from '@meursyphus/flitter';
class FoldableCard extends StatefulWidget {
createState() {
return new FoldableCardState();
}
}
class FoldableCardState extends State<FoldableCard> {
isExpanded = false;
build() {
return Card({
child: Column({
children: [
// 헤더
ListTile({
title: Text("폴더블 카드"),
trailing: Icon(
this.isExpanded ? Icons.expand_less : Icons.expand_more
),
onTap: () => {
this.setState(() => {
this.isExpanded = !this.isExpanded;
});
},
}),
// 확장 가능한 컨텐츠
AnimatedPadding({
padding: this.isExpanded
? EdgeInsets.fromLTRB({ left: 16, top: 0, right: 16, bottom: 16 })
: EdgeInsets.all(0),
duration: 300,
curve: Curves.easeOut,
child: this.isExpanded ? Column({
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"자세한 내용",
{ style: TextStyle({ fontWeight: "bold", fontSize: 16 }) }
),
SizedBox({ height: 8 }),
Text(
"이 카드는 클릭하면 확장되며, "
"내부 컨텐츠의 패딩이 애니메이션되어 "
"부드럽게 나타납니다."
),
SizedBox({ height: 12 }),
Row({
children: [
ElevatedButton({
onPressed: () => {},
child: Text("액션 1"),
}),
SizedBox({ width: 8 }),
OutlinedButton({
onPressed: () => {},
child: Text("액션 2"),
}),
],
}),
],
}) : SizedBox.shrink(),
}),
],
}),
});
}
}
예제 2: 반응형 마진
import { AnimatedPadding, Container, EdgeInsets, Curves } from '@meursyphus/flitter';
class ResponsiveMargin extends StatefulWidget {
createState() {
return new ResponsiveMarginState();
}
}
class ResponsiveMarginState extends State<ResponsiveMargin> {
screenSize = "mobile"; // mobile, tablet, desktop
get currentPadding() {
switch (this.screenSize) {
case "mobile":
return EdgeInsets.all(8);
case "tablet":
return EdgeInsets.all(16);
case "desktop":
return EdgeInsets.all(32);
default:
return EdgeInsets.all(8);
}
}
build() {
return Column({
children: [
// 화면 크기 선택
Row({
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton({
onPressed: () => this.changeScreenSize("mobile"),
child: Text("모바일"),
style: ButtonStyle({
backgroundColor: this.screenSize === "mobile" ? "blue" : "gray",
}),
}),
SizedBox({ width: 8 }),
ElevatedButton({
onPressed: () => this.changeScreenSize("tablet"),
child: Text("태블릿"),
style: ButtonStyle({
backgroundColor: this.screenSize === "tablet" ? "blue" : "gray",
}),
}),
SizedBox({ width: 8 }),
ElevatedButton({
onPressed: () => this.changeScreenSize("desktop"),
child: Text("데스크탑"),
style: ButtonStyle({
backgroundColor: this.screenSize === "desktop" ? "blue" : "gray",
}),
}),
],
}),
SizedBox({ height: 20 }),
// 반응형 컨테이너
Container({
width: 400,
height: 300,
color: "#f0f0f0",
child: AnimatedPadding({
padding: this.currentPadding,
duration: 400,
curve: Curves.easeInOut,
child: Container({
decoration: BoxDecoration({
color: "white",
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow({
color: "rgba(0,0,0,0.1)",
blurRadius: 4,
offset: Offset({ x: 0, y: 2 }),
}),
],
}),
child: Center({
child: Column({
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
`현재 모드: ${this.screenSize}`,
{ style: TextStyle({ fontSize: 18, fontWeight: "bold" }) }
),
SizedBox({ height: 8 }),
Text(
`패딩: ${this.currentPadding.top}px`,
{ style: TextStyle({ color: "gray" }) }
),
],
}),
}),
}),
}),
}),
],
});
}
changeScreenSize(size: string) {
this.setState(() => {
this.screenSize = size;
});
}
}
예제 3: 리스트 아이템 간격 조절
import { AnimatedPadding, Container, ListView, EdgeInsets, Curves } from '@meursyphus/flitter';
class DynamicListSpacing extends StatefulWidget {
createState() {
return new DynamicListSpacingState();
}
}
class DynamicListSpacingState extends State<DynamicListSpacing> {
isCompact = false;
items = [
{ title: "첫 번째 아이템", color: "#FF6B6B" },
{ title: "두 번째 아이템", color: "#4ECDC4" },
{ title: "세 번째 아이템", color: "#45B7D1" },
{ title: "네 번째 아이템", color: "#96CEB4" },
{ title: "다섯 번째 아이템", color: "#FFEAA7" },
];
build() {
return Column({
children: [
// 밀도 컨트롤
Padding({
padding: EdgeInsets.all(16),
child: Row({
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"리스트 밀도",
{ style: TextStyle({ fontSize: 18, fontWeight: "bold" }) }
),
Switch({
value: this.isCompact,
onChanged: (value) => {
this.setState(() => {
this.isCompact = value;
});
},
}),
],
}),
}),
// 리스트
Expanded({
child: ListView({
children: this.items.map((item, index) => {
return AnimatedPadding({
key: ValueKey(index),
padding: this.isCompact
? EdgeInsets.symmetric({ horizontal: 16, vertical: 4 })
: EdgeInsets.symmetric({ horizontal: 16, vertical: 8 }),
duration: 200,
curve: Curves.easeOut,
child: Container({
height: this.isCompact ? 60 : 80,
decoration: BoxDecoration({
color: item.color,
borderRadius: BorderRadius.circular(8),
}),
child: Center({
child: Text(
item.title,
{ style: TextStyle({ color: "white", fontSize: 16 }) }
),
}),
}),
});
}),
}),
}),
],
});
}
}
예제 4: 비대칭 패딩 애니메이션
import { AnimatedPadding, Container, EdgeInsets, Curves } from '@meursyphus/flitter';
class AsymmetricPadding extends StatefulWidget {
createState() {
return new AsymmetricPaddingState();
}
}
class AsymmetricPaddingState extends State<AsymmetricPadding> {
paddingIndex = 0;
paddingConfigs = [
EdgeInsets.all(20),
EdgeInsets.only({ left: 50, right: 10, top: 20, bottom: 20 }),
EdgeInsets.only({ left: 10, right: 50, top: 40, bottom: 10 }),
EdgeInsets.symmetric({ horizontal: 30, vertical: 10 }),
EdgeInsets.fromLTRB({ left: 60, top: 10, right: 20, bottom: 40 }),
];
build() {
const currentPadding = this.paddingConfigs[this.paddingIndex];
return GestureDetector({
onTap: () => this.nextPadding(),
child: Container({
width: 350,
height: 250,
color: "#2C3E50",
child: AnimatedPadding({
padding: currentPadding,
duration: 800,
curve: Curves.backOut,
child: Container({
decoration: BoxDecoration({
gradient: LinearGradient({
colors: ["#74b9ff", "#0984e3"],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
}),
borderRadius: BorderRadius.circular(12),
}),
child: Center({
child: Column({
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"비대칭 패딩",
{ style: TextStyle({ color: "white", fontSize: 20, fontWeight: "bold" }) }
),
SizedBox({ height: 8 }),
Text(
`L:${currentPadding.left} T:${currentPadding.top} R:${currentPadding.right} B:${currentPadding.bottom}`,
{ style: TextStyle({ color: "white", fontSize: 14 }) }
),
SizedBox({ height: 16 }),
Text(
"탭하여 다음 패딩",
{ style: TextStyle({ color: "white", fontSize: 12, opacity: 0.8 }) }
),
],
}),
}),
}),
}),
}),
});
}
nextPadding() {
this.setState(() => {
this.paddingIndex = (this.paddingIndex + 1) % this.paddingConfigs.length;
});
}
}
주의사항
- 패딩 값은 음수가 될 수 없습니다
- EdgeInsetsGeometry의 모든 속성이 동시에 애니메이션됩니다
- 너무 큰 패딩 값은 성능에 영향을 줄 수 있습니다
- 애니메이션 도중 새로운 패딩 값이 설정되면 현재 값에서 새 값으로 부드럽게 전환됩니다
- 자식 위젯이 없어도 패딩 공간은 유지됩니다
관련 위젯
- Padding: 애니메이션 없이 패딩을 설정하는 기본 위젯
- AnimatedContainer: 여러 속성을 동시에 애니메이션으로 전환
- AnimatedAlign: 정렬을 애니메이션으로 전환
- Container: 패딩과 마진을 동시에 설정할 수 있는 다용도 위젯