Flutter에서 기본 애니메이션을 어떻게 만드나요?

질문

Flutter에서 기본 애니메이션을 어떻게 만드나요?

답변

Flutter에서는 두 가지 주요 애니메이션 접근 방식이 있습니다: 암시적(implicit) 애니메이션과 명시적(explicit) 애니메이션입니다. 각 방식은 서로 다른 사용 사례에 적합합니다.

1. 암시적(Implicit) 애니메이션

암시적 애니메이션은 Animated로 시작하는 위젯을 사용하여 간단하게 구현할 수 있습니다. 이러한 위젯은 속성 값이 변경될 때 자동으로 애니메이션을 적용합니다.

예시: AnimatedContainer

class MyAnimationWidget extends StatefulWidget {
  @override
  _MyAnimationWidgetState createState() => _MyAnimationWidgetState();
}

class _MyAnimationWidgetState extends State<MyAnimationWidget> {
  bool _isExpanded = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        setState(() {
          _isExpanded = !_isExpanded;
        });
      },
      child: AnimatedContainer(
        width: _isExpanded ? 200.0 : 100.0,
        height: _isExpanded ? 200.0 : 100.0,
        color: _isExpanded ? Colors.blue : Colors.red,
        alignment: _isExpanded ? Alignment.center : Alignment.topLeft,
        duration: Duration(milliseconds: 500),
        curve: Curves.fastOutSlowIn,
        child: FlutterLogo(size: 50),
      ),
    );
  }
}

이 예시에서는 컨테이너를 탭하면 크기, 색상, 정렬이 애니메이션되면서 변경됩니다.

주요 암시적 애니메이션 위젯

  • AnimatedContainer: 컨테이너의 속성 변화에 애니메이션 적용
  • AnimatedOpacity: 투명도 변화에 애니메이션 적용
  • AnimatedPadding: 패딩 변화에 애니메이션 적용
  • AnimatedPositioned: 위치 변화에 애니메이션 적용
  • AnimatedCrossFade: 두 위젯 간의 교차 페이드 애니메이션
  • AnimatedSwitcher: 자식 위젯 교체 시 애니메이션 적용
  • AnimatedSize: 크기 변화에 애니메이션 적용
  • AnimatedAlign: 정렬 변화에 애니메이션 적용

예시: 여러 애니메이션 속성 조합

AnimatedContainer(
  duration: Duration(milliseconds: 800),
  width: _isExpanded ? 200.0 : 100.0,
  height: _isExpanded ? 200.0 : 100.0,
  padding: _isExpanded
      ? EdgeInsets.all(20)
      : EdgeInsets.all(10),
  decoration: BoxDecoration(
    color: _isExpanded ? Colors.blue : Colors.red,
    borderRadius: _isExpanded
        ? BorderRadius.circular(20)
        : BorderRadius.circular(5),
    boxShadow: _isExpanded
        ? [BoxShadow(blurRadius: 10, color: Colors.black45)]
        : [],
  ),
  transform: _isExpanded
      ? Matrix4.rotationZ(0.1)
      : Matrix4.rotationZ(0),
  curve: Curves.easeInOutBack,
  child: FlutterLogo(),
)

AnimatedSwitcher 사용 예시

위젯 전환에 애니메이션을 적용하려면:

AnimatedSwitcher(
  duration: Duration(milliseconds: 500),
  transitionBuilder: (Widget child, Animation<double> animation) {
    return ScaleTransition(scale: animation, child: child);
  },
  child: _isExpanded
      ? Container(
          key: ValueKey('container1'),
          color: Colors.blue,
          height: 200,
          width: 200,
        )
      : Container(
          key: ValueKey('container2'),
          color: Colors.red,
          height: 100,
          width: 100,
        ),
)

2. 명시적(Explicit) 애니메이션

명시적 애니메이션은 더 세밀한 제어가 필요한 경우 사용합니다. 이를 위해 AnimationControllerAnimation 객체를 직접 만들고 관리해야 합니다.

기본 명시적 애니메이션 예시

class ExplicitAnimationDemo extends StatefulWidget {
  @override
  _ExplicitAnimationDemoState createState() => _ExplicitAnimationDemoState();
}

class _ExplicitAnimationDemoState extends State<ExplicitAnimationDemo>
    with SingleTickerProviderStateMixin {  // TickerProvider 믹스인 추가

  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();

    // 애니메이션 컨트롤러 초기화
    _controller = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,  // 틱 제공자
    );

    // 애니메이션 생성 및 구성
    _animation = Tween<double>(begin: 0, end: 1).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Curves.elasticOut,
      ),
    );

    // 애니메이션 시작
    _controller.forward();
  }

  @override
  void dispose() {
    _controller.dispose();  // 컨트롤러 자원 해제
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('명시적 애니메이션')),
      body: Center(
        child: AnimatedBuilder(
          animation: _animation,
          builder: (context, child) {
            return Transform.scale(
              scale: _animation.value,
              child: child,
            );
          },
          child: Container(
            width: 200,
            height: 200,
            color: Colors.blue,
            child: Center(
              child: Text(
                '애니메이션',
                style: TextStyle(color: Colors.white, fontSize: 24),
              ),
            ),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          if (_controller.status == AnimationStatus.completed) {
            _controller.reverse();
          } else {
            _controller.forward();
          }
        },
        child: Icon(Icons.play_arrow),
      ),
    );
  }
}

애니메이션 컨트롤러 메서드

애니메이션 컨트롤러는 다양한 제어 메서드를 제공합니다:

  • forward(): 애니메이션 시작 또는 재개
  • reverse(): 애니메이션 역방향 재생
  • repeat(): 애니메이션 반복
  • reset(): 애니메이션 초기 상태로 리셋
  • stop(): 애니메이션 정지
  • animateTo(double target): 특정 값으로 애니메이션 진행
  • animateBack(double target): 특정 값으로 역방향 애니메이션 진행

Tween 사용 예시

다양한 값 유형에 애니메이션을 적용할 수 있습니다:

// 더블 값 애니메이션
Animation<double> _sizeAnimation = Tween<double>(begin: 100, end: 200).animate(_controller);

// 색상 애니메이션
Animation<Color?> _colorAnimation = ColorTween(begin: Colors.red, end: Colors.blue).animate(_controller);

// 오프셋 애니메이션
Animation<Offset> _offsetAnimation = Tween<Offset>(
  begin: Offset.zero,
  end: Offset(1.5, 0),
).animate(_controller);

// 정수 애니메이션
Animation<int> _intAnimation = IntTween(begin: 0, end: 100).animate(_controller);

여러 애니메이션 속성 적용하기

@override
Widget build(BuildContext context) {
  return AnimatedBuilder(
    animation: _controller,
    builder: (context, child) {
      return Container(
        width: _sizeAnimation.value,
        height: _sizeAnimation.value,
        color: _colorAnimation.value,
        transform: Matrix4.translationValues(
          _offsetAnimation.value.dx * 100,
          0,
          0,
        ),
        child: Center(
          child: Text(
            '${_intAnimation.value}%',
            style: TextStyle(color: Colors.white, fontSize: 24),
          ),
        ),
      );
    },
  );
}

3. 애니메이션 리스너 사용하기

애니메이션 진행 상태를 감지하고 그에 따라 로직을 실행할 수 있습니다:

_controller.addStatusListener((status) {
  if (status == AnimationStatus.completed) {
    print('애니메이션 완료');
    // 애니메이션 완료 시 실행할 작업
  } else if (status == AnimationStatus.dismissed) {
    print('애니메이션 초기 상태');
    // 애니메이션이 초기 상태로 돌아왔을 때 실행할 작업
  }
});

_controller.addListener(() {
  // 애니메이션 값이 변경될 때마다 호출됨
  print('현재 애니메이션 값: ${_controller.value}');
  setState(() {
    // 필요한 경우 상태 업데이트
  });
});

4. 여러 애니메이션 조합하기

복잡한 애니메이션 효과를 만들기 위해 여러 애니메이션을 조합할 수 있습니다.

순차 애니메이션 (Sequential Animations)

// 첫 번째 애니메이션 (0%-50%)
final firstAnimation = CurvedAnimation(
  parent: _controller,
  curve: Interval(0.0, 0.5, curve: Curves.easeOut),
);

// 두 번째 애니메이션 (50%-100%)
final secondAnimation = CurvedAnimation(
  parent: _controller,
  curve: Interval(0.5, 1.0, curve: Curves.easeIn),
);

// 크기 애니메이션 (첫 번째 반절 동안)
final sizeAnimation = Tween<double>(begin: 100, end: 200).animate(firstAnimation);

// 색상 애니메이션 (두 번째 반절 동안)
final colorAnimation = ColorTween(begin: Colors.blue, end: Colors.red).animate(secondAnimation);

5. 트랜지션 위젯 사용하기

Flutter는 일반적인 애니메이션 효과를 쉽게 구현할 수 있는 여러 트랜지션 위젯을 제공합니다:

// 페이드 효과
FadeTransition(
  opacity: _animation,
  child: Container(color: Colors.blue, width: 200, height: 200),
)

// 크기 조절 효과
ScaleTransition(
  scale: _animation,
  child: Container(color: Colors.green, width: 200, height: 200),
)

// 회전 효과
RotationTransition(
  turns: _animation,
  child: Container(color: Colors.orange, width: 200, height: 200),
)

// 위치 이동 효과
SlideTransition(
  position: _offsetAnimation,
  child: Container(color: Colors.purple, width: 200, height: 200),
)

// 크기 조절 효과 (조정 가능)
SizeTransition(
  sizeFactor: _animation,
  axis: Axis.horizontal,
  child: Container(color: Colors.red, width: 200, height: 200),
)

6. 애니메이션 효과 커스텀하기

커스텀 커브를 정의하여 독특한 애니메이션 효과를 만들 수 있습니다:

class CustomBounce extends Curve {
  final double a;
  final double b;

  CustomBounce({this.a = 0.5, this.b = 2.0});

  @override
  double transformInternal(double t) {
    if (t < a) {
      return b * t * t;
    } else {
      t = t - a;
      t = 1.0 - t;
      return 1.0 - b * t * t;
    }
  }
}

// 사용 예시
_animation = CurvedAnimation(
  parent: _controller,
  curve: CustomBounce(a: 0.3, b: 1.5),
);

완전한 예제: 복합 애니메이션

다음은 여러 애니메이션 효과를 조합한 완전한 예제입니다:

class ComplexAnimationDemo extends StatefulWidget {
  @override
  _ComplexAnimationDemoState createState() => _ComplexAnimationDemoState();
}

class _ComplexAnimationDemoState extends State<ComplexAnimationDemo>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _scaleAnimation;
  late Animation<double> _rotationAnimation;
  late Animation<Color?> _colorAnimation;
  late Animation<Offset> _slideAnimation;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(
      duration: Duration(seconds: 3),
      vsync: this,
    );

    // 크기 애니메이션 (0%-60%)
    _scaleAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Interval(0.0, 0.6, curve: Curves.elasticOut),
      ),
    );

    // 회전 애니메이션 (20%-80%)
    _rotationAnimation = Tween<double>(begin: 0.0, end: 2.0).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Interval(0.2, 0.8, curve: Curves.easeInOut),
      ),
    );

    // 색상 애니메이션 (40%-100%)
    _colorAnimation = ColorTween(begin: Colors.blue, end: Colors.purple).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Interval(0.4, 1.0, curve: Curves.easeIn),
      ),
    );

    // 슬라이드 애니메이션 (60%-100%)
    _slideAnimation = Tween<Offset>(begin: Offset.zero, end: Offset(0.2, 0)).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Interval(0.6, 1.0, curve: Curves.easeInOut),
      ),
    );

    _controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        Future.delayed(Duration(milliseconds: 500), () {
          _controller.reverse();
        });
      } else if (status == AnimationStatus.dismissed) {
        Future.delayed(Duration(milliseconds: 500), () {
          _controller.forward();
        });
      }
    });

    _controller.forward();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('복합 애니메이션')),
      body: Center(
        child: AnimatedBuilder(
          animation: _controller,
          builder: (context, child) {
            return SlideTransition(
              position: _slideAnimation,
              child: Transform.rotate(
                angle: _rotationAnimation.value * 3.14,
                child: Transform.scale(
                  scale: _scaleAnimation.value,
                  child: Container(
                    width: 200,
                    height: 200,
                    decoration: BoxDecoration(
                      color: _colorAnimation.value,
                      borderRadius: BorderRadius.circular(20 * _scaleAnimation.value),
                    ),
                    child: Center(
                      child: Text(
                        '애니메이션',
                        style: TextStyle(
                          color: Colors.white,
                          fontSize: 24 * _scaleAnimation.value,
                        ),
                      ),
                    ),
                  ),
                ),
              ),
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          if (_controller.isAnimating) {
            _controller.stop();
          } else {
            if (_controller.status == AnimationStatus.forward) {
              _controller.forward();
            } else {
              _controller.reverse();
            }
          }
        },
        child: Icon(_controller.isAnimating ? Icons.pause : Icons.play_arrow),
      ),
    );
  }
}

7. 애니메이션 성능 최적화 팁

  1. RepaintBoundary 사용: 애니메이션이 있는 위젯을 RepaintBoundary로 감싸 다시 그려지는 영역을 제한합니다.
RepaintBoundary(
  child: AnimatedBuilder(
    animation: _animation,
    builder: (context, child) { ... },
  ),
)
  1. 리소스 관리: 반드시 dispose() 메서드에서 애니메이션 컨트롤러를 해제하세요.

  2. 상수 자식 사용: AnimatedBuilder의 child 인자를 사용하여 불필요한 재빌드를 방지하세요.

  3. 불필요한 setState() 호출 피하기: 애니메이션 값이 변경될 때마다 setState()를 호출하지 마세요. 대신 AnimatedBuilder 또는 AnimatedWidget을 사용하세요.

결론

Flutter는 다양한 애니메이션 기법을 제공하여 매력적인 사용자 경험을 만들 수 있게 합니다. 간단한 애니메이션에는 AnimatedContainer와 같은 암시적 애니메이션 위젯을 사용하고, 더 복잡하거나 세밀한 제어가 필요한 경우 AnimationController와 같은 명시적 애니메이션 도구를 활용하세요.

애니메이션은 애플리케이션의 디자인과 사용자 경험을 크게 향상시킬 수 있지만, 과도하게 사용하면 사용자가 혼란스러울 수 있고 성능에 영향을 줄 수 있습니다. 목적에 맞게 적절히 사용하는 것이 중요합니다.

results matching ""

    No results matching ""