Tween 애니메이션이란 무엇이며 어떻게 사용되나요?

질문

Flutter에서 Tween 애니메이션이란 무엇이며 어떻게 사용되나요?

답변

Tween(Tweening의 줄임말, 'in-betweening'에서 유래)은 Flutter에서 두 값 사이의 보간(interpolation)을 정의하는 클래스입니다. 애니메이션에서 Tween은 시작 값과 끝 값을 정의하고, 애니메이션이 진행됨에 따라 두 값 사이의 중간 값을 계산합니다.

Tween의 기본 개념

Tween 클래스는 다음과 같은 기능을 합니다:

  1. 시작 값(begin)과 끝 값(end)을 정의합니다.
  2. 0.0에서 1.0 사이의 진행 값(t)을 입력받아 그에 해당하는 중간 값을 계산합니다.
  3. 다양한 데이터 유형(숫자, 색상, 위치 등)에 대한 보간을 지원합니다.

기본 Tween 사용법

// 가장 기본적인 형태의 Tween 생성
final tween = Tween<double>(begin: 0.0, end: 100.0);

// 50% 지점의 값 얻기
double value = tween.transform(0.5); // 결과: 50.0

Flutter 애니메이션에서 Tween 사용하기

Tween은 일반적으로 AnimationController와 함께 사용되어 명시적(explicit) 애니메이션을 구현합니다:

class TweenAnimationExample extends StatefulWidget {
  @override
  _TweenAnimationExampleState createState() => _TweenAnimationExampleState();
}

class _TweenAnimationExampleState extends State<TweenAnimationExample>
    with SingleTickerProviderStateMixin {

  late AnimationController _controller;
  late Animation<double> _sizeAnimation;

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

    // 애니메이션 컨트롤러 생성
    _controller = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,
    );

    // Tween과 컨트롤러 연결
    _sizeAnimation = Tween<double>(
      begin: 50.0,
      end: 200.0,
    ).animate(_controller);

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

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

  @override
  Widget build(BuildContext context) {
    return Center(
      child: AnimatedBuilder(
        animation: _controller,
        builder: (context, child) {
          return Container(
            width: _sizeAnimation.value,
            height: _sizeAnimation.value,
            color: Colors.blue,
          );
        },
      ),
    );
  }
}

다양한 Tween 유형

Flutter는 다양한 데이터 유형을 애니메이션화할 수 있는 여러 Tween 하위 클래스를 제공합니다:

1. Tween\

숫자 값을 보간하는 가장 기본적인 Tween입니다:

final sizeTween = Tween<double>(begin: 50.0, end: 200.0);
final opacityTween = Tween<double>(begin: 0.0, end: 1.0);

2. ColorTween

색상 간 보간을 수행합니다:

final colorTween = ColorTween(
  begin: Colors.blue,
  end: Colors.red,
);

// 사용 예시
Container(
  color: colorTween.evaluate(_controller),
  width: 100,
  height: 100,
)

3. IntTween

정수 값 사이의 보간을 수행합니다:

final countTween = IntTween(begin: 0, end: 100);

// 사용 예시
Text(
  '${countTween.evaluate(_controller)}%',
  style: TextStyle(fontSize: 24),
)

4. RectTween

사각형(Rect) 객체 간의 보간을 수행합니다:

final rectTween = RectTween(
  begin: Rect.fromLTWH(0, 0, 50, 50),
  end: Rect.fromLTWH(200, 200, 100, 100),
);

5. BorderTween

테두리(Border) 속성을 보간합니다:

final borderTween = BorderTween(
  begin: Border.all(width: 1, color: Colors.red),
  end: Border.all(width: 5, color: Colors.blue),
);

6. BorderRadiusTween

모서리 반경(BorderRadius) 간 보간을 수행합니다:

final radiusTween = BorderRadiusTween(
  begin: BorderRadius.circular(0),
  end: BorderRadius.circular(25),
);

7. EdgeInsetsTween

패딩/마진(EdgeInsets) 값 간 보간을 수행합니다:

final paddingTween = EdgeInsetsTween(
  begin: EdgeInsets.all(0),
  end: EdgeInsets.all(16),
);

8. TextStyleTween

텍스트 스타일 간 보간을 수행합니다:

final textStyleTween = TextStyleTween(
  begin: TextStyle(fontSize: 12, color: Colors.black),
  end: TextStyle(fontSize: 24, color: Colors.blue),
);

복합 애니메이션을 위한 Tween 조합

여러 Tween을 조합하여 복잡한 애니메이션을 만들 수 있습니다:

class ComplexAnimationExample extends StatefulWidget {
  @override
  _ComplexAnimationExampleState createState() => _ComplexAnimationExampleState();
}

class _ComplexAnimationExampleState extends State<ComplexAnimationExample>
    with SingleTickerProviderStateMixin {

  late AnimationController _controller;
  late Animation<double> _sizeAnimation;
  late Animation<Color?> _colorAnimation;
  late Animation<double> _opacityAnimation;
  late Animation<Offset> _slideAnimation;

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

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

    _sizeAnimation = Tween<double>(
      begin: 50.0,
      end: 200.0,
    ).animate(_controller);

    _colorAnimation = ColorTween(
      begin: Colors.blue,
      end: Colors.red,
    ).animate(_controller);

    _opacityAnimation = Tween<double>(
      begin: 0.0,
      end: 1.0,
    ).animate(_controller);

    _slideAnimation = Tween<Offset>(
      begin: Offset(-1.0, 0.0),
      end: Offset(0.0, 0.0),
    ).animate(_controller);

    _controller.forward();
  }

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

  @override
  Widget build(BuildContext context) {
    return Center(
      child: AnimatedBuilder(
        animation: _controller,
        builder: (context, child) {
          return SlideTransition(
            position: _slideAnimation,
            child: Opacity(
              opacity: _opacityAnimation.value,
              child: Container(
                width: _sizeAnimation.value,
                height: _sizeAnimation.value,
                color: _colorAnimation.value,
              ),
            ),
          );
        },
      ),
    );
  }
}

Tween과 커브(Curve) 결합하기

Tween은 CurvedAnimation과 결합하여 비선형 애니메이션을 만들 수 있습니다:

// 기본 컨트롤러 설정
final controller = AnimationController(
  duration: Duration(seconds: 2),
  vsync: this,
);

// 커브 애니메이션 생성
final curvedAnimation = CurvedAnimation(
  parent: controller,
  curve: Curves.easeInOut,
);

// Tween과 커브 애니메이션 결합
final sizeAnimation = Tween<double>(
  begin: 50.0,
  end: 200.0,
).animate(curvedAnimation);

순차 애니메이션에서 Tween 사용하기

Interval 커브를 사용하여 Tween이 특정 시간 범위에서만 동작하도록 만들 수 있습니다:

// 크기 애니메이션 (0%-50% 구간)
final sizeAnimation = Tween<double>(
  begin: 50.0,
  end: 200.0,
).animate(
  CurvedAnimation(
    parent: controller,
    curve: Interval(0.0, 0.5, curve: Curves.easeOut),
  ),
);

// 색상 애니메이션 (50%-100% 구간)
final colorAnimation = ColorTween(
  begin: Colors.blue,
  end: Colors.red,
).animate(
  CurvedAnimation(
    parent: controller,
    curve: Interval(0.5, 1.0, curve: Curves.easeIn),
  ),
);

사용자 정의 Tween 만들기

사용자 정의 클래스를 애니메이션화하려면 Tween을 상속하여 자신만의 Tween을 만들 수 있습니다:

// 사용자 정의 클래스
class MyCustomObject {
  final double width;
  final double height;
  final Color color;

  MyCustomObject({
    required this.width,
    required this.height,
    required this.color,
  });
}

// 사용자 정의 Tween
class MyCustomTween extends Tween<MyCustomObject> {
  MyCustomTween({
    required MyCustomObject begin,
    required MyCustomObject end,
  }) : super(begin: begin, end: end);

  @override
  MyCustomObject lerp(double t) {
    return MyCustomObject(
      width: lerpDouble(begin!.width, end!.width, t)!,
      height: lerpDouble(begin!.height, end!.height, t)!,
      color: Color.lerp(begin!.color, end!.color, t)!,
    );
  }
}

// 사용 예시
final customTween = MyCustomTween(
  begin: MyCustomObject(width: 50, height: 50, color: Colors.blue),
  end: MyCustomObject(width: 200, height: 100, color: Colors.red),
);

final animation = customTween.animate(controller);

TweenSequence 사용하기

하나의 애니메이션에서 여러 Tween을 순차적으로 실행하려면 TweenSequence를 사용할 수 있습니다:

final Animation<Color?> colorAnimation = TweenSequence<Color?>(
  [
    // 0% - 33%: 파란색에서 초록색으로
    TweenSequenceItem(
      tween: ColorTween(begin: Colors.blue, end: Colors.green),
      weight: 1,
    ),
    // 33% - 66%: 초록색에서 노란색으로
    TweenSequenceItem(
      tween: ColorTween(begin: Colors.green, end: Colors.yellow),
      weight: 1,
    ),
    // 66% - 100%: 노란색에서 빨간색으로
    TweenSequenceItem(
      tween: ColorTween(begin: Colors.yellow, end: Colors.red),
      weight: 1,
    ),
  ],
).animate(controller);

TweenSequenceItem의 weight는 전체 애니메이션에서 해당 Tween이 차지하는 비율을 나타냅니다. 모든 weight의 합은 애니메이션의 전체 시간을 나타냅니다.

실제 예시: 로딩 버튼 애니메이션

Tween을 활용한 실제 예시로, 버튼이 누르면 로딩 상태로 애니메이션되는 예제를 살펴보겠습니다:

class AnimatedLoadingButton extends StatefulWidget {
  final VoidCallback onPressed;

  AnimatedLoadingButton({required this.onPressed});

  @override
  _AnimatedLoadingButtonState createState() => _AnimatedLoadingButtonState();
}

class _AnimatedLoadingButtonState extends State<AnimatedLoadingButton>
    with SingleTickerProviderStateMixin {

  late AnimationController _controller;
  late Animation<double> _widthAnimation;
  late Animation<double> _borderRadiusAnimation;
  late Animation<Color?> _colorAnimation;
  bool _isLoading = false;

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

    _controller = AnimationController(
      duration: Duration(milliseconds: 300),
      vsync: this,
    );

    _widthAnimation = Tween<double>(
      begin: 200.0,
      end: 50.0,
    ).animate(_controller);

    _borderRadiusAnimation = Tween<double>(
      begin: 4.0,
      end: 25.0,
    ).animate(_controller);

    _colorAnimation = ColorTween(
      begin: Colors.blue,
      end: Colors.green,
    ).animate(_controller);
  }

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

  void _handleTap() async {
    if (_isLoading) return;

    setState(() {
      _isLoading = true;
    });

    await _controller.forward();

    widget.onPressed();

    // 작업 완료 후 원래 상태로 복귀
    Future.delayed(Duration(seconds: 2), () {
      _controller.reverse().then((_) {
        setState(() {
          _isLoading = false;
        });
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return GestureDetector(
          onTap: _handleTap,
          child: Container(
            width: _widthAnimation.value,
            height: 50,
            decoration: BoxDecoration(
              color: _colorAnimation.value,
              borderRadius: BorderRadius.circular(_borderRadiusAnimation.value),
            ),
            child: Center(
              child: _isLoading
                  ? SizedBox(
                      width: 20,
                      height: 20,
                      child: CircularProgressIndicator(
                        color: Colors.white,
                        strokeWidth: 2,
                      ),
                    )
                  : Text(
                      '버튼',
                      style: TextStyle(
                        color: Colors.white,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
            ),
          ),
        );
      },
    );
  }
}

결론

Tween은 Flutter 애니메이션 시스템의 핵심 구성 요소로, 시작 값과 끝 값 사이의 보간을 정의합니다. 다양한 데이터 유형에 대한 보간을 지원하고, 커브와 결합하여 정교한 애니메이션을 만들 수 있습니다. 또한 TweenSequence를 통해 복잡한 다단계 애니메이션을 구현할 수 있으며, 필요에 따라 사용자 정의 Tween을 만들어 특별한 애니메이션 효과를 구현할 수 있습니다.

Tween을 효과적으로 사용하면 부드럽고 매력적인 UI 트랜지션을 만들어 사용자 경험을 크게 향상시킬 수 있습니다.

results matching ""

    No results matching ""