Ticker
Ticker와 TickerProvider mixin에 대해 알아봅니다.읽는데 8분 정도 걸려요.Flutter로 개발하다보면 TabController, AnimationController와 같은 Controller에 vsync: this
를 반드시 종속성 주입해야 하는 경우가 있습니다.
이럴 때 마다 TickerProviderStateMixin 을 상속받아 사용해서 this를 사용할 수 있게 되는데, 이게 항상 궁금했었죠...
그러다, Flutter 애니메이션을 공부하다 그 이유를 알게되었고, 그 내용을 정리해보고자 합니다.
Ticker
우선 Ticker에 대해 알아야 합니다.
Ticker는 매 프레임마다 함수를 호출하는 역할을 수행하는데, 아래의 코드를 살펴봅시다.
late final Ticker _ticker;
void initState() {
super.initState();
_ticker = Ticker(
(elapsed) => print(elapsed);
);
_ticker.start();
}
void dispose() {
_ticker.dispose();
super.dispose();
}
이렇게 하면, 이 위젯이 위젯트리에 올라가서 상태가 컨텍스트에 등록되는 순간부터 매 프레임마다 elapsed가 찍히게 됩니다. (대략 60fps)
만약, _ticker.dispose()
를 호출하지 않는다면, 위젯트리에서 내려가도 Ticker가 계속 함수를 매 프레임마다 호출하게 될 겁니다.
정리하자면, 매 프레임마다 함수를 호출하게 해주는 클래스가 Ticker 입니다.
하지만, 왜 Ticker를 사용하는 걸까요?
바로 부드러운 화면 모션 때문입니다.
매 프레임마다 함수를 실행한다는 것은, 매 프레임마다 화면을 업데이트 시킬 수도 있다는 뜻입니다.
자원을 많이 잡아먹긴 하겠지만, 최적화만 잘 한다면, 부드러운 애니메이션을 보여줄 수 있겠죠!
TickerProvider
이제, Ticker가 뭔지, 왜 사용하는지를 알았습니다.
하지만, 강력한 기능을 가진 만큼, 취급에 주의해야 하겠죠...
생성하고, 함수를 등록하고, 반드시 dispose 해야하는...
이런 과정을 개발자에게 맡겼다간 반드시 실수가 발생할 것입니다.
따라서 Ticker의 사용을 은닉화 하기위해 우선 Ticker을 생성하는 인터페이스(정확히는 abstract class)가 제공되는데, 이게 바로 TickerProvider 입니다.
abstract class TickerProvider {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const TickerProvider();
/// Creates a ticker with the given callback.
///
/// The kind of ticker provided depends on the kind of ticker provider.
Ticker createTicker(TickerCallback onTick);
}
이 클래스를 상속받은 클래스는 반드시 createTicker 구현해서 사용해야 하며, 이름에서 알 수 있듯 이 함수는 Ticker를 반환하게 됩니다.
예로 들어, TickerProvider의 구현체 중 하나인 SingleTickerProviderStateMixin의 내부를 살펴보겠습니다.
mixin SingleTickerProviderStateMixin<T extends StatefulWidget> on State<T> implements TickerProvider {
Ticker? _ticker;
Ticker createTicker(TickerCallback onTick) {
_ticker = Ticker(onTick, debugLabel: kDebugMode ? 'created by ${describeIdentity(this)}' : null);
_updateTickerModeNotifier();
_updateTicker(); // Sets _ticker.mute correctly.
return _ticker!;
}
void dispose() {
_tickerModeNotifier?.removeListener(_updateTicker);
_tickerModeNotifier = null;
super.dispose();
}
}
간단하게만 보면 해당 mixin에서 Ticker 생성과 삭제에 필요한 모든 기능을 수행한다고 볼 수 있습니다.
필요한 모든 기능 중, 가장 중요한 기능은 자원 관리
라고 생각됩니다.
실제로 설명의 최상단에도 언급되어 있습니다.
Provides a single [Ticker] that is configured to only tick while the current tree is enabled, as defined by [TickerMode].
즉, 사용을 강제하는 주된 이유는 위젯이 위젯 트리에 등록된 상태에서만 Ticker를 제공함으로서 자원을 효율적으로 실수 없이 관리하기 위함이라고 생각됩니다.
Controller
생성했으면 이를 사용해야겠죠?
이 때, vsync 가 사용됩니다.
class AnimationController extends Animation<double>
with AnimationEagerListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin {
AnimationController({
double? value,
this.duration,
this.reverseDuration,
this.debugLabel,
this.lowerBound = 0.0,
this.upperBound = 1.0,
this.animationBehavior = AnimationBehavior.normal,
required TickerProvider vsync,
}) : assert(upperBound >= lowerBound),
_direction = _AnimationDirection.forward {
_ticker = vsync.createTicker(_tick);
_internalSetValue(value ?? lowerBound);
}
}
vsync는 TickerProvider를 인자로 받게 되는데, TickerProviderStateMixin을 상속받은 클래스(위젯)에서 vsync: this
를 다형성의 원리에 의해 종속성 주입하면 해당 클래스(위젯)의 createTicker 함수를 호출할 수 있게 되는 것입니다.
즉, Controller는 부드러운 애니메이션을 위해 Ticker를 사용해야 하는데, 개발자에게 이를 맡겼다간 실수가 발생할 수도 있고, 기타 여러 이유 때문에 TickerProvider 로부터 ticker를 받도록 구현되었고, 그 과정에서 vsync로 종속성을 주입받게 되었다고 정리해볼 수 있겠습니다.
Animation 엿보기
정리하는 차원에서 _animationController.forward()
의 과정을 추적해봅시다.
TickerFuture forward({ double? from }) {
_direction = _AnimationDirection.forward;
if (from != null) {
value = from;
}
return _animateToInternal(upperBound);
}
애니메이션의 방향(_direction)을 설정하고 _animateToInternal
함수를 실행합니다.
TickerFuture _animateToInternal(double target, { Duration? duration, Curve curve = Curves.linear }) {
double scale = 1.0;
if (SemanticsBinding.instance.disableAnimations) {
switch (animationBehavior) {
case AnimationBehavior.normal:
scale = 0.05;
case AnimationBehavior.preserve:
break;
}
}
Duration? simulationDuration = duration;
if (simulationDuration == null) {
final double range = upperBound - lowerBound;
final double remainingFraction = range.isFinite ? (target - _value).abs() / range : 1.0;
final Duration directionDuration =
(_direction == _AnimationDirection.reverse && reverseDuration != null)
? reverseDuration!
: this.duration!;
simulationDuration = directionDuration * remainingFraction;
} else if (target == value) {
simulationDuration = Duration.zero;
}
stop();
if (simulationDuration == Duration.zero) {
if (value != target) {
_value = clampDouble(target, lowerBound, upperBound);
notifyListeners();
}
_status = (_direction == _AnimationDirection.forward) ?
AnimationStatus.completed :
AnimationStatus.dismissed;
_checkStatusChanged();
return TickerFuture.complete();
}
return _startSimulation(_InterpolationSimulation(_value, target, simulationDuration, curve, scale));
}
_startSimulation
실행에 필요한 값들을 연산하고, 기존에 실행 중이던 애니메이션을 stop 하는 것으로 보입니다.
이 후, _startSimulation
함수를 실행합니다.
TickerFuture _startSimulation(Simulation simulation) {
_simulation = simulation;
_lastElapsedDuration = Duration.zero;
_value = clampDouble(simulation.x(0.0), lowerBound, upperBound);
final TickerFuture result = _ticker!.start();
_status = (_direction == _AnimationDirection.forward) ?
AnimationStatus.forward :
AnimationStatus.reverse;
_checkStatusChanged();
return result;
}
초기 값을 설정하고 _ticker를 시작하는 모습입니다.
참고로 위 코드를 보면 아시겠지만, _ticker는 _tick
함수를 매 프레임마다 호출하게 됩니다.
따라서 _tick
함수도 살펴봅시다.
void _tick(Duration elapsed) {
_lastElapsedDuration = elapsed;
final double elapsedInSeconds = elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond;
_value = clampDouble(_simulation!.x(elapsedInSeconds), lowerBound, upperBound);
if (_simulation!.isDone(elapsedInSeconds)) {
_status = (_direction == _AnimationDirection.forward) ?
AnimationStatus.completed :
AnimationStatus.dismissed;
stop(canceled: false);
}
notifyListeners();
_checkStatusChanged();
}
매 프레임마다 _value값이 업데이트 되고, notifyListeners를 통해 화면을 업데이트 하는 모양새입니다.