AnimatedWidget class
주어진 Listenable의 값이 변경될 때 다시 빌드되는 위젯입니다.
AnimatedWidget 은 일반적으로 Listenable 인 Animation 객체와 함께 사용되지만 ChangeNotifier 및 ValueNotifier를 포함한 모든 Listenable 과 함께 사용할 수 있습니다.
AnimatedWidget 은 상태가 없는 위젯에 가장 유용합니다. AnimatedWidget을 사용하려면 하위 클래스를 만들고 build 함수를 구현해야 합니다.
공식 문서 코드
애니메이션 효과를 위한 많은 위젯들과 옵션들이 존재한다. 또한 가끔은 위젯에 애니메이션을 주고 싶을 것이다. 많은 경우에 있어서 원하는 애니메이션을 주고 싶다면 가장 적합한 위젯은 바로 AnimatedWidget 일 것이다.
이 AnimatedWidget은 애니메이션 효과를 보다 쉽게 구현하기 위해 제공되는 추상 클래스인데 이 클래스는 Listenable 객체의 변경을 감지하고 변경이 있을 때마다 자동으로 build 메서드를 호출하여 위젯을 다시 그리게 된다. 조금 어렵지만 바로 알아보자.
예제 영상에서 나온 애니메이션을 만들어 볼 것인데, 먼저 사용하기 위해선 AnimatedWidget을 확장한 class를 생성해줘야 한다.
class ButtonTransition extends AnimatedWidget {
}
이렇게 AnimatedWidget을 확장시켰다면 아마 오류가 뜰 것이다.

오류가 뜨는 이유로는 생성자를 추가하지 않아서 그렇다. 코드액션( Quick Fix / Intention Actions ) 기능으로 생성자를 추가해 주면 아래와 같은 코드가 될 것인데 여기서 반드시 전달해야 하는 것이 있다.
class ButtonTransition extends AnimatedWidget {
const ButtonTransition({super.key});
}
바로 listenable인데 이게 이 AnimatedWidget의 핵심이라고 불러도 무방한 것이다.
이 Listenable은 변화가 있을 때 리스너에게 알릴 수 있는 객체로 Animation이나 ChangeNotifier를 대표적으로 생각하면 되는데 이 AnimatedWidget을 확장시킨 클래스에서 변화가 생기면 이 클래스, 현재 코드에선 ButtonTransition을 호출하고 있는 곳에 알림을 보내는 역할을 한다고 생각하면 된다. 암튼 이 친구를 생성자에 반드시 넣어야 한다.
근데 지금 나는 예제 영상에서 border의 width가 커지고 작아지는 애니메이션을 넣으려고 하기 때문에 생성자에 width를 하나 추가해 주겠다.
class ButtonTransition extends AnimatedWidget {
const ButtonTransition({super.key, required Animation<double> width})
: super(listenable: width);
}
이렇게 만들어주면 된다.
이제 그 밑에 build() 메서드를 하나 작성해줘야 한다.
class ButtonTransition extends AnimatedWidget {
const ButtonTransition({super.key, required Animation<double> width})
: super(listenable: width);
@override
Widget build(BuildContext context) {
return Container();
}
}
여기서 한번 설명하고 넘어가겠다.
먼저 내가 만든 ButtonTransition은 AnimatedWidget을 상속받은 커스텀 위젯이다. 이제 이 위젯에서 생성자를 전달하게 되는데 이 생성자 안에 들어있는 값으로는 Animation<double> 타입의 width 값을 받은 super(listenable: width) 이 되겠다.
즉, 이 위젯은 내부적으로 width의 값이 바뀔 때마다 build() 메서드를 자동 호출하게 된다. 다시 말해 이 위젯은 width 애니메이션 값을 감지하고 자동으로 리빌드 되는 위젯이라는 것이다. 이제 대충 이해가 되었는가?
근데 여기서 필수로 해야 할 것이 끝난 게 아니다. 바로 getter를 하나 만들어줘야 하는데..
class ButtonTransition extends AnimatedWidget {
const ButtonTransition({super.key, required Animation<double> width})
: super(listenable: width);
Animation<double> get width => listenable as Animation<double>; // 추가
@override
Widget build(BuildContext context) {
return Container();
}
}
이렇게 만들어주는 이유는 이것이 AnimatedWidget을 상속받는 곳에서 애니메이션 값을 간결하고 안전하게 가져오기 위한 패턴이기 때문이다.
각각의 의미 설명을 해보자.
super(listenable: width)
위의 코드에서 AnimatedWidget은 Listenable을 인자로 받는데 생성자에서 만들어둔 width의 타입인 Animation<double>은 Listenable을 상속받기 때문에 사용이 가능하게 된다. 그래서 타입을 이런 식으로 (Animaton<T>) 지정해놓지 않게 되면 listenable에 width를 전달할 수 없게 된다. 이제 이걸로 Animation<double>이 변경될 때마다 build()가 자동 호출되겠다.
Animation<double> get width => listenable as Animation<double>;
그렇다면 이 위의 코드는 무엇이냐? 기본적으로 listenable은 AnimatedWidget 내부에서 선언되어 있는데 그 코드는 이러하다.
final Listenable listenable; 그런데 현재 여기서 이것을 Animation<double>로 사용하고 싶으니 형변환(as)해서 꺼내는 것이 되겠다. 이렇게 하면 .value를 사용하여 현재 애니메이션 값을 얻을 수 있게 된다. 이것은 아래의 코드를 보면 알 것이다.
이제 여기에서 build() 메서드를 조금 만들어주겠다.

예제 영상에 있는 위와 같은 위젯을 제작할 것인데 Container를 사용하여 커스텀으로 만들어줬다.
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () {
print('Hello');
},
child: Container(
width: 175,
height: 50,
decoration: BoxDecoration(
border: Border.all(width: width.value, color: Colors.blue), // 여기!
borderRadius: BorderRadius.all(Radius.circular(8)),
),
child: Center(
child: Text(
'Click Me!',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w600,
),
),
),
),
);
}
build() 메서드는 이렇게 만들어 주었다. 이렇게 하면 사진과 얼추 비슷한 결과물이 나올 것인데 이제 이 위의 코드에서 우리가 주목해야 할 부분은 하나이다. 바로 Border.all(width: width.value) 이곳인데 위에서 말했듯이 .value를 사용하여 현재 애니메이션 값을 얻을 수 있다고 말했었다. 즉 이 코드에서는 보더의 width의 크기가 애니메이션 값에 따라 커지고 작아지게 될 것이라는 것이다.
이제 이렇게 커스텀 위젯을 제작했으니 사용해주기만 하면 되는 데 사용해 주는 것도 은근 난관이다.
먼저 AnimationController를 제작해줘야 한다. 그러니 당연하게도 Mixin도 해줘야 할 것이다.
class _TestState extends State<Test> with TickerProviderStateMixin {
late final AnimationController animationController;
@override
void initState() {
super.initState();
animationController = AnimationController(
vsync: this,
duration: Duration(seconds: 1)
)..repeat(reverse: true);
}
@override
void dispose() {
animationController.dispose();
super.dispose();
}
Minxin은 TickerProviderStateMixin을 사용하였고 애니메이션컨트롤러는 이니스테이트에서 선언해 주었다. 또한 repeat를 붙여 무한 반복하게 만들어줬다.
근데 여기서 끝나면 안 된다. 지금 내가 하려고 하는 것은 컨테이너의 보더 크기를 조정하려는 것, 즉 Animation<double> 타입의 컨트롤러도 하나 만들어놔야 한다는 것이다.
class _TestState extends State<Test> with TickerProviderStateMixin {
late final AnimationController animationController;
late final Animation<double> widthAnimation; // 선언
@override
void initState() {
super.initState();
animationController = AnimationController(
vsync: this,
duration: Duration(seconds: 1)
)..repeat(reverse: true);
widthAnimation = Tween<double>(begin: 1, end: 7).animate(animationController);
// 버튼 테투리-> 1~7
}
왜 이렇게 작성했냐 하면 AnimationController는 기본적으로 0.0~1.0 사이의 숫자만 다루기 때문에, 원하는 범위로 바꾸려면 Tween을 써서 확장하거나 변형해야 했다. 이 위의 코드에서는 그렇게 변경하여 width가 될 값을 넣어줬다. 즉 나는 지금 버튼의 테두리 두께를 1px에서 7px까지 변화시키고 싶기 때문에 Tween(begin: 1, end: 7), 이렇게 사용해 준 것이다.
이제 마지막으로 만든 커스텀 위젯을 불러오고 애니메이션컨트롤러를 넣어주면 끝이 되겠다.
@override
Widget build(BuildContext context) {
return ButtonTransition(width: widthAnimation);
}
이렇게 AnimatedWidget에 대하여 알아보았다. 솔직히 뭐 대부분의 위젯에 다 적용이 가능한 애니메이션이라곤 하나 제작하는 과정이 은근히 귀찮고 또 초보자는 그냥 쓰지 말라고 만들어놓은 느낌이라 퍽 거부감이 드는 것이 어쩔 수 없겠다. 또한 공식 문서의 설명도 친절한 편이 아니라 직접 공부하게 된다면 시간이 삭제가 된다는 점도 마음에 들지 않는다.. 암튼 도움이 되었길 바라며 마치겠다.