개요

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 + backOut
  • Curves.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: 패딩과 마진을 동시에 설정할 수 있는 다용도 위젯