• Flutter 从源码扒一扒动画机制


    一、 关键类

    1、AnimationController

    AnimationController 类是Flutter中动画的基础类,它提供了一个动画控制器,用于控制动画的播放、暂停、停止等操作。

    2、TickerProvider

    TickerProvider 是一个接口,它定义了创建 Ticker 对象的方法,并指定的 onTick 回调关联,Ticker 对象用于控制动画的播放速度和时间。

    3、SingleTickerProviderStateMixin

     mixin SingleTickerProviderStateMixin on State implements TickerProvider
    
    • 1

    SingleTickerProviderStateMixin 是一个 mixin 类,它实现了 TickerProvider 接口,并提供了创建 Ticker 对象的方法。

    单一Ticker提供者:
    当你的 State 类只需要一个 AnimationController(即一个 Ticker)时,应使用 SingleTickerProviderStateMixin。
    此 Mixin 保证在该 State 实例的生命期内仅创建和管理一个 Ticker。
    如果尝试在此 State 中创建多个 AnimationController,会抛出异常,提示“multiple tickers were created”。
    适用于包含单个简单动画的场景,如页面过渡动画、单个控件的旋转、淡入淡出等。
    当你确信一个 State 中不会有多个并发动画时,使用此 Mixin 可以避免不必要的资源消耗。

    4、TickerProviderStateMixin

    mixin TickerProviderStateMixin on State implements TickerProvider 
    
    • 1

    TickerProviderStateMixin 是一个 mixin 类,它实现了 TickerProvider 接口,并提供了创建 Ticker 对象的方法。
    多Ticker提供者:
    当你的 State 类需要管理多个独立的 AnimationController(每个对应一个 Ticker)时,应使用 TickerProviderStateMixin。
    这个 Mixin 允许你在同一 State 实例中创建任意数量的 AnimationController,为每个动画控制器分配单独的 Ticker。
    适用于复杂场景,比如一个页面中有多个独立动画需要同时运行或按需控制
    适用于需要管理多个并发动画的复杂场景,如多元素协同动画、不同触发条件下运行的不同动画序列等。
    当页面或者组件内包含多个相互独立或有依赖关系的动画控制器时,使用此 Mixin 可确保所有动画都能正确创建和管理各自的 Ticker。

    5、SchedulerBinding

    SchedulerBinding 是 Flutter 框架中一个核心的绑定类,它负责管理 Flutter 应用程序中的任务调度与时间线事件处理。以下是对 SchedulerBinding 类的主要功能和用途的详细说明:

    a. 任务调度:SchedulerBinding
    负责协调 Flutter 应用中的异步操作、动画帧绘制、事件处理等任务的执行顺序。它实现了基于优先级的任务队列,确保不同类型的任务在适当的时机得到调度。
    通过 scheduleFrame() 方法,SchedulerBinding 可以触发下一帧的绘制请求。每当设备屏幕刷新时,此方法会被调用,以驱动 Flutter 的渲染循环。

    b.生命周期管理:
    SchedulerBinding 监听并响应应用程序的生命周期事件,如启动、暂停、恢复和停止。开发者可以通过监听其提供的 WidgetsBindingObserver 接口中的相关回调方法(如 didChangeAppLifecycleState())来适时调整应用状态或资源。

    c.定时器管理:
    SchedulerBinding 提供了创建、管理和取消定时器的功能。使用 Timer.run()、Timer.periodic() 等方法可以安排一次性或周期性任务。这些定时器任务会在特定时间点被调度执行,且遵循优先级规则。
    手势与事件处理:
    SchedulerBinding 整合了对触摸、键盘输入等用户交互事件的处理。它将这些事件转换为 GestureEvent 或 PointerEvent,并通过调度机制传递给相应的 GestureDetector 或 Listener 组件进行处理。

    e. 调度监听与回调:
    开发者可以注册监听 SchedulerBinding 上的特定事件或阶段,例如:
    addPersistentFrameCallback():添加一个持续的帧回调,该回调将在每一帧绘制前被调用。
    addPostFrameCallback():添加一个在当前帧绘制完成后执行的回调,常用于需要在视图布局完成后进行的操作。
    addTimingsCallback():添加一个时间线回调,用于收集和分析应用性能数据。

    二、 深入源码

    初始化一个AnimationController的时候
    注册了一个回调函数,并创建了一个Ticker对象

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    这个_tick函数的代码

    void _tick(Duration elapsed) {
        _lastElapsedDuration = elapsed;
        final double elapsedInSeconds = elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond;
        assert(elapsedInSeconds >= 0.0);
        _value = clampDouble(_simulation!.x(elapsedInSeconds), lowerBound, upperBound);
        if (_simulation!.isDone(elapsedInSeconds)) {
        _status = (_direction == _AnimationDirection.forward) ?
        AnimationStatus.completed :
        AnimationStatus.dismissed;
        stop(canceled: false);
        }
        notifyListeners();
        _checkStatusChanged();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    Ticker类

    @override
    Ticker createTicker(TickerCallback onTick) {
      assert(() {
        if (_ticker == null) {
          return true;
        }
      _ticker = Ticker(onTick, debugLabel: kDebugMode ? 'created by ${describeIdentity(this)}' : null);
      _updateTickerModeNotifier();
      _updateTicker(); // Sets _ticker.mute correctly.
      return _ticker!;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    _tick函数被封装成了Ticker对象,往后看在哪调用的,后面有解释

      void notifyListeners() {
        final List localListeners = _listeners.toList(growable: false);
        for (final VoidCallback listener in localListeners) {  
        
          try {
            if (_listeners.contains(listener)) {
              listener();
            }
          } catch (exception, stack) {
            FlutterError.reportError(FlutterErrorDetails(
              exception: exception,
              stack: stack,
              library: 'animation library',
              context: ErrorDescription('while notifying listeners for $runtimeType'),
              informationCollector: collector,
            ));
          }
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    notifyListeners()这个就是通知监听,就是我们调用的 addListener的方法

    我们看看_checkStatusChanged方法

    void _checkStatusChanged() {
      final AnimationStatus newStatus = status;
      if (_lastReportedStatus != newStatus) {
        _lastReportedStatus = newStatus;
        notifyStatusListeners(newStatus);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这个方法就是通知状态的监听,就是我们调用的addStatusListener的方法。

    而真正开启动画的,就是调用controller.forward()

    TickerFuture forward({ double? from }) {
        _direction = _AnimationDirection.forward;
        if (from != null) {
        value = from;
        }
        return _animateToInternal(upperBound);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    _animateToInternal 方法又调用了_startSimulation方法

    TickerFuture _startSimulation(Simulation simulation) {
        assert(!isAnimating);
        _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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    从上面的代码我们可以看到调用了,我们刚才在AnimationController初始化创建的Ticker对象的start方法

    TickerFuture start() {
        _future = TickerFuture._();
        if (shouldScheduleTick) {
           scheduleTick();
        }
        if (SchedulerBinding.instance.schedulerPhase.index > SchedulerPhase.idle.index &&
           SchedulerBinding.instance.schedulerPhase.index < SchedulerPhase.postFrameCallbacks.index) {
        _startTime = SchedulerBinding.instance.currentFrameTimeStamp;
        }
        return _future!;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    我们看看scheduleTick();

    void scheduleTick({ bool rescheduling = false }) {
        _animationId = SchedulerBinding.instance.scheduleFrameCallback(_tick, rescheduling: rescheduling);
    }
    
    • 1
    • 2
    • 3

    该函数用于安排一个帧回调函数_tick,以便在下一帧被执行。rescheduling参数决定是否允许重新安排回调。通过SchedulerBinding.instance.scheduleFrameCallback方法来实现。

    这个_tick函数是Ticker类的方法,不是AnimationController的方法,下面的_onTick这个方法才是调用AnimationController的_tick函数,这个_tick函数就是我们上面传入Ticker对象里的onTick。就是在这里调用的

      void _tick(Duration timeStamp) {
        _animationId = null;
    
        _startTime ??= timeStamp;
        _onTick(timeStamp - _startTime!);
    
        // The onTick callback may have scheduled another tick already, for
        // example by calling stop then start again.
        if (shouldScheduleTick) {
          scheduleTick(rescheduling: true);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    SchedulerBinding类的

    int scheduleFrameCallback(FrameCallback callback, { bool rescheduling = false }) {
        scheduleFrame();
        _nextFrameCallbackId += 1;
        _transientCallbacks[_nextFrameCallbackId] = _FrameCallbackEntry(callback, rescheduling: rescheduling);
        return _nextFrameCallbackId;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    scheduleFrame()用来调度一个绘制帧

    void scheduleFrame() {
            if (_hasScheduledFrame || !framesEnabled) {
                  return;
                }
                ensureFrameCallbacksRegistered();
                platformDispatcher.scheduleFrame();
                _hasScheduledFrame = true;
            }
    
        void ensureFrameCallbacksRegistered() {
            platformDispatcher.onBeginFrame ??= _handleBeginFrame;
            platformDispatcher.onDrawFrame ??= _handleDrawFrame;
        }
    
    
    
    void _handleBeginFrame(Duration rawTimeStamp) {
      if (_warmUpFrame) {
        _rescheduleAfterWarmUpFrame = true;
        return;
      }
      handleBeginFrame(rawTimeStamp);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    _transientCallbacks[_nextFrameCallbackId] = _FrameCallbackEntry(callback, rescheduling: rescheduling), 这行代码是将我们的_tick函数封装成一个_FrameCallbackEntry对象,然后存到了_transientCallbacks 这个Map对象里。
    从这里我们就可以猜到,应该就是等到下一个垂直同步信号的到来,然后再遍历调用_transientCallbacks的对象里的回调方法

    我们找到了遍历_transientCallbacks的方法是handleBeginFrame

    void handleBeginFrame(Duration? rawTimeStamp) {
        _frameTimelineTask?.start('Frame');
        _firstRawTimeStampInEpoch ??= rawTimeStamp;
        _currentFrameTimeStamp = _adjustForEpoch(rawTimeStamp ?? _lastRawTimeStamp);
        if (rawTimeStamp != null) {
        _lastRawTimeStamp = rawTimeStamp;
        }
    _hasScheduledFrame = false;
    try {
      // TRANSIENT FRAME CALLBACKS
      _frameTimelineTask?.start('Animate');
      _schedulerPhase = SchedulerPhase.transientCallbacks;
      final Map callbacks = _transientCallbacks;
      _transientCallbacks = {};
      callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {
        if (!_removedIds.contains(id)) {
          _invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp!, callbackEntry.debugStack);
        }
      });
      _removedIds.clear();
    } finally {
      _schedulerPhase = SchedulerPhase.midFrameMicrotasks;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    }

    从上面代码可以看出,_handleBeginFrame函数注册了一下,然后等垂直同步信号的到来.
    platformDispatcher.scheduleFrame()这个就是开始请求绘制帧的。

    当垂直同步信号到来后,就开始回调handleBeginFrame方法,接着就开始遍历调用_transientCallbacks里的对象,也就会回调上面说的_tick函数,这样整个动画就开始了。

    三、最后

    其实还有很多源码细节,我就不去抠了,先从整体上把握整个流程我感觉就可以了。

  • 相关阅读:
    【无标题】
    (面试经典刷题)挑战一周刷完150道-Python版本-第3天(40个题)-I(前10个题)
    使用uni-app和Golang开发影音类小程序
    Android 10 SystemUI 如何添加4G信号和WiFi图标
    【爬虫】scrapy创建运行爬虫、解析页面(嵌套url)、自定义中间件(设置UserAgent和代理IP)、自定义管道(保存到mysql)
    【UI自动化】通过剪切板发送文本
    Redis:持久化RDB与AOF
    【Vue3响应式原理#01】Reactivity
    R语言layout () 函数
    面试阿里、字节全都一面挂,被面试官说我的水平还不如应届生
  • 原文地址:https://blog.csdn.net/hjjdehao/article/details/138065916