플러터에서는 2가지 방법으로 애니메이션을 구현할 수 있다. 바로 암묵적 애니메이션(Implicit animation)과 명시적 애니메이션(explicit animation)이다.
암묵적 애니메이션은 미리 정의된 스타일과 애니메이션을 사용하기 때문에 구현이 아주 간단하다. 만약 미리 정의된게 아니라 커스터마이징해서 애니메이션을 직접 만들고 싶다면 명시적 애니메이션을 쓰면 된다. 대신 적어야 하는 코드가 조금 늘어난다.
플러터에는 다음 2가지 애니메이션이 있다: 간단한 암묵적 애니메이션, 커스터마이징 할 수 있는 명시적 애니메이션.
그러면 각각의 특징을 조금 더 알아보자.
위젯 내부에서 자체적으로 애니메이션이 일어난다.
암묵적 애니메이션 위젯에서는 제공하지 않는, 애니메이션 커스터마이징을 할 수 있다.
AnimationController
를 등록해야 한다.AnimationBuilder
를 사용해서 내 입맛대로 애니메이션을 구현할 수 있다.우리가 해줄게 없다. 너는 이미.. 다 컸어! 알아서 잘 해보렴.
여기 (친절하게도, 플러터 공식 문서다. 심지어 유투브 영상이다!)에 들어가면 다양한 암묵적 애니메이션 위젯을 만날 수 있다.
여기도 공식 사이트인데, 이런걸 만들 수 있는 튜토리얼을 제공한다.
명시적 애니메이션을 구현하는 방식에도 종류가 있다.
~~Transition
으로 끝나는 위젯을 사용하면 된다.AnimatedBuilder
로 감싸는 식으로 구현하게 된다. 좀 더 커스터마이징을 많이 할 수 있다.이번 글에서는 방법 1에 해당하는 ~~Transition
위젯 사용법을 소개하고, 다음 글에서 AnimatedBuilder
에 대해 적으려고 한다.
명시적 애니메이션을 사용하기 위해서 만들어야 할게 2개 있다. 컨트롤러와 애니메이션 객체다.
AnimationController
애니메이션을 시작하거나/거꾸로 재생하거나/멈춘다.
주어진 value
로 애니메이션을 조절한다.
애니메이션의 lowerBound와 upperBound, 즉 설정한 애니메이션의 몇 %까지 진행하고 초기값은 몇 %에서 시작할 것인지를 결정한다.
lowerBound
가 0.1이라면 실제로 투명도는 10%인 상태에서 시작한다. 애니메이션이 시작되기 전에도 투명도는 10%가 된다.필수 파라미터
이름 | 정체 | 넘겨준 객체 |
---|---|---|
vsync | TickerProvider. Ticker를 제공한다. 1프레임마다 주어진 콜백 함수를 실행한다. | this (위젯의 state에 TickerProviderStateMixin 를 사용했기 때문에 자기 자신을 넘겨도 된다.) |
Animation
클래스는 abstract class이다. Tween
, CurvedAnimation
, TrainHoppingAnimation
등을 사용해 객체를 만들 수 있다.CurvedAnimation
를 사용했다. 이 클래스의 필수 파라미터는...이름 | 정체 | 넘겨준 객체 |
---|---|---|
parent | AnimationController. 여기에 addStatusListener 를 달아서 변화를 감지한다. | 위에서 만들어준 animationController 객체. |
curve | 애니메이션이 어떤 흐름으로 진행될지 정한다. 직접 보는게 훨씬 이해가 빠르다. 여기를 보자. | 원하는 Curve 객체. |
연습삼아 총 2가지 위젯을 구현했다. 무한 반복 애니메이션과 끝난 뒤 역재생되는 애니메이션이다.
class _TransitionAnimationScreenState extends State<TransitionAnimationScreen>
with TickerProviderStateMixin {
late final AnimationController _controller = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
)..repeat(reverse: true);
late final Animation<double> _animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInToLinear,
);
void dispose() {
_controller.dispose();
super.dispose();
}
...
이렇게 셋팅을 한 다음, 애니메이션을 사용할 위젯에서는 이렇게 써먹으면 된다. 아래는 컨테이너의 투명도를 조절하는 예제다.
FadeTransition(
opacity: _animation,
child: Container(
color: Colors.green,
width: 300,
height: 100,
),
),
원래는 애니메이션을 실행시키는 코드도 직접 적어야하지만, _controller
를 만들 때 repeat
설정을 해줬기 때문에 애니메이션이 무한반복된다. 따라서 이 화면에 들어가기만 해도 바로 애니메이션이 실행된다.
reverse: true
옵션을 주면 애니메이션이 이런 순서로 반복된다: -시작-끝-시작-끝.reverse: false
라면 이렇게 된다: 시작-시작-시작-시작애니메이션 컨트롤러는 프레임의 변화를 감지하는 ticker를 사용한다. 따로 dispose
해주지 않으면 리소스를 계속해서 잡아먹게 된다.
class _TransitionAnimationScreenState extends State<TransitionAnimationScreen>
with TickerProviderStateMixin {
late final AnimationController _rotationController = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
);
late final Animation<double> _rotationAnimation =
CurvedAnimation(parent: _rotationController, curve: Curves.bounceIn);
void dispose() {
_controller.dispose();
super.dispose();
}
...
첫번째 예시 코드와 다른 점은 애니메이션 컨트롤러를 만들 때 repeat
함수를 호출하지 않았다는 점이다. 앞의 예제에서는 repeat
함수를 사용하면 애니메이션이 자동으로 시작되었기 때문에 애니메이션 트리거를 지정해주지 않았다. 트랜지션 애니메이션은 트리거를 직접 지정해줘야하기 때문에 repeat
함수를 쓰지 않은 이번 예시에서는 애니메이션이 언제 시작할 것인지 우리가 정해줘야 한다.
RotationTransition(
turns: _rotationAnimation,
child: GestureDetector(
onTap: () {
_rotationController.forward();
},
child: Container(
margin: EdgeInsets.all(90),
color: Colors.lightGreen,
width: 300,
height: 100,
child: Text("탭하면 애니메이션 시작"),
),
);
애니메이션 관리는 컨트롤러가 한다. 즉, 컨트롤러에게 애니메이션을 시작할 수 있는 함수가 들어있다. forward
함수를 사용하면 애니메이션이 정방향으로 실행된다. GestureDetector.onTap
에 해당 코드를 넣었다.
한번 재생하기만 하면 재미없으니까 애니메이션이 완료된 다음 역재생하게 만들었다. reverse
함수로 역재생할 수 있다. 이 때 주의할 점은 이렇게 적으면 안된다는거다.
_rotationController.forward();
_rotationController.reverse();
언뜻 보기에는 정방향으로 한번 재생한 다음 역재생하는 코드 같다. 나도 처음에는 이렇게 적었다. 하지만 이렇게 하면 눈으로 보기에 아무 변화도 일어나지 않는다. 1 프레임 내에서 forward
, reverse
함수를 모두 실행해버리기 때문이다. 그래서 가만히 있는 것처럼 보인다. 제대로 하려면 아래 코드처럼 적어야 한다.
switch (_rotationController.status) {
case AnimationStatus.dismissed:
_rotationController.forward();
break;
case AnimationStatus.completed:
_rotationController.reverse();
break;
default:
}