Lerp (애니메이션 선형 보간)

애니메이션을 이용해서 선형 보간(Linear Interpolation)를 쉽게 구현해봅시다.읽는데 8분 정도 걸려요.

Flutter Color의 lerp() 와 연결되는 내용은 아닙니다.
(이론적인 부분은 일맥상통 하지만, 정확히는 애니메이션에 대한 포스트 입니다)

우선 시작하기에 앞서 gyro_provider 플러그인을 소개합니다~
저의 첫 라이브러리이자, pub.dev 데뷔 작품(?)입니다 ㅎㅎ

gyro_provider_demo

모바일 기기의 자이로스코프 센서를 이용해서 위젯을 변형시키거나, 센서 값을 제공하는 라이브러리 입니다.
이번 포스트는 이 플러그인을 개발하며 배웠던 내용을 소개 하겠습니다.

문제 인지

Flutter Animation은 AnimationController가 지정한 시간(Duration)동안 동작합니다.

.dart
_animationController = AnimationController(
  vsync: this,
  duration: Duration(seconds: 1),
)

그리고 Animation은 AnimationController에 의해 변화되는 값을 보관하고 있죠.

.dart
_xAnimation = Tween<double>(
  begin: 0,
  end: _xTarget,
).animate(_animationController);

예로 들어 AnimationController의 값이 0에서 11초 동안 변할 동안 _xAnimation의 값은 0에서 _xTarget까지 1초 동안 변화하는 것이죠.
즉, AnimationController에 의해 Animation의 값이 선형 보간되는 것입니다.

만약 _xTarget의 값이 도중에 변한다면 어떻게 될까요?

문제 발생

여기서 문제가 발생합니다.

애니메이션은 새로운 _xTarget 값을 향해 바뀌는 것이 아닌, 바뀌기 전의 _xTarget값을 향해 선형 보간될 것입니다.


Lerp

바뀐 후의 _xTarget값을 향하여 선형 보간을 하기 위해서는 AnimatedContainer를 리셋시키고 Animation을 재정의 해야 합니다.

.dart
_animationController.reset();

_xAnimation = Tween<double>(
  begin: 0,
  end: _xTarget, // new value
).animate(_animationController);

_animationController.forward();

하지만 이런식으로 코드를 짠다면 문제가 발생하겠죠.
시작점(begin)이 0으로 초기화 되기 때문입니다.

따라서 begin의 값은 AnimationController를 초기화 하기 직전의 Animation의 값을 가지고 있어야 할 것 입니다.

.dart
var currX = _animation.value;

_animationController.reset();

_xAnimation = Tween<double>(
  begin: currX,
  end: _xTarget, // new value
).animate(_animationController);

_animationController.forward();

위와 같이 코드를 수정한다면, _xTarget가 변경되었을 때, 위 코드를 실행하면
진행되던 애니메이션이 이어서 부드럽게 새로운 애니메이션이 되어 새로운 _xTarget를 향하여 진행할 것입니다.

장점

AnimatedContainer의 Duration보다 짧은 시간안에 _xTarget 값이 변하더라도 애니메이션을 부드럽게 진행시킬 수 있습니다.

즉, _xTarget이 변할 때마다 새로운 선형 보간을 적용할 수 있게 되는 것입니다.

단점

AnimatedContainer의 Duration이 길수록 '뭔가 늦게 반응한다' 라는 느낌을 받으실 수 있습니다.

그렇다고 Duration을 _xTarget이 변화하는 주기보다 짧게 설정할 경우에는 애니메이션 중간마다 끊기는 느낌을 받을 수 있습니다.


코드 리뷰

그럼 선형 보간과 관련한 gyro_provider의 코드를 리뷰해보며 이해를 굳혀봅시다.

forward

우선 애니메이션이 진행 중입니다.

gyro_provider.dart
  
  void initState() {
    super.initState();
    ...
    // Initialize animation
    _animationController = AnimationController(
      vsync: this,
      duration: widget.animationDuration,
    )
      ..addListener(_animationListener)
      ..addStatusListener(_animationStatusListener);

    _linearCurve = CurvedAnimation(
      parent: _animationController,
      curve: Curves.linear,
    );
    _easeCurve = CurvedAnimation(
      parent: _animationController,
      curve: Curves.easeOut,
    );

    _xAnimation = Tween<double>(
      begin: 0,
      end: _xTarget,
    ).animate(_linearCurve);
    _yAnimation = Tween<double>(
      begin: 0,
      end: _yTarget,
    ).animate(_linearCurve);
    _animationController.forward();
  }

0에서 _xTarget 으로 이동 중이겠죠.

change target

그러다 _xTarget의 값이 변화합니다.

gyro_provider.dart
  void _gyroListener() {
    var value = _gyroscopeController.value;
    _gyroData.value = value;
    widget.gyroscope?.call(value);

    if (value.x.abs() < 0.1 && value.y.abs() < 0.1 && !widget.centerLock) {
      ...
    }
    // Change the target rotation angle by the amount the sensor value changes
    // and animate toward that value.
    else {
      _xTarget += value.x;
      _yTarget += value.y;
    }
    // The animation only changes when the widget is not being moved to the center.
    if (!_onCenter) {
      _animation(curve: _linearCurve);
    }
  }

하지만, 현재 AnimatedContainer가 변형시키는 Animation의 _xTarget 값은 변화하지 않았습니다.

reset

따라서 애니메이션을 중지시키고 재설정을 합니다.
하지만, 중지 시키기 전에 현재까지 변화한 Animation의 값을 보관합니다.

gyro_provider.dart
  void _animation({required CurvedAnimation curve}) {
    if (!mounted) return;

    var xCurr = _xAnimation.value;
    var yCurr = _yAnimation.value;
    _animationController.reset();
    _xAnimation = Tween<double>(
      begin: xCurr,
      end: _xTarget,
    ).animate(curve);
    _yAnimation = Tween<double>(
      begin: yCurr,
      end: _yTarget,
    ).animate(curve);
    _animationController.forward();
  }

(re)forward

그리고 다시 애니메이션을 동작 시킵니다.

gyro_provider.dart
  void _animation({required CurvedAnimation curve}) {
    if (!mounted) return;

    var xCurr = _xAnimation.value;
    var yCurr = _yAnimation.value;
    _animationController.reset();
    _xAnimation = Tween<double>(
      begin: xCurr,
      end: _xTarget,
    ).animate(curve);
    _yAnimation = Tween<double>(
      begin: yCurr,
      end: _yTarget,
    ).animate(curve);
    _animationController.forward();
  }

마무리

코드 전문은 오픈소스로 공개된 gyro_provider 레포에서 확인하실 수 있습니다.

Flutter Animation에 대해 잘 모르는 상태로 플러그인 개발 프로젝트를 시작했는데,
무작정 시도하긴 했지만, 배워가는 점도 많았던거 같습니다.
(적어도 AnimationController, Animation은 나름 자유자재로 사용할 수 있게 되었다는 거?)

사실 네이티브 코드에서 센서의 인터벌(interval)을 매우 빠르게 설정하면(초당 60 이상) 애니메이션을 구현할 필요도 없을 것입니다.

하지만, 초당 60번의 통신은 성능 저하의 원인이 될 수 있다는 생각이 들었고,
기기 사양이 안좋은 경우에는 초당 60회의 통신이 불가능할 수도 있을지도 몰랐기에 이런 방식으로 구현했습니다.
(초당 10~15회 통신, 기기 주사율에 맞도록 사이의 값은 애니메이션을 이용해 lerp(선형 보간))
이 부분을 문제 인지에 적는게 나았을지도..

사실 신경 쓸 필요도 없을지도 모르지만, 이런 고민 덕분에 새로운 시도를 할 수 있게 되어 오히려 좋았던 거 같습니다.