본문 바로가기
flutter/Widget of the Week

Flutter[플러터] / AnimatedWidget 를 사용하여 위젯에 애니메이션 넣기 (추상, Listenable, 효과, 지속되는, stroke width change, 컨트롤러, AnimationController, Animation<T>) AnimatedWidget (Flutter Widget of the Week)

by ch5c 2025. 7. 6.
반응형

AnimatedWidget class

주어진 Listenable의 값이 변경될 때 다시 빌드되는 위젯입니다.

AnimatedWidget 은 일반적으로 Listenable 인 Animation 객체와 함께 사용되지만 ChangeNotifier  ValueNotifier를 포함한 모든 Listenable 과 함께 사용할 수 있습니다.

AnimatedWidget 은 상태가 없는 위젯에 가장 유용합니다. AnimatedWidget을 사용하려면 하위 클래스를 만들고 build 함수를 구현해야 합니다.

https://youtu.be/LKKgYpC-EPQ

공식 문서 코드

 


애니메이션 효과를 위한 많은 위젯들과 옵션들이 존재한다. 또한 가끔은 위젯에 애니메이션을 주고 싶을 것이다. 많은 경우에 있어서 원하는 애니메이션을 주고 싶다면 가장 적합한 위젯은 바로 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을 호출하고 있는 곳에 알림을 보내는 역할을 한다고 생각하면 된다. 암튼 이 친구를 생성자에 반드시 넣어야 한다.

근데 지금 나는 예제 영상에서 borderwidth가 커지고 작아지는 애니메이션을 넣으려고 하기 때문에 생성자에 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();
  }
}

여기서 한번 설명하고 넘어가겠다.

먼저 내가 만든 ButtonTransitionAnimatedWidget을 상속받은 커스텀 위젯이다. 이제 이 위젯에서 생성자를 전달하게 되는데 이 생성자 안에 들어있는 값으로는 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)

위의 코드에서 AnimatedWidgetListenable을 인자로 받는데 생성자에서 만들어둔 width의 타입인 Animation<double>Listenable을 상속받기 때문에 사용이 가능하게 된다. 그래서 타입을 이런 식으로 (Animaton<T>) 지정해놓지 않게 되면 listenablewidth를 전달할 수 없게 된다. 이제 이걸로 Animation<double>이 변경될 때마다 build()가 자동 호출되겠다.

Animation<double> get width => listenable as Animation<double>;

그렇다면 이 위의 코드는 무엇이냐? 기본적으로 listenableAnimatedWidget 내부에서 선언되어 있는데 그 코드는 이러하다.

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();
  }

MinxinTickerProviderStateMixin을 사용하였고 애니메이션컨트롤러는 이니스테이트에서 선언해 주었다. 또한 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에 대하여 알아보았다. 솔직히 뭐 대부분의 위젯에 다 적용이 가능한 애니메이션이라곤 하나 제작하는 과정이 은근히 귀찮고 또 초보자는 그냥 쓰지 말라고 만들어놓은 느낌이라 퍽 거부감이 드는 것이 어쩔 수 없겠다. 또한 공식 문서의 설명도 친절한 편이 아니라 직접 공부하게 된다면 시간이 삭제가 된다는 점도 마음에 들지 않는다.. 암튼 도움이 되었길 바라며 마치겠다.

반응형