交錯動畫


到目前為止建立的動畫,都是單一效果,有沒有辦法一個動畫中含有多個效果呢?例如〈繪圖與動畫〉中,圓畫一半後略為放大再畫剩下的一半?

在〈自訂緩動曲線〉中談到,緩動曲線的時間標準化為 0.0 到 1.0,Flutter 提供了 CurvedAnimation,可以讓你決定在緩動曲線的哪一個時間區隔,套用指定的緩動曲線,例如:

...

class _AnimatedPie extends State<AnimatedPie> with SingleTickerProviderStateMixin {
  AnimationController _animation;
  Animation<double> radian1;
  Animation<double> scale;
  Animation<double> radian2;

  @override
  void initState() {
    super.initState();

    _animation = AnimationController(
      duration: widget.duration,
      vsync: this,
    )..repeat(); // 不斷重複

    radian1 = Tween<double>(
      begin: 0,
      end: pi
    ).animate(CurvedAnimation(      // 使用 CurvedAnimation
      parent: _animation,
      curve: Interval(0, 0.33),     // 在時間軸的 0 ~ 0.33 部份套用 
    ));

    scale = Tween<double>(
      begin: 1,
      end: 1.1
    ).animate(CurvedAnimation(
      parent: _animation,
      curve: Interval(0.34, 0.66),  // 在時間軸的 0.34 ~ 0.66 部份套用
    ));

    radian2 = Tween<double>(
      begin: 0,
      end: pi
    ).animate(CurvedAnimation(
      parent: _animation,
      // 在時間軸的 0.66 ~ 1.0 部份套用
      curve: Interval(0.67, 1.0, 
          curve: Curves.ease       // 可設定 curve
      ), 
    ));
  }

  ...

Intervalcurve 還可以指定緩動曲線種類,預設是 Curves.linear,在設計 radian1scaleradian2 這些 Animation<double> 時,除了時間區設定之外,另外還要思考的是,這些 Animation<double>value 要做哪種用途,或者是如何結合計算來完成想要的動畫,例如,scalevalue 可用來乘上 widget.radius,而 radian1.value + radian2.value 的結果,可以用來設定給 sweepAngle

...

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
        animation: _animation,
        builder: (_, __) {
          return Pie(
             radius: widget.radius * scale.value,
             sweepAngle: radian1.value + radian2.value
          );
        }
    );
  }

...

有點像是在編輯時間軸的概念,只是要多點計算,來個完整的程式示範:

import 'package:flutter/material.dart';
import 'dart:math';

void main() => runApp(
  MaterialApp(
    home: Center(
      child: AnimatedPie(radius: 50, duration: Duration(seconds: 2)),
    ),
  )
);

class AnimatedPie extends StatefulWidget {
  final double radius;
  final Duration duration;

  AnimatedPie({this.radius, this.duration});

  @override
  State<StatefulWidget> createState() => _AnimatedPie();

}

class _AnimatedPie extends State<AnimatedPie> with SingleTickerProviderStateMixin {
  AnimationController _animation;
  Animation<double> radian1;
  Animation<double> scale;
  Animation<double> radian2;

  @override
  void initState() {
    super.initState();
    _animation = AnimationController(
      duration: widget.duration,
      vsync: this,
    )..repeat(); // 不斷重複

    radian1 = Tween<double>(
        begin: 0,
        end: pi
    ).animate(CurvedAnimation(
      parent: _animation,
      curve: Interval(0, 0.33),
    ));

    scale = Tween<double>(
        begin: 1,
        end: 1.1
    ).animate(CurvedAnimation(
      parent: _animation,
      curve: Interval(0.34, 0.66),
    ));

    radian2 = Tween<double>(
        begin: 0,
        end: pi
    ).animate(CurvedAnimation(
      parent: _animation,
      curve: Interval(0.67, 1.0, curve: Curves.ease),
    ));
  }

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

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animation,
      builder: (_, __) {
        return Pie(
           radius: widget.radius * scale.value,
           sweepAngle: radian1.value + radian2.value
        );
      }
    );
  }
}

class Pie extends StatelessWidget {
  final startAngle = pi * 1.5;
  final double radius;
  final double sweepAngle;

  Pie({this.radius, this.sweepAngle});

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      size: Size.infinite,
      painter: PiePainter(radius: radius, sweepAngle: sweepAngle),
    );
  }
}

class PiePainter extends CustomPainter {
  final startAngle = pi * 1.5;
  final double radius;
  final double sweepAngle;

  PiePainter({this.radius, this.sweepAngle});

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
      ..color = Colors.blue
      ..strokeWidth = radius / 2
      ..style = PaintingStyle.stroke;

    canvas.drawArc(
        Rect.fromCircle(center: Offset(size.width / 2, size.height / 2), radius: radius),
        startAngle, sweepAngle,
        false,
        paint
    );
  }

  @override
  bool shouldRepaint(PiePainter other) => other.radius != radius || other.sweepAngle != sweepAngle;
}

完成的效果如下:

交錯動畫