Flutter動畫的使用

chonglingliu發表於2021-04-08

動畫能提高使用者的使用體驗,使APP更流暢。那麼在Flutter中如何實現動畫以及選擇使用什麼樣的動畫呢?

開門見山,我們直接上圖:

動畫概覽

繪製依賴的動畫

繪製依賴的動畫是指我們用動畫庫沒法直接實現的動畫,這時候有兩種實現方式:

  1. Canvas不斷繪製形成動畫(CustomPainter或者自定義RenderObjectWidget);
  2. 使用設計師提供的動畫檔案,然後結合三方庫來使用(Lottie或者Flare等)。

Lottie

lottie-flutter借鑑自Lottie,使用方法很也很簡單。

  • 新增依賴
dependencies:
  lottie: ^1.0.1
複製程式碼
  • 引入庫檔案
import 'package:lottie/lottie.dart';
複製程式碼
  • 使用
Lottie.network('https://raw.githubusercontent.com/xvrh/lottie-flutter/master/example/assets/Mobilo/A.json')
複製程式碼

lottie

CustomPainter

CustomPainter是系統提供的一個能夠繪製內容的底層API。

  • CustomPainter繪製
class SquarePainter extends CustomPainter {
  final double radians;
  SquarePainter(this.radians);

  @override
  void paint(Canvas canvas, Size size) {
    var paint = Paint()
      ..color = Colors.pink
      ..strokeWidth = 5
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round;

    var path = Path();

    var angle = (math.pi * 2) / 4.0;

    Offset center = Offset(size.width / 2, size.height / 2);
    Offset startPoint =
        Offset(100 * math.cos(radians), 100 * math.sin(radians));

    path.moveTo(startPoint.dx + center.dx, startPoint.dy + center.dy);

    for (int i = 1; i <= 4; i++) {
      double x = 100 * math.cos(radians + angle * i) + center.dx;
      double y = 100 * math.sin(radians + angle * i) + center.dy;
      path.lineTo(x, y);
    }
    path.close();
    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
  
}
複製程式碼
  • CustomPaint這個Widget封裝CustomPainter的繪製內容
CustomPaint(
    painter: SquarePainter(animation.value),
    child: Container(),
);
複製程式碼
  • AnimationController驅動動畫

全部相關程式碼

提示: CustomPainter可以實現很複雜的繪製,本案例僅僅用CustomPainter簡單繪製了一個正方形,然後進行不斷的旋轉動畫。

CustomPainter

隱性動畫

隱性動畫的WidgetImplicitlyAnimatedWidget,它的特點是在屬性發生改變後會自動進行動畫,不需對動畫進行控制,所以叫做隱性動畫。

Flutter中的隱性動畫和iOS中的隱性動畫的概念類似。

abstract class ImplicitlyAnimatedWidget extends StatefulWidget {
    const ImplicitlyAnimatedWidget({
        Key? key,
        this.curve = Curves.linear,
        required this.duration,
        this.onEnd,
    })
}
複製程式碼

ImplicitlyAnimatedWidget可以設定動畫的速率曲線curve(可以設定的型別),動畫時間duration和動畫結束後的回撥函式onEnd。屬性發生變化後,動畫就依據這些引數自動進行。

官方提供了很多的隱性動畫Widget,他們被命名為Animated**。接下來我們就來一個個看下這些Widget的動畫效果。

AnimatedAlign

Align的可動畫版本,alignment的發生變化引發動畫效果。

AnimatedAlign

class Body extends StatefulWidget {
  @override
  _BodyState createState() => _BodyState();
}

class _BodyState extends State<Body> with TickerProviderStateMixin {
  bool selected = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('CustomPainter'),
      ),
      body: GestureDetector(
        onTap: () {
          setState(() {
            selected = !selected;
          });
        },
        child: Center(
          child: Container(
            width: 250.0,
            height: 250.0,
            color: Colors.red,
            child: AnimatedAlign(
              alignment: selected ? Alignment.topRight : Alignment.bottomLeft,
              duration: const Duration(seconds: 1),
              curve: Curves.fastOutSlowIn,
              child: Logo(),
            ),
          ),
        ),
      ),
    );
  }
}
複製程式碼

AnimatedContainer

Container的可動畫版本,AnimatedContainer的各種屬性發生變化後有動畫效果。

AnimatedContainer

AnimatedContainer(
    width: selected ? 300.0 : 150.0,
    height: selected ? 300.0 : 150.0,
    decoration: BoxDecoration(
        color: selected ? Colors.amber[500] : Colors.amber[200],
        borderRadius: BorderRadius.circular(selected ? 20 : 0)),
        alignment: selected
            ? AlignmentDirectional.bottomCenter
            : AlignmentDirectional.topCenter,
    duration: Duration(seconds: 1),
    curve: Curves.fastOutSlowIn,
    child: Logo(),
)
複製程式碼

AnimatedDefaultTextStyle

TextStyle的可動畫版本,TextStyle發生變化引發動畫效果。

AnimatedDefaultTextStyle

AnimatedDefaultTextStyle(
    duration: Duration(seconds: 1),
    curve: Curves.bounceOut,
    style: TextStyle(
        fontSize: selected ? 20 : 16,
        color: selected ? Colors.amber : Colors.red,
        fontWeight: FontWeight.bold,
    ),
    child: Text("Animated DefaultTextStyle"),
)
複製程式碼

AnimatedOpacity

Opacity的可動畫版本,透明度改變引發動畫效果。

AnimatedOpacity

AnimatedOpacity(
    opacity: selected ? 1.0 : 0.0,
    duration: Duration(seconds: 1),
    curve: Curves.linear,
    child: Logo(),
)
複製程式碼

AnimatedPadding

Padding的可動畫版本,Padding改變引發動畫效果。

AnimatedPadding

AnimatedPadding(
    curve: Curves.easeInOut,
    duration: Duration(seconds: 1),
    child: Container(
        child: Logo(),
    ),
    padding: EdgeInsets.symmetric(
        horizontal: selected ? 10 : 40,
        vertical: selected ? 10 : 30),
)
複製程式碼

AnimatedPhysicalModel

PhysicalModel的可動畫版本,borderRadiuselevation改變引發動畫效果。

AnimatedPhysicalModel

AnimatedPhysicalModel(
    duration: const Duration(milliseconds: 500),
    curve: Curves.fastOutSlowIn,
    elevation: selected ? 0 : 10.0,
    shape: BoxShape.rectangle,
    shadowColor: Colors.red,
    color: Colors.white,
    borderRadius: selected
        ? BorderRadius.all(Radius.circular(0))
        : BorderRadius.all(Radius.circular(10)),
    child: Container(
        color: Colors.blue[100],
        child: Logo(),
    ),
)
複製程式碼

AnimatedPositioned

Position的可動畫版本,Position改變引發動畫效果。

AnimatedPositioned

Stack(
    children: [
        AnimatedPositioned(
            width: selected ? 100.0 : 100.0,
            height: selected ? 100.0 : 100.0,
            top: selected ? 150.0 : 50.0,
            left: selected ? 150.0 : 100.0,
            duration: Duration(seconds: 1),
            curve: Curves.fastOutSlowIn,
                child: Container(
                color: Colors.blue,
            ),
        ),
    ],
),
複製程式碼

AnimatedCrossFade

兩個子Widget相互切換的動畫。

AnimatedCrossFade

AnimatedCrossFade(
    firstChild: Logo(),
    secondChild: FlutterLogo(
        size: 120,
    ),
    crossFadeState: selected
        ? CrossFadeState.showSecond
        : CrossFadeState.showFirst,
    duration: Duration(seconds: 1),
    firstCurve: Curves.easeIn,
    secondCurve: Curves.easeIn,
)
複製程式碼

AnimatedSize

子Widget大小發生變化後會發生動畫。AnimatedContainer是自生大小發生變化引發動畫,這是它們的主要區別。

AnimatedSize

解釋:子Widget(Colors.amber)的大小改變沒有動畫,子Widget的大小改變後AnimatedSize(Colors.red)開始執行動畫。

AnimatedSize(
    duration: Duration(seconds: 1),
    reverseDuration: Duration(seconds: 1),
    curve: Curves.fastOutSlowIn,
    child: Container(
        width: selected ? 150 : 100,
        height: selected ? 150 : 100,
    color: Colors.amber,
    ),
    vsync: this,
)
複製程式碼

AnimatedSwitcher

這個Widget功能比較全,可以實現

  • 新增、刪除Widget的動畫;
  • 切換Widget的動畫;
  • Widget屬性變化的動畫;

AnimatedSwitcher

AnimatedSwitcher(
    child: Text(
        "$elapsed",
        key: ValueKey(elapsed),
            style: TextStyle(fontSize: 34),
        ),
        duration: Duration(seconds: 2),
        transitionBuilder:
            (Widget child, Animation<double> animation) {
                final offsetAnimation = TweenSequence([
                      TweenSequenceItem(
                          tween: Tween<Offset>(
                                  begin: Offset(0.0, 1.0),
                                  end: Offset(0.0, 0.0))
                              .chain(CurveTween(curve: Curves.easeInOut)),
                          weight: 1),
                      TweenSequenceItem(
                          tween: ConstantTween(Offset(0.0, 0.0)), weight: 4),
                    ]).animate(animation);
                    return ClipRRect(
                      child: SlideTransition(
                        position: offsetAnimation,
                        child: child,
                ),
            );
        },
        layoutBuilder: (currentChild, previousChildren) {
            return currentChild;
    },
)
複製程式碼

前面介紹了一系列官方提供的隱性動畫Widget,除了AnimatedCrossFade,AnimatedSizeAnimatedSwitcher外都是ImplicitlyAnimatedWidget的子類。使用這些類能夠方便的實現動畫,當遇到沒法實現的功能又想用隱性動畫的需求時,TweenAnimationBuilder就是我們很好的選擇。

TweenAnimationBuilder

實現自定義隱性動畫。

TweenAnimationBuilder

TweenAnimationBuilder(
    duration: Duration(seconds: 2),
    curve: Curves.easeInOut,
    tween: Tween<double>(begin: 0, end: selected ? 180 : 0),
    builder: (context, value, child) {
        return RotationWidget(rotationY: value);
    },
)

class RotationWidget extends StatelessWidget {
  static const double degrees2Radians = pi / 180;
  final double rotationY;

  const RotationWidget({Key key, this.rotationY = 0}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Transform(
        alignment: FractionalOffset.center,
        transform: Matrix4.identity()
          ..setEntry(3, 2, 0.001)
          ..rotateY(rotationY * degrees2Radians),
        child: rotationY <= 90
            ? Logo(size: 250)
            : Transform(
                alignment: FractionalOffset.center,
                transform: Matrix4.identity()
                  ..setEntry(3, 2, 0.001)
                  ..rotateY(180 * degrees2Radians),
                child: FlutterLogo(size: 200),
              ));
  }
}
複製程式碼

顯性動畫

顯性動畫是指需要開發者去控制動畫,也就是說需要開發者去使用動畫Animation相關的類去控制動畫過程。

動畫(Animation)相關類

  • Animation
abstract class Animation<T> extends Listenable implements ValueListenable<T> {

  void addListener(VoidCallback listener);
  void removeListener(VoidCallback listener);
    
  void addStatusListener(AnimationStatusListener listener);
  void removeStatusListener(AnimationStatusListener listener);

  AnimationStatus get status;
    
  T get value;

  bool get isDismissed => status == AnimationStatus.dismissed;
  bool get isCompleted => status == AnimationStatus.completed;
}
複製程式碼

Animation這個類代表了動畫的當前值和狀態,以及告知監聽者這兩個值的改變

  1. value代表動畫當前的值,addListener()removeListener()可以新增和移除監聽者,當value變化後,會通知監聽者值的變化;
  2. status代表動畫的狀態,有初始狀態dismissed,正向動畫狀態forward,反向動畫狀態reverse動畫已完成狀態completed四種,addStatusListener()removeStatusListener()可以新增和移除監聽者,當status變化後,會通知監聽者狀態的變化。
  • AnimationController
class AnimationController extends Animation<double> {

    void reset() {}

    TickerFuture forward({ double? from }) {}
    
    TickerFuture reverse({ double? from }) {}
    
    TickerFuture animateTo(double target, { Duration? duration, Curve curve = Curves.linear }) {}
    
    TickerFuture animateBack(double target, { Duration? duration, Curve curve = Curves.linear }) {}
    
    TickerFuture repeat({ double? min, double? max, bool reverse = false, Duration? period }) {}
    
    void stop({ bool canceled = true }) {}
}
複製程式碼

Animation只能代表當前動畫值和動畫的狀態,沒法控制動畫。AnimationController繼承自Animation是對動畫進行控制的類。譬如它能重置動畫reset(),正向進行動畫forward(),反向進行動畫reverse(),重複進行動畫repeat()和停止動畫stop()等。

  • CurvedAnimation
class CurvedAnimation extends Animation<double> {
    
    final Animation<double> parent;
    
    Curve curve;
    
    Curve? reverseCurve;
}
複製程式碼

CurvedAnimation的功能是給parent設定一個動畫速率變化的曲線。也就是說動畫值的變化率可以不是固定的。curve是正向動畫的曲線,reverseCurve是反向動畫的曲線。可以設定的型別和隱性動畫一樣

  • Tween
class Tween<T extends dynamic> extends Animatable<T> {

  T? begin;


  T? end;
複製程式碼

Tween主要是給``設定一個動畫的開始值begin和動畫的結束值end

系統提供了一些列的Tween封裝類供我們使用:

ColorTween

SizeTween

RectTween

IntTween

StepTween

ConstantTween

CurveTween

  • TweenSequence
class TweenSequence<T> extends Animatable<T> {

  final List<TweenSequenceItem<T>> _items = <TweenSequenceItem<T>>[];
  final List<_Interval> _intervals = <_Interval>[];

}
複製程式碼

TweenSequence可以設定一系列的TweenSequenceItem,相當於設定一些關鍵幀,可以實現類似關鍵幀動畫

動畫實現的案例

我們來用上面提到的一些類來實現一個圖片方法然後縮小的迴圈動畫,效果如下:

animation

  • 1.建立AnimationController
// vsync引數值是同步訊號,duration引數值是動畫的時間
AnimationController _controller = AnimationController(vsync: this, duration: Duration(seconds: 2));
複製程式碼
  • 2.給_controller設定Curve
// parent引數值是需要設定Curve的Animation,curve引數值是正向動畫的Curve,reverseCurve引數值是反向動畫的Curve
Animation _animation = CurvedAnimation(
                        parent: _controller,
                        curve: Curves.bounceOut,
                        reverseCurve: Curves.bounceIn);
複製程式碼
  • 3.給_animation設定動畫的開始值和結束值
Animation_sizeAnim = Tween(begin: 100.0, end: 200.0).animate(_animation);
複製程式碼
  • 4.監聽_controller動畫值的改變,然後進行介面更新
_controller.addListener(() {
    setState(() {});
});
複製程式碼
  • 5.監聽聽_controller動畫的狀態改變,然後進行迴圈動畫
_controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        _controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        _controller.forward();
      }
});
複製程式碼
  • 6.介面的展示邏輯
Logo(
    size: _sizeAnim.value,
)
複製程式碼

通過上面幾個步驟,動畫的程式碼就寫完了。所有程式碼如下:

class Body extends StatefulWidget {
  @override
  _BodyState createState() => _BodyState();
}

class _BodyState extends State<Body> with SingleTickerProviderStateMixin {
  // 建立AnimationController
  AnimationController _controller;
  Animation _animation;
  Animation _sizeAnim;

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

    // 1.建立AnimationController
    _controller =
        AnimationController(vsync: this, duration: Duration(seconds: 2));

    // 2.設定Curve的值
    _animation = CurvedAnimation(
        parent: _controller,
        curve: Curves.bounceOut,
        reverseCurve: Curves.bounceIn);

    // 3. 設定動畫的開始值和結束值
    _sizeAnim = Tween(begin: 100.0, end: 200.0).animate(_animation);

    // 4. 監聽動畫值的改變
    _controller.addListener(() {
      setState(() {});
    });

    // 5. 監聽動畫的狀態改變
    _controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        _controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        _controller.forward();
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Animation"),
      ),
      body: Center(
        child: Logo(
          size: _sizeAnim.value,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.play_arrow),
        onPressed: () {
          _controller.forward();
        },
      ),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}
複製程式碼

AnimatedWidget

上面的程式碼我們實現了動畫,但是有兩個問題:

  1. 我們需要監聽_controller的值的變化,然後呼叫setState(() {});這個方法;
  2. 動畫的繪製涉及到了Body的重構ReBuild,其實我們只需要Logo進行重構ReBuild就行。

我們可以將Logo重構為一個AnimatedWidget,這樣就可以解決上面兩問題了:

class LogoAnimatedWidget extends AnimatedWidget {
  LogoAnimatedWidget(Animation anim) : super(listenable: anim);

  @override
  Widget build(BuildContext context) {
    Animation anim = listenable;
    return Logo(size: anim.value);
  }
}
複製程式碼

解釋下程式碼邏輯:

  1. 繼承AnimatedWidget的子類的建構函式需要傳入一個Animation;
  2. 重寫build方法,返回Widget,這裡就是我們的Logo

接下來刪除_controller.addListener(), 然後使用LogoAnimatedWidget

class Body extends StatefulWidget {
  @override
  _BodyState createState() => _BodyState();
}

class _BodyState extends State<Body> with SingleTickerProviderStateMixin {
  // 建立AnimationController
  AnimationController _controller;
  Animation _animation;
  Animation _sizeAnim;

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

    // 1.建立AnimationController
    _controller =
        AnimationController(vsync: this, duration: Duration(seconds: 2));

    // 2.設定Curve的值
    _animation = CurvedAnimation(
        parent: _controller,
        curve: Curves.bounceOut,
        reverseCurve: Curves.bounceIn);

    // 3. 設定動畫的開始值和結束值
    _sizeAnim = Tween(begin: 100.0, end: 200.0).animate(_animation);

    // 4. 監聽動畫值的改變
    // _controller.addListener(() {
    //   setState(() {});
    // });

    // 5. 監聽動畫的狀態改變
    _controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        _controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        _controller.forward();
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Animation"),
      ),
      body: Center(
        child: LogoAnimatedWidget(_sizeAnim),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.play_arrow),
        onPressed: () {
          _controller.forward();
        },
      ),
    );
  }

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

class LogoAnimatedWidget extends AnimatedWidget {
  LogoAnimatedWidget(Animation anim) : super(listenable: anim);

  @override
  Widget build(BuildContext context) {
    Animation anim = listenable;
    return Logo(size: anim.value);
  }
}
複製程式碼

AnimatedBuilder

AnimatedWidget其實也有一些問題:

  1. 需要新建一個AnimatedWidget,程式碼量增加了;
  2. AnimatedWidget如果動畫的Widget含有子Widget,那子Widget也會重構ReBuild

AnimatedBuilder可以解決上面兩問題。程式碼如下:

  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Animation"),
      ),
      body: Center(
        child: AnimatedBuilder(
            animation: _sizeAnim,
            builder: (ctx, child) {
              return Logo(
                size: _sizeAnim.value,
              );
            }),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.play_arrow),
        onPressed: () {
          _controller.forward();
        },
      ),
    );
  }
複製程式碼

AnimatedBuilder有兩個引數,animation引數值是Animation, builder引數值是一個函式,會提供BuildContext子Widget,一個提供Widget的構造環境,一個提供child可以複用,無需重構子Widget

官方提供的AnimatedWidget

官方提供了一些AnimatedWidget,使用方式和使用自定義的AnimatedWidget類似。

由於和隱式動畫的版本類似,這裡就不一一貼出效果了,只是列出來供大家參閱:

AlignTransition

DecoratedBoxTransition

DefaultTextStyleTransition

PositionedTransition

RelativePositionedTransition

RotationTransition

ScaleTransition

SizeTransition

SlideTransition

FadeTransition

總結

至此,我們將Flutter中的動畫實現方式總結完了。

我們知道動畫的邏輯就是不斷的重繪,那Animation相關的類是如何引發重繪的呢?隱式動畫又是如何對開發者遮蔽了Animation類實現動畫的呢?

相關的問題,我們在下一節將深入底層去為你揭開面紗!

相關文章