개요

AnimatedFractionallySizedBox는 부모 위젯의 크기에 비례하여 자식 위젯의 크기를 설정하고, widthFactor나 heightFactor가 변경될 때 자동으로 애니메이션을 적용하는 위젯입니다. alignment가 변경될 때도 위치가 애니메이션됩니다. Flutter의 AnimatedFractionallySizedBox 위젯에서 영감을 받아 구현되었습니다.

Animated version of FractionallySizedBox which automatically transitions the child’s size over a given duration whenever the given widthFactor or heightFactor changes, as well as the position whenever the given alignment changes.

Flutter 참조: https://api.flutter.dev/flutter/widgets/AnimatedFractionallySizedBox-class.html

언제 사용하나요?

  • 부모 크기에 비례하여 위젯 크기를 동적으로 조절할 때
  • 반응형 레이아웃에서 크기를 부드럽게 전환할 때
  • 화면 크기에 따라 콘텐츠의 크기를 애니메이션으로 조정할 때
  • 확대/축소 효과를 부드럽게 구현할 때
  • 모달이나 다이얼로그의 크기를 단계적으로 전환할 때

기본 사용법

import { AnimatedFractionallySizedBox, Container, StatefulWidget } from '@meursyphus/flitter';

class SizeAnimationExample extends StatefulWidget {
  createState() {
    return new SizeAnimationExampleState();
  }
}

class SizeAnimationExampleState extends State<SizeAnimationExample> {
  isExpanded = false;

  build() {
    return Container({
      width: 300,
      height: 300,
      color: "lightgray",
      child: GestureDetector({
        onTap: () => {
          this.setState(() => {
            this.isExpanded = !this.isExpanded;
          });
        },
        child: AnimatedFractionallySizedBox({
          widthFactor: this.isExpanded ? 0.8 : 0.4,
          heightFactor: this.isExpanded ? 0.8 : 0.4,
          duration: 1000, // 1초
          child: Container({
            color: "blue",
          }),
        }),
      }),
    });
  }
}

Props

duration (필수)

값: number

애니메이션 지속 시간을 밀리초 단위로 지정합니다.

widthFactor (선택)

값: number | undefined

부모 위젯 너비에 대한 비율로 자식 위젯의 너비를 설정합니다.

  • 0.5 = 부모 너비의 50%
  • 1.0 = 부모 너비의 100%
  • 지정하지 않으면 자식의 고유 너비 사용

heightFactor (선택)

값: number | undefined

부모 위젯 높이에 대한 비율로 자식 위젯의 높이를 설정합니다.

  • 0.5 = 부모 높이의 50%
  • 1.0 = 부모 높이의 100%
  • 지정하지 않으면 자식의 고유 높이 사용

alignment (선택)

값: Alignment (기본값: Alignment.center)

자식 위젯의 정렬 위치를 지정합니다. 사전 정의된 값들:

  • Alignment.topLeft: 좌상단
  • Alignment.topCenter: 상단 중앙
  • Alignment.topRight: 우상단
  • Alignment.centerLeft: 좌측 중앙
  • Alignment.center: 중앙
  • Alignment.centerRight: 우측 중앙
  • Alignment.bottomLeft: 좌하단
  • Alignment.bottomCenter: 하단 중앙
  • Alignment.bottomRight: 우하단

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 { AnimatedFractionallySizedBox, Container, Card, Curves } from '@meursyphus/flitter';

class ResponsiveCard extends StatefulWidget {
  createState() {
    return new ResponsiveCardState();
  }
}

class ResponsiveCardState extends State<ResponsiveCard> {
  isExpanded = false;

  build() {
    return Container({
      width: 400,
      height: 600,
      color: "#f0f0f0",
      child: Center({
        child: GestureDetector({
          onTap: () => {
            this.setState(() => {
              this.isExpanded = !this.isExpanded;
            });
          },
          child: AnimatedFractionallySizedBox({
            widthFactor: this.isExpanded ? 0.9 : 0.6,
            heightFactor: this.isExpanded ? 0.7 : 0.4,
            duration: 600,
            curve: Curves.easeInOut,
            alignment: Alignment.center,
            child: Card({
              elevation: 4,
              child: Container({
                decoration: BoxDecoration({
                  gradient: LinearGradient({
                    colors: ["#4A90E2", "#7B68EE"],
                    begin: Alignment.topLeft,
                    end: Alignment.bottomRight,
                  }),
                }),
                child: Center({
                  child: Column({
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Icon({
                        icon: this.isExpanded ? Icons.unfold_less : Icons.unfold_more,
                        size: 48,
                        color: "white",
                      }),
                      SizedBox({ height: 16 }),
                      Text(
                        this.isExpanded ? "클릭하여 축소" : "클릭하여 확대",
                        { style: TextStyle({ color: "white", fontSize: 18 }) }
                      ),
                    ],
                  }),
                }),
              }),
            }),
          }),
        }),
      }),
    });
  }
}

예제 2: 다단계 크기 전환

import { AnimatedFractionallySizedBox, Container, Row, Curves } from '@meursyphus/flitter';

class MultiSizeTransition extends StatefulWidget {
  createState() {
    return new MultiSizeTransitionState();
  }
}

class MultiSizeTransitionState extends State<MultiSizeTransition> {
  sizeLevel = 0; // 0: small, 1: medium, 2: large
  sizes = [
    { width: 0.3, height: 0.3 },
    { width: 0.6, height: 0.5 },
    { width: 0.9, height: 0.8 },
  ];

  build() {
    const currentSize = this.sizes[this.sizeLevel];
    
    return Column({
      children: [
        // 컨트롤 버튼들
        Row({
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton({
              onPressed: () => this.changeSize(0),
              child: Text("작게"),
              style: ButtonStyle({
                backgroundColor: this.sizeLevel === 0 ? "blue" : "gray",
              }),
            }),
            SizedBox({ width: 8 }),
            ElevatedButton({
              onPressed: () => this.changeSize(1),
              child: Text("보통"),
              style: ButtonStyle({
                backgroundColor: this.sizeLevel === 1 ? "blue" : "gray",
              }),
            }),
            SizedBox({ width: 8 }),
            ElevatedButton({
              onPressed: () => this.changeSize(2),
              child: Text("크게"),
              style: ButtonStyle({
                backgroundColor: this.sizeLevel === 2 ? "blue" : "gray",
              }),
            }),
          ],
        }),
        SizedBox({ height: 20 }),
        
        // 애니메이션 영역
        Expanded({
          child: Container({
            color: "#e0e0e0",
            child: AnimatedFractionallySizedBox({
              widthFactor: currentSize.width,
              heightFactor: currentSize.height,
              duration: 800,
              curve: Curves.anticipate,
              child: Container({
                decoration: BoxDecoration({
                  color: "deepPurple",
                  borderRadius: BorderRadius.circular(16),
                  boxShadow: [
                    BoxShadow({
                      color: "rgba(0,0,0,0.3)",
                      blurRadius: 10,
                      offset: Offset({ x: 0, y: 5 }),
                    }),
                  ],
                }),
                child: Center({
                  child: Text(
                    `${Math.round(currentSize.width * 100)}% x ${Math.round(currentSize.height * 100)}%`,
                    { style: TextStyle({ color: "white", fontSize: 24, fontWeight: "bold" }) }
                  ),
                }),
              }),
            }),
          }),
        }),
      ],
    });
  }

  changeSize(level: number) {
    this.setState(() => {
      this.sizeLevel = level;
    });
  }
}

예제 3: 정렬 변경과 함께 크기 애니메이션

import { AnimatedFractionallySizedBox, Container, Alignment, Curves } from '@meursyphus/flitter';

class AlignmentAndSizeAnimation extends StatefulWidget {
  createState() {
    return new AlignmentAndSizeAnimationState();
  }
}

class AlignmentAndSizeAnimationState extends State<AlignmentAndSizeAnimation> {
  currentIndex = 0;
  configs = [
    { alignment: Alignment.topLeft, widthFactor: 0.3, heightFactor: 0.3 },
    { alignment: Alignment.topRight, widthFactor: 0.5, heightFactor: 0.2 },
    { alignment: Alignment.center, widthFactor: 0.8, heightFactor: 0.8 },
    { alignment: Alignment.bottomLeft, widthFactor: 0.4, heightFactor: 0.6 },
    { alignment: Alignment.bottomRight, widthFactor: 0.6, heightFactor: 0.4 },
  ];

  build() {
    const config = this.configs[this.currentIndex];
    
    return GestureDetector({
      onTap: () => this.nextConfiguration(),
      child: Container({
        width: double.infinity,
        height: double.infinity,
        color: "lightblue",
        child: AnimatedFractionallySizedBox({
          alignment: config.alignment,
          widthFactor: config.widthFactor,
          heightFactor: config.heightFactor,
          duration: 1000,
          curve: Curves.easeInOut,
          child: Container({
            decoration: BoxDecoration({
              color: "orange",
              borderRadius: BorderRadius.circular(20),
              border: Border.all({ color: "darkorange", width: 3 }),
            }),
            child: Center({
              child: Column({
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text(
                    `위치 ${this.currentIndex + 1}/5`,
                    { style: TextStyle({ fontSize: 20, fontWeight: "bold" }) }
                  ),
                  SizedBox({ height: 8 }),
                  Text(
                    "탭하여 다음",
                    { style: TextStyle({ fontSize: 16 }) }
                  ),
                ],
              }),
            }),
          }),
        }),
      }),
    });
  }

  nextConfiguration() {
    this.setState(() => {
      this.currentIndex = (this.currentIndex + 1) % this.configs.length;
    });
  }
}

예제 4: 비율 유지 이미지 컨테이너

import { AnimatedFractionallySizedBox, Container, Image, Curves } from '@meursyphus/flitter';

class AspectRatioImageContainer extends StatefulWidget {
  createState() {
    return new AspectRatioImageContainerState();
  }
}

class AspectRatioImageContainerState extends State<AspectRatioImageContainer> {
  isFullscreen = false;

  build() {
    return Container({
      width: double.infinity,
      height: double.infinity,
      color: "black",
      child: GestureDetector({
        onTap: () => {
          this.setState(() => {
            this.isFullscreen = !this.isFullscreen;
          });
        },
        child: AnimatedFractionallySizedBox({
          widthFactor: this.isFullscreen ? 1.0 : 0.7,
          heightFactor: this.isFullscreen ? 1.0 : 0.5,
          duration: 500,
          curve: Curves.easeOut,
          alignment: Alignment.center,
          child: Container({
            decoration: BoxDecoration({
              boxShadow: this.isFullscreen ? [] : [
                BoxShadow({
                  color: "rgba(0,0,0,0.5)",
                  blurRadius: 20,
                  spreadRadius: 5,
                }),
              ],
            }),
            child: ClipRRect({
              borderRadius: BorderRadius.circular(this.isFullscreen ? 0 : 16),
              child: Image({
                src: "https://example.com/landscape.jpg",
                fit: "cover",
              }),
            }),
          }),
        }),
      }),
    });
  }
}

주의사항

  • widthFactor와 heightFactor는 0보다 크거나 같아야 합니다
  • widthFactor와 heightFactor가 모두 undefined일 경우, 자식 위젯의 고유 크기가 사용됩니다
  • 부모 위젯의 크기가 변경되면 AnimatedFractionallySizedBox의 실제 크기도 자동으로 조정됩니다
  • alignment와 factor 값이 동시에 변경되면 모두 함께 애니메이션됩니다
  • 성능을 고려하여 너무 빈번한 크기 변경은 피하는 것이 좋습니다

관련 위젯

  • FractionallySizedBox: 애니메이션 없이 부모 크기에 비례하여 크기를 설정하는 기본 위젯
  • AnimatedContainer: 여러 속성을 동시에 애니메이션으로 전환
  • AnimatedAlign: 정렬만 애니메이션으로 전환
  • SizedBox: 고정 크기를 설정하는 위젯