Flutter實戰之動畫實現篇

JulyYu 發表於 2019-08-22

前言

Flutter動畫主分為兩大類:補間動畫、物理動畫。

這裡主要介紹幾種方式實現動畫效果。雖然使用的動畫元件有所不同,但真正閱讀原始碼分析會發現動畫實現離不開幾個關鍵物件:AnimationController、Animations、Tween、Ticker。

實現方式

AnimationController實現

AnimationController實現了Animation抽象類,T的Value為Double型別,其數值在0.0到1.0之間。可以認為是動畫數值的控制器,通過內部Ticker實現數值變化。

class AnimationController extends Animation<double>
  with AnimationEagerListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin {
  
  AnimationController({
    double value, //初始化數值
    this.duration, //正向持續時間
    this.reverseDuration,//逆向持續時間
    this.debugLabel, 
    this.lowerBound = 0.0, //定義dismissed觸發數值
    this.upperBound = 1.0,  //定義completed觸發數值
    this.animationBehavior = AnimationBehavior.normal,
    @required TickerProvider vsync,
    ......
}
複製程式碼

Demo程式碼

分別通過呼叫forward和reverse執行,設定addListener介面監聽當前數值,addStatusListener介面監聽動畫執行狀態,另外支援Stop、Reset等操作。AnimationStatus定義動畫的四種狀態: dismissed、forward、 reverse、 completed。forward和completed是一對狀態,表示動畫正向觸發和正向觸發完成並結束。reverse和dismissed是一對狀態,表示動畫逆向觸發和逆向觸發完成並結束。

PS:需要注意比如執行了forward並回撥了completed動畫狀態,下次再次執行forward無法再次進行動畫,因為當前AnimationController內部value已經是最大值1.0。可以在狀態值返回completed後reset控制器或者執行forward時入參處理。

    _controller = AnimationController(
      animationBehavior: AnimationBehavior.preserve,
      vsync: this, // the SingleTickerProviderStateMixin
      duration: Duration(seconds: 2),
    );
    _controller.addListener(() {
      //每次動畫執行回撥介面
      _controller.value;
    });
    _controller.addStatusListener((status){
      //動畫狀態變化回撥介面
    });
    //正向執行動畫
    _controller.forward();
    //逆向執行動畫
    _controller.reverse();
複製程式碼

AnimatedContainer實現

AnimatedContainer可以算是傻瓜式動畫實現,內部自帶動畫控制器。只要對它可執行動畫的引數進行修改就能會有動畫效果。它支援一些元件基礎變化的動畫過渡效果,例如長寬變、顏色變化、位置變化、邊框變化等。

Demo程式碼

AnimatedContainer自身也是元件並繼承自StatefulWidget,所以通過State去更新元件的一些引數。通過設定Duration控制動畫持續時間,然後修改引數更新state後AnimatedContainer並會實現動畫效果。當然也不用擔心生命週期的問題,在元件移除時內部已經幫我們做好了dispose操作。

PS:暫時未找到可控制AnimatedContainer動畫執行的方法。比如在執行過程中希望暫停或者取消等。可以認為AnimatedContainer傻瓜式的動畫效果不可控制吧。

class AnimationContainerView extends StatefulWidget {
  @override
  _AnimationContainerViewState createState() => _AnimationContainerViewState();
}

class _AnimationContainerViewState extends State<AnimationContainerView> {
  bool selected = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        setState(() {
          selected = !selected;
        });
      },
      child: Center(
        child: AnimatedContainer(
          width: selected ? 200.0 : 100.0,
          height: selected ? 100.0 : 200.0,
          color: selected ? Colors.red : Colors.blue,
          alignment:
              selected ? Alignment.center : AlignmentDirectional.topCenter,
          duration: Duration(seconds: 2),
          curve: Curves.fastOutSlowIn,
          child: FlutterLogo(size: 75),
        ),
      ),
    );
  }
}
複製程式碼

AnimatedWidget實現

AnimatedWidget就不像AnimatedContainer傻瓜式實現方式。例項化元件必填Listenable引數,Listenable可以是Animation或者ChangeNotifier。內部為Listenable設定監聽,當外部動畫控制器Control數值改變時通知到AnimatedWidget,根據數值大小變化從而實現元件動畫效果。這樣看來AnimatedWidget比AnimatedContainer靈活度高,需要實現的動畫效果和執行由開發者控制。

abstract class AnimatedWidget extends StatefulWidget {
    
    const AnimatedWidget({
        Key key,
        @required this.listenable,
      }) : assert(listenable != null),
           super(key: key);
    ......
}

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

  @override
  void didUpdateWidget(AnimatedWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.listenable != oldWidget.listenable) {
      oldWidget.listenable.removeListener(_handleChange);
      widget.listenable.addListener(_handleChange);
    }
  }

  @override
  void dispose() {
    widget.listenable.removeListener(_handleChange);
    super.dispose();
  }

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

  @override
  Widget build(BuildContext context) => widget.build(context);
}
複製程式碼

Demo程式碼

動畫的數值更新由外部動畫控制器控制,所以可以通過控制AnimationController執行從而控制AnimatedWidget動畫執行。例如例項AnimationController,通過Tween對映動畫引數得到最終Animation。然後AnimationController執行動畫,AnimatedWidget通過監聽回撥相應得到Animation最新數值去執行動畫。

abstract class AnimatedWidgetView extends StatefulWidget {
  @override
  _AnimatedWidgetViewState createState() => _AnimatedWidgetViewState();
}

class _AnimatedWidgetViewState extends State<AnimatedWidgetView>
    with SingleTickerProviderStateMixin {
  AnimationController controller;
  Animation<double> animation;

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        FlatButton(
          child: Text("forward"),
          onPressed: () {
            controller.forward();
          },
        ),
        AnimatedLogo(
          animation: animation,
        ),
        FlatButton(
          child: Text("reverse"),
          onPressed: () {
            controller.reverse();
          },
        ),
      ],
    );
  }

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 10, end: 60).animate(controller);
  }
}

class AnimatedLogo extends AnimatedWidget {
  AnimatedLogo({Key key, Animation<double> animation})
      : super(key: key, listenable: animation);

  Widget build(BuildContext context) {
    final Animation<double> animation = listenable;
    return Center(
      child: Container(
        margin: EdgeInsets.symmetric(vertical: 10),
        height: animation.value,
        width: animation.value,
        child: FlutterLogo(),
      ),
    );
  }
}
複製程式碼

AnimatedBuilder實現

AnimatedBuilder繼承AnimatedWidget。若使用AnimatedWidget實現動畫必須繼承它實現一個元件,而AnimatedBuilder並不關心實現動畫元件只負責將動畫與元件關聯。在例項化元件多了TransitionBuilder成員實現內部佈局。可以適用於建立複雜元件佈局包含一部分動畫並結合其他動畫元件場景。

PS:個人覺得AnimatedBuilder像是裝飾設計模式,對普通元件進行一次包裝賦予新的動畫能力。

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 TransitionBuilder builder;
  
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return builder(context, child);
  }
  ......
    
}
複製程式碼

Demo程式碼

例如希望內部Container元件結合旋轉操作實現動畫。動畫控制器和子元件是可動態配置,實現子元件與動畫隔離。

....
@override
Widget build(BuildContext context) {
  return AnimatedBuilder(
    animation: _controller,
    child: Container(width: 200.0, height: 200.0, color: Colors.green),
    builder: (BuildContext context, Widget child) {
      return Transform.rotate(
        angle: _controller.value * 2.0 * math.pi,
        child: child,
      );
    },
  );
}
.....
複製程式碼

Hero實現

Hero動畫和Android中元素共享效果相同,動畫元素在切換頁面時從一個頁面過渡到另一個頁面中。實現效果涉及到Hero、InkWell、Material以及Navigator,例項程式碼較多推薦參考官方文件。 官方例子

原理

以上幾種動畫實現方式歸根結底還是離不開AnimationController的存在。例如AnimatedContainer內部state擁有AnimationController成員物件,AnimationController又是動畫執行控制者。AnimationController是通過改變動畫執行數值來控制動畫執行,但當我直接使用double型別的數值賦值給Widget改變它的寬高並執行迴圈並通過setState去更新的時候卻導致應用卡死崩潰。所以說執行動畫離不開AnimationController。

AnimationController

AnimationController內部有TickerProvider成員,該成員用於建立Ticker。

例如執行控制器的正向執行方法程式碼:assert檢查跳過,動畫方向設定為forward,然後執行_animateToInternal私有方法。

 TickerFuture forward({ double from }) {
    _direction = _AnimationDirection.forward;
    if (from != null)
      value = from;
    return _animateToInternal(upperBound);
  }
複製程式碼

_animateToInternal方法主要根據當前動畫數值計算出_animateToInternal和是否繼續執行動畫,若target等於value也就是動畫最終結束數值,則結束動畫執行,反之繼續執行_startSimulation方法。

TickerFuture _animateToInternal(double target, { Duration duration, Curve curve = Curves.linear }) {
    double scale = 1.0;
    if (SemanticsBinding.instance.disableAnimations) {
      switch (animationBehavior) {
        case AnimationBehavior.normal:
          // Since the framework cannot handle zero duration animations, we run it at 5% of the normal
          // duration to limit most animations to a single frame.
          // TODO(jonahwilliams): determine a better process for setting duration.
          scale = 0.05;
          break;
        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) {
      // Already at target, don't animate.
      simulationDuration = Duration.zero;
    }
    // 先停止之前的動畫
    stop();
    // 判斷持續時間是否結束
    if (simulationDuration == Duration.zero) {
      if (value != target) {
        _value = target.clamp(lowerBound, upperBound);
        notifyListeners();
      }
      _status = (_direction == _AnimationDirection.forward) ?
        AnimationStatus.completed :
        AnimationStatus.dismissed;
      _checkStatusChanged();
      //動畫結束呼叫complete方法回撥結果
      return TickerFuture.complete();
    }
    // 動畫還未結束繼續呼叫_startSimulation方法。
    return _startSimulation(_InterpolationSimulation(_value, target, simulationDuration, curve, scale));
  }
複製程式碼

執行_startSimulation方法,當前狀態賦值最後執行_checkStatusChanged方法更新狀態,這裡的狀態是AnimationStatus(dismissed、forward、 reverse、 completed)

TickerFuture _startSimulation(Simulation simulation) {
    assert(simulation != null);
    assert(!isAnimating);
    _simulation = simulation;
    _lastElapsedDuration = Duration.zero;
    _value = simulation.x(0.0).clamp(lowerBound, upperBound);
    final TickerFuture result = _ticker.start();
    _status = (_direction == _AnimationDirection.forward) ?
      AnimationStatus.forward :
      AnimationStatus.reverse;
    _checkStatusChanged();
    return result;
  }
void _checkStatusChanged() {
    final AnimationStatus newStatus = status;
    if (_lastReportedStatus != newStatus) {
      _lastReportedStatus = newStatus;
      notifyStatusListeners(newStatus);
    }
  }

複製程式碼

正向執行流程

forward -> _animateToInternal -> _startSimulation -> _checkStatusChanged

Ticker

Ticker用於監聽動畫幀,通過SchedulerBinding驅動執行。 在上述的_startSimulation方法可以知道有_ticker.start()執行。start方法中判斷shouldScheduleTick是否去執行時間片更新函式,shouldScheduleTick 根據muted、isActive、scheduled進行判斷,也就是通過shouldScheduleTick確認動畫是否持續進行下去。

@protected
bool get shouldScheduleTick => !muted && isActive && !scheduled;
  
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;
  }
複製程式碼

當執行scheduleTick方法呼叫SchedulerBinding的scheduleFrameCallback方法回撥,根據情況在_tick中進行下一個scheduleTick從而實現動畫連續繪製知道動畫結束。

 @protected
  void scheduleTick({ bool rescheduling = false }) {
    _animationId = SchedulerBinding.instance.scheduleFrameCallback(_tick, rescheduling: rescheduling);
  }
  
 void _tick(Duration timeStamp) {
    assert(isTicking);
    assert(scheduled);
    _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);
  }
複製程式碼

參考