Flutter系列九:Flutter动画源码解析

17 篇文章 8 订阅
16 篇文章 0 订阅

上篇文章我们详细介绍了动画的使用,本文我们将从源码的角度解析动画的底层逻辑。

动画的实现机制

动画的控制是由AnimationController来实现的,这样我们就先从AnimationController来入手研究。

  • AnimationController构造函数
<!-- AnimationController -->
AnimationController({
    double? value,
    this.duration,
    this.reverseDuration,
    this.debugLabel,
    this.lowerBound = 0.0,
    this.upperBound = 1.0,
    this.animationBehavior = AnimationBehavior.normal,
    required TickerProvider vsync,
}) : _direction = _AnimationDirection.forward {
     _ticker = vsync.createTicker(_tick);
     _internalSetValue(value ?? lowerBound);
}

void _tick(Duration elapsed) {
    // 省略内容...
}

Ticker? _ticker;

AnimationController的构造函数中用vsync.createTicker(_tick)方法初始化了属性_ticker,并设置了动画的初始值。

_ticker中一个重要的作用是持有了一个回调函数void _tick(Duration elapsed),这个_tick回调函数的作用我们后面会详细解释。

  • AnimationControllerforward方法
<!-- AnimationController -->
TickerFuture forward({ double? from }) {
    _direction = _AnimationDirection.forward;
    if (from != null)
      value = from;
    return _animateToInternal(upperBound);
}

TickerFuture _animateToInternal(double target, { Duration? duration, Curve curve = Curves.linear }) {
    stop();
    // 省略内容...
    return _startSimulation(_InterpolationSimulation(_value, target, simulationDuration, curve, scale));
}

TickerFuture _startSimulation(Simulation simulation) {
    // start
    final TickerFuture result = _ticker!.start();
    return result;
}

void stop({ bool canceled = true }) {
    // stop
    _ticker!.stop(canceled: canceled);
}

forward方法中先调用了_ticker!.stop(canceled: true),然后调用了_ticker!.start()方法。

上面提到的两个方法都是_ticker的方法,那它们做了什么工作呢?

  • Ticker
<!-- Ticker -->
void stop({ bool canceled = false }) {
    // 取消调度Tick
    unscheduleTick();
}
  
TickerFuture start() {
    // 省略内容...
    if (shouldScheduleTick) {
      // 1 开始调度Tick
      scheduleTick();
    }
    if (SchedulerBinding.instance!.schedulerPhase.index > SchedulerPhase.idle.index &&
        SchedulerBinding.instance!.schedulerPhase.index < SchedulerPhase.postFrameCallbacks.index)
      // 2 记录动画开始时间
      _startTime = SchedulerBinding.instance!.currentFrameTimeStamp;
    return _future!;
}  
  1. stop方法调用了unscheduleTick方法取消Tick调度
  2. start方法调用了scheduleTick方法开始Tick调度,并且记录了开始时间。

Tick调度 是什么意思呢?

<!-- Ticker -->
void scheduleTick({ bool rescheduling = false }) {
    _animationId = SchedulerBinding.instance!.scheduleFrameCallback(_tick, rescheduling: rescheduling);
}

void unscheduleTick() {
    if (scheduled) {
      SchedulerBinding.instance!.cancelFrameCallbackWithId(_animationId!);
      _animationId = null;
    }
}
<!-- SchedulerBinding -->
int scheduleFrameCallback(FrameCallback callback, { bool rescheduling = false }) {
    scheduleFrame();
    _nextFrameCallbackId += 1;
    _transientCallbacks[_nextFrameCallbackId] = _FrameCallbackEntry(callback, rescheduling: rescheduling);
    return _nextFrameCallbackId;
}

void cancelFrameCallbackWithId(int id) {
    _transientCallbacks.remove(id);
    _removedIds.add(id);
}
  1. scheduleTick是将AnimationController_tick加入到了SchedulerBinding_transientCallbacks数组中,然后返回了一个对应的回调ID,然后请求刷新界面。
  2. unscheduleTick是根据回调IDAnimationController_tickSchedulerBinding_transientCallbacks数组中移除。
  • SchedulerBinding

进入了我们熟悉的SchedulerBinding了,如果你有阅读过前面的文章应该对它的功能和调用逻辑有印象。

void handleBeginFrame(Duration? rawTimeStamp) {
    // 1. 时间
    _currentFrameTimeStamp = _adjustForEpoch(rawTimeStamp ?? _lastRawTimeStamp);
    if (rawTimeStamp != null)
      _lastRawTimeStamp = rawTimeStamp;

    _hasScheduledFrame = false;
    try {
      
      final Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks;
      _transientCallbacks = <int, _FrameCallbackEntry>{};
      callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {
        if (!_removedIds.contains(id))
          // 2 回调
          _invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp!, callbackEntry.debugStack);
      });
      _removedIds.clear();
    } finally {
    }
}

每次进行界面刷新的时候,Flutter Engine会回调SchedulerBindinghandleBeginFrame方法,还传过来了一个时间戳。这时候会一一调用_transientCallbacks数组中的回调函数,并且将时间戳传进去。

handleBeginFrame是在刷新界面函数_handleDrawFrame之前调用的,这里我们就可以知道handleBeginFrame主要是为了在绘制之前处理和设置好动画的中间值,便于重新绘制。

我们回过来再看看AnimationController_tick的逻辑。

  • _tick方法
void _tick(Duration elapsed) {
    
    _lastElapsedDuration = elapsed;
    // 1. 计算已经动画的时间
    final double elapsedInSeconds = elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond;
    // 2. 计算当前时间对应的动画的值
    _value = _simulation!.x(elapsedInSeconds).clamp(lowerBound, upperBound);
    // 3. 如果动画已经完成进行状态的设置和回调函数的取消
    if (_simulation!.isDone(elapsedInSeconds)) {
      _status = (_direction == _AnimationDirection.forward) ?
        AnimationStatus.completed :
        AnimationStatus.dismissed;
      stop(canceled: false);
    }
    // 4. 通知监听者value发生了变化
    notifyListeners();
    // 5. 通知监听者status发生了变化
    _checkStatusChanged();
}

_tick方法的逻辑是

  1. 先根据SchedulerBinding回调过来的时间戳算出当前动画的时间;
  2. 然后根据时间算出对应的value值;
  3. 如果动画已经完成进行状态的设置和回调函数的取消;
  4. 通知监听者value发生了变化
  5. 通知监听者status发生了变化

至此,动画的实现逻辑就清晰了。

动画逻辑

动画的中间值的计算

我们从_tick方法中看到了动画的中间值是根据时间elapsed来计算的。AnimationController默认是从0-1,假设动画的时长是2s。如果曲线是速率变化线性的,那么elapsed是1的时候,AnimationControllervalue就变成了0.5。这个很好理解。

动画时间动画值
00
0.50.25
1.00.5
1.50.75
21
  • 设置了CurvedAnimation之后呢?
class CurvedAnimation extends Animation<double> {
    double get value {
        final Curve? activeCurve = _useForwardCurve ? curve : reverseCurve;
    
        final double t = parent.value;
        if (activeCurve == null)
          return t;
        if (t == 0.0 || t == 1.0) {
          return t;
        }
        return activeCurve.transform(t);
    }
}

设置了CurvedAnimation后,动画的值会调用Curvetransform方法,转换成新的值。

Curve.decelerate为例:

double transformInternal(double t) {
    t = 1.0 - t;
    return 1.0 - t * t;
}
动画时间动画值
00
0.50.4375
1.00.75
1.50.9375
21
  • 设置了Tween之后呢?

Tween其实也是调用了transform方法进行了一次转换,例如:

Tween(begin: 100.0, end: 200.0).animate(_animation);

动画时间动画值
0100
0.5143.75
1.0175
1.5193.75
2200

上面的逻辑就是AnimationControllerCurvedAnimationTween一起共同决定动画中间值的一个逻辑。

AnimatedWidget为什么不需要手动刷新?
abstract class AnimatedWidget extends StatefulWidget {

  @override
  _AnimatedState createState() => _AnimatedState();

}

class _AnimatedState extends State<AnimatedWidget> {
  @override
  void initState() {
    super.initState();
    widget.listenable.addListener(_handleChange);
  }

  void _handleChange() {
    setState(() {
    });
  }

}

代码中我们看到AnimatedWidget继承自StatefulWidget_AnimatedStateinitState加入了一个动画的监听_handleChange函数。_handleChange函数中调用了setState进行刷新。

AnimatedBuilder如何避免了子Widget的重构?
class AnimatedBuilder extends AnimatedWidget {
  
  const AnimatedBuilder({
    Key? key,
    required Listenable animation,
    required this.builder,
    this.child,
  }) : assert(animation != null),
       assert(builder != null),
       super(key: key, listenable: animation);

  final Widget? child;

  @override
  Widget build(BuildContext context) {
    return builder(context, child);
  }
}

代码中我们看到构造函数传入的childbuilder方法传出去的child是同一个,这样就达到了child的复用逻辑。

是否对这种复用方式有印象?Provider其实也有类似的设计。

AnimatedBuilder

ImplicitlyAnimatedWidget如何实现自动动画的?
abstract class ImplicitlyAnimatedWidget extends StatefulWidget {

  @override
  ImplicitlyAnimatedWidgetState<ImplicitlyAnimatedWidget> createState();
}

abstract class ImplicitlyAnimatedWidgetState<T extends ImplicitlyAnimatedWidget> extends State<T> with SingleTickerProviderStateMixin<T> {
  
  @protected
  // 1
  AnimationController get controller => _controller;
  late final AnimationController _controller = AnimationController(
    duration: widget.duration,
    debugLabel: kDebugMode ? widget.toStringShort() : null,
    vsync: this,
  );

  // 2
  Animation<double> get animation => _animation;
  late Animation<double> _animation = _createCurve();

  @override
  void initState() {
    super.initState();
    _controller.addStatusListener((AnimationStatus status) {
      switch (status) {
        case AnimationStatus.completed:
          if (widget.onEnd != null)
            widget.onEnd!();
          break;
        case AnimationStatus.dismissed:
        case AnimationStatus.forward:
        case AnimationStatus.reverse:
      }
    });
    _constructTweens();
    didUpdateTweens();
  }
  
  // 2.
  CurvedAnimation _createCurve() {
    return CurvedAnimation(parent: _controller, curve: widget.curve);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  bool _shouldAnimateTween(Tween<dynamic> tween, dynamic targetValue) {
    return targetValue != (tween.end ?? tween.begin);
  }

  void _updateTween(Tween<dynamic>? tween, dynamic targetValue) {
    if (tween == null)
      return;
    tween
      ..begin = tween.evaluate(_animation)
      ..end = targetValue;
  }

  void didUpdateWidget(T oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.curve != oldWidget.curve)
      _animation = _createCurve();
    _controller.duration = widget.duration;
    if (_constructTweens()) {
      forEachTween((Tween<dynamic>? tween, dynamic targetValue, TweenConstructor<dynamic> constructor) {
        _updateTween(tween, targetValue);
        return tween;
      });
      _controller
        ..value = 0.0
        ..forward();
      didUpdateTweens();
    }
  }
  
  bool _constructTweens() {
    bool shouldStartAnimation = false;
    forEachTween((Tween<dynamic>? tween, dynamic targetValue, TweenConstructor<dynamic> constructor) {
      if (targetValue != null) {
        tween ??= constructor(targetValue);
        if (_shouldAnimateTween(tween, targetValue))
          shouldStartAnimation = true;
      } else {
        tween = null;
      }
      return tween;
    });
    return shouldStartAnimation;
  }

  @protected
  void forEachTween(TweenVisitor<dynamic> visitor);

  @protected
  void didUpdateTweens() { }
}

ImplicitlyAnimatedWidget的代码也很清晰,使用的也是AnimationControllerCurvedAnimationTween的组合,只是内部实现了。

当属性发生变化的时候,会调用didUpdateWidget,然后调用AnimationControllerforward方法开始动画。

总结

本文通过源码的分析,解读了动画的一些相关内容。后面我们将会进入状态管理的使用和分析,欢迎点赞和关注。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值