민프

[Flutter] animation 효과를 적용해보자 (animationController, SingleTickerProviderStateMixin, vsync) 본문

[Flutter]

[Flutter] animation 효과를 적용해보자 (animationController, SingleTickerProviderStateMixin, vsync)

민프야 2023. 9. 7. 17:50

Flutter에서 Animation을 적용해보려고 합니다.

 

Flutter에서 Animation을 보다 쉽게 적용하는 방법은 크게

1. animation 위젯 사용

2. animationController 사용

이 있습니다.

 

예제 화면 설명은

영상이 플레이 되고 있는 상태 인 화면을 터치하면 AnimatedOpacity가 적용된 플레이 이모티콘이 나오면서 영상이 정지가 되고,

정지 된 영상 화면을 클릭하면 AnimatedOpacity가 적용된 플레이 이모티콘이 사라지게 되면서 영상이 플레이 되는 화면 입니다.

1. animation 위젯 사용

https://docs.flutter.dev/ui/widgets/animation

 

Animation and motion widgets

A catalog of Flutter's animation widgets.

docs.flutter.dev

애니메이션 위젯들은 위 링크를 통해서 어떤 것들이 있는지 확인하면 되고 이번 포스팅에서는 AnimatedOpacity를 선택해서 적용을 해보겠습니다.

 

  @override
  Widget build(BuildContext context) {
    return VisibilityDetector(
      key: Key("${widget.index}"),
      onVisibilityChanged: (VisibilityInfo info) {
        _onVisibilityChanged(info);
      },
      child: Stack(
        children: [
          Positioned.fill(
            child: _videoPlayerController.value.isInitialized
                ? VideoPlayer(_videoPlayerController)
                : Container(
                    color: Colors.teal,
                  ),
          ),
          Positioned.fill(
            child: GestureDetector(
              onTap: _onTogglePause,
            ),
          ),
          Positioned.fill(
              child: IgnorePointer(
            child: Center(
              child: AnimatedOpacity(
                opacity: _isPaused ? 1 : 0,
                duration: _animationDuration,
                child: const FaIcon(
                  FontAwesomeIcons.play,
                  color: Colors.white,
                  size: Sizes.size48,
                ),
              ),
            ),
          )),
        ],
      ),
    );
  }

아래 결과 화면을 보면 AnimatedOpacity가 잘 적용된 것을 확인하실 수 있습니다. 

결과화면

2. animationController 사용

그럼 두번째로 animationController를 이용해서 애니메이션을 추가해보겠습니다.

animationController를 이용해서 이벤트를 추가할때
AnimatedOpacity가 부드럽게 변하는 것 과 같은 효과를 주려면

build 메서드에게 소수점 단위로 업데이트해줘서 부드럽게 변경을 해줘야합니다.

 

예를 들어서)

Opacity가 1 -> 0으로 갈때

0.9 -> 0.8 -> 0.7 ... 0 으로 가야 부드럽게 변경되는 것 처럼 보일 수 있게 됩니다.

근데 만약 1에서 0으로 바로 바뀌게 된다면 부드럽게 보이는 것 보다는 on / off 처럼 켰다 꺼졌다 처럼 보이게 될 것 입니다.

 

그럼 animationController를 어떻게 사용해야할까요? 

개발자 문서에 보면

animationController를 사용할 때 vsync인자를 받게 되는데 여기에서 vsync를 사용할 때 SingleTickerProviderStateMixin를 추가해서 정의하라고 나와있습니다.

 

그럼 여기에서 

vsync,

SingleTickerProviderStateMixin

는 뭘까?

 

문서에 의하면

vsync는 위젯이 안보일 때는 애니메이션이 작동하지 않도록 하는 것, 

SingleTickerProviderStateMixin는 현재 위젯 트리가 활성화된 동안만 Tick 하는 단일 Ticker를 제공하는 것 즉, 위젯이 화면에 보일때만 Ticker를 제공해준 다는 말 입니다.

 

그럼 실제 animationController를 정의하는 아래 코드를 보면

class _VideoPostState extends State<VideoPost>
    with SingleTickerProviderStateMixin {
.
.
.

 @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      vsync: this,
      lowerBound: 1.0,
      upperBound: 1.5,
      value: 1.5,
      duration: _animationDuration,
    );
  }

with SingleTickerProviderStateMixin를 하여서 SingleTickerProviderStateMixin의 클래스 및 메서드들을 가져온 것을 확인할 수 있고, 

vsync : this를 하여서 animationController에 필요한 SingleTickerProviderStateMixin 내부에 있는 vsync 메서드를 적용시킨 것을 확인할 수 있습니다.

 

그럼 위 컨트롤러가 어떻게 동작하는걸까요?

animationController는 부드럽게 처리가 되어야합니다.

그래서 매 초, 매 밀리초도 아니고 애니메이션의 프레임마다 실행됩니다. 

 

예를 들어서)

초당 60프레임에 0부터 1까지 엄청 부드럽게 애니메이션 효과를 줘야한다고 했을 때,

애니메이션에 callback을 제공해주는 게 바로 Ticker 입니다.

 

실제로 SingleTickerProviderStateMixin의 내부 코드를 보면

.
.
.
@optionalTypeArgs
mixin SingleTickerProviderStateMixin<T extends StatefulWidget> on State<T> implements TickerProvider {
  Ticker? _ticker;

  @override
  Ticker createTicker(TickerCallback onTick) {
    assert(() {
      if (_ticker == null) {
        return true;
      }
      throw FlutterError.fromParts(<DiagnosticsNode>[
        ErrorSummary('$runtimeType is a SingleTickerProviderStateMixin but multiple tickers were created.'),
        ErrorDescription('A SingleTickerProviderStateMixin can only be used as a TickerProvider once.'),
        ErrorHint(
          'If a State is used for multiple AnimationController objects, or if it is passed to other '
          'objects and those objects might use it more than one time in total, then instead of '
          'mixing in a SingleTickerProviderStateMixin, use a regular TickerProviderStateMixin.',
        ),
      ]);
    }());
    _ticker = Ticker(onTick, debugLabel: kDebugMode ? 'created by ${describeIdentity(this)}' : null);
    _updateTickerModeNotifier();
    _updateTicker(); // Sets _ticker.mute correctly.
    return _ticker!;
  }
 .
 .
 .

이렇게 시작이 되는데 this로 호출이 되면 매 프레임마다 아래 코드가 callback(onTick)되어서 Ticker가 생성 되게 됍니다.

    _ticker = Ticker(onTick, debugLabel: kDebugMode ? 'created by ${describeIdentity(this)}' : null);

그 이후 애니메이션이 Ticker를 잡을 거고,

애니메이션이 재생되어야 할 때가 오면 Ticker가 그걸 애니메이션에게 알려주게 됩니다.

 

 

따라서 animationController에서 업데이트 된 부분을 build 메서드에게 업데이트가 되는 것을 전달해야하는데

전달하는 방법은 

1. setState({})

2. AnimatedBuilder

가 있습니다.

 

결과화면은 아래와 같습니다.

- 1. setState({})

첫번째 방법은 아래 코드와 같이 setState를 호출해서 build메서드에게 전달해주는 방법입니다.

  // 변경 된 수치를 setState로 Build에게 업데이트 요청
     _animationController.addListener(() {
      setState(() {});
    });

전체 코드는 아래와 같습니다.

.
.
.
late final AnimationController _animationController;

@override
  void initState() {
    super.initState();
    _initVideoPlayer();
    // 애니메이션 컨트롤러 설정
    _animationController = AnimationController(
      vsync: this,
      lowerBound: 1.0,
      upperBound: 1.5,
      value: 1.5,
      duration: _animationDuration,
    );
    
    // 변경 된 수치를 setState로 Build에게 업데이트 요청
     _animationController.addListener(() {
      setState(() {});
    });

  }  
  
  void _onTogglePause() {
    if (_videoPlayerController.value.isPlaying) {
      _videoPlayerController.pause();
      _animationController.reverse();
    } else {
      _videoPlayerController.play();
      _animationController.forward();
    }

    setState(() {
      _isPaused = !_isPaused;
    });
  }

  @override
  Widget build(BuildContext context) {
    return VisibilityDetector(
      key: Key("${widget.index}"),
      onVisibilityChanged: (VisibilityInfo info) {
        _onVisibilityChanged(info);
      },
      child: Stack(
        children: [
          Positioned.fill(
            child: _videoPlayerController.value.isInitialized
                ? VideoPlayer(_videoPlayerController)
                : Container(
                    color: Colors.teal,
                  ),
          ),
          Positioned.fill(
            child: GestureDetector(
              onTap: _onTogglePause,
            ),
          ),
          Positioned.fill(
              child: IgnorePointer(
            child: Center(
              child: Transform.scale(
                scale: _animationController.value,
                child: AnimatedOpacity(
                  opacity: _isPaused ? 1 : 0,
                  duration: _animationDuration,
                  child: const FaIcon(
                    FontAwesomeIcons.play,
                    color: Colors.white,
                    size: Sizes.size48,
                  ),
                ),
              ),
            ),
          )),
        ],
      ),
    );
  }

- 2. AnimatedBuilder

아래 코드에서와 같이 AnimatedBuilder를 사용하게 되면

_animationController가 업데이트 될 때 마다 build에게 전달이 되어서 애니메이션이 잘 동작하는 것을 확인할 수 있습니다.

.
.
.
late final AnimationController _animationController;

@override
  void initState() {
    super.initState();
    _initVideoPlayer();
    // 애니메이션 컨트롤러 설정
    _animationController = AnimationController(
      vsync: this,
      lowerBound: 1.0,
      upperBound: 1.5,
      value: 1.5,
      duration: _animationDuration,
    );

  }
  
  void _onTogglePause() {
    if (_videoPlayerController.value.isPlaying) {
      _videoPlayerController.pause();
      _animationController.reverse();
    } else {
      _videoPlayerController.play();
      _animationController.forward();
    }

    setState(() {
      _isPaused = !_isPaused;
    });
  }


@override
  Widget build(BuildContext context) {
    return VisibilityDetector(
      key: Key("${widget.index}"),
      onVisibilityChanged: (VisibilityInfo info) {
        _onVisibilityChanged(info);
      },
      child: Stack(
        children: [
          Positioned.fill(
            child: _videoPlayerController.value.isInitialized
                ? VideoPlayer(_videoPlayerController)
                : Container(
                    color: Colors.teal,
                  ),
          ),
          Positioned.fill(
            child: GestureDetector(
              onTap: _onTogglePause,
            ),
          ),
          Positioned.fill(
              child: IgnorePointer(
            child: Center(
              child: AnimatedBuilder(
                animation: _animationController,
                builder: (context, child) {
                  //_animationController 값이 변경 될 때마다 실행됨
                  return Transform.scale(
                    scale: _animationController.value,
                    child: child,
                  );
                },
                child: AnimatedOpacity(
                  opacity: _isPaused ? 1 : 0,
                  duration: _animationDuration,
                  child: const FaIcon(
                    FontAwesomeIcons.play,
                    color: Colors.white,
                    size: Sizes.size48,
                  ),
                ),
              ),
            ),
          )),
        ],
      ),
    );
  }

참고링크

https://docs.flutter.dev/ui/animations/tutorial

 

Animations tutorial

A tutorial showing how to build explicit animations in Flutter.

docs.flutter.dev

https://api.flutter.dev/flutter/widgets/SingleTickerProviderStateMixin-mixin.html

 

SingleTickerProviderStateMixin mixin - widgets library - Dart API

SingleTickerProviderStateMixin mixin Provides a single Ticker that is configured to only tick while the current tree is enabled, as defined by TickerMode. To create the AnimationController in a State that only uses a single AnimationController, mix in this

api.flutter.dev

https://api.flutter.dev/flutter/scheduler/Ticker-class.html

 

Ticker class - scheduler library - Dart API

Calls its callback once per animation frame. When created, a ticker is initially disabled. Call start to enable the ticker. A Ticker can be silenced by setting muted to true. While silenced, time still elapses, and start and stop can still be called, but n

api.flutter.dev

 

Comments